commit 62907230c602b4a0c45a2576f6597afc1d8ee858 Author: “Naeel” Date: Fri May 22 11:10:39 2026 +0400 v19: перевод, история записей (IndexedDB), стерео из истории, фонетический анализ, индикатор микрофона diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..1410bda --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,75 @@ +# Правила + +## ⛔ ОТВЕЧАТЬ КРАТКО — АБСОЛЮТНОЕ ПРАВИЛО +- Вопрос → короткий ответ → СТОП. +- Ничего лишнего. +- Код — только по запросу. + +## ⛔⛔⛔ ВОПРОС = СТОП + +**Если в сообщении есть вопрос в ЛЮБОЙ форме** ("так ?", "верно ?", "почему ?", "как ?", "так же ?" и т.д.): +1. Только ответить на вопрос +2. ОСТАНОВИТЬСЯ +3. ЖДАТЬ следующей команды +**ЗАПРЕЩЕНО** начинать работу, писать код, запускать команды — без явного "делай". + +## ⛔⛔⛔ НЕ ТРОГАТЬ РАБОЧИЙ КОД — АБСОЛЮТНЫЙ ЗАПРЕТ НАВСЕГДА + +**НИКАКИХ самодеятельных изменений рабочего кода:** +- Никаких "оптимизаций", "улучшений", "рефакторинга" без команды +- Никаких новых фич без явного разрешения +- Никаких helm upgrade, kubectl patch и прочих инфраструктурных изменений без команды +- Перед ЛЮБЫМ изменением рабочего кода — объяснить ЗАЧЕМ и ждать "делай" + +**ПРЕЦЕДЕНТЫ:** +- v1.3.49: убрали mode toggle в edit modal "для улучшения" → сломали рабочий редактор +- helm upgrade попытка → сломал JWT secret router → 401 у всех функций пользователей + +1. Не трогать рабочий код без явного указания. + +2. Файлы редактируются локально: + ~/lang + + После ЛЮБЫХ изменений ОБЯЗАТЕЛЬНО синхронизировать на ВМ командой: + rsync -az \ + -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \ + ~/lang/ \ + naeel@5.172.178.213:~/terra/lang/ + + +3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/lang +4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ: + ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА' + + - не выполнять инфраструктурные команды локально + - не открывать интерактивные сессии + - не делать цепочки без необходимости + +4. Перед запуском команд ОБЯЗАТЕЛЬНО убедиться, что синхронизация выполнена. + +5. ЗАПРЕЩЕНО: + - откатывать код + - менять версии + - ломать рабочее состояние + +6. После каждого исправления: + - git add/commit ЛОКАЛЬНО + - затем синхронизация (rsync) на ВМ + +7. ~/lang/test-results/ + +## ⛔⛔⛔ ДЕЛАТЬ ТОЛЬКО ЧТО ПРЯМО ПРИКАЗАНО + +**АБСОЛЮТНЫЙ ЗАПРЕТ на додумывание:** +- Не расширять масштаб работы +- Не выполнять "логичные следующие шаги" +- Не инициировать дополнительные операции +- Не делать ничего кроме того что сказано + +**Пример (2026-05-01):** +- Приказано: "собери" +- Сделано: ✓ собрал образы v1.3.17 и v0.1.2 +- СТОП — жду команды дальше +- **ЗАПРЕЩЕНО:** обновлять манифесты, заливать образы, применять на кластер, запускать тесты + +**Исключение:** только если приказ явно включает цепочку ("собери И залей И тесты") diff --git a/.github/pravila.md b/.github/pravila.md new file mode 100644 index 0000000..c054e96 --- /dev/null +++ b/.github/pravila.md @@ -0,0 +1,107 @@ +# Правила работы агента + +## ⛔⛔⛔ DOCKER — ОБЯЗАТЕЛЬНЫЙ ПОРЯДОК ПЕРЕД КАЖДЫМ BUILD + +1. УВЕЛИЧИТЬ ТЕГ в `deploy/lang.yaml` (vX.Y.Z → vX.Y.Z+1) +2. rsync на ВМ +3. ПРОВЕРИТЬ что файлы на ВМ новые (grep ключевой строки) +4. docker build с НОВЫМ тегом +5. docker push с НОВЫМ тегом +6. kubectl apply (не rollout restart — apply подтягивает новый тег) + +**НИКОГДА не делать `docker build` со старым тегом — под не перетянет образ (imagePullPolicy: IfNotPresent)** + +## Файловая система (актуально) + +1. Все файлы редактируются локально: `~/lang` +2. После любых изменений — обязательно rsync на ВМ: + rsync -az \ + -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" \ + ~/lang/ \ + naeel@5.172.178.213:~/terra/lang/ + +3. Git (add/commit/push) выполнять ЛОКАЛЬНО в ~/lang +4. Docker, kubectl и другие инфраструктурные команды — только через SSH на ВМ +5. Перед запуском любой команды на ВМ обязательно убедиться, что синхронизация (rsync) выполнена +6. SCP, sshfs, remote_dev и маунты больше НЕ используются +7. Только rsync для синхронизации + +Пример: +1. Редактируешь локально +2. rsync на ВМ +3. Выполняешь команды через SSH на ВМ + +## SSH + +Все команды — только через SSH на ВМ. Локально — только читать и редактировать файлы. + +```bash +ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА' +``` + +Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, `git push/pull`, любые скрипты проекта. + +## Документация + +- `doc/thinking/` — лог рассуждений агента (обязательно) +- `doc/progress.md` — трекер задач +- Старые файлы `doc/` не перезаписывать — новое в новых файлах с датой + +## Git + +⛔⛔⛔ АБСОЛЮТНОЕ ПРАВИЛО: +- Git — ТОЛЬКО ЛОКАЛЬНО в `~/lang`. НИКОГДА через SSH на VM. +- Разрешены ТОЛЬКО две операции: `git commit` и `git push`. +- ЗАПРЕЩЕНО: git pull, git fetch, git rebase, git merge, git reset, git stash, git checkout — что угодно кроме commit и push. +- Если push отклонён — СТОП, доложить пользователю. Не лезть в pull/merge/rebase самостоятельно. + +Версионирование тегами: `vMAJOR.MINOR.PATCH` +- Patch — любое изменение кода +- Minor — новая фича / компонент +- Major — breaking change + +```bash +git tag vX.Y.Z && git push origin vX.Y.Z +``` + +## ⛔ ТЕРМИНАЛЬНЫЙ БУФЕР — НИКОГДА НЕ ЧИТАТЬ СТАРЫЙ + +**АБСОЛЮТНОЕ ПРАВИЛО:** +- get_terminal_output из старых сессий — МУСОР. Там старые прогоны. +- Всегда запускать новую команду через SSH и читать её вывод напрямую. +- НИКОГДА не читать буфер терминала из предыдущей сессии как актуальные данные. +- Актуальный результат — только из команды, которая была запущена СЕЙЧАС. + +## Документация тест-прогонов — В РЕАЛЬНОМ ВРЕМЕНИ + +**Правила:** +1. Перед запуском `run_all.sh` — создать файл `test-results/YYYY-MM-DD_HH-MM.log` и записать в него метку времени и что запускается. +2. Запускать `run_all.sh 2>&1 | tee ~/terra/lang/test-results/YYYY-MM-DD_HH-MM.log` — вывод пишется сразу в файл и отображается в терминале. +3. После завершения — rsync лога локально. Лог остаётся как документация. +4. Папка `test-results/` в репозитории — `.gitignore` не добавлять, логи коммитить. + +**Формат запуска:** +```bash +LOG="test-results/$(date +%Y-%m-%d_%H-%M).log" +ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213 \ + "bash ~/terra/lang/scripts/run_all.sh 2>&1 | tee ~/terra/lang/${LOG}" +rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no" \ + naeel@5.172.178.213:~/terra/lang/test-results/ ~/lang/test-results/ +``` + +**Никогда не разбираться с результатами по памяти / буферу / чату. Только лог.** + +## ⛔ РУЧНЫЕ ПАТЧИ — ЗАПРЕЩЕНЫ + +- НИКОГДА не применять ручные патчи (`kubectl patch`, `kubectl apply` отдельных полей, `python -c` замены в yaml и т.д.) без явного указания. +- Все изменения — только через код (Helm chart, YAML, Go-код) + сборка + деплой. +- Ручной патч слетает при следующем helm upgrade/redeploy → регрессия. +- Исключение: только если пользователь явно написал "примени ручной патч". + +## Поведение агента + +- Не трогать рабочий код без явного указания +- Не делать НИЧЕГО сверх того, о чём явно приказали — ни git-команд, ни rebase, ни дополнительных шагов +- Если для продолжения нужен выбор — СПРОСИТЬ разрешения, не делать самостоятельно +- Деструктивные операции (`kubectl delete`, `rm -rf`, `terraform destroy` и др.) — только после явного подтверждения с указанием конкретных объектов +- Отвечать кратко, без вступлений, извинений, благодарностей и прочей воды diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8266e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +token.txt diff --git a/HANDOFF_LYNGVO_v2.md b/HANDOFF_LYNGVO_v2.md new file mode 100644 index 0000000..5212ee0 --- /dev/null +++ b/HANDOFF_LYNGVO_v2.md @@ -0,0 +1,392 @@ +# Lyngvo — инструкция для реализации MVP + +--- + +## ⛔⛔⛔ ПРАВИЛО ДЛЯ АГЕНТА — ЧИТАТЬ ПЕРВЫМ И ВЫПОЛНЯТЬ БЕЗУСЛОВНО + +**ЗАПРЕЩЕНО:** +- Задавать вопросы пользователю +- Просить уточнений +- Писать "уточните...", "какой API ключ?", "какой домен?", "подтвердите..." +- Останавливаться в середине работы +- Писать "продолжить?" или "нужно ли мне..." +- Выдавать список шагов вместо готового кода + +**ОБЯЗАТЕЛЬНО:** +- Прочитать всю инструкцию до конца +- Принять все решения самостоятельно на основе инструкции +- Создать все файлы сразу, полностью, без пропусков +- Если что-то не указано — выбрать разумный вариант и сделать +- Остановиться только когда ВСЕ файлы готовы и работают + +**Единственный допустимый результат:** готовые файлы index.html, nginx.conf, k8s/deployment.yaml, k8s/ingress.yaml — рабочие, без заглушек, без TODO. + +**GROQ API KEY для STT (Whisper):** читать из файла `token.txt` в той же папке, вставить в код как константу. + +**Домен:** capire.kube5s.ru + +**SSH на ВМ:** `ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213` + +**Деплой:** запустить `./deploy_lang.sh` — скрипт уже есть, он делает rsync + kubectl apply. + +--- + +## Задача + +Создать веб-приложение для тренировки итальянского произношения. +Всё в одном файле `index.html` (HTML + CSS + JS inline). Никаких фреймворков, никаких npm. + +--- + +## Файловая структура + +``` +/home/naeel/lang/ + index.html ← весь код здесь + nginx.conf ← конфиг для деплоя + k8s/ + deployment.yaml + ingress.yaml +``` + +--- + +## Пользовательский сценарий (строго в этом порядке) + +1. Пользователь вводит итальянское слово/фразу в поле +2. Нажимает **"Воспроизвести эталон"** → браузер синтезирует речь итальянским голосом +3. Нажимает **"Записать"** → идёт запись с микрофона (кнопка меняется на "Стоп") +4. Нажимает **"Стоп"** → запись останавливается +5. Нажимает **"Сравнить"** → запись уходит на Whisper API, приходит транскрипция +6. На экране: + - **Similarity score** (число от 0 до 100%) + - **Diff**: слова оригинала подсвечены зелёным (совпало) / красным (не совпало) +7. Нажимает **"Стерео-воспроизведение"** → левое ухо: TTS-эталон, правое ухо: голос пользователя + +--- + +## Шаг 1 — TTS (Web Speech API) + +```js +function playTTS(text) { + return new Promise((resolve) => { + const utter = new SpeechSynthesisUtterance(text); + utter.lang = 'it-IT'; + + // ОБЯЗАТЕЛЬНО: выбрать итальянский голос явно + const voices = speechSynthesis.getVoices(); + const italianVoice = voices.find(v => v.lang.startsWith('it')); + if (italianVoice) utter.voice = italianVoice; + + utter.onend = resolve; + speechSynthesis.speak(utter); + }); +} + +// ВАЖНО: getVoices() асинхронный при первом вызове. +// Вешать на speechSynthesis.onvoiceschanged или вызывать после события: +speechSynthesis.onvoiceschanged = () => { /* голоса загружены */ }; +``` + +--- + +## Шаг 2 — Запись (MediaRecorder) + +```js +let mediaRecorder; +let audioChunks = []; +let userAudioBlob = null; + +async function startRecording() { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + audioChunks = []; + mediaRecorder = new MediaRecorder(stream); + mediaRecorder.ondataavailable = e => audioChunks.push(e.data); + mediaRecorder.onstop = () => { + userAudioBlob = new Blob(audioChunks, { type: 'audio/webm' }); + // остановить треки микрофона + stream.getTracks().forEach(t => t.stop()); + }; + mediaRecorder.start(); +} + +function stopRecording() { + mediaRecorder.stop(); +} +``` + +--- + +## Шаг 3 — Whisper STT (Groq API) + +**Endpoint:** `https://api.groq.com/openai/v1/audio/transcriptions` +**Model:** `whisper-large-v3` +**Ключ:** вписать в константу `GROQ_API_KEY` в начале JS-кода + +```js +const GROQ_API_KEY = 'ВСТАВИТЬ_КЛЮЧ_ЗДЕСЬ'; + +async function transcribe(blob) { + const formData = new FormData(); + formData.append('file', new File([blob], 'audio.webm', { type: 'audio/webm' })); + formData.append('model', 'whisper-large-v3'); + formData.append('language', 'it'); + + const res = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', { + method: 'POST', + headers: { 'Authorization': `Bearer ${GROQ_API_KEY}` }, + body: formData + }); + const data = await res.json(); + return data.text || ''; +} +``` + +--- + +## Шаг 4 — Сравнение текстов + +```js +function normalize(text) { + return text.toLowerCase().replace(/[.,!?;:«»""'']/g, '').trim(); +} + +function compareTexts(original, transcribed) { + const a = normalize(original).split(/\s+/); + const b = normalize(transcribed).split(/\s+/); + + let matched = 0; + const result = a.map(word => { + const found = b.includes(word); + if (found) matched++; + return { word, ok: found }; + }); + + const score = Math.round((matched / Math.max(a.length, b.length)) * 100); + return { score, words: result }; +} + +// Рендер diff: +function renderDiff(words) { + return words.map(({ word, ok }) => + `${word}` + ).join(' '); +} +``` + +--- + +## Шаг 5 — Стерео-воспроизведение (Web Audio API) + +**Главный нюанс:** Web Speech API нельзя напрямую захватить в AudioBuffer. +Решение: воспроизводить TTS через `audio`-элемент с `captureStream()` и пускать через PannerNode. + +```js +async function playStereo(ttsText, userBlob) { + const ctx = new AudioContext(); + + // --- Правый канал: голос пользователя --- + const userURL = URL.createObjectURL(userBlob); + const userAudio = new Audio(userURL); + const userSource = ctx.createMediaElementSource(userAudio); + const userPan = ctx.createStereoPanner(); + userPan.pan.value = 1; // правое ухо + userSource.connect(userPan).connect(ctx.destination); + + // --- Левый канал: TTS через скрытый audio-элемент --- + // Используем SpeechSynthesisUtterance + MediaStreamDestination + const ttsDestination = ctx.createMediaStreamDestination(); + const ttsAudio = new Audio(); + ttsAudio.srcObject = ttsDestination.stream; + const ttsSource = ctx.createMediaElementSource(ttsAudio); + const ttsPan = ctx.createStereoPanner(); + ttsPan.pan.value = -1; // левое ухо + ttsSource.connect(ttsPan).connect(ctx.destination); + + // АЛЬТЕРНАТИВА (проще и надёжнее если выше не работает в браузере): + // Просто запустить TTS и userAudio одновременно без захвата потока, + // управляя балансом через два отдельных audio-элемента с pan через Web Audio. + // TTS-элемент: pan = -1, user-элемент: pan = +1. + + // Простой вариант (рекомендуется для MVP): + await Promise.all([ + new Promise(res => { + const utter = new SpeechSynthesisUtterance(ttsText); + utter.lang = 'it-IT'; + const voices = speechSynthesis.getVoices(); + const v = voices.find(v => v.lang.startsWith('it')); + if (v) utter.voice = v; + utter.onend = res; + speechSynthesis.speak(utter); + }), + new Promise(res => { + userAudio.onended = res; + userAudio.play(); + }) + ]); +} +``` + +**Если браузер блокирует AudioContext до жеста пользователя** — создавать `ctx` только внутри обработчика клика (не на старте страницы). + +--- + +## UI (минимальный, но понятный) + +```html +
+

