# Розгортання на VPS

Розгортається **панель обведення областей** (`editor/serve.py`) — єдиний
довгий сервіс проєкту. Решта конвеєра працює разовими прогонами.

## Швидкий старт

```bash
git clone https://github.com/kupolua/bibletime-interactive-pdf.git
cd bibletime-interactive-pdf

cp .env.example .env
python3 -c "import secrets; print('EDITOR_TOKEN=' + secrets.token_urlsafe(24))" >> .env
$EDITOR .env          # прибери порожній EDITOR_TOKEN= згори

docker compose up -d --build
```

Перший старт довший: контейнер рендерить 24 сторінки уроку в PNG (їх немає
в репозиторії — вони відтворювані). Готовність видно так:

```bash
docker compose ps            # health: healthy
docker compose logs -f editor
```

## Як зайти

Типово порт відкрито **лише на 127.0.0.1 самого VPS** — назовні нічого не
світиться. З ноутбука:

```bash
ssh -N -L 8765:127.0.0.1:8765 user@vps
```

і в браузері `http://127.0.0.1:8765/editor/regions.html?token=…` — токен
із `.env`, **разом зі знаком питання**. Без нього панель покаже «каталог не
прочитано»: це 401, а не втрата даних.

### Чому саме тунель, а не відкритий порт

Панель пише файли на диск і запускає підпроцеси — це її робота. Захищає її
один токен, і по звичайному HTTP він їде відкритим текстом. Тунель прибирає
обидві проблеми одразу: трафік шифрує SSH, а порт не видно сканерам.

Якщо доступ із браузера без тунелю все ж потрібен — бери профіль `public`
нижче, але не міняй `ports` на `0.0.0.0`.

## Публічний доступ по HTTPS

Потрібен домен, у якого A-запис указує на VPS.

```bash
# у .env
PUBLIC_HOST=editor.example.com
ACME_EMAIL=ти@пошта

docker compose --profile public up -d
```

Caddy сам візьме й оновлюватиме сертифікат Let's Encrypt. Порт 8765 назовні
не публікується — Caddy ходить у контейнер по внутрішній мережі.

`PUBLIC_HOST` потрапляє ще й у `--allow-host`: `serve.py` звіряє заголовок
`Host` (захист від DNS-rebinding — щоб чужий домен, наведений на твою адресу,
не писав тобі на диск через браузер). Не назвеш домен — дістанеш 403.

## Разові прогони конвеєра

```bash
docker compose run --rm cli python ua_build/fields_detect.py файл.pdf --out data/fields.json
docker compose run --rm cli python ua_build/art_apply.py regions_p16_q2_full.json
```

Оригінали уроків лежать **поза репозиторієм** (`../lessons/`), тож для повного
циклу `extract` → `build` підмонтуй їх сам:

```bash
docker compose run --rm -v /srv/lessons:/lessons cli python ua_build/extract.py Level3_A1
```

## Дані

| Том | Що там | Чому не в образі |
|---|---|---|
| `editor-data` → `/app/editor` | набори областей, PNG сторінок | правки мають пережити перезбірку образу |

`editor/` тримає і дані, і **код** панелі (`regions.html`, `serve.py`). Том
заморозив би код на стані першого запуску, тож образ несе еталонну копію в
`/opt/editor-dist`, і entrypoint щоразу кладе свіжий код поверх тому. Дані
не чіпає. Через це `git pull && docker compose up -d --build` оновлює панель
насправді, а не лише образ.
| `forms-out` → `/app/forms` | PDF, які збирає `art_apply.py` | результат роботи, не код |

Резервна копія:

```bash
docker run --rm -v interactive-pdf_editor-data:/d -v "$PWD":/b alpine \
    tar czf /b/editor-backup.tar.gz -C /d .
```

## Шрифти: що в контейнері не таке, як на маку

Код звертається до шрифтів за macOS-шляхами. У контейнері за тими самими
шляхами лежить **Liberation Sans** — він метрично сумісний з Arial, тобто
кожен гліф тієї самої ширини. Перевірено вимірюванням:

| | mac | контейнер |
|---|---|---|
| Arial, «Біблійна Година» @20pt | 151.00 | 151.00 |
| Arial Bold, те саме | 163.35 | 163.35 |

Верстка рахує `text_length()`, тож збіг тут — не косметика, а умова того, що
рядки лягають на місце.

**Два винятки, де контейнер дасть інший результат:**

* **Arial Black** метрично сумісного відповідника не має — підставлено
  Liberation Sans Bold. Контурний вірш рівня 0 вийде тоншим.
* **SFNSRounded** (заміна SassoonInfantDtB, дитячий заокруглений шрифт) —
  відповідника немає, підставлено звичайний Liberation Sans. Пунктирні написи
  для обведення виглядатимуть інакше.

Обидва потрібні лише при повній збірці уроку, не для панелі. **Фінальні PDF
для друкарні збирай на маку.**

## Обслуговування

```bash
docker compose logs -f editor           # журнал
docker compose restart editor           # перезапуск
git pull && docker compose up -d --build   # оновлення коду
docker compose down                     # зупинити (томи лишаються)
docker compose down -v                  # зупинити І СТЕРТИ дані областей
```

## Якщо не працює

| Симптом | Причина |
|---|---|
| `401` на API, панель «каталог не прочитано» | відкрито без `?token=…` |
| `403` на API | заголовок `Host` не в списку — додай домен у `PUBLIC_HOST` |
| `required variable EDITOR_TOKEN is missing` | немає `.env` або порожній токен |
| токен не збігається, хоча введено правильно | у токені кирилиця — заголовки HTTP її не переносять, лише латиниця й цифри |
| панель без сторінок | не знайдено `ART_SRC`; перевір `docker compose logs editor` |
| «Не вдалося відкрити page_…_v07_…png» | стара версія панелі: вона трималася за жорстко вписане ім'я. Онови (`git pull && docker compose up -d --build`) — тепер сторінка береться з `/api/index`. Обхід без оновлення: додати `?png=page_Level2_UA_form_v06_p16_150dpi.png` |
