docs: project instructions and approved design

AGENTS.md/CLAUDE.md for agents, docs/DESIGN.md as the agreed architecture.
UI language is English (code, comments, commits and UI); documents stay Polish.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
homelab-updaterandClaude Sonnet 5 committed 2026-09-18 23:36:52 +02:00
1 parent 1d6bfb0bb5
commit 8bcd0bc485
5 files changed
+142

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
node_modules/
dist/
coverage/
*.log
*.sqlite
*.sqlite-wal
*.sqlite-shm
.env
.env.*
.claude/settings.local.json
+1
View File
@@ -0,0 +1 @@
24
+24
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+106
View File
@@ -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/<projekt>/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 <p>` → `manifest_sha256`, obraz |
| `image_tags` | kod | `image_tags.sh <repo> --pattern <tag_pattern> --current <tag>` → `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 <repo>:<candidate>` → `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 <p>` → `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.<usł>.image` |
| `verify` | kod | `verify.sh` (running, image, http, retry 6×5 s, odbiór ze skilla) |
| `rollback` | kod | `rollback.sh <p> --from <dir> --service <usł>` |
| `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.<usługa>`, 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.