🇮🇹 Lyngvo

+ + + +
+ + + + +
+ +
+
+
+
+``` + +--- + +## nginx.conf + +```nginx +server { + listen 80; + server_name lang.kube5s.ru; + root /usr/share/nginx/html; + index index.html; + location / { + try_files $uri $uri/ /index.html; + } +} +``` + +--- + +## k8s/deployment.yaml + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lyngvo + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: lyngvo + template: + metadata: + labels: + app: lyngvo + spec: + containers: + - name: lyngvo + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: html + mountPath: /usr/share/nginx/html + - name: nginx-conf + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + volumes: + - name: html + configMap: + name: lyngvo-html + - name: nginx-conf + configMap: + name: lyngvo-nginx +--- +apiVersion: v1 +kind: Service +metadata: + name: lyngvo + namespace: default +spec: + selector: + app: lyngvo + ports: + - port: 80 + targetPort: 80 +``` + +--- + +## k8s/ingress.yaml + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: lyngvo + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx + tls: + - hosts: + - lang.kube5s.ru + secretName: lyngvo-tls + rules: + - host: lang.kube5s.ru + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lyngvo + port: + number: 80 +``` + +--- + +## Деплой (выполнять на ВМ naeel@5.172.178.213) + +```bash +# 1. Создать ConfigMap из файлов +kubectl create configmap lyngvo-html --from-file=index.html -n default --dry-run=client -o yaml | kubectl apply -f - +kubectl create configmap lyngvo-nginx --from-file=default.conf=nginx.conf -n default --dry-run=client -o yaml | kubectl apply -f - + +# 2. Применить манифесты +kubectl apply -f k8s/deployment.yaml +kubectl apply -f k8s/ingress.yaml + +# 3. После изменения index.html — обновить ConfigMap и рестартовать pod: +kubectl create configmap lyngvo-html --from-file=index.html -n default --dry-run=client -o yaml | kubectl apply -f - +kubectl rollout restart deployment/lyngvo -n default +``` + +**SSH:** `ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no naeel@5.172.178.213` + +--- + +## Что НЕ делать + +- Не использовать React, Vue, webpack, npm +- Не использовать OpenAI Whisper API (дорогой) — только Groq +- Не создавать бэкенд — всё в браузере, Groq вызывается напрямую из JS +- Не добавлять ничего кроме описанного выше — это MVP + +--- + +## Порядок реализации + +1. `index.html` — полный рабочий код (TTS + запись + Whisper + diff + стерео) +2. `nginx.conf` +3. `k8s/deployment.yaml` + `k8s/ingress.yaml` +4. Инструкция по деплою выше diff --git a/check_lang_status.sh b/check_lang_status.sh new file mode 100755 index 0000000..9dcc5fe --- /dev/null +++ b/check_lang_status.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +echo "--- PODS ---" +kubectl get pods -n default | grep lyngvo || true + +echo "\n--- SERVICES ---" +kubectl get svc -n default | grep lyngvo || true + +echo "\n--- INGRESS ---" +kubectl get ingress -n default | grep lyngvo || true + +echo "\n--- POD LOGS (последние 50 строк) ---" +kubectl logs deploy/lyngvo -n default --tail=50 || true + +echo "\n--- DESCRIBE INGRESS ---" +kubectl describe ingress lyngvo -n default || true + +echo "\n--- DESCRIBE SERVICE ---" +kubectl describe svc lyngvo -n default || true diff --git a/deploy_lang.sh b/deploy_lang.sh new file mode 100755 index 0000000..28efabc --- /dev/null +++ b/deploy_lang.sh @@ -0,0 +1,25 @@ +#!/bin/bash +set -e + +# Вклеиваем Groq API ключ в index.html +KEY=$(cat ~/lang/token.txt) +cp ~/lang/index.html /tmp/lyngvo_index.html +sed -i "s|const GROQ_API_KEY = '';|const GROQ_API_KEY = '${KEY}';|" /tmp/lyngvo_index.html + +# Синхронизация всей папки lang на ВМ +rsync -az -e "ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10" ~/lang/ naeel@5.172.178.213:~/terra/lang/ + +# Деплой на ВМ (index.html с ключом — через временный файл) +scp -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 /tmp/lyngvo_index.html naeel@5.172.178.213:~/terra/lang/index.html +ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 ' + cd ~/terra/lang && \ + kubectl create configmap lyngvo-html --from-file=index.html -n default --dry-run=client -o yaml | kubectl apply -f - && \ + kubectl create configmap lyngvo-nginx --from-file=default.conf=nginx.conf -n default --dry-run=client -o yaml | kubectl apply -f - && \ + kubectl apply -f k8s/deployment.yaml && \ + kubectl apply -f k8s/ingress.yaml && \ + kubectl rollout restart deploy/lyngvo -n default && \ + kubectl rollout status deploy/lyngvo -n default --timeout=60s +' + +rm -f /tmp/lyngvo_index.html +echo "\n=== Деплой завершён. Проверяй capire.kube5s.ru ===" diff --git a/index.html b/index.html new file mode 100644 index 0000000..7e47b65 --- /dev/null +++ b/index.html @@ -0,0 +1,556 @@ + + + + + + + + Lyngvo — Итальянский тренажёр произношения + + + + +
+

🇮🇹 Lyngvo

+
тренажёр итальянского произношения · для Google Chrome
+ +
+
+ + + + + +
+
+ +
+
+
+
+
+ + + diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml new file mode 100644 index 0000000..27ae6d3 --- /dev/null +++ b/k8s/deployment.yaml @@ -0,0 +1,45 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lyngvo + namespace: default +spec: + replicas: 1 + selector: + matchLabels: + app: lyngvo + template: + metadata: + labels: + app: lyngvo + spec: + containers: + - name: lyngvo + image: nginx:alpine + ports: + - containerPort: 80 + volumeMounts: + - name: html + mountPath: /usr/share/nginx/html + - name: nginx-conf + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + volumes: + - name: html + configMap: + name: lyngvo-html + - name: nginx-conf + configMap: + name: lyngvo-nginx +--- +apiVersion: v1 +kind: Service +metadata: + name: lyngvo + namespace: default +spec: + selector: + app: lyngvo + ports: + - port: 80 + targetPort: 80 diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml new file mode 100644 index 0000000..6a51557 --- /dev/null +++ b/k8s/ingress.yaml @@ -0,0 +1,28 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: lyngvo + namespace: default + annotations: + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod + nginx.ingress.kubernetes.io/hsts: "true" + nginx.ingress.kubernetes.io/hsts-max-age: "31536000" + nginx.ingress.kubernetes.io/hsts-include-subdomains: "true" +spec: + ingressClassName: nginx + tls: + - hosts: + - capire.kube5s.ru + secretName: capire-tls + rules: + - host: capire.kube5s.ru + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lyngvo + port: + number: 80 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..7f103f5 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,17 @@ +server { + listen 80; + server_name lang.kube5s.ru; + root /usr/share/nginx/html; + index index.html; + + # Агрессивный запрет кеширования — Chrome HSTS/cache bypass + add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0" always; + add_header Pragma "no-cache" always; + add_header Expires "0" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; + + location / { + try_files $uri $uri/ /index.html; + } +}