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

11 KiB
Raw Permalink Blame History

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, это норма).