9.5 KiB
AGENTS.md
PWA для менеджмента homelab: монорепозиторий Go-сервер + SPA-фронт, общаются через ConnectRPC. Таргет — iPhone «На экран Домой» (standalone PWA).
Стек
- Сервер: Go + echo +
connectrpc.com/connect. Отдаёт RPC на/homelab.v1.HomelabService/*и статику изdist/(SPA-fallback включён). Порт из envPORT(дефолт:8080). - Фронт: Preact + Vite + TypeScript, daisyUI v5 (tailwind v4), lucide-preact. Клиент —
@connectrpc/connect-web, same-origin transport, в dev — прокси на :8080. - Контракт:
proto/homelab/v1/homelab.proto→ кодоген вgen/(Go) иweb/src/gen/(TS). Сгенерённый код коммитится, buf CLI не используется — только protoc. - Конфиг:
config.yaml(секции → items → bash-скрипты). Мониторы гоняются по таймеру на сервере, response-скрипты запускаются по кнопке с аргументами.
Структура
proto/homelab/v1/ контракт (единственный источник правды)
gen/ сгенерённый Go-код (коммитим)
server/main.go весь сервер: конфиг, мониторы, RPC, статика, docs API, services API (systemd + docker)
web/src/App.tsx layout, топбар (desktop) + dock (mobile), 5 хардкод-табов, поллинг ListContent (3s)
web/src/components/ MonitorCard, ScriptCard, ServiceCard, Docs (ридер + textarea-редактор)
web/src/docsStore.ts localStorage доков (hub:doc:*), dirty-флаги, syncDocs: pull по хэшам + flush раз в 5s
web/src/gen/ сгенерированный TS-клиент
web/public/ PWA-обвязка: manifest, sw.js, иконки
scripts/ bash-скрипты, пути из config.yaml
docs/ markdown-доки, НЕ в репо — живут только на сервере рядом с бинарьём
Категории
Хардкод в бандле (App.tsx CATEGORIES): Links (из config links), Monitor (config monitor), Exec (config exec), Docs (папка docs/, API ListDocs/GetDoc/SaveDoc), Services. Оффлайн: контент-стейт держит последний ответ, доки — localStorage; правки в оффлайне помечаются dirty и флашатся на сервер каждые 5s (client authority — последняя запись побеждает, без base-hash check).
Services (systemd + docker)
Единый список карточек, едет в ответе ListContent (repeated Service, поле services) — своего RPC-поллинга нет.
- systemd (
kind: SYSTEMD): юниты изsrvDir—*.service, имя валидируется regex^[a-z0-9_.-]+$. Путь: envSRV_DIR, иначе~/srv(на боксе юниты НЕ рядом с WorkingDirectory). Статус:systemctl --user show <unit> -p ActiveState,SubState,LoadState,UnitFileState— парсить ТОЛЬКО по ключамKey=Value(вывод не в порядке -p!).LoadState=not-found= юнит выключен юзером намеренно, не ошибка. - docker (
kind: DOCKER):docker ps -aq+docker inspect -f(labels проекта, status, restart policy). Группировка поcom.docker.compose.project, без лейбла — своя карточка по имени контейнера. Агрегат: все running → active, ни одного → inactive, иначе partial «up/total». One-shot хелперы (policy=no+ не running: init/migrations) исключаются из агрегата и из кнопок. - Кнопки: RPC
ServiceAction(name, kind, op)— systemd →systemctl --user start|stop|restart; docker →docker start|stop|restartнад нужными контейнерами проекта (start — только остановленные, stop/restart — только запущенные). - Status-вывод: RPC
ServiceInfo(name, kind)→systemctl status --no-pager -n 0(exit 3 у остановленного — валидный вывод!) илиdocker ps -aпо label проекта (пусто → fallback по имени). Показывается в модалке по кнопке ⓘ (ServiceCard). - Десктоп: секция Services рендерится шире остальных (
max-w-5xl), карточки в grid 2–3 колонки.
Команды (всё через make)
make dev— сервер :8080 + vite :5173 (работать на http://localhost:5173)make dev-srv/make dev-web— по отдельности в два терминалаmake proto— кодоген Go+TS из protomake web— сборка фронта вdist/(tsc --noEmit + vite build;tsc— линтер, падение = ошибка типов)make server— Go-бинарьbin/hub-apiMOCK=1 make dev-srv— mock-режим: секция Services отдает фикстуры (все виды статусов), кнопки и info-модалка работают по ним; реальных systemd/docker-проб нетmake test— smoke-тест RPC (поднимает сервер на :8080, дергает curl'ом)
Никаких других способов сборки нет: CI делает ровно make web server.
Деплой (CI)
.gitea/workflows/deploy.yml: push в master → checkout → make web server → cp на бокс → systemctl --user restart hub-api.
На боксе юнит должен иметь WorkingDirectory=~/apps/hub — сервер ищет dist/, config.yaml, scripts/ относительно cwd, юниты сервисов — в ~/srv (env SRV_DIR переопределяет). Локально всё то же самое из корня репо.
Конвенции
- Один корень статики:
dist/. Не вводить fallback-ов (web/distне существует послеmake web— vite собирает вweb/dist, make копирует в./dist). int64в proto →bigintв TS (неnumber!).- Поля protobuf в TS — camelCase (
arg_types→argTypes). - Новый item-тип или поле в yaml = обновить proto + валидацию в
loadConfig+ компонент на фронте. - Иконки: хардкод в компонентах из lucide-preact поимённо (не тянуть весь lucide через
icons— раздувает бандл в 5 раз). - Маркдаун:
marked(~10KB gzip) + dangerouslySetInnerHTML без санитайзера (контент свой). Если редактор перестанет нравиться — переход на MDXEditor ( agreed upgrade path, изолирован в компоненте Docs). - index.html и sw.js отдаются с
Cache-Control: no-cache— иначе iOS не подхватит новый бандл. - Логи сервера: monitors, RunScript и ServiceAction/ServiceInfo пишут в stdout; ошибки проб сервисов (
probe <key>: ...) логируются один раз на изменение (появление/восстановление), не на каждый 3s тик;ListContent(поллинг-шум) исключён из HTTP-лога echo. - SW:
web/public/sw.js, версия кэшаhomelab-shell-vN— бампить при изменении офлайн-логики, иначе iOS не обновит. - Тесты:
make test(smoke RPC) +tsc --noEmitвнутриmake web. Не плодить фреймворки. - После правок Go-кода — прогонять
gofmt -w server/(агент делает это сам после любых правок).
Подводные камни
-
systemctl showвыводит свойства в порядке внутренней таблицы systemd, а НЕ в порядке-p— позиционный парс ломает статики незаметно (все карточки «остановлен»). Только парсингKey=Valueпо ключу. -
systemctl statusвозвращет exit 3 для неактивного юнита и 4 для отсутствующего — непустой вывод = валидный результат, err игнорировать. -
Docker-кнопки бьют по контейнерам выборочно (start → не-running, stop/restart → running), т.к.
docker stopостановленного возвращает ошибку у некоторых версий, а рестарт one-shot init-контейнеров повторяет миграции. -
iOS PWA требует https (или localhost). Через Caddy-домен бокса — ок.
-
SW на iOS у установленной PWA обновляется неохотно: правки SW → удалить иконку, открыть в Safari, добавить заново. Bump версии кэша в sw.js обязателен.
-
Connect JSON:
int64сериализуется строкой — в smoke-тестах grep'ать с кавычками. -
Джобы CI — host (не docker), user
homelab; не добавлять docker-шаги.