diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..18507a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +coverage/ +*.log +*.sqlite +*.sqlite-wal +*.sqlite-shm +.env +.env.* +.claude/settings.local.json diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ab89d9d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,24 @@ +# homelab-upgrade — instrukcje + +Kolejka kroków (SQLite) + driver + appka web do nocnych aktualizacji aplikacji Docker Compose na VM103. +Pełny, zatwierdzony projekt: `docs/DESIGN.md` — przeczytaj przed pracą nad architekturą lub nowym krokiem. +Stary system (do portu, tylko odczyt): `/root/homelab-modernization`. Nie czytaj tam `backups/` (kopie z `.env`). + +## Środowisko + +- Ten host to Proxmox (Debian 11) — tylko do rozwoju i testów. Docelowo kod działa na VM103 w Dockerze. +- Node 24 przez nvm (`.nvmrc`). W powłoce nieinteraktywnej przed `node`/`npm`: `. ~/.nvm/nvm.sh`. +- Nic nie instalujesz globalnie. Nie ruszasz Proxmoxa, VM103 ani działających kontenerów. +- Skryptów ze starego repo, które mutują (`apply.sh`, `rollback.sh`, `backup.sh`), nie uruchamiasz bez zlecenia właściciela. Testy idą na fejkach i zapisanych fixture'ach. +- Sekrety (`.env`, klucze, tokeny) nigdy w repo, logach ani wyjściu. +- Remote: `ssh://git@git.mariuszpacek.pl:2244/mariusz.pacek/homelab-upgrade.git`, gałąź `master`. Commity małe i opisowe; commit/push tylko na prośbę właściciela. + +## Kod + +- TypeScript `strict`, ESM, Node 24. Kod, identyfikatory, komentarze, commity i UI appki po angielsku; dokumenty (`docs/`, instrukcje) po polsku. +- Warstwy: `domain` (czyste typy i reguły, bez I/O) → `app` (handlery kroków, driver) → `infra` (adaptery) → `http`/`web`. Zależności tylko do środka; reguła lintu to pilnuje. +- Abstrakcja tylko tam, gdzie jest szew testowy albo wymienność: `StepStore`, `CommandRunner`, `ModelExecutor`, `Notifier`, `RegistryClient`, `Clock`. Reszta to zwykłe funkcje. +- Małe funkcje i moduły, wstrzykiwanie zależności przez argumenty/fabryki, discriminated unions zamiast flag, brak `any`, brak globalnego stanu. +- Walidacja na granicach (zod): wyjście skryptów, odpowiedź modelu, HTTP. Ten sam schemat daje typ. +- Testy Vitest obok kodu; `domain` i `app` pisz test-first. Przed commitem `npm run check` (typecheck + lint + test) musi być zielone. +- Nie dodawaj zależności bez powodu; każda nowa to jedna linia uzasadnienia w PR/commicie. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md new file mode 100644 index 0000000..31795a4 --- /dev/null +++ b/docs/DESIGN.md @@ -0,0 +1,106 @@ +# Projekt: kolejka kroków do nocnych aktualizacji homelabu + +Stan na 2026-09-18: projekt uzgodniony z właścicielem, kodu jeszcze nie ma. +Diagramy: artifact „Kolejka decyzji STOP” (https://claude.ai/artifact/5WdgWwxHGFa9qM5N5c8cpR, czytaj przez Artifact `read`). Ten plik jest streszczeniem bez diagramów. + +## Cel + +Nocna aktualizacja aplikacji Docker Compose na VM103 (192.168.0.139, manifesty `/opt/homelab/compose//compose.yaml`) bez nadzoru. +STOP tylko wtedy, gdy właściciel naprawdę musi zdecydować. Decyzję podejmuje w appce (LAN), po pushu z Home Assistant. +Zastępuje: `nightly-update.sh`, agentów `homelab-orchestrator`/`homelab-updater`, `docs/log.md`, `docs/costs.tsv` ze starego repo. + +## Mechanizm + +- Jedna generyczna pętla: driver bierze najstarszy krok `queued`, wykonuje go (skrypt/kod albo model), zapisuje wynik i dokłada następny krok. STOP i error to zwykły status kroku, nie osobna ścieżka. +- Wznowienie po STOP = nowy krok `queued` wstawiony z appki. Ten sam driver podejmie go przy następnym sprawdzeniu (pętla co ~60 s). +- Jedna instancja drivera (blokada), jeden krok naraz, jedna usługa naraz. Piszę do `steps` tylko driver. +- Krok, który utknął: limit czasu per typ kroku; po restarcie `running` starszy niż limit → `error`. `apply` i `rollback` nigdy nie są powtarzane automatycznie. +- Kroki z odroczeniem (`not_before`): ponowienie po `rate_limited`, przypomnienie po 24 h. +- Wersje krok po kroku: po `verify = ok` driver dokłada świeży `image_tags` dla tej aplikacji. Nowsza wersja w progu `Auto` → kolejny łańcuch; powyżej progu → STOP; nic nowszego → koniec. +- Dwa procesy z jednego kodu: `driver` (uprawnienia wykonawcze) i `server` (appka w LAN, bez uprawnień wykonawczych: tylko baza i compose do odczytu). Wspólna baza SQLite w trybie WAL na jednym hoście. Decyzja w appce to wstawienie wiersza `queued`. + +## Dane (SQLite) + +- `runs`: round_id PK, type (nightly|manual), started_at, finished_at, cost_usd (suma kroków). +- `steps`: id PK, round_id FK, seq, app (null = krok roundu), step, status (queued|running|ok|stop|error), executor (script|sonnet|opus), reason_code, detail, resolution (≤10 słów), resolved_at, input, result, cost_usd, not_before, started_at, finished_at. +- `app_config`: app PK, enabled, position, service, image_repo, tag_pattern, auto (none|patch|minor|major), http_url, expect_status, release_notes_executor, migrate_executor (null = autonomiczne migracje wyłączone), prompt_hint, notes (≤10 słów). +- Status aplikacji = jej ostatni wiersz w `steps`. Kolejka decyzji: `status IN ('stop','error') AND resolved_at IS NULL`. +- Retencja 12 miesięcy (czyszczenie skryptem miesięcznie). Baza jest objęta nocnym vzdump VM103. +- `docs/log.md` i `docs/costs.tsv` znikają; historia i koszt są w bazie. + +## Powód STOP + +`reason_code` (znane przypadki) + `detail` (konkretny tekst: polecenie, kod wyjścia, pierwsza linia stderr, komunikat wyjątku albo powód modelu ≤10 słów; po maskowaniu sekretów). Nieznany przypadek = `unexpected` z prawdziwym tekstem, nigdy pusty „error”. +`stop` = potrzebna decyzja właściciela (znany powód); `error` = coś się zepsuło. +Wstępna lista: no_backup, no_space, version_above_auto, version_unknown, migration_required, schema_change, forbidden_change, regression, verify_failed, rolled_back, no_skill, no_tool, model_unparseable, rate_limited, auth_failed, timeout, unexpected. + +## Kroki (mapowanie starej pętli z `/root/homelab-modernization/AGENTS.md`) + +| Krok | Wykonawca | Stary odpowiednik | +|---|---|---| +| `plan` | kod | wszystkie `enabled` z `app_config` wg `position`, każdej wstawia `status` | +| `status` | kod | `status.sh

` → `manifest_sha256`, obraz | +| `image_tags` | kod | `image_tags.sh --pattern --current ` → `candidate`, `version_change`; bramka: wyżej niż `auto` albo `unknown` → STOP | +| `release_notes` | model (bez narzędzi) | czytanie release notes; wynik proceed / migrate / stop | +| `migrate` (`repair`) | model z narzędziami | nowe | +| `image_resolve` | kod | `image_resolve.sh :` → `pinned` (tag@digest) | +| `backup_check` | kod | `pvesm list … 103` młodsza niż 48 h → host zapisuje plik z datą ostatniej udanej kopii, boks czyta go tylko do odczytu | +| `preflight` | kod | `preflight.sh` → `storage.sufficient` (≥ 5368709120 B wolnego w `/var/lib/docker`) | +| `backup` | kod | `backup.sh

` → `backup_dir` (kopia manifestu i `.env`; katalog z sekretami, chmod 700) | +| `apply_dry_run`, `apply` | kod | `apply.sh … --dry-run`, potem to samo bez; dla update `diff.changed_paths` musi być tylko `services..image` | +| `verify` | kod | `verify.sh` (running, image, http, retry 6×5 s, odbiór ze skilla) | +| `rollback` | kod | `rollback.sh

--from

--service ` | +| `skill_gen`, `skill_publish` | model bez narzędzi / kod | rejestracja aplikacji w appce: szkic → zatwierdzenie → zapis | + +Kody wyjścia `apply.sh`: 2 błędne argumenty; 3 `floating_tag`/`missing_tag` → najpierw repin, `missing_digest` → dodaj digest, diff szerszy niż obraz → STOP; 4 manifest zmieniony → `status`, nowy hash, od `preflight`; 5 pull/up nie przeszedł → `verify`, jeśli usługa nie działa → `rollback` i STOP. +Repin (digest bez zmian, nazwanie wersji): `status` → `image_version.sh` → `proposed_image` (`no_version_label` → STOP) → backup → apply dry-run → apply → verify. + +## Granice (niezależne od modelu) + +Absolutny STOP, także dla Opusa: sekrety, sieć, porty, UID/GID, mounty, ruszanie traefik/mqtt/unifi/homeassistant/Proxmox/TrueNAS, kasowanie danych/wolumenów/VM, `latest`, „update all”, `prune`, `down -v`, `--remove-orphans`. +Egzekwowane w kodzie, nie w prompcie: po `migrate` diff compose jest sprawdzany deny-listą (`privileged`, `cap_add`, `devices`, `network_mode: host`, mounty poza katalogami aplikacji, porty, sieci, sekrety). Trafienie = STOP. +`release_notes` ma trzy wyniki: `proceed`, `migrate` (w zakresie tego, co może zrobić wskazany model), `stop`. `migrate_executor = null` wyłącza `migrate` dla aplikacji. + +## Wykonawcy modeli: CLI z abonamentu + +- API bezpośrednio odpada: abonament go nie obejmuje, a Anthropic zabrania routowania żądań przez dane logowania planu Pro/Max i przechowywania tokenów sesji. Dozwolone jest uruchamianie niezmodyfikowanego `claude` zalogowanego własnym kontem właściciela. +- Port `ModelExecutor`, adaptery CLI: `claude -p` (start), Codex CLI później (logowanie kontem ChatGPT w automatyzacji jest odradzane; sprawdzić w praktyce). Adaptery API możliwe w przyszłości bez zmian w reszcie. +- `--bare` odpada (wymaga klucza API). Uruchamiaj w pustym katalogu roboczym i z osobnym katalogiem konfiguracji (do potwierdzenia: `CLAUDE_CONFIG_DIR`), żeby żaden CLAUDE.md/AGENTS.md ani ustawienia użytkownika nie wchodziły do kroków. +- Wynik: `--output-format json --json-schema …` → pole `structured_output` (walidacja zod), `total_cost_usd` to szacunek po stronie klienta. Prompt przez stdin (limit 10 MB). +- Profil A (`release_notes`, `skill_gen`): bez narzędzi. Driver sam pobiera release notes (tylko linki ze skilla, limit rozmiaru) — to niezaufany tekst. +- Profil B (`migrate`/`repair`): CLI z narzędziami (Read, Edit, ograniczony Bash) dla jednej aplikacji, po świeżej kopii. `--allowedTools` z wzorcami, `dontAsk`, hook `PreToolUse` odrzucający polecenia z listy zakazów. Po naprawie `verify` (kod) decyduje: ok albo `rollback`. +- Limity planu: przy limicie krok kończy się `rate_limited` i wraca z `not_before`. Poświadczenia w boksie wygasają → `auth_failed` + push. +- Wersję CLI przypinamy w obrazie i aktualizujemy świadomie. + +## Boks (do decyzji przed uruchomieniem modeli na prawdziwych danych) + +Właściciel chce: nic nie wycieka poza boks i brak kontroli nad VM. Uwaga: `docker.sock` to praktycznie root na VM. +Kierunek: kontener bez roota, system plików tylko do odczytu poza wolumenami, montowanie tylko katalogu compose i wolumenu SQLite; Docker przez proxy z allowlistą albo cienka nakładka na `docker` z kontrolą argumentów (wszystko zawężone do jednej aplikacji); ruch wychodzący tylko do API Anthropic i logowania Anthropic, rejestrów obrazów, GitHuba, HA; bez dostępu do reszty LAN (TrueNAS, Proxmox, MQTT, UniFi). Szczegóły są otwarte. + +## Powiadomienia i appka + +- Push HA per aplikacja, tylko przy stop/error, z linkiem do kroku (`scripts/notify.sh` ze starego repo: POST do `notify.`, konfiguracja `HA_URL`, `HA_TOKEN`, `HA_NOTIFY`; HA stoi na VM103). Przypomnienie raz po 24 h. +- Watchdog na hoście: brak `runs.finished_at` po nocnym przebiegu → jeden push „przebieg nie ruszył”. +- Appka: tylko LAN, za traefik; logowanie do przemyślenia przed wystawieniem poza dom. Widoki: przebiegi, historia aplikacji, koszty, kolejka STOP z akcjami (proceed / skip / rollback / zmiana executora / notatka), edycja `app_config`, rejestracja aplikacji z listy `/opt/homelab/compose` (montowanej tylko do odczytu) ze `skill_gen` i zatwierdzeniem szkicu (domyślnie `auto = patch`). Odświeżanie na żywo przez SSE. +- Skille: parametry mechaniczne w `app_config`; `SKILL.md` tylko fakty dla modelu. Import 15 istniejących skilli jednorazowo (na razie sprawdzony format tylko Bazarr). + +## Stary kod do portu (`/root/homelab-modernization`, tylko odczyt) + +`skills/homelab-compose-ops/scripts/` (≈1230 linii bash + Python): apply, apply_support.py, backup, compose_ps_json.py, guest_lib.sh, image_resolve, image_tags, image_version, plan, preflight, preflight_support.py, pve_agent_exec.py, rollback, status, verify. Skille aplikacji: `skills/*-compose-upgrade/SKILL.md` (15). +Stan docelowy: czysty TypeScript, bash jako wzorzec. Port po jednym skrypcie z testem kontraktowym (wynik TS zgodny z bashem na zapisanym JSON-ie). `guest_lib.sh` znika, bo driver działa na VM103 i `CommandRunner` wykonuje polecenia lokalnie. Kolejność od najmniej ryzykownych: status, image_tags, image_resolve, preflight, verify; potem backup, apply, rollback (mutacje, najwięcej testów). + +## Plan etapów + +0. Decyzje: repo, Node, klucz — zrobione. Do zrobienia przez właściciela: domena appki w traefik, logowanie `claude` w boksie, kształt boksu. +1. **Fundamenty (pierwsza sesja):** szkielet TS, domain, SQLite (schemat + migracje), driver z pętlą zdarzeń i fałszywymi handlerami, serwer HTTP + SSE, mały frontend (przebiegi, kroki, kolejka STOP, akcja rozstrzygnięcia), testy, `npm run check`. +2. Port skryptów na TS z `CommandRunner` (lokalnie na VM103). +3. Handlery kroków skryptowych + import skilli + przebieg suchy. +4. Boks. +5. Kroki z modelem (`ClaudeCliExecutor`, szablony, parser, deny-lista). +6. Appka: pełna (rejestracja aplikacji, `skill_gen`). +7. Powiadomienia i dwa małe skrypty po stronie hosta (data kopii, watchdog). +8. Przełączenie: kilka nocy równolegle ze starym przebiegiem (tryb suchy), potem wyłączenie starego, odchudzenie AGENTS.md starego repo, usunięcie agentów i `log.md`/`costs.tsv`. + +## Otwarte + +Kształt boksu; nakładka na `docker`; hosty logowania Anthropic do allowlisty; Codex przez konto ChatGPT; logowanie do appki poza LAN; wybór sterownika SQLite (`better-sqlite3` albo wbudowany `node:sqlite` — sprawdź stabilność w Node 24); domyślny język kodu (angielski) i stack (Fastify, Vite + Preact, zod, Vitest) do potwierdzenia przy szkielecie.