@@ -0,0 +1,139 @@
|
||||
# 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, это норма).
|
||||
Reference in New Issue
Block a user