Развёртывание на Debian 12/13: Docker, nginx, работа за обратным прокси
- приложение перенесено в public/, инструкции в docs/ - Dockerfile (nginx без root-прав), docker-compose.yml с ограничениями - deploy/split-trainer.conf: CSP и заголовки безопасности, no-cache по ETag, /healthz, реальный IP из X-Forwarded-For только от частных сетей, редиректы без внутреннего хоста - deploy/install-debian.sh: установка и обновление без Docker - docs/DEPLOY.md: требования, установка, примеры прокси (nginx, Caddy, Traefik), подпуть Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
42b90a056c
commit
e897a6c5eb
@@ -0,0 +1,7 @@
|
||||
.git
|
||||
.claude
|
||||
.gitignore
|
||||
.gitattributes
|
||||
*.md
|
||||
*.vil
|
||||
docs
|
||||
@@ -0,0 +1,7 @@
|
||||
# Файлы для сервера обязаны быть с LF, даже если репозиторий клонируют на Windows
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.conf text eol=lf
|
||||
*.yml text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
.dockerignore text eol=lf
|
||||
@@ -0,0 +1,9 @@
|
||||
# Сплит-тренер: статический сайт на nginx без root-прав (слушает 8080).
|
||||
FROM nginxinc/nginx-unprivileged:1.28-alpine
|
||||
|
||||
COPY deploy/split-trainer.conf /etc/nginx/conf.d/default.conf
|
||||
COPY public/ /usr/share/nginx/html/
|
||||
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD wget -q --spider http://127.0.0.1:8080/healthz || exit 1
|
||||
@@ -3,14 +3,24 @@
|
||||
Тренажёр слепой печати для сплит-клавиатур: русская и английская раскладки, цифры, знаки препинания, статистика,
|
||||
чтение раскладки Vial (.vil) и упражнения на переключение слоёв.
|
||||
|
||||
Без сборки и зависимостей: откройте `index.html` в браузере.
|
||||
Без сборки и зависимостей: статический сайт.
|
||||
|
||||
Подробно: [ИНСТРУКЦИЯ.md](ИНСТРУКЦИЯ.md).
|
||||
## Быстрый старт
|
||||
|
||||
## Файлы
|
||||
- **Локально:** откройте `public/index.html` в браузере.
|
||||
- **На сервере (Debian 12/13), в том числе за обратным прокси:**
|
||||
|
||||
- `index.html` - запуск
|
||||
- `app.js` - логика приложения
|
||||
- `vial.js` - разбор файла Vial и маршруты по слоям
|
||||
- `data.js` - раскладки, слова и тексты упражнений
|
||||
- `styles.css` - оформление
|
||||
```bash
|
||||
docker compose up -d --build # или: sudo ./deploy/install-debian.sh
|
||||
```
|
||||
|
||||
Требования, настройка прокси, обновление: [docs/DEPLOY.md](docs/DEPLOY.md).
|
||||
|
||||
Как пользоваться: [docs/ИНСТРУКЦИЯ.md](docs/ИНСТРУКЦИЯ.md).
|
||||
|
||||
## Структура
|
||||
|
||||
- `public/` - сайт: `index.html`, `app.js` (логика), `vial.js` (разбор Vial и маршруты по слоям), `data.js` (раскладки, слова, тексты), `styles.css`
|
||||
- `deploy/` - конфиг nginx и скрипт установки для Debian
|
||||
- `Dockerfile`, `docker-compose.yml` - запуск в контейнере
|
||||
- `docs/` - инструкции
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
# Установка и обновление Сплит-тренера на Debian 12/13: nginx + статические файлы.
|
||||
#
|
||||
# sudo ./deploy/install-debian.sh # за обратным прокси: слушаем 127.0.0.1:8080
|
||||
# sudo LISTEN=80 DOMAIN=trainer.example.com ./deploy/install-debian.sh # без прокси, напрямую на порту 80
|
||||
#
|
||||
# Повторный запуск обновляет файлы сайта и конфиг. Ничего, кроме этого сайта, скрипт не трогает.
|
||||
set -euo pipefail
|
||||
|
||||
LISTEN="${LISTEN:-127.0.0.1:8080}" # адрес:порт или просто порт
|
||||
DOMAIN="${DOMAIN:-_}" # server_name; "_" = любое имя
|
||||
WEBROOT="${WEBROOT:-/var/www/split-trainer}"
|
||||
NAME="split-trainer"
|
||||
|
||||
die() { echo "Ошибка: $*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "запустите от root: sudo $0"
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SRC="$HERE/../public"
|
||||
CONF_TEMPLATE="$HERE/$NAME.conf"
|
||||
[ -f "$SRC/index.html" ] || die "не найден $SRC/index.html: запускайте скрипт из клонированного репозитория"
|
||||
[ -f "$CONF_TEMPLATE" ] || die "не найден $CONF_TEMPLATE"
|
||||
|
||||
# Значения подставляются в sed: не пускаем туда ничего, кроме безопасных символов
|
||||
[[ "$LISTEN" =~ ^[][0-9A-Za-z.:_-]+$ ]] || die "некорректный LISTEN: $LISTEN"
|
||||
[[ "$DOMAIN" =~ ^[0-9A-Za-z._*-]+( [0-9A-Za-z._*-]+)*$ ]] || die "некорректный DOMAIN: $DOMAIN"
|
||||
[[ "$WEBROOT" =~ ^/[0-9A-Za-z._/-]+$ ]] || die "некорректный WEBROOT: $WEBROOT"
|
||||
|
||||
. /etc/os-release
|
||||
if [ "${ID:-}" != "debian" ] || { [ "${VERSION_ID:-}" != "12" ] && [ "${VERSION_ID:-}" != "13" ]; }; then
|
||||
[ "${FORCE:-0}" = "1" ] || die "скрипт рассчитан на Debian 12/13 (у вас: ${PRETTY_NAME:-неизвестно}). Чтобы всё равно продолжить: FORCE=1"
|
||||
fi
|
||||
|
||||
echo "==> Устанавливаю nginx"
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq --no-install-recommends nginx >/dev/null
|
||||
|
||||
echo "==> Копирую сайт в $WEBROOT"
|
||||
install -d -m 755 "$WEBROOT"
|
||||
find "$WEBROOT" -mindepth 1 -maxdepth 1 -type f -delete # убираем только файлы прошлой версии
|
||||
install -m 644 "$SRC"/* "$WEBROOT"/
|
||||
|
||||
echo "==> Настраиваю nginx (listen $LISTEN, server_name $DOMAIN)"
|
||||
CONF="/etc/nginx/sites-available/$NAME.conf"
|
||||
sed -e "s|^\(\s*\)listen 8080;.*|\1listen $LISTEN;|" \
|
||||
-e "s|^\(\s*\)server_name _;.*|\1server_name $DOMAIN;|" \
|
||||
-e "s|^\(\s*\)root /usr/share/nginx/html;.*|\1root $WEBROOT;|" \
|
||||
"$CONF_TEMPLATE" > "$CONF"
|
||||
ln -sf "$CONF" "/etc/nginx/sites-enabled/$NAME.conf"
|
||||
|
||||
nginx -t
|
||||
systemctl enable --now nginx >/dev/null 2>&1
|
||||
systemctl reload nginx
|
||||
|
||||
# Проверяем, что сайт действительно отвечает
|
||||
HOSTPORT="$LISTEN"; [[ "$HOSTPORT" =~ ^[0-9]+$ ]] && HOSTPORT="127.0.0.1:$HOSTPORT"
|
||||
[ "${HOSTPORT%%:*}" = "0.0.0.0" ] && HOSTPORT="127.0.0.1:${HOSTPORT##*:}"
|
||||
if command -v curl >/dev/null 2>&1 && ! curl -fsS -m 5 -o /dev/null "http://$HOSTPORT/healthz" -H "Host: ${DOMAIN%% *}"; then
|
||||
echo "Предупреждение: проверка http://$HOSTPORT/healthz не прошла. Если порт 80 занят сайтом по умолчанию, см. docs/DEPLOY.md" >&2
|
||||
fi
|
||||
|
||||
echo "==> Готово: http://$HOSTPORT/"
|
||||
@@ -0,0 +1,58 @@
|
||||
# Сплит-тренер: статический сайт. Один и тот же файл годится и для контейнера, и для nginx на хосте.
|
||||
# Скрипт deploy/install-debian.sh заменяет ровно три строки с пометкой "# <-": listen, server_name и root.
|
||||
# Совместимо с nginx 1.22 (Debian 12) и 1.26 (Debian 13).
|
||||
|
||||
server {
|
||||
listen 8080; # <- порт или адрес:порт
|
||||
server_name _; # <- имя сайта
|
||||
root /usr/share/nginx/html; # <- каталог с файлами
|
||||
index index.html;
|
||||
|
||||
server_tokens off;
|
||||
|
||||
# За обратным прокси редиректы не должны подставлять внутренний хост и порт
|
||||
absolute_redirect off;
|
||||
port_in_redirect off;
|
||||
|
||||
# Настоящий адрес клиента (для логов) берём из X-Forwarded-For, но только от прокси из частных сетей и localhost
|
||||
set_real_ip_from 127.0.0.1;
|
||||
set_real_ip_from ::1;
|
||||
set_real_ip_from 10.0.0.0/8;
|
||||
set_real_ip_from 172.16.0.0/12;
|
||||
set_real_ip_from 192.168.0.0/16;
|
||||
set_real_ip_from fc00::/7;
|
||||
real_ip_header X-Forwarded-For;
|
||||
real_ip_recursive on;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 512;
|
||||
gzip_types text/css application/javascript text/javascript text/plain application/json image/svg+xml;
|
||||
|
||||
# Проверка живости для мониторинга и healthcheck контейнера
|
||||
location = /healthz {
|
||||
access_log off;
|
||||
default_type text/plain;
|
||||
return 200 "ok\n";
|
||||
}
|
||||
|
||||
# Все заголовки безопасности заданы в одном location: add_header не наследуется между уровнями.
|
||||
# no-cache не запрещает кеш, а заставляет браузер сверять версию (ETag) при каждом открытии:
|
||||
# после обновления никто не застрянет на старом app.js.
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
|
||||
add_header Cache-Control "no-cache" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;
|
||||
add_header Content-Security-Policy "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'self'" always;
|
||||
# HSTS не ставим здесь: HTTPS завершает внешний прокси, он и должен отдавать Strict-Transport-Security.
|
||||
}
|
||||
|
||||
# Скрытые файлы (.git, .env и т. п.) не отдаём никогда
|
||||
location ~ /\. {
|
||||
return 404;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
# Запуск: docker compose up -d --build
|
||||
# По умолчанию порт открыт только на localhost: сайт предназначен для работы за обратным прокси.
|
||||
# BIND=0.0.0.0 откроет его наружу, PORT=9000 сменит порт (переменные можно положить в файл .env рядом).
|
||||
services:
|
||||
split-trainer:
|
||||
build: .
|
||||
image: split-trainer:latest
|
||||
container_name: split-trainer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${BIND:-127.0.0.1}:${PORT:-8080}:8080"
|
||||
read_only: true
|
||||
tmpfs:
|
||||
- /tmp
|
||||
cap_drop:
|
||||
- ALL
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
mem_limit: 64m
|
||||
pids_limit: 64
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "5m"
|
||||
max-file: "3"
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
# Развёртывание на своём сервере (Debian 12 / 13)
|
||||
|
||||
Сплит-тренер состоит из статических файлов (HTML, CSS, JS) и ничего не хранит на сервере. Нет бэкенда, базы данных, куки и обращений к внешним сайтам. Сервер только отдаёт файлы, поэтому требования минимальные.
|
||||
|
||||
## 1. Минимальные требования
|
||||
|
||||
### Сервер
|
||||
|
||||
| | Минимум | Комментарий |
|
||||
|---|---|---|
|
||||
| ОС | Debian 12 (bookworm) или Debian 13 (trixie), amd64 или arm64 | |
|
||||
| CPU / RAM | 1 vCPU, 128 МБ свободной памяти | nginx в простое занимает 5–10 МБ; для контейнера задан лимит 64 МБ |
|
||||
| Диск | порядка 500 МБ с Docker Engine (сам образ занимает несколько десятков МБ) или ~10 МБ с nginx из пакетов | сам сайт меньше 1 МБ |
|
||||
| Сеть | один TCP-порт; по умолчанию `127.0.0.1:8080` | наружу выставляет обратный прокси |
|
||||
| Исходящий доступ | только на время установки (apt или docker pull) | во время работы сайту интернет не нужен |
|
||||
| ПО (вариант A) | Docker Engine 24+ с плагином Compose v2 | рекомендуется официальный репозиторий: docs.docker.com/engine/install/debian |
|
||||
| ПО (вариант B) | nginx 1.22+ из пакетов Debian (в Debian 12 это 1.22, в 13 это 1.26) | ставится скриптом |
|
||||
| Права | root или sudo на время установки | сайт работает без root-прав |
|
||||
|
||||
Нужно выбрать один вариант: A или B.
|
||||
|
||||
### Домен и HTTPS
|
||||
- Для работы сайта HTTPS **не обязателен**: приложение не использует API, требующие защищённого контекста.
|
||||
- Но на публичном адресе HTTPS нужен: его даёт ваш обратный прокси (раздел 4).
|
||||
|
||||
### Браузеры пользователей
|
||||
Современные: Chrome и Edge 111+, Firefox 113+, Safari 16.2+ (примерно 2023 год и новее). Для работы нужен включённый JavaScript и `localStorage`.
|
||||
|
||||
## 2. Что важно знать про данные
|
||||
|
||||
- Прогресс и статистика хранятся **в браузере пользователя** (`localStorage`), на сервер ничего не отправляется. Резервировать на сервере нечего.
|
||||
- Данные привязаны к адресу сайта (схема + домен + порт). Если сменить домен, пользователи начнут «с нуля»; смена пути (`/trainer/`) прогресс не затрагивает.
|
||||
- Файл раскладки Vial (.vil) читается локально в браузере и никуда не загружается.
|
||||
|
||||
## 3. Установка
|
||||
|
||||
Склонируйте репозиторий на сервер:
|
||||
|
||||
```bash
|
||||
git clone https://gittea.khad1997pav.ru/khadzhibekovpa/split-trainer.git
|
||||
cd split-trainer
|
||||
```
|
||||
|
||||
### Вариант A: Docker (рекомендуется)
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
curl -i http://127.0.0.1:8080/healthz # должно ответить 200 и "ok"
|
||||
```
|
||||
|
||||
- Порт по умолчанию открыт **только на localhost**. Изменить: файл `.env` рядом с `docker-compose.yml`, например `PORT=9000` и/или `BIND=0.0.0.0` (откроет наружу: тогда закройте порт файрволом для всех, кроме прокси).
|
||||
- Контейнер работает без root-прав, с файловой системой «только чтение», без лишних возможностей ядра и с лимитом памяти.
|
||||
- Если прокси тоже в Docker, уберите блок `ports:` и подключите сервис к общей сети прокси: он слушает `8080` внутри контейнера.
|
||||
|
||||
### Вариант B: nginx из пакетов Debian
|
||||
|
||||
```bash
|
||||
sudo ./deploy/install-debian.sh
|
||||
curl -i http://127.0.0.1:8080/healthz
|
||||
```
|
||||
|
||||
Скрипт ставит nginx, копирует файлы в `/var/www/split-trainer` и подключает отдельный конфиг `split-trainer.conf`. Чужие сайты и настройки он не трогает.
|
||||
|
||||
Параметры (переменные окружения):
|
||||
|
||||
| Переменная | По умолчанию | Назначение |
|
||||
|---|---|---|
|
||||
| `LISTEN` | `127.0.0.1:8080` | адрес и порт (или только порт) |
|
||||
| `DOMAIN` | `_` | `server_name`, можно несколько через пробел |
|
||||
| `WEBROOT` | `/var/www/split-trainer` | куда копировать файлы |
|
||||
| `FORCE` | `0` | `1`: разрешить запуск не на Debian 12/13 |
|
||||
|
||||
Без прокси, прямо на порту 80:
|
||||
|
||||
```bash
|
||||
sudo LISTEN=80 DOMAIN=trainer.example.com ./deploy/install-debian.sh
|
||||
```
|
||||
|
||||
Если порт 80 уже занят «сайтом по умолчанию» nginx и вы не задали `DOMAIN`, будет открываться страница «Welcome to nginx»: укажите домен или отключите сайт по умолчанию (`sudo rm /etc/nginx/sites-enabled/default && sudo systemctl reload nginx`).
|
||||
|
||||
### Обновление
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker compose up -d --build # вариант A
|
||||
# или
|
||||
sudo ./deploy/install-debian.sh # вариант B
|
||||
```
|
||||
|
||||
Браузеры подхватят новую версию сами: сайт отдаётся с `Cache-Control: no-cache`, то есть при каждом открытии версия сверяется по ETag.
|
||||
|
||||
## 4. За обратным прокси
|
||||
|
||||
От прокси ничего особенного не требуется: сайт статический, без WebSocket, без нестандартных заголовков, все ссылки внутри относительные. Достаточно передать запрос на `127.0.0.1:8080`.
|
||||
|
||||
### nginx
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name trainer.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/trainer.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/trainer.example.com/privkey.pem;
|
||||
|
||||
# HTTPS завершается здесь, поэтому HSTS должен ставить именно прокси
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Caddy
|
||||
|
||||
```
|
||||
trainer.example.com {
|
||||
reverse_proxy 127.0.0.1:8080
|
||||
}
|
||||
```
|
||||
|
||||
HTTPS-сертификат Caddy получит сам.
|
||||
|
||||
### Traefik (Docker, метки в `docker-compose.override.yml`)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
split-trainer:
|
||||
ports: !reset []
|
||||
networks: [traefik]
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.trainer.rule=Host(`trainer.example.com`)
|
||||
- traefik.http.routers.trainer.tls.certresolver=letsencrypt
|
||||
- traefik.http.services.trainer.loadbalancer.server.port=8080
|
||||
networks:
|
||||
traefik:
|
||||
external: true
|
||||
```
|
||||
|
||||
Имена сети и резолвера замените на свои. Тег `!reset` требует Docker Compose 2.24 или новее.
|
||||
|
||||
### Под подпутём (`https://example.com/trainer/`)
|
||||
|
||||
Приложение можно отдавать из подкаталога: все пути в нём относительные, свой конфиг менять не нужно. Прокси должен убрать префикс (завершающий `/` в `proxy_pass` обязателен):
|
||||
|
||||
```nginx
|
||||
location = /trainer { return 301 /trainer/; }
|
||||
location /trainer/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; }
|
||||
```
|
||||
|
||||
Для Caddy:
|
||||
|
||||
```
|
||||
example.com {
|
||||
redir /trainer /trainer/
|
||||
handle_path /trainer/* { reverse_proxy 127.0.0.1:8080 }
|
||||
}
|
||||
```
|
||||
|
||||
### Настоящий IP клиента в логах
|
||||
|
||||
Конфиг доверяет заголовку `X-Forwarded-For` только от прокси из частных сетей и localhost (`127.0.0.0/8`, `10/8`, `172.16/12`, `192.168/16`). Если ваш прокси стоит на другом сервере с публичным адресом, добавьте его в `set_real_ip_from` в `deploy/split-trainer.conf` и пересоберите или переустановите.
|
||||
|
||||
## 5. Что отдаёт сервер
|
||||
|
||||
- `GET /healthz` отвечает `200 ok` (для мониторинга и healthcheck).
|
||||
- Заголовки безопасности: строгий `Content-Security-Policy` (только свои скрипты и стили, никаких внешних адресов), `X-Content-Type-Options`, `Referrer-Policy: no-referrer`, `X-Frame-Options`, `Permissions-Policy`.
|
||||
- Файлы сжимаются gzip, скрытые файлы (`.git`, `.env`) не отдаются, версия nginx в ответах скрыта.
|
||||
|
||||
## 6. Если что-то не работает
|
||||
|
||||
| Симптом | Причина и решение |
|
||||
|---|---|
|
||||
| 502 / 504 от прокси | Сайт не запущен или слушает другой порт: `docker compose ps`, `curl http://127.0.0.1:8080/healthz` |
|
||||
| Белая страница под `/trainer/` | Нет завершающего `/` в `proxy_pass` или нет редиректа `/trainer` на `/trainer/` (см. выше) |
|
||||
| После обновления видна старая версия | Обновите страницу с Ctrl+F5; если стоит CDN или кеш на прокси, исключите сайт из кеширования |
|
||||
| Прогресс пропал после смены адреса | `localStorage` привязан к адресу сайта (схема + домен + порт); вернуть старый адрес |
|
||||
| `nginx -t` ругается (вариант B) | Покажите вывод `sudo nginx -t`; чаще всего порт занят другим сайтом |
|
||||
| Нужно изменить текст упражнений | Правьте `public/data.js` и обновите сайт командой из раздела «Обновление» |
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
## 1. Запуск
|
||||
|
||||
1. Откройте файл `index.html` двойным кликом (лучше в Chrome или Edge).
|
||||
1. Откройте файл `public/index.html` двойным кликом (лучше в Chrome или Edge). Если приложение установлено на сервере, откройте его адрес в браузере.
|
||||
2. Ничего устанавливать не нужно. Интернет не нужен.
|
||||
|
||||
Прогресс и статистика хранятся в самом браузере. Не переносите папку с приложением в другое место и не меняйте браузер, иначе прогресс «потеряется» (он привязан к браузеру и адресу файла).
|
||||
Прогресс и статистика хранятся в самом браузере. Не переносите папку с приложением в другое место и не меняйте браузер или адрес сайта, иначе прогресс «потеряется» (он привязан к браузеру и адресу).
|
||||
|
||||
## 2. Главное правило: раскладка Windows
|
||||
|
||||
@@ -110,4 +110,4 @@
|
||||
|
||||
## 9. Файлы приложения
|
||||
|
||||
`index.html` (запуск), `app.js` (логика), `vial.js` (чтение Vial и маршруты по слоям), `data.js` (раскладки, слова и тексты упражнений), `styles.css` (оформление). Слова и предложения для упражнений можно править в `data.js`.
|
||||
В папке `public/`: `index.html` (запуск), `app.js` (логика), `vial.js` (чтение Vial и маршруты по слоям), `data.js` (раскладки, слова и тексты упражнений), `styles.css` (оформление). Слова и предложения для упражнений можно править в `data.js`. Установка на сервер описана в `docs/DEPLOY.md`.
|
||||
Reference in New Issue
Block a user