Files
pwa-homelab-mon/SPEC.md
T
alexey.bagno 20e1d5a726
Deploy / deploy (push) Successful in 11s
Srv watch
2026-08-31 11:08:45 +05:00

140 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPEC: hub — экран «Сервисы» + экран «Прокси (Caddy)»
> Для агента-разработчика. Проект уже активен: Go API + Vite SPA, живёт под `hub`.
> Репозиторий — в Gitea на боксе, CI через act-runner (host-режим, от пользователя `homelab`).
> Факты окружения ниже проверены 2026-08-30 — менять не нужно, если не сказано иначе.
## 1. Что за проект
Личный хаб-дашборд для телефона (PWA). Три части:
1. **Клиент** — Vite SPA (сборка → `web/dist/`, раздаёт Caddy с SPA-fallback на `/index.html`).
2. **API** — Go, один бинарник `hub-api`, слушает **`127.0.0.1:8484`** (unit задаёт `PORT=8484`).
3. **Раздача/прокси** — Caddy, домен `hub.alexey-homelab.duckdns.org`:
- `/api/*` и `/homelab.v1.HomelabService/*` → `127.0.0.1:8484`
- всё остальное → `~/apps/hub/dist/` (SPA).
Деплой: push в `main` → Gitea Actions (`deploy.workflow.yml`) → `make web server` →
`~/apps/hub/dist/` + `~/apps/hub/hub-api` (атомарный mv!) → `systemctl --user restart hub-api`.
Юнит сервиса: `~/srv/hub-api.service` (user, symlink в `~/.config/systemd/user/`).
Уже есть в конфиге (`~/apps/hub/config.yaml`): `links` (карточки-ссылки), `monitor`
(cpu/disk/mem через скрипты), `exec` (greet). Это работает — не ломай.
## 2. Задача: экран «Сервисы»
Единый список всех сервисов бокса. Источник — **два разных мира**, их нужно объединить
в один список с полем `kind` и иконкой для отличия:
- `kind: native` — бинарники под user-systemd (rathole, OliveTin, hermes-*, hub-api...)
- `kind: docker` — контейнеры Docker, сгруппированные по compose-проектам
### 2.1. Native-сервисы
- Список: парсить **`~/srv/*.service`** (каждый файл = сервис). Часть файлов — symlink'и
на `~/.config/systemd/user/*.service` — это нормально, читай через symlink.
- Статус (машиночитаемо, всегда exit 0, не путай с `status`):
```
systemctl --user show <unit> -p ActiveState,SubState,LoadState,UnitFileState --value
```
- Кнопки: `systemctl --user start|stop|restart <unit>` — **без sudo**, от homelab.
- **Disabled-состояние**: если юнит не найден в systemd (`LoadState=not-found`) — это НЕ ошибка,
пользователь намеренно сделал `systemctl disable` (сейчас так: ntfy, olivetin, pairdrop,
uptime-kuma). Показывай бейдж «выключен» (disabled), серым, без алертов.
### 2.2. Docker-сервисы
- Список: **`docker ps -a`** (не юниты!). Контейнеров 37, юнитов на них нет/мало — источник
истины только Docker.
- Для каждого: `docker inspect -f '{{.State.Running}}|{{.State.Status}}' <name>` +
label проекта `com.docker.compose.project` (через `docker inspect -f '{{index .Config.Labels "com.docker.compose.project"}}'`).
- **Группировка**: контейнеры одного проекта — одна карточка проекта (itsaplan, pullmd,
affine, kurir, minepanel, searxng...), одиночные (gitea, homepage, samba...) — свои карточки.
- **Агрегированный статус проекта**: все рабочие running → «работает»; часть → «частично
(N/M)»; ни одного → «остановлен». Служебные контейнеры (`*-redis`, `*-postgres`,
`*-minio-init-1`, `affine_migration` — они `policy=no`, `running=false`) сворачивать,
не показывать как отдельные карточки. Иконка-отличие docker: 🐳 (или своя, по вкусу UI).
- Кнопки на проект: `docker start|stop <все контейнеры проекта>` (без sudo — homelab в группе docker).
- **Семантика stop**: `docker stop` + policy `unless-stopped` = контейнер не поднимется при
ребуте (docker считает его намеренно остановленным). Это ожидаемое поведение кнопки
«остановить до ребута» — задокументируй в UI тултипом, не «чини».
### 2.3. Редактирование unit-файла в браузере
- GET/PUT `~/srv/<name>.service` (write — через symlink в реальный юнит, ок).
- **После сохранения — обязательно `systemctl --user daemon-reload`**, иначе правки не применятся.
- ⚠️ Валидация имени сервиса: только `^[a-z0-9_.-]+$` — защита от path traversal.
- Предупреждение в UI: правка unit = смена ExecStart = выполнение произвольных команд;
restart для docker-обёрток (`oneshot` + `RemainAfterExit`) = stop+start, а не restart.
## 3. Задача: экран «Прокси» (Caddy)
Минимум: отдельная страница со списком доменов и возможностью **руками редактировать конфиг**.
Идеал: на странице сервиса поле «домен» — но это позже, начни с отдельной страницы.
**Как устроен Caddy на боксе (не менять схему!):**
- Рабочая копия конфига: **`~/caddy-sync/Caddyfile`** — её и правим (она в homelab-зоне, writable).
- Применение (NOPASSWD sudo уже настроен):
```
sudo -n /usr/local/sbin/caddy-sync # validate + копия в /etc/caddy/Caddyfile + backup
sudo -n /usr/local/sbin/caddy-reload # validate + reload + автоген ~/www/util/domains
sudo -n /usr/local/sbin/hosts-sync # /etc/hosts; ОБЯЗАТЕЛЕН при новом duckdns-домене
```
- Read: `sudo -n /usr/local/sbin/caddy-cat` (весь Caddyfile) или admin API
`http://127.0.0.1:2019/config/` (машиночитаемо, список хостов).
- **Запрещено трогать**: секцию ab-hl (внешний выход на VPS), rathole-туннели, knot-resolver.
Внешний VPS-выход — отдельная будущая задача.
**UI страницы «Прокси»:**
- Таблица сайтов: домен → куда проксируется (или file_server) → статус (из списка выше).
- Кнопка «Редактировать» → редактор (textarea/monaco-лайт) с **рабочей копией** `~/caddy-sync/Caddyfile`.
- Кнопка «Применить» → `caddy-sync` → `caddy-reload` → `hosts-sync`, показать вывод каждой
команды (это твой «результат валидации» — caddy-sync сам вернёт REJECT при ошибке).
- После успеха — обновить таблицу.
## 4. Контракт API (добавить в Go-бэкенд)
Предлагаемые эндпоинты (паттерн REST, JSON; auth — как уже сделано в проекте):
| Метод | Путь | Описание |
|---|---|---|
| GET | `/api/services` | список: `[{name, kind, group, status, substate, containers?}]` |
| POST | `/api/services/{name}/start` | native: systemctl start; docker: docker start (проект) |
| POST | `/api/services/{name}/stop` | аналогично |
| POST | `/api/services/{name}/restart` | native: restart; docker: stop+start |
| GET | `/api/services/{name}/file` | содержимое `~/srv/<name>.service` |
| PUT | `/api/services/{name}/file` | сохранить + `daemon-reload` |
| GET | `/api/caddy` | список сайтов + рабочий конфиг |
| PUT | `/api/caddy` | сохранить рабочий конфиг |
| POST | `/api/caddy/apply` | sync → reload → hosts-sync, вернуть вывод |
Имя `{name}` всегда валидировать: `^[a-z0-9_.-]+$`. Команды — фиксированные, без
интерполяции пользовательского ввода в shell (используй `exec.Command` с аргументами,
не shell-строку).
## 5. Критерии приёмки
1. Экран «Сервисы» показывает единый список: native из `~/srv` + docker-проекты из `docker ps -a`,
с иконками 🐳/⚙️ и агрегированными статусами.
2. Кнопки start/stop/restart работают для обоих видов без sudo; disabled-юниты показываются
серым как «выключен», не как ошибка.
3. Редактирование unit-файла сохраняет файл и делает daemon-reload; кривое имя сервиса не
проходит валидацию.
4. Экран «Прокси»: таблица доменов, редактор рабочей копии `~/caddy-sync/Caddyfile`,
«Применить» выполняет sync/reload/hosts-sync и показывает вывод; секция ab-hl не меняется.
5. Всё работает через существующий auth, без новых открытых портов; PWA-обновление
подхватывается на iOS (skipWaiting + clientsClaim, как в дизайн-доке).
6. Сборка/деплой проходят через существующий CI (`deploy.workflow.yml`), юнит `hub-api` переживает рестарт.
## 6. Справочные факты бокса (не перепроверять)
- Все сервисы живут от пользователя `homelab`; user-systemd: `systemctl --user` (XDG_RUNTIME_DIR
нужен только из root-сессий).
- `~/srv/README.md` — документация списка сервисов; обновить её, если меняется формат.
- Порт API: **8484** (не менять — Caddy смотрит туда).
- Сборка Go: `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build` (тулчейн на боксе есть,
`~/apps/go/bin` в PATH CI).
- Фронт: Vite (node 22 / pnpm 11 в `~/.local/bin`).
- Сейчас в `~/srv` 26 юнитов: 18 docker-обёрток, 9 native (act-runner, hermes-dashboard,
hermes-gateway, hub-api, itsaplan-runner, olivetin, rathole, rathole-status, ttyd-rathole),
4 disabled (ntfy, olivetin, pairdrop, uptime-kuma — LoadState=not-found, это норма).