homelab-codex-ws/docs/backlog.md
oskar da151fc8d3 fix(control-plane): redeploy wykonywalny — dispatch do host-side deploy-runnera
Executor odpalal scripts/deploy/deploy-node.sh <node> <service> wewnatrz
swojego kontenera: skrypt ignoruje oba argumenty i wymaga repo w
${HOME}/homelab-codex-ws (w kontenerze HOME=/home/homelab) -> exit 1 w 18.
linii. Za tym brak git, brak klienta docker w obrazie, a gdyby przeszedl —
deploy calego zestawu uslug hosta executora zamiast wezla z akcji. Kazdy
redeploy padal (recon D14/D15; 18 pending / 0 completed).

Redeploy idzie teraz ta sama sciezka pull co container_restart — VPS nigdy
nie inicjuje polaczenia do wezla:
  executor -> actions/deploy/<node>/<id>.json
  -> deploy-runner (systemd na hoscie) rsync-pull, walidacja, deploy
  -> action_result event -> executor rozlicza completed/failed

- scripts/deploy/deploy-service.sh: deploy jednej uslugi, wspoldzielony z
  deploy-node.sh, wiec inwokacja compose (a przez to nazwa projektu) jest
  identyczna jak przy deployu recznym
- jobs/deploy-runner/: host-level, nie kontener — compose rozwiazuje
  wzgledne bindy i nazwe projektu tak jak przy deployu czlowieka;
  niezalezny od node-agenta, wiec potrafi zredeployowac takze jego
- walidacja: tylko typ redeploy, node musi sie zgadzac, usluga musi byc w
  hosts/<node>/services.yaml, zadna tresc z payloadu nie trafia do shella
- --force-recreate bez --build i bez --remove-orphans: redeploy to
  rekoncyliacja, nie wysylka kodu
- executor: REDEPLOY_TIMEOUT_SECS=900, /repo zjechany do :ro (nieuzywany)

248 testow zielonych; deploy-node.sh przecwiczony na atrapie dockera —
argv compose bez zmian. Instalacja unitow na wezlach i E2E: backlog.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 18:26:30 +02:00

71 KiB
Raw Blame History

Tech-debt backlog

Centralny tracker tech-długu i znanych usterek. Wpisy ze sesji — dodawaj z datą i kontekstem.


deploy-runner: instalacja na węzłach + E2E redeployu (2026-08-03)

Data: 2026-08-03 Źródło: sesja task/redeploy-fix — recon D14/D15 + OPEN QUESTION 4 (nieopróżniona kolejka akcji). Było: redeploy nie mógł się wykonać nigdy. Executor odpalał scripts/deploy/deploy-node.sh <node> <service> wewnątrz swojego kontenera — skrypt ignoruje oba argumenty i wymaga repo w ${HOME}/homelab-codex-ws (w kontenerze HOME=/home/homelab, katalog nie istnieje) → exit 1 w 18. linii. Za tym stały jeszcze trzy blokady: brak git, brak klienta docker w obrazie oraz — gdyby przeszedł — deploy całego zestawu usług hosta executora, nie węzła z akcji. Stąd healthcheck_failed przekierowany 2026-07-29 na container_restart i 18 pending / 0 completed. Zrobione w repo (ta sesja): scripts/deploy/deploy-service.sh (deploy jednej usługi, wspólny z deploy-node.sh — ta sama inwokacja compose, więc ta sama nazwa projektu), jobs/deploy-runner/ (systemd na hoście: rsync-pull akcji, walidacja, deploy, action_result z powrotem), executor dispatchuje redeploy do actions/deploy/<node>/ i rozlicza je jak container_restart (REDEPLOY_TIMEOUT_SECS=900). 248 testów zielonych. Do zrobienia (runtime, wymaga operatora):

  1. Deploy control-plane na VPS (nowy executor + ../..:/repo:ro).
  2. Instalacja jobs/deploy-runner/ na vps, piha, solaria — patrz README („Install (per node)"). Na VPS VPS_EVENTS_HOST musi zostać puste.
  3. E2E na benignej usłudze na PIHA (wzorzec test-e2e-b z 2026-07-23), potem opróżnienie kolejki 18 pending — w tym redeploy-vps-gokapi.
  4. SOLARIA: group_add: "996" dla node-agenta wciąż niewdrożony (recon 641-649) — dopóki nie wejdzie, container_restart tam nie działa (redeploy działa, bo nie idzie przez node-agenta).

disk_cleanup w executorze jest zepsuty tak samo jak był redeploy (2026-08-03)

Data: 2026-08-03 Źródło: sesja task/redeploy-fix (znalezione przy okazji, poza zakresem zadania). Problem: executor._execute_disk_cleanup() odpala ssh oskar@<node> …, a obraz control-plane (python:3.11-slim + pip install pyyaml) nie ma klienta sshsubprocess.run(["ssh", …]) leci FileNotFoundError, akcja ląduje w failed/. To ta sama klasa błędu co redeploy i sprzeczne z decyzją „bez SSH w executorze". Fix: przenieść disk_cleanup na model dispatch (node-agent albo deploy-runner — runner ma już hosta i uprawnienia) albo usunąć typ akcji. Do decyzji przy okazji opróżniania kolejki.


scripts/deploy/deploy-host.sh to pusty plik (2026-08-03)

Data: 2026-08-03 Źródło: sesja task/redeploy-fix. Problem: 0-bajtowy, wykonywalny stub w scripts/deploy/ — wygląda jak entrypoint, nie robi nic. Kandydat do skasowania (jak deploy-role.sh w etapie 0).


Cutover HA "ken": kontener piha to legacy, prawdziwy dom to RPi4/HAOS (2026-07-22)

Data: 2026-07-22 Źródło: recon — dwie instancje HA równolegle sterowały domem (kontener homeassistant5 na piha + RPi4 HAOS 192.168.31.7), patrz services/home-assistant/DESIGN.md sekcja "Incident log". instances.yaml naprawiony w tej samej sesji: ken = 192.168.31.7 (api), ken-legacy = dawny kontener piha (docker-exec, archived).

Do zrobienia:

  1. ha-diag-agent na piha: przepiąć z http://localhost:8123 (celuje w legacy!) na http://192.168.31.7:8123 — wymaga nowego tokenu diag_agent wystawionego na instancji 31.7 (obecny token jest dla kontenera piha i nie zadziała na nowym targecie).
  2. Wygaszenie homeassistant5: import archiwalny do services/home-assistant/config/ken-legacy/docker stop (BEZ rm) → 7 dni obserwacji (upewnić się, że nic w domu nie polega na tym kontenerze) → decyzja o docker rm.
  3. ZROBIONE (2026-07-22) — Adapter api w import.sh dla ken: automatyzacje/skrypty/sceny przez /api/config/<domain>/config/<id> (REST), dashboardy/area+entity registry/input_* helpery przez websocket API (scripts/ha/lib/ha_api.py, ha_ws.py, import_api.py). Pierwszy realny import ken zaimportował 118 automatyzacji, 5 skryptów, 3 sceny, 7 dashboardów (default + 6 named; jeden zarejestrowany dashboard nigdy nie skonfigurowany — config_not_found, odnotowany w raporcie, nie twardy błąd). Pełny import /config pozostaje poza zasięgiem (HAOS bez SSH) — patrz DESIGN.md.
  4. ZROBIONE (2026-07-22) — scripts/ha/deploy.sh, adapter api, zakres automations/scripts/scenes: drift-check (świeży re-import vs. HEAD, dowolna różnica poza plikami z tego deployu = abort z diffem) → walidacja (lokalny sanity check + check_config na instancji) → zapis per obiekt (POST /api/config/<domain>/config/<id>) → verify (GET + porównanie, bez auto-rollbacku). --dry-run zweryfikowany na żywym ken (read-only, bez różnic). Testy offline: scripts/ha/tests/test_deploy_api_offline.sh. Poza zakresem: dashboardy/ helpery (WS, brak mutującej komendy), adapter docker-exec, DELETE obiektów usuniętych z repo (tylko ostrzeżenie).

Nowy podprojekt: Home Assistant configs-as-code (szkielet)

Data: 2026-07-21 Branch: task/ha-skeleton

Szkielet struktury dla services/home-assistant/ — configs-as-code dla instancji HA (ken na PIHA, chelsty-ha). Na razie tylko struktura + read-only import (scripts/ha/import.sh), bez deployu. Fazowanie, wybór adaptera per instancja, model sync i otwarte pytania — services/home-assistant/DESIGN.md.


Plan: Monitoring floty — Prometheus jako źródło prawdy

Data: 2026-06-22 Źródło: sesja 2026-06-22 (docs/sessions/2026-06-22.md) Decyzja: Prometheus (pull, up{}) zastępuje warstwę WYKRYWANIA liveness (node-agent shipper + rsync/ssh + event-store + observer prune/checkpoint + ręczne TTL) — przyczynę nawracających awarii (uid≠1000, ślepy ssh-mount, bloat ~242k eventów, race prune↔checkpoint, NOMINAL-bez-TTL). Osobny fleet-Prometheus pod GitOps, nie adopcja domowego instance PIHA. BEZ Alertmanagera — alert przez brain-watchdog. Placement: VPS. Granica: zostają supervisor (remediacja), observer/panel, ha-diag-agent, historia incydentów, out-of-band watchdog.

Zastępuje wcześniejszy szkic (blackbox + Alertmanager) z sesji 2026-06-17.

Kroki (priorytetowo):

  1. Scaffold serwisu fleet-prometheus pod GitOps (worktree task/fleet-prometheus, wzorzec services/vikunja/): compose + env.example + service.yaml + README + healthcheck.sh; rejestracja w hosts/vps/services.yaml + inventory/topology.yaml; exposure tailscale-internal; pusty scrape na start (self + lokalny node_exporter VPS).
  2. ZROBIONE (2026-06-26, commit 7d4014e) — Inwentaryzacja nodów floty 100.x do scrape. Dodane: piha/solaria/lustro (node: label), vps zachowany. Saturn pominięty (workstation), chelsty/chelsty-infra pominięte (node_exporter down z VPS → osobny wpis).
  3. Container-layer exporter (cAdvisor lub lekki docker-state) — node_exporter nie widzi kontenerów.
  4. ZROBIONE (2026-06-30, commit d417000) — Reguły liveness (up==0 for: 5m). rules/liveness.yml: NodeDown expr up{node=~"vps|piha"}==0 for 5m severity critical. Only always-on (vps, piha); solaria/lustro świadomie wykluczone (intermittent → anomaly detection). Deploy: fleet-prometheus Recreated (zmiana compose), reguła inactive=poprawnie.
  5. ZROBIONE (2026-06-30, commit 62d6fc0) — brain-watchdog: drugie wejście — poll Prometheus /api/v1/alerts (firing) → Telegram. Architektura A: dwa niezależne tory, mózg NIETKNIĘTY, debounce per-alert (klucz alertname:node w state.json). 12 testów pass. PENDING zamknięty 2026-07-02: poll POTWIERDZONY reconem (obraz zbudowany po 62d6fc0, PROMETHEUS_URL w .env i w env kontenera, zero poll failed) — patrz docs/infra/inventory-verify-2026-07-02.md. END-TO-END UDOWODNIONE 2026-07-06 (Etap 0 cutoveru): tor Prometheus → watchdog → Telegram potwierdzony w produkcji testem AlertTestEtap0 (firing → log polla → Telegram → rollback) — patrz docs/sessions/2026-07-06.md.
  6. Rotacja tokenu HAOS w domowym prom (plaintext).
  7. Przepięcie observer / panel agents.okit.pl na Prometheus jako źródło — największy znak zapytania przy cutoverze.
  8. Parallel-run obok rury eventowej; cutover dopiero gdy Prometheus-truth się udowodni.

Aktywne

scripts/ha/deploy.sh --delete: brak wspólnego toru kasacji automatyzacji

Data: 2026-07-27 Źródło: sesja porządków po audycie (task/ha-porzadki); kontekst bezpośredni: kasacja pkt 10 (36b43e5, para Tymka/powitanie-test/notify-router/para-prototyp) zrobiona pętlą ręcznych curl DELETE po API zamiast przez repo tooling. Problem: deploy.sh ma tylko WRITE (POST /api/config/<domain>/config/<id>) — zgodnie z DESIGN.md ("Deploy path") i istniejącym wpisem w tym backlogu (sekcja "Cutover HA ken", krok 4) DELETE obiektów usuniętych z repo jest poza zakresem, drift-check tylko ostrzega (warnings), nigdy nie kasuje. Efekt: jedyna droga usunięcia automatyzacji z żywej instancji to ręczne wywołanie API, bez drift-check/check_config/verify, czyli bez żadnej z gwarancji, które deploy.sh daje dla write. Fix: deploy.sh <instance> --delete <plik...> (albo --delete jako tryb pracy na plikach usuniętych z repo, wykrytych przez drift_warnings) z tym samym rytmem co write: dry-run domyślny, --dry-run/LIVE jak dziś, DELETE /api/config/<domain>/config/<id> per obiekt, verify (GET → oczekiwane 404) zamiast porównania treści. Scope jak przy write: automations/scripts/scenes.


HA ken: input_number.klima_salon_tolerancja nie ma triggera — zmiana nie przelicza progu

Data: 2026-07-27 Źródło: sesja porządków po audycie (task/ha-porzadki), przy okazji przeglądu 1784804667795 dla konwencji automatyzacji (pkt 17) Problem: "Klima salon: włącz chłodzenie i synchronizuj cel" (1784804667795) ma trigger id: sync na input_number.klima_salon_temp_docelowa (zmiana celu od razu przelicza próg), ale brak odpowiednika dla input_number.klima_salon_tolerancja — to ta sama klasa buga co "trigger brzegowy bez lustrzanego warunku" z audytu (sekcja 3, wzorzec klima_salon z fixu faa2e2a), tylko po stronie triggerów, nie warunków: zmiana tolerancji na dashboardzie nic nie przelicza do najbliższej naturalnej zmiany sensor.thsalon_temperature. Fix: dodać trigger state na input_number.klima_salon_tolerancja do gałęzi sync (albo do analogicznej gałęzi w automatyzacji OFF, jeśli tolerancja wpływa też na próg wyłączenia) w 1784804667795, tak samo jak istniejący trigger na _temp_docelowa.


HA ken: guard TRV kalibracji przed sezonem grzewczym (~2026-09)

Data: 2026-07-23 Źródło: services/home-assistant/docs/audyt-automatyzacji-2026-07-23.md sekcja 1.4 (pkt 2 checklisty operatora) Problem: kalibracje TRV sypialnia (1764751049013) i Tymek (1765817937658) liczą room_temp jako średnią z czujnika zhimi, który jest unavailable od 2026-07-17 → float(0) w formule zaniża temperaturę o połowę → kalibracja dojechała do 5.0 (potwierdzone w fixtures). Latem (TRV off) nieszkodliwe; w sezonie grzewczym = trwałe przegrzewanie obu pokoi. Dodatkowo wszystkie 6 automatyzacji TRV pollują co 4050s (baterie 2038%), a SalonPrawy ma clamp diff ±5 zamiast ±1.5 jak reszta. Fix (przed sezonem): (1) guard is not unavailable na czujnikach zhimi zamiast ślepego uśredniania; (2) ręczny reset kalibracji sypialnia/Tymek po naprawie; (3) zwolnić pętle do ≥5 min lub trigger na zmianę stanu; (4) ujednolicić clamp SalonPrawy; (5) reanimować albo wyłączyć TRV łazienki (number.*_calibration unavailable — urządzenie zniknęło z sieci).


HA ken: przycisk graceful shutdown klimy salonowej na kartę dashboardu

Data: 2026-07-23 Źródło: services/home-assistant/docs/audyt-automatyzacji-2026-07-23.md sekcja 3.1, fix-pack 1 (task/ha-fix-pack-1, DESIGN.md „Decyzje operatora po audycie 2026-07-23") Kontekst: po fix-packu 1 „Klima salon: wyłącz…" (1784804668795) respektuje klima_salon_auto = on dla gałęzi sunset/balkon; suszenie parownika przy ręcznym zgaszeniu klima_salon_auto działa już tylko jako efekt uboczny przełącznika trybu auto (osobna gałąź choose na trigger auto_off). Brakuje wygodnego jednego przycisku „wyłącz klimę i osusz teraz" niezależnego od przełącznika auto. Fix: dodać na dashboard przycisk/skrypt wywołujący script.klima_salon_dry_off bezpośrednio (bez przełączania klima_salon_auto), żeby ręczne graceful shutdown nie wymagało znajomości wewnętrznej logiki automatyzacji.


HA ken: diagnoza wspólnej awarii sprzętowej 2026-07-17 (czujniki ruchu, pilot 4button, xiaomi_miot)

Data: 2026-07-23 Źródło: services/home-assistant/docs/audyt-automatyzacji-2026-07-23.md sekcje 1.2, 1.6, 4.5 (pkt 1, 3, 15 checklisty operatora) Problem: restart HA / update Supervisora 2026-07-17 15:07 zbiega się z unavailable na: klaster czujników ruchu/obecności (mdwejscie, mdsypialnia, mdheli — przynajmniej od restartu; occusalon/mdtymka/mdubikacja gasną w kolejnych dniach — obraz siadających baterii), całej integracji xiaomi_miot (fan.zhimi_mb3/mc2, czujniki temperatury zhimi używane w kalibracjach TRV) i sensor.4button_battery (mimo że pilot 4button strzelał jeszcze 2026-07-12 — 8 automatyzacji salonu na tym urządzeniu). Nie jest jasne, czy to jedna awaria (Zigbee coordinator/integracja) czy zbieg kilku. Fix: (1) sprawdzić fizycznie baterie/re-pairing 6 czujników ruchu + czujnik wycieku WC (pkt 1 checklisty); (2) zdiagnozować integrację xiaomi_miot po stronie HA zamiast łatać pojedyncze automatyzacje (pkt 3); (3) sprawdzić fizycznie baterię pilota 4button (pkt 15). Reanimacja czujników ruchu odblokuje też ~15 cicho martwych automatyzacji (alerty on-leave, nocne gaszenie, Poranek start) — patrz audyt sekcja 1.2 dla pełnej listy skutków.


HA ken: projekt „architektura night_mode" (konsolidacja sleep/night mode)

Data: 2026-07-23 Źródło: services/home-assistant/docs/audyt-automatyzacji-2026-07-23.md sekcje 2.2, 2.4, 6 (pkt 7, 9 checklisty operatora — świadomie NIE załatane w fix-packu 1, patrz DESIGN.md „Decyzje operatora po audycie 2026-07-23") Problem: cztery flagi trybu (sleep_mode, night_mode, passive_mode, movie_mode) o częściowo pokrywającej się semantyce, sprawdzane niespójnie (raz jedna flaga, raz druga, raz OR obu); automation.turn_off_sleep_mode_at_sunrise steruje w rzeczywistości night_mode. Cztery nakładające się automatyzacje gaszą ten sam zestaw świateł nocą (1702844937214 martwa, 1763384250145 martwa, 1768946230585 enforcer co ~10 min całą noc, 1700832676138 o 3:00) — po reanimacji martwych czujników (patrz wpis wyżej) trzy z nich ożyją naraz i zaczną się ścigać. 1768946230585 ma dodatkowo trigger time_pattern /5 obok krawędziowego off→on, więc mimo aliasu „5 minut po aktywacji" gasi światła cyklicznie całą noc — do decyzji, czy to zamierzone. Fix (projekt, nie one-liner): skonsolidować do jednego input_select.tryb_domu + jednej automatyzacji „nocne domknięcie" z jasnym priorytetem trybów i enforcerem (cykliczny dozorca vs. jednorazowe zadziałanie po aktywacji — decyzja pkt 7), plus finalny/ostateczny wyłącznik nocny zastępujący dzisiejsze cztery. Zakres większy niż fix-pack — osobny task.


Executor: zepsuty JSON w approved/ retry'owany w nieskończoność co 10s zamiast trafić do failed//rejected/

Data: 2026-07-23 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: podczas testów E2E remediacji bez SSH zepsuty JSON akcji w approved/ (przyczyna: podstawienie zmiennej lokalnej w cudzysłowie w zagnieżdżonym heredoc przez ssh) powodował, że executor próbował go sparsować co cykl (10s), logując "Failed to move ... to running: Expecting value" bez końca — plik nigdy nie trafiał do failed//rejected/. Fix: executor powinien przenosić nieparsowalny JSON do failed/ (albo dedykowanego malformed/) po pierwszej próbie, nie zostawiać go do nieskończonego retry w approved/.


Uprawnienia actions/ naprawione TYLKO na PIHA (ręcznie) — SOLARIA i lustro niezweryfikowane

Data: 2026-07-23 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: fix uprawnień /opt/homelab/actions (grupa pi + setgid, patrz Zamknięte) zastosowany ręcznie tylko na PIHA. SOLARIA (oskar tam podobno uid 1000 — do sprawdzenia czy problem w ogóle występuje) i lustro (repo-less, patrz osobny wpis niżej) niezweryfikowane — remediacja na tych węzłach może paść identycznie przy pierwszej próbie. Fix docelowy: node-agent powinien sam tworzyć dispatch/<node>/ z właściwymi prawami (grupa współdzielona + setgid) przy starcie, żeby to nie wracało za każdym razem jako ręczna interwencja — patrz też „Tech-debt: globalny porządek uid/gid" niżej.


deploy-local.sh (control-plane): brak twardego checka, że .env istnieje i TAILSCALE_BIND_IP jest ustawione

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: przy fixie bindu operator_ui.py:18180 odkryto footgun: bez .env, docker compose tylko OSTRZEGA i po cichu wraca do bindu 0.0.0.0 zamiast failować — dokładnie ten sam wzorzec błędu jak historyczny bug fleet-prometheus/deploy-node.sh (brak --env-file, patrz Zamknięte, commit 686aca7), tym razem w deploy-local.sh/control-plane. Fix: dodać twardy check w deploy-local.sh.env musi istnieć i TAILSCALE_BIND_IP musi być ustawiony, inaczej abort przed docker compose up.


operator_ui.py nie ma ŻADNEJ autoryzacji

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: po zamknięciu publicznego bindu 18180 (patrz Zamknięte, commit 9a5c160) operator_ui.py nadal przyjmuje /action/mutate (w tym przejście do approved) bez żadnego uwierzytelnienia — chroni tylko granica Tailscale mesh, nie autoryzacja per operator. Fix: osobny temat — do zaprojektowania (token/basic auth/mTLS w mesh), poza zakresem fixu bindu.


alert_only zapycha approval queue

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: przy reconie stanu wyjściowego 16 z 18 pending actions to były liveness-transitions typu alert_only, nie realne decyzje do klikania — operator musi przewijać szum, żeby znaleźć akcje, które faktycznie wymagają Approve/Reject. Fix: rozważyć osobny widok/filtr dla alert_only w operator UI, albo auto-acknowledge bez wejścia do tej samej kolejki co container_restart/redeploy.


Crash-loop nie generuje container_restart — gap detekcja→akcja

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: node_exporter na PIHA miał 1048 restartów i event [high], ale supervisor nie wygenerował żadnej akcji container_restart — crash-loop widoczny w evencie nie przekłada się na akcję remediacyjną. Fix: zbadać routing supervisora dla crash-loop sygnałów (disk_pressure i containers_not_running mają jasne mapowanie na akcje w CLAUDE.md — crash-loop najwyraźniej nie).


Telegram yes/no dla pending actions brakuje

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: brain-watchdog ma send_telegram (używane dla alertów Prometheus), ale nic nie łączy go z actions/pending — operator musi wejść do operator UI, żeby zatwierdzić/odrzucić akcję, zamiast dostać yes/no bezpośrednio na Telegramie. Fix: rozszerzyć brain-watchdog (albo osobny konsument) o powiadomienie z przyciskami approve/reject per pending action.


homeassistant5 na PIHA: Exited (0), nie podnosi się mimo restart: unless-stopped

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: kontener homeassistant5 (legacy HA "ken", patrz cutover docs/backlog.md sekcja "Cutover HA ken") zaobserwowany w stanie Exited (0) — exit code 0 = czyste zatrzymanie, więc Docker restart: unless-stopped świadomie go nie podnosi (to nie crash). Do zweryfikowania, czy to zamierzone wygaszenie z cutoveru czy coś zatrzymało kontener niezamierzenie. Fix: sprawdzić czy to spójne z planem wygaszenia homeassistant5 (patrz cutover HA ken, krok 2 „Wygaszenie homeassistant5") — jeśli tak, brak akcji; jeśli nie, zbadać czemu wyszedł z kodem 0.


lustro: node_agent.py wymaga ręcznego wypchnięcia do /opt/homelab/deploy/node-agent (repo-less)

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Problem: węzeł lustro nie ma repo/gita — node_agent.py (z fixami remediacji bez SSH, commit 2dac154) musi być ręcznie skopiowany do /opt/homelab/deploy/node-agent, żeby dotrzeć na ten węzeł. Ryzyko rozjazdu repo↔runtime identyczne jak przy innych repo-less węzłach. Fix: ustalić mechanizm dystrybucji kodu node-agenta na węzły repo-less (rsync przy deployu / paczka artefaktu / inny kanał) zamiast ręcznego kopiowania.


🔴 npm@VPS panel admina :81 publicznie osiągalny z internetu

Data: 2026-07-10 Źródło: sesja scripts/npm/npm_api.py (skrypt do zarządzania NPM przez REST API) Problem: services/npm/docker-compose.yml mapuje 81:81 bez ograniczenia do interfejsu — Docker bindem domyślnym wystawia to na 0.0.0.0, czyli panel admina NPM@VPS jest osiągalny z publicznego IP (135.181.153.108:81), nie tylko przez Tailscale mesh (100.95.58.48:81). Panel admina (login+hasło, bez 2FA wymuszonego) nie powinien być publiczny. Brak hosts/vps/runtime/npm/docker-compose.override.yml ograniczającego bind. Fix: dodać override z bindem 127.0.0.1:81:81 (dostęp tylko przez Tailscale/SSH tunnel) albo <tailscale_ip>:81:81, zachowując 80/443 publiczne (to jest ich rola). Zweryfikować po zmianie, że npm_api.py --npm vps nadal łączy się przez 100.95.58.48:81.


Ghosty B WRÓCIŁY — hash-prefixed control-plane na VPS nieposprzątane

Data: 2026-07-06 Źródło: sesja 2026-07-06 (docs/sessions/2026-07-06.md) — snapshot panelu agents.okit.pl Problem: hash-prefixed kontenery control-plane znów widoczne na VPS — wpis „ZNIKNĘŁY (potwierdzone reconem 2026-07-02)" w Zamkniętych zdezaktualizowany. Fix: ręczny docker rm hash-prefixed kontenerów control-plane na VPS; przy okazji sprawdzić, skąd wróciły (fix A 3b71707 miał blokować źródło divergence). Update 2026-07-16 (sesja docs/sessions/2026-07-16.md): po odetkaniu supervisora (event flood + pętla zamrożona, oba naprawione dziś) potwierdzone, że wpisy error w topologii panelu to dokładnie te ghosty — hash-prefixed w world_state observera, NIE żywe kontenery (docker ps -a exited=0 na VPS). Observer nie prune'uje wpisów po zniknięciu kontenerów spod tych nazw. To trzyma System Status: ERROR fałszywie. Fix (do zrobienia): observer powinien weryfikować faktyczny stan kontenera przy budowaniu world_state i prune'ować wpisy dla kontenerów, których już nie ma (ta sama klasa błędu co „Rozjazd world-state observera: NOMINAL przed istnieniem" niżej).


shadow_mode → remediacja: decyzja o auto-restart

Data: 2026-07-16 Źródło: sesja 2026-07-16 (docs/sessions/2026-07-16.md) — wątek control-plane Problem: po odetkaniu supervisora (event flood + pętla zamrożona, oba naprawione dziś) kolejka akcji nadal pusta częściowo dlatego, że shadow_mode=True downgrade'uje HA container_restart do alert_only — supervisor widzi problem, ale świadomie nie enqueue'uje akcji restartu. Do decyzji: czy i kiedy włączyć auto-restart padłych kontenerów — wymaga architektury (guardraile, cooldowny, blast radius per serwis) zanim shadow_mode=false. Patrz też istniejący wpis „ha-diag-agent deploy ZABLOKOWANY" niżej — przed shadow_mode=false tam wymieniony konkretny target (homeassistant5).


gokapi: deploy-node VPS rzuca błąd — brakujący .env

Data: 2026-07-16 Źródło: sesja 2026-07-16 (docs/sessions/2026-07-16.md) — wątek control-plane Problem: deploy-node.sh na VPS rzuca błąd na serwisie gokapi z powodu brakującego .env (/opt/homelab/config/gokapi/.env nieutworzony lub niepełny — wzorzec z sesji 2026-07-09 docs/sessions/2026-07-09-kb-configi-gokapi.md). Fix: sprawdzić services/gokapi/env.example, utworzyć/uzupełnić .env na VPS wg konwencji env.example/opt/homelab/config/<service>/.env.


elasticsearch + diskover w stanie error na PIHA — observer zna usunięte serwisy

Data: 2026-07-06 Źródło: sesja 2026-07-06 (docs/sessions/2026-07-06.md) — snapshot panelu agents.okit.pl Problem: elasticsearch i diskover usunięte z PIHA w module 0 (2026-07-02), ale observer wciąż je zna i raportuje error w panelu. World-state nie zapomina serwisów, które przestały istnieć — ta sama klasa błędu co „NOMINAL przed istnieniem" (patrz wpis z 2026-06-25). Fix: wyczyścić martwe wpisy z world_state / dodać wygaszanie serwisów nieobecnych w desired state i w dockerze.


Gotchas (z 2026-06-30 — migracja kapala.org → Cloudflare/wildcard)

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md)

  • NPM custom WS config + Websockets Support: NIE wklejać proxy_http_version 1.1; do Advanced gdy Websockets Support = ON. NPM dodaje tę dyrektywę sam → duplikat → nginx -t failuje → plik proxy_host/.conf się NIE generuje → "unrecognized name" mimo dobrego certu. Objaw mylący (wygląda jak problem certu/DNS). Diagnoza: strings /data/database.sqlite | grep <domena> → pole nginx_err.

  • Cloudflare auto-proxy na import: CF proxuje A/CNAME przy dodaniu strefy. DKIM CNAME (fm1/2/3._domainkey) proxied = zepsuty podpis maila. Zawsze przełączyć na DNS only (szara chmurka) przed aktywacją. Reserved/CGNAT IP (Tailscale 100.x) CF wymusza DNS only automatycznie.


Migracja okit.pl → Cloudflare (większy projekt, firmowa domena)

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md) Naprawi wszystkie wygasłe certy okit.pl naraz (HTTP-01 failuje przy mesh DNS): ap, audiobooks, code-server, dysk, forgejo, ha-embed, hagc, ngpm, node-red, okit.pl, pihole, ha.okit.pl. Wzorzec jak kapala.org: NS na CF, wildcard *.okit.pl przez DNS-01, przepiąć hosty. UWAGA: okit.pl ma usługi publiczne (foty) — rozdzielić mesh-only od publicznych. Ostrożnie — firmowa domena.


foty.kapala.org renew failuje (#47, expired 2026-06-19, HTTP-01)

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md) Foty są PUBLICZNE celowo (zewn. dostęp za hasłem NPM) — port 80 powinien być dostępny, więc HTTP-01 powinno działać. Sprawdzić czemu failuje (DNS foty wskazuje na zły IP? port 80 zablokowany?). NIE przenosić na mesh.


Cleanup po błędnej ścieżce HA-Tailscale-addon (z 2026-06-29)

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md)

  • ha-ken: Stop + Uninstall add-on Tailscale
  • Tailscale admin: usunąć node ha-ken (100.98.128.40)
  • 42.pl/okit.pl: usunąć rekord ha-ken → 87.205.110.38 (jeśli jest)

Stary ha.okit.pl (cert wygasł 6/28, teraz "Not Used")

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md) Zostawić lub usunąć proxy host + DNS. Niepilne (martwy, nie szkodzi).


Stopniowa migracja usług domowych okit.pl → kapala.org (mesh-only)

Data: 2026-06-30 Źródło: sesja 2026-06-30 (docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md) dysk, audiobooks, budget, code-server, home, ha-embed, node-red... wg wzorca migracji usługi na kapala.org (patrz sesja: CF rekord A → 100.108.208.3 DNS only, NPM proxy host z wildcard *.kapala.org, Advanced PUSTE, weryfikacja grep+curl).


deploy-node.sh nie reloaduje config-driven serwisów (cicha rozbieżność deploy↔config)

Data: 2026-06-26 Źródło: sesja 2026-06-26 (docs/sessions/2026-06-26.md) Problem: po zmianie prometheus.yml (bez zmiany obrazu) deploy-node.sh NIE recreate'uje kontenera. Compose widzi "kontener działa, obraz ten sam" → zostawia Running, NIE podmienia configu → Prometheus trzyma stary config w pamięci. Deploy raportuje green, a zmiana configu nie wchodzi w życie. Dziś wymagało ręcznego docker compose ... up -d --force-recreate. Dotyczy KAŻDEGO serwisu config-driven bez zmiany obrazu (nie tylko Prometheus). Fix: po zmianie configu serwisu albo --force-recreate, albo POST /-/reload dla serwisów z lifecycle API (fleet-prometheus ma --web.enable-lifecycle). Rozważyć wykrywanie zmiany plików config w deploy i wymuszanie recreate.


Zbadać: chelsty + chelsty-infra node_exporter DOWN z VPS

Data: 2026-06-26 Źródło: sesja 2026-06-26 (docs/sessions/2026-06-26.md) Problem: przy inwentaryzacji targetów floty do fleet-prometheusa, node_exporter na chelsty i chelsty-infra był nieosiągalny z VPS — pominięte w scrape. To LTE edge (intermittent uplink), więc DOWN może być normą, ale wymaga rozróżnienia: brak node_exportera vs odcięty uplink vs zablokowany port. Fix: ustalić, czy node_exporter w ogóle działa na obu chelsty (compose/proces), czy jest osiągalny po Tailscale z VPS, i czy ma sens go scrape'ować mimo LTE (prawdopodobnie tak — up==0 na LTE = sygnał dla anomaly detection, nie fałszywy alarm).


Supervisor nie enqueue'uje akcji remediacji przy error-state

Data: 2026-06-25 (powtórka sygnału z 2026-06-19) Źródło: sesja 2026-06-25 (docs/sessions/2026-06-25.md) Problem: Action Queue pusta mimo System Status ERROR widocznego w panelu. Supervisor nie generuje container_restart / redeploy dla serwisów w stanie error. Objaw zaobserwowany co najmniej dwukrotnie — wymaga izolowanego dochodzenia. Podejrzane: supervisor może nie reagować na error-state jeśli źródłem są ghost kontenery (błędne project-name), nie realne health-check failures. Fix: zbadać osobno — sprawdzić, czy supervisor otrzymuje właściwe eventy od observera, czy ma własną logikę de-duplifikacji blokującą enqueue. Update 2026-07-02: ghost kontenery (bug B) zniknęły z VPS — jeśli objaw wróci, hipoteza "źródłem są ghosty" jest już nieaktualna. UWAGA: ślepy supervisor na SATURN (brak mountu repo, patrz sesja 2026-07-02) to INNY przypadek — nie mylić z tym bugiem. Update 2026-07-16 (sesja docs/sessions/2026-07-16.md, druga połowa dnia): dwie głębsze przyczyny pustej kolejki znalezione i naprawione (petla supervisora zamrożona ~24h — patrz „Supervisor: pętla zamrożona…" w Zamkniętych; event flood 358k plików paraliżujący reconcile — patrz „Event flood…" w Zamkniętych). Po obu fixach supervisor tika i reconcile się kończy, ALE objaw z tego wpisu (brak redeploy mimo widocznego error — elasticsearch/diskover na piha, ollama solaria) nadal aktualny — drift→action nie domyka się mimo odetkanego mózgu. Zostaje otwarte jako osobne dochodzenie.


Rozjazd world-state observera: panel pokazuje serwis NOMINAL przed jego istnieniem

Data: 2026-06-25 Źródło: sesja 2026-06-25 (docs/sessions/2026-06-25.md) Problem: panel wykazał fleet-prometheus jako nominal na SOLARII zanim kontener w ogóle istniał — observer world_state rozjechany z dockerem. Artefakt rejestracji w manifeście bez realnego kontenera. Podobna klasa błędu jak ghost kontenery. Fix: observer powinien weryfikować faktyczny stan kontenera przy budowaniu world_state zamiast opierać się wyłącznie na zarejestrowanych serwisach.


🔴 BLOKUJĄCE — FLOTA-BOMBA: node-agent SSH mount ślepy po recreate

Data: 2026-06-11 Źródło: sesja lustro ssh shipping fix Problem: solaria/piha/chelsty to stare root kontenery node-agenta (piha Created 2026-05-27, uid 0) — sprzed dodania user: "1000:1000" do bazowego compose. Ich override montuje klucz SSH w /root/.ssh, co działa tylko dla uid 0. Pierwszy --force-recreate / reboot hosta / update obrazu przełączy kontener na uid 1000 (homelab, HOME=/home/homelab) i shipping eventów na VPS padnie z "Permission denied" — dokładnie jak na lustrze (naprawione a5a1352). ssh w _ship_events_to_vps() nie ma -i i szuka klucza w $HOME/.ssh. ⚠️ NIE RECREATE node-agenta na solaria/piha/chelsty przed fixem. Fix: ujednolicić mount → /home/homelab/.ssh we wszystkich hosts/*/runtime/node-agent/docker-compose.override.yml (wzór: hosts/lustro/) ALBO dodać -i $HOME/.ssh/id_rsa w _ship_events_to_vps().


ha-diag-agent deploy ZABLOKOWANY (placeholder token)

Data: 2026-06-11 Źródło: sesja — deploy config merged (5e9db5c), .env na piha utworzony (/opt/homelab/config/ha-diag-agent/.env, chmod 600) ale token = PLACEHOLDER. Blokada: chelsty-ha offline → brak tokenu i połączenia. Do decyzji: cel HA — chelsty-ha vs HA Ken (homeassistant5 na piha; z kontenera NIE localhost). Przed shadow_mode=false: target restartu w supervisorze = nazwa kontenera homeassistant5; curl endpointu HA z tokenem = HTTP 200.


observer-poison-quarantine — review brancha (78c9e4a)

Data: 2026-06-11 Źródło: sesja — patch Codexa zachowany na task/observer-poison-quarantine, NIE w master. Do zrobienia: zweryfikować, czy observer realnie wiesza się na malformed evencie (poison NIE był przyczyną awarii lustra — hipoteza niezweryfikowana, obalona przez verify-before-fix). Realny bug → merge; inaczej → drop brancha i worktree.


node_agent.py — drobne sprzątanie shippingu

Data: 2026-06-11 Źródło: sesja lustro ssh shipping fix

  1. Stale komentarz node_agent.py:546-548 — twierdzi, że kontener "runs as root"; nieaktualne od user: "1000:1000".
  2. Sukces shippingu na logger.debug → podnieść do info lub dodać licznik — działający shipping jest niewidoczny w logach przy INFO, co utrudniało diagnozę (cicha awaria wyglądała identycznie jak ciche działanie).

event-bloat: wyczyścić spłynięty backlog lustro na VPS

Data: 2026-06-11 Źródło: sesja — po fixie shippingu 7600+ plików backlogu spłynęło do /opt/homelab/events/lustro/ na VPS. Fix: wyczyścić stare pliki (observer już je przetworzył); docelowo polityka retencji w event-store.


rsync --omit-dir-times (node-agent)

Data: 2026-06-09 Źródło: flota recovery session Objaw: rsync exit code 23 po każdym push — set-times na katalogu /opt/homelab/events/ zwraca EPERM (oskar nie jest właścicielem katalogu; aerbot jest). Pliki są kopiowane poprawnie, ale exit 23 zaśmieca logi i może maskować prawdziwe błędy. Fix: dodać --omit-dir-times do wywołania rsync w node-agent.py. Lokalizacja: services/node-agent/src/node_agent.py — wywołanie rsync w pętli push. Update 2026-06-11: potwierdzone flotowo — każdy node loguje fałszywe "Event shipping failed" (rsync code 23) co cykl, mimo że pliki przechodzą; katalogi /opt/homelab/events/* na VPS należą do aerbot, klient nie ustawi na nich czasów.


Deklaratywny zapis oskar ∈ aerbot w manifeście VPS

Data: 2026-06-09 Źródło: flota recovery — root cause: oskar spoza grupy aerbot(1000) → rsync Permission denied Problem: przynależność do grupy jest zarządzana ręcznie (usermod -aG 1000 oskar ad-hoc). Brak gwarancji po przeinstalowaniu VPS lub zmianie usera. Fix: dodać do hosts/vps/host.yaml lub hosts/vps/capabilities.yaml sekcję users: oskar: groups: [aerbot] — i wyegzekwować w deploy/bootstrap skrypcie VPS. Alternatywa: zmienić właściciela /opt/homelab/events/ na oskar:oskar i zaktualizować node-agent deploy skrypty.


Rozdzielenie worktree per task (agent.sh)

Data: 2026-06-09 Źródło: sesja — homelab-codex-ws-node-onboarding używany raz dla task/node-onboarding, raz dla task/fix-event-bloat przez ręczne git checkout. Problem: jeden worktree współdzielony przez dwa branche = anty-wzorzec. git branch mogło wskazywać zły branch; + w listingu = pozornie "w innym worktree" ale nieprawda. Prowadzi do commitowania na złej gałęzi. Fix: egzekwować — jeden task = jeden worktree (agent.sh new <task-name>). Przy wejściu do worktree zawsze git branch --show-current i weryfikacja .agent-task. Długoterminowo: agent.sh new powinien odmawiać jeśli żądana gałąź jest już sprawdzona.


Zamknięte

Publiczny bind operator_ui.py:18180 na VPS bez autoryzacji — NAPRAWIONE (2026-07-22, commit 9a5c160)

Data: 2026-07-22 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Było: operator_ui.py (port 18180) nasłuchiwał na 0.0.0.0 na publicznym VPS (135.181.153.108) bez żadnej autoryzacji. curl z zewnątrz do /actions zwracał HTTP 200. do_POST /action/mutate przenosi akcje między stanami WŁĄCZNIE z "approved" — dowolna osoba z internetu mogła zatwierdzić akcję remediacyjną. Jedyne co chroniło do tej pory: executor nie umiał jeszcze wykonać akcji (brak SSH, patrz wpis niżej) — przypadek, nie zabezpieczenie. Naprawione: dual-bind wg wzorca fleet-prometheus — 127.0.0.1:18180:8080 + ${TAILSCALE_BIND_IP}:18180:8080, nowy services/control-plane/env.example. Zweryfikowane po deployu: publiczny IP → HTTP 000, Tailscale → HTTP 200. Konsumenty (node-agent VPS, materializer PIHA) nietknięte. Footgun zapamiętany: brak .envdocker compose tylko OSTRZEGA i po cichu wraca do bindu 0.0.0.0, nie failuje — patrz wpis „deploy-local.sh: brak twardego checka .env" w Aktywnych. Pozostaje osobno: operator_ui.py nadal bez żadnej autoryzacji (bind zamknięty chroni przed internetem, nie przed kimkolwiek w mesh Tailscale) — patrz Aktywne.


Remediacja floty bez SSH: executor→node-agent pull przez rsync — ZROBIONE (2026-07-22, commit 2dac154), E2E potwierdzone 2026-07-23

Data: 2026-07-22 (implementacja), 2026-07-23 (pierwszy udany cykl E2E) Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Było: łańcuch supervisor→pending→approved→running działał, ale executor próbował ssh oskar@{node} docker restart {container} — kontener control-plane nie ma klienta ssh, klucza, ani rozwiązywalnych nazw węzłów. Wykonanie zawsze failowało. Decyzja architektoniczna: bez SSH w executorze (kontener na publicznym VPS z powłoką na całą flotę = zły blast radius). Kierunek PULL: executor zleca (zapis actions/dispatch/<node>/<action_id>.json), node-agent na docelowym węźle wykonuje lokalnie przez własny docker.sock, wynik wraca eventem action_result przez istniejący rsync-push. VPS nigdy nie inicjuje połączenia do węzła. Zabezpieczenia: walidacja node == self.node_name; whitelist typów akcji = {container_restart}; guard przed restartem samego node-agenta; brak wykonywania dowolnych poleceń z payloadu; idempotencja (ponowne zlecenie = no-op). 183 testy. Potwierdzone w boju (2026-07-23): cykl test-e2e-b — Executing → Dispatched → Completed w 31 sekund, node_exporter na PIHA realnie zrestartowany (uptime 30h→ minuty), zero połączeń SSH. Pierwszy w historii systemu pełny cykl remediacji.


Uprawnienia actions/ na PIHA (uid/gid oskar 1004 vs kontener 1000) — NAPRAWIONE ręcznie (2026-07-23, poza repo)

Data: 2026-07-23 Źródło: sesja 2026-07-22/23 (docs/sessions/2026-07-23-control-plane-remediation-e2e.md) Było: pierwszy test E2E remediacji bez SSH padał — agent logował [Errno 13] Permission denied: /opt/homelab/actions/dispatch co cykl. /opt/homelab/actions było oskar:oskar drwxr-xr-x (utworzone w maju), a działający wzorzec to /opt/homelab/events = oskar:pi drwxrwsr-x (grupa pi, zapis grupowy, setgid). Ten sam motyw uid/gid (host oskar 1004 vs kontener 1000) uderzył już czwarty raz — patrz sekcja „Tech-debt: globalny porządek uid/gid/uprawnień we flocie". Naprawione (ręcznie, tylko na PIHA): chgrp -R pi + chmod -R g+w + chmod g+s na /opt/homelab/actions. Executor zachował się poprawnie podczas awarii: po 300s timeoutu przeniósł akcję do failed z czytelnym powodem, nic nie zawisło. Pozostaje osobno: fix zastosowany TYLKO na PIHA — SOLARIA i lustro niezweryfikowane; docelowy fix systemowy to node-agent tworzący dispatch/<node>/ z właściwymi prawami przy starcie — patrz Aktywne.


Supervisor: pętla zamrożona ~24h, healthy ale nie tika — NAPRAWIONE (2026-07-16, commit 409b583)

Data: 2026-07-16 Źródło: sesja 2026-07-16 (docs/sessions/2026-07-16.md) — wątek control-plane Było: kontener supervisora healthy, ale pętla reconcile() nie tikała od ~24h, zero logów. Root cause zweryfikowany na /proc (State:S hrtimer_nanosleep, NIE deadlock): glob po EVENTS_DIR co cykl przy 358k plikach → cykl przekracza brak timeoutu → nigdy się nie kończy. Logi na DEBUG maskowały objaw. Naprawione: każdy cykl w ThreadPoolExecutor z future.result(timeout=90s, env SUPERVISOR_RECONCILE_TIMEOUT); try/except owija cykl (wyjątek nie zabija pętli); tick-log co 10 cykli (SUPERVISOR_TICK_LOG_EVERY) na INFO; healthcheck sprawdza świeżość heartbeat, nie tylko czy proces żyje. Zweryfikowane w boju: pętla tika (cycle #340→#480), cykl #1 timeoutował ale pętla kontynuowała = odporność działa. Lekcja: „healthy kontener ≠ tikająca pętla" — healthcheck musi sprawdzać świeżość ostatniego cyklu, nie samo czy proces odpowiada.


Event flood 358k plików + retencja martwa od fixu checkpointu — NAPRAWIONE (2026-07-16, commit dff76ec)

Data: 2026-07-16 Źródło: sesja 2026-07-16 (docs/sessions/2026-07-16.md) — wątek control-plane Było: EVENTS_DIR = 358k plików, 91% to service_healthy emitowany co cykl per serwis (stan-jako-zdarzenie, antywzorzec). Retencja _cleanup_control_plane_fs() była martwa od fixu checkpointu d5139c9 (2026-07-15, patrz „Bug: checkpoint observera po ścieżce leksykalnej" niżej) — porównanie str(ścieżka) <= checkpoint_int rzucało TypeError cicho łapany przez szeroki except → backlog rósł bez ograniczenia. Naprawiając checkpoint wczoraj, złamaliśmy retencję, która na nim polegała. Naprawione: (a) node-agent emituje service_healthy tylko na transition unhealthy→healthy (funkcja dla observer.process_event zachowana); (b) retencja naprawiona epoch-do-epoch; (c) scripts/maintenance/cleanup_event_backlog.py (dry-run + --apply). 150 testów pass. Cleanup wykonany na PIHA+VPS: 272 232 pliki usunięte, backlog 358k→12,7k. Wynik: reconcile supervisora przestał timeoutować. Lekcja: migracja typu pola (ścieżka→epoch int) musi audytować WSZYSTKICH konsumentów tego pola — szeroki except maskował dokładnie tę klasę regresji.


Ghost kontenery w panelu (Problem B) — ZNIKNĘŁY (potwierdzone reconem 2026-07-02)

⚠️ ZDEZAKTUALIZOWANE 2026-07-06: ghosty znów widoczne na VPS — patrz wpis „Ghosty B WRÓCIŁY" w Aktywnych (docs/sessions/2026-07-06.md).

Data: 2026-06-24 (wykryte), 2026-07-02 (zamknięte) Źródło: sesje 2026-06-24/25/26; recon docs/infra/inventory-verify-2026-07-02.md Było: martwe kontenery ze starych project-name'ów (8547b46c0317_control-plane-supervisor itp.) raportowane przez observera jako errorSystem Status ERROR w panelu mimo zdrowego mózgu. Zamknięte: recon 2026-07-02 — 24 kontenery na VPS, ZERO hash-prefixed. Prawdopodobnie recreate'y z kolejnych deployów je zmiotły. Zero akcji ręcznej. Rozważenie czyszczenia obcych project-name przy deployu — już nieaktualne (fix A 3b71707 blokuje źródło divergence).


brain-watchdog: poll Prometheus — POTWIERDZONY (recon 2026-07-02)

Data: 2026-06-30 (pending), 2026-07-02 (zamknięte) Źródło: recon docs/infra/inventory-verify-2026-07-02.md Było: log startowy nie wypisuje PROMETHEUS_URL → brak pewności, że polling aktywny; diagnoza opierała się na .env i braku błędów. Zamknięte: recon potwierdził — obraz zbudowany po 62d6fc0, PROMETHEUS_URL w .env I w env kontenera, zero poll failed w logach. Poll aktywny. Jednolinijkowy log startowy (polling enabled/disabled) pozostaje opcjonalną kosmetyką. Pełne end-to-end (firing → Telegram) POTWIERDZONE 2026-07-06 testem AlertTestEtap0 (Etap 0 cutoveru, docs/sessions/2026-07-06.md) — bez czekania na realną awarię.


Miny #1/#2/#3 z weryfikacji inwentaryzacji — ROZBROJONE (2026-07-02)

Źródło: docs/infra/inventory-verify-2026-07-02.md, sesja docs/sessions/2026-07-02.md (tam szczegóły i lekcje).

  • #1 PIHA checkout: gałąź wciąż task/kb-gmail-import po resecie z 2026-06-30 (reset --hard przesuwa gałąź, nie przełącza) → checkout master && pull, 30 commitów nadrobione.
  • #2 control-plane na SATURN: supervisor ślepy (brak mountu repo) → docker compose down, wolumeny zachowane. Jedyny mózg = VPS.
  • #3 owner_node: forgejo→piha, mosquitto→vps (commit 886bc85).

🔴 KRYTYCZNY — deploy.sh vps niszczył control-plane — NAPRAWIONE (commit 3b71707)

Data: 2026-06-24 (wykryte), 2026-06-25 (incydent w produkcji), 2026-06-26 (naprawione) Źródło: sesja 2026-06-24 + 2026-06-25 + 2026-06-26 (docs/sessions/2026-06-26.md) Było (Problem A): control-plane w hosts/vps/services.yaml jako zwykły serwis pętli deploy-node.sh. Pętla używała innego COMPOSE_PROJECT_NAME niż deploy-local.sh (cwd=services/control-plane). Niezgodność → Recreate → No such container: <hash>_control-plane-observerset -e przerywa pętlę → observer/supervisor/executor/ui znikają. Każdy deploy.sh vps rozkładał mózg (potwierdzone w produkcji 2026-06-25). Naprawione: guard w deploy-node.sh pomijający serwisy z własnym services/<svc>/deploy-local.sh. control-plane ZOSTAJE w services.yaml (gate pytest+build nadal go testuje), pomijana jest tylko destrukcyjna pętla deployu. Potwierdzone w boju: deploy.sh vps wypisał Skipping control-plane: ma własną ścieżkę deployu, mózg Up 25h healthy, nietknięty. Pozostało osobno: ghosty z poprzednich rozjazdów (Problem B — patrz Aktywne).


Flaky testy control-plane — state-leak w pytest — NAPRAWIONE (commit 992ff7c)

Data: 2026-06-24 (zgłoszone), 2026-06-25 (naprawione) Źródło: sesja 2026-06-24 + sesja 2026-06-25 (docs/sessions/2026-06-25.md) Było: test_incident_lifecycle.py flaky przez state-leak — OBSERVER_STATE_FILE wyprowadzany przy imporcie, helpery patchowały STATE_DIR ale nie OBSERVER_STATE_FILE → checkpointy pisane na realny dysk /opt/homelab/state/ z ścieżkami otagowanymi numerem przebiegu pytest. Gate deploy.sh czerwony przy zdrowym kodzie. Naprawione: autouse monkeypatch fixture redirectujący WSZYSTKIE ścieżki stanu w tym OBSERVER_STATE_FILE; usunięto buggy _make_observer; posprzątano zatruty realny checkpoint. Weryfikacja: 6/6 przebiegów → 28 passed. Gate rzetelny.


deploy-node.sh — brak --env-file (bind 0.0.0.0) — NAPRAWIONE (commit 686aca7)

Data: 2026-06-25 Źródło: sesja 2026-06-25 (docs/sessions/2026-06-25.md) Było: deploy-node.sh nie przekazywał --env-file do compose up → zmienne .env (jak TAILSCALE_BIND_IP) interpolowały się do pustego stringa → bind 0.0.0.0 zamiast Tailscale IP. Potencjalna dziura na publicznym VPS. Naprawione: guard if [ -f "services/<svc>/.env" ] + --env-file per-serwis przed docker compose up. Worktree task/deploy-envfile-fix, merge ff-only.


Swap 24 GB na VPS — ZROBIONE

Data: 2026-06-22 (zgłoszone PENDING w sesji 2026-06-09 flota-recovery) Źródło: sesja 2026-06-22 (docs/sessions/2026-06-22.md) Było: VPS (3.7 GB RAM) z swap=0 → OOM 2026-06-01. Zrobione: /swapfile 4 GB aktywny i trwały (wpis w /etc/fstab); vm.swappiness=10 na żywo i w /etc/sysctl.conf. Weryfikacja: free -hSwap: 4.0Gi (0B used), /proc/swaps zawiera /swapfile. Uwaga: host-level one-off (nie czysto GitOps), udokumentowany jako celowy host-state w hosts/vps/host.yaml. Brak commitu kodu — zmiana host-side gotowcem.


Observer staleness — martwy node pokazywany NOMINAL

Data: 2026-06-08 (złapane), status: OTWARTY w sensie implementacji Problem: observer/supervisor trzyma ostatni znany stan; brak heartbeat TTL. Chelsty-infra milczy, ale status NOMINAL podważa zaufanie do panelu. Fix: heartbeat TTL → po przekroczeniu oznacz status stale lub down. Powiązane: brain-watchdog ślepy na per-node freshness. (Otwarty jako TODO implementacyjny — przeniesiony z sesji 2026-06-08)

Anomaly detection liveness — mózg uczy się wzorca dobowego per node (pomysł 2026-06-26)

Idea: zamiast statycznych okien czasowych w regułach alertowych (np. "lustro 7-23"), mózg (supervisor/observer) czyta historię metryk z Prometheus (range queries / Grafana) i SAM wykrywa wzorzec dobowy każdego węzła. up==0 zgodne z nauczonym wzorcem offline (lustro zwykle off nocą, solaria nieregularnie) = NIE anomalia, nie alarmuj. up==0 odbiegające od wzorca = realna awaria → alert. Inteligencja w mózgu + dane jako źródło wzorca, nie sztywne godziny wpisywane ręcznie.

Warunek: wymaga TYGODNI historii metryk. fleet-prometheus postawiony 2026-06-25 → realne dopiero za ~2-4 tygodnie, gdy uzbiera się wzorzec dobowy.

Pułapka: uczący się system może przeoczyć realną awarię pokrywającą się z typowym oknem offline (statyczna reguła jest głupia, ale przewidywalna). Uwzględnić przy projektowaniu.

Na teraz: targety scrape'owane BEZ polityki alertowej, label tylko node:. Prometheus gromadzi historię. Anomaly detection = osobny świadomy projekt później (CC, z testami).


Rozjazdy repo<->rzeczywistosc (z inwentaryzacji 2026-06-30)

Zrodlo: docs/infra/inventory-2026-06-30.md (23 rozjazdy, pelna tabela tam). Weryfikacja 2026-07-02: docs/infra/inventory-verify-2026-07-02.md — bilans: 20 wciaz aktualnych, 2 zmienione, 1 wyjasniony (storage SOLARIA = partycja Windows dual-boot, NIE rozjazd — zdjety z listy). Ponizej te wymagajace akcji, pogrupowane wg ryzyka. Naprawa = osobny task/kilka.

Grupa A — czyste docs, zero ryzyka

  • ZROBIONE (2026-07-02, commit 886bc85) — forgejo service.yaml owner_node: saturn -> piha (biega na PIHA always-on)
  • ZROBIONE (2026-07-02, commit 886bc85) — mosquitto service.yaml owner_node: piha -> vps (biega na VPS, nie na PIHA)
  • capabilities SATURN: RAM 8 -> 14GiB; dysk sd-card 64GB -> /dev/sda 159GB
  • capabilities SOLARIA: CPU 24 -> 32 nproc
  • hosts/saturn/services.yaml nie istnieje — 5 kontenerow bez deklaracji
  • hosts/vps/services.yaml niekompletne (4 z 9 z topology); solaria tez (brak planner-agent)

Grupa B — wymaga decyzji

  • npm x2: PIHA (LAN ingress :80/:443) + VPS (public). Repo zna jedna (owner=vps). Decyzja: zostawic oba (intentional, wildcard cert via NPM@PIHA) czy usunac PIHA? Jesli oba zamierzone -> dodac piha do service.yaml + hosts/piha.
  • ZROBIONE (2026-07-02) — control-plane na SATURN: docker compose down (wolumeny zachowane). Supervisor byl SLEPY (brak mountu repo, WARNING loop "Hosts directory /repo/hosts does not exist" co 30s) — zero ryzyka zdublowanych remediacji przez te 3 dni. Jedyny control-plane = produkcyjny na VPS.
  • ollama: service.yaml owner=solaria ale NIE biega. Wdrozyc czy wyrzucic z repo?

Grupa C — sprzatanie

  • control-plane-ui healthcheck: uzywa curl ktorego NIE MA w obrazie -> failuje w kolko -> UNHEALTHY + log spam (4.2G syslog na SATURN). Fix: wget/nc w healthcheck albo curl w Dockerfile. (Przyczyna rozjazdu #6 znaleziona przy gaszeniu dysku.)
  • homeassistant5 na PIHA (HA "ken" :8123) niedeklarowany -> dodac do hosts/piha + topology
  • VPS: outline-postgres-1 anonimowy image (4e6e670bb069) -> named tag; humanai-landing/mailer/umami do repo. joplin-db postgres:18 -> 17/16 (ocena zdezaktualizowana 2026-07-02: PG18 GA od 09/2025, nie pre-release — bez akcji)
  • PIHA: 33 shadow kontenery poza GitOps (immich, vaultwarden, wikijs, actual, audiobookshelf, elasticsearch, grafana, prom, portainer, code-server, diskover...) -> audyt + stopniowo do hosts/piha/services.yaml
  • zigbee2mqtt topology mowi chelsty-infra, biega na PIHA -> poprawic topology
  • stability-agent / node_exporter owner_node single, biegaja wielomiejscowo -> per-host

Followupy z weryfikacji + rozbrajania min (2026-07-02)

Zrodlo: docs/infra/inventory-verify-2026-07-02.md + sesja 2026-07-02. Zgloszone przy fixie owner_node (886bc85), swiadomie NIE ruszone — osobne decyzje.

  • forgejo brak wpisu w hosts/piha/services.yaml; mosquitto brak w hosts/vps/services.yaml — schemat hostowy wymaga role/exposure/depends_on (miny #2/#3/#16 z audytu).
  • mosquitto na VPS bez mem_limit override w hosts/vps/runtime/ — narusza konwencje CLAUDE.md (kazdy serwis VPS deklaruje mem_limit).
  • drugi mosquitto na chelsty-infra (offline'owa instancja) — pojedyncze owner_node jej nie opisuje; wzorzec per-host jak stability-agent / node_exporter (miny #17/#18).
  • topology.yaml:75: mosquitto zadeklarowany tez jako komponent ai-cluster — rozstrzygnac, czyj jest broker :1883.
  • pi-watchtower-1 na LUSTRO w restart-loopie (nowe z reconu; node-agent healthy).
  • alias lustro nie rezolwuje z SOLARII (nowe z reconu).
  • fleet-prometheus bez formalnego override mem_limit w hosts/vps/runtime/ — limit siedzi w bazowym compose (kosmetyka).

Po odchudzaniu PIHA (2026-07-02, faza 2 modulu 0)

  • llm-gateway: zlokalizowac/zarchiwizowac zrodlo — kod (wlasny FastAPI router -> Ollama@SOLARIA) moze zyc TYLKO w /opt/llm-gateway na PIHA, bez gita; przeszukanie PIHA i repo nie znalazlo innej kopii. Zarchiwizowac do repo/Forgejo zanim padnie nosnik.
  • Prometheus@PIHA: target llm-gateway blednie nazwany watchtower — celuje w :8080 i odpytuje /v1/metrics, dostaje wieczne 404 (llm-gateway nie serwuje metryk). Naprawic nazwe/endpoint albo usunac target.

Tech debt SATURN (z gaszenia dysku 2026-06-30)

  • /opt/anaconda3 16G — najwiekszy pojedynczy zjadacz dysku (env-y Pythona). Decyzja Oskara kiedy/czy czyscic.
  • Dysk 91% -> 83% ugaszone (docker prune + journal + syslog), ale /home zaszyfrowany i ciasny strukturalnie. SATURN dzwiga dev + drugi control-plane + agent-webui — napiecie.

Tech-debt: globalny porządek uid/gid/uprawnień we flocie (2026-07-10)

Diagnoza. Flota NIE ma spójnej mapy uid/gid. "oskar" ma różne uid per host (PIHA: 1004, inne hosty: prawdopodobnie 1000/inne). Kontenery agentów zakładają uid 1000 (user "homelab"). Bind-mounty przenoszą SUROWE uid (nie nazwy) między hostem a kontenerem → gdy uid hosta ≠ uid zakładany przez kontener, pliki stają się "cudze" i wybucha cicha awaria (klucz nieczytelny, rsync nie tworzy plików, socket permission denied). To NIE są przypadki — to systemowy brak kanonicznej mapy uid/gid.

Historia incydentów (dowód że systemowe):

  • 2026-07-10: node-agent PIHA (uid 1000 homelab) montował /home/oskar/.ssh (pliki uid 1004) → "Load key id_rsa: Permission denied" → rsync padał → 21 dni bez eventów (wykryte przez shadow-read). Fix: dedykowany /opt/homelab/agent-ssh chown 1000.
  • Wcześniej: oskar spoza grupy aerbot na VPS → rsync push nie tworzył plików → brak cleanup → 8-dniowa cicha awaria floty. Fix: usermod -aG aerbot oskar.
  • 2026-07-10 (świeże, PENDING): node-agent PIHA "Docker unavailable: PermissionError(13)" po recreate — agent nie czyta /var/run/docker.sock (grupa docker/uid). Osobny od shippingu (nie blokuje eventów), ale ten sam rodzaj problemu — do naprawy (grupa docker w kontenerze / gid socketu).
  • LUSTRO uid pi=1000 vs PIHA oskar=1004 — różne uid "pierwszego usera" per host.

Kierunek naprawy (do rozważenia, osobny projekt):

  • Ustalić KANONICZNE uid/gid per rola: agent=1000 wszędzie; dedykowane grupy dla współdzielonych zasobów (aerbot dla events/rsync-sink na VPS, docker dla socketu).
  • Audyt id <user> na KAŻDYM hoście floty (saturn/solaria/piha/vps/lustro) — zmapować realne uid/gid, udokumentować rozjazdy.
  • Rozważyć deklaratywny zapis oczekiwanych uid/gid w hosts/*/host.yaml lub capabilities.yaml (żeby deploy mógł weryfikować/wymuszać spójność).
  • Agenci NIE powinni montować prywatnego .ssh użytkownika — zawsze dedykowany katalog z własnym kluczem pod właściwym uid (wzorzec z fixa 2026-07-10).

Bug: checkpoint observera po ścieżce leksykalnej — kruchy, zatruwa węzeł na zawsze (2026-07-12)

Objaw. PIHA była "martwa" dla observera ~34 dni mimo działającego node-agenta. Eventy dojeżdżały na VPS (7344 plików w /opt/homelab/events/piha/), ale observer ich NIE konsumował — last_seen nie drgnął, shadow-read logował SHADOW_LIVENESS_MISMATCH node=piha event=dead prom=up z rosnącym wiekiem.

Root cause. observer_checkpoint.json trzyma per-węzeł ostatnio przetworzoną ŚCIEŻKĘ i porównuje ją LEKSYKALNIE (stringowo), awansując tylko "do przodu". Checkpoint PIHA utknął na evt-unknown-1781254800-ha_update_available-homeassistant-951.json (event z HA, który wpadł do katalogu piha/ z node="unknown"). Nowe eventy nazywają się evt-piha-<ts>-..., a leksykalnie "evt-piha-…" < "evt-unknown-…" (bo p < u), więc KAŻDY nowy event był uznawany za starszy niż checkpoint i pomijany.

Fix doraźny (zastosowany). Usunięcie wpisu piha z node_checkpoints + restart observera → 7344 eventy przetworzone, last_seen_age spadł z 2 082 036 s (~24 dni) do 19 s, status=online/fresh, mismatch zniknął.

Fix systemowy (ZROBIONE 2026-07-14, task/fix-observer-checkpoint). Checkpoint per-węzeł trzyma teraz TIMESTAMP (int epoch), nie ścieżkę. „Nowy event" = ts_z_nazwy_pliku > checkpoint_ts_węzła; kolejność przetwarzania sortowana po timestampie, nie leksykalnie. Timestamp parsowany z nazwy evt-<node>-<unixts>-… (regex -(\d{9,11})-, ten sam co operator_ui._event_file_ts); fallback na mtime gdy nazwa nie pasuje — nieparsowalna nazwa NIGDY nie zwraca 0 (0 = leksykalne „starszy niż checkpoint" = dokładnie ten poison). Migracja starych checkpointów (ścieżka→ts) przy starcie; nieparsowalna wartość → 0 (reprocess wszystkiego — bezpieczne, process_event jest idempotentne na last_seen/world_state; lepiej przetworzyć duplikaty niż zgubić węzeł). Testy regresyjne w test_incident_lifecycle.py (sekcja 9). Znany, akceptowalny warunek brzegowy: strict > może pominąć event o ts == checkpoint dostarczony w PÓŹNIEJSZYM cyklu niż inne eventy z tej samej sekundy — nierealne przy cadence shippingu (rsync co 60 s wysyła całą partię danej sekundy razem; kolejne partie są ~60 s od siebie).

Uwaga do idempotencji (zbadane). Reprocess tego samego eventu NIE psuje world_state (status/last_seen deterministyczne, resolve incydentu guardowany na status=="active"), ALE _handle_incident/deployment_* inkrementują occurrence_count i dopisują do events[] przy każdym przetworzeniu — reprocess (np. jednorazowo po migracji) zawyża te liczniki. To kosmetyka, nie korupcja stanu. Docelowo można dedupować po event.id w events[] — osobny, drobny task.

Bug: ha-diag-agent emituje eventy z node="unknown" do katalogu innego węzła (2026-07-14) — ZROBIONE (2026-07-15, f2ba81b)

Kontekst. To był plik-truciciel z buga checkpointu wyżej: evt-unknown-1781254800-ha_update_available-homeassistant-951.json w events/piha/. Node w evencie = unknown, ale plik wylądował w katalogu piha/. Sufiks -951 to _seq emittera → agent nachodził długo, wyemitował 951 eventów, wszystkie jako node="unknown".

Root cause (config-wiring). Tożsamość agenta (node_name) i KATALOG eventów pochodzą z DWÓCH niezależnych źródeł:

  • services/ha-diag-agent/src/ha_diag/config.py:20node_name: str = "unknown" (domyślne, gdy env NODE_NAME nie dotrze do procesu w kontenerze).
  • services/ha-diag-agent/docker-compose.yml:12 → wolumen /opt/homelab/events/${NODE_NAME:-ha-diag}:/events${NODE_NAME} jest interpolowane po stronie HOSTA (compose), a katalog jest dodatkowo twardo przypięty do piha w hosts/piha/runtime/ha-diag-agent/docker-compose.override.yml.

Jeśli NODE_NAME trafi do interpolacji wolumenu/override (→ piha), ale NIE do environment: procesu (albo Settings.load() przez os.environ.setdefault go nie nadpisze), aplikacja czyta node_name="unknown" i pisze eventy node="unknown" do katalogu events/piha/. Rozjazd między nazwą w evencie a katalogiem docelowym.

Skutek. Poza zatruciem checkpointu (już naprawione osobno): eventy node="unknown" są bezużyteczne dla world_state (observer tworzy węzeł-widmo unknown, potem prune go kasuje bo nie ma go w topologii) — realny sygnał z ha-diag na piha przepada.

Fix — ZROBIONE (2026-07-15, f2ba81b, docs/sessions/2026-07-15.md). Wariant (a): node_name NIGDY nie może być "unknown" w produkcji. config.py Field(default="unknown", validate_default=True) + validator odrzuca ""/"unknown"; main.pySystemExit(1) FATAL przy braku NODE_NAME; EventEmitter.__init__ jako ostatnia bramka przed nazwą pliku eventu. +18 testów, 0 regresji. Zmergowany i zdeployowany na PIHA (rebuild, NODE_NAME=piha dochodzi do procesu). Pliki evt-unknown-* na VPS/PIHA: 0 (potwierdzone). Pozostaje osobno (druga warstwa obrony, nadal TODO): observer/emitter powinien docelowo odrzucać/kwarantannować event, którego node w treści != katalog docelowy — dzisiejszy fix zamyka źródło (unknown nie powstaje), ale nie waliduje spójności node↔katalog dla innych, przyszłych źródeł eventów.

Bug: deploy-local.sh control-plane pada na ghost-kontenerach i zostawia mózg rozłożony (2026-07-12)

Objaw. bash deploy-local.sh przerwał się w połowie: Error response from daemon: No such container: 5ee6fec5455d_control-plane-ui → deploy abort → ZERO kontenerów control-plane (docker ps -a pusty). Mózg całkowicie down. Zdarzyło się DWA RAZY tego samego dnia. Ratunek: ponowny deploy-local.sh (gdy ghost już zniknął, compose stawia czysto).

Root cause (podejrzenie). Ten sam COMPOSE_PROJECT_NAME divergence / ghost hash-prefixed containers, co przy deploy.sh vps (naprawione guardem 3b71707). Compose próbuje Recreate kontenera pod nazwą <hash>_control-plane-ui, której Docker już nie zna → błąd → set -e przerywa deploy w połowie.

Fix (DO ZROBIENIA). deploy-local.sh powinien być odporny: cleanup ghostów przed recreate (docker compose down --remove-orphans albo jawne usunięcie hash-prefixed kontenerów), ewentualnie jawny -p (COMPOSE_PROJECT_NAME) żeby nazwy były deterministyczne. Deploy mózgu NIE MOŻE zostawiać control-plane w stanie zero-kontenerów.

Fix: paperless-worker@SOLARIA — dwa bugi configu, naprawione i zweryfikowane na żywo (2026-07-12)

Kontekst. Moduł 3 (services/paperless-worker/, split-host OCR worker) był zdeployowany i brał zadania z kolejki, ale miał dwa bugi w compose:

  1. command: celery ... nie odpalał celery. Obraz paperless-ngx (/sbin/docker-entrypoint.sh) routuje każdy argument NIE zaczynający się od / do manage.py — więc celery lądował jako nieznana subkomenda Django, nie jako program. Fix: command: /usr/sbin/gosu paperless /usr/local/bin/celery ... (ścieżka absolutna wchodzi w gałąź exec "$@" entrypointu; gosu paperless z przodu bo ta gałąź nie dostaje automatycznego gosu, inaczej proces poszedłby jako root i zepsuł właściciela plików na NFS).
  2. Brak współdzielonego SCRATCH_DIR. Paperless@PIHA staguje wgrywany plik w /tmp/paperless (domyślny SCRATCH_DIR) i niesie tę ścieżkę w payloadzie zadania celery jako ścieżkę absolutną. Worker@SOLARIA miał własny, lokalny /tmp/paperless — gdy odbierał zadanie zamiast workera PIHA, padał Cannot consume ...: File not found. Fix: NFS volume paperless_scratch (ten sam wzorzec co data/media/consume) na /opt/homelab/data/paperless/scratch (już istniał na PIHA, chown 1000:1000), mount na /tmp/paperless po obu stronach.

Oba fixy + uzasadnienie: services/paperless/docker-compose.yml, services/paperless-worker/docker-compose.yml, services/paperless-worker/README.md. Zweryfikowane end-to-end na żywo (branch task/paperless-worker-fix, jeszcze niezmergowany do master w momencie pisania tego wpisu): 3 dokumenty testowe wrzucone do consume/ na PIHA, jeden odebrany i dokończony przez worker@SOLARIA (log: ocrmypdf/tesseractConsumeTaskPlugin completed with: Success), zero File not found. Dokumenty testowe usunięte po teście (document.delete()

  • ręczny cleanup plików) — produkcyjne 6 dokumentów nietknięte.

Otwarte (świadomie odłożone, nie blokuje działania):

  • hosts/solaria/services.yaml i inventory/topology.yaml nie mają wpisu paperless-worker (SOLARIA ma tam tylko node-agent) — było zaplanowane w cutover checkliście README jako krok "przy deployu", ale nigdy nie zrobione. Bez tego wpisu supervisor/observer nie widzą tego serwisu w desired-state — drift (np. worker padnie i nie wstanie) nie zostanie automatycznie wykryty przez agent system, tylko przez brak przetwarzania kolejki.
  • Test formalnego fallbacku (stop worker@SOLARIA → kolejka mieli na PIHA → start → drenaż) nie był wykonany w tej sesji — mechanizm nie zmienił się tym fixem (był już OK), ale warto zweryfikować przy okazji.

Nowe serwisy KB nie sa w monitoringu (desired-state)

Data: 2026-07-12

Problem: Zdeployowane serwisy filaru dokumentow nie maja wpisow w hosts/*/services.yaml i inventory/topology.yaml, wiec supervisor/observer ich NIE WIDZA w desired-state:

  • paperless + paperless-db + paperless-broker (PIHA) — Deploy 1, 2026-07-10
  • paperless-worker (SOLARIA) — Deploy 2, 2026-07-12 (SOLARIA ma tam tylko node-agent)

Skutek: drift nie jest wykrywany. Jesli worker padnie i nie wstanie, albo paperless przestanie dzialac — agent system tego nie zglosi. Dowiesz sie dopiero po tym, ze kolejka nie jest przetwarzana / strona nie odpowiada.

Fix: dodac wpisy do hosts/piha/services.yaml, hosts/solaria/services.yaml, inventory/topology.yaml. Zweryfikowac ze observer/supervisor je widza (healthcheck, liveness). Dotyczy tez przyszlych: nextcloud, gokapi.

Zasada na przyszlosc: rejestracja w services.yaml/topology to CZESC deployu, nie osobny krok "kiedys" — inaczej kazdy nowy serwis to slepy punkt monitoringu.

Bug: deploy-node.sh nie przebudowuje obrazu — deploy "OK" ale nowy kod nie wchodzi (2026-07-15) — ZROBIONE (2026-07-16, commit 77defff)

Objaw. deploy-node.sh <serwis> robi docker compose up -d BEZ --build. Dla serwisów z Dockerfile (ha-diag-agent, node-agent, llm-gateway, brain-watchdog, itd.), gdy zmienia się TYLKO kod (nie compose/env), compose widzi "kontener działa, obraz ten sam" → status Running (0.0s), NIE przebudowuje i NIE recreatuje. Nowy kod z repo NIE wchodzi w życie mimo git pull i "Deployment Complete".

Skutek — cicha rozbieżność repo↔runtime. Deploy raportuje sukces, a kontener biega na starym obrazie. Ugryzło DWA razy: fleet-prometheus (config nie wchodził bez force-recreate) i ha-diag-agent 2026-07-15 (fix node_name był w repo f2ba81b, ale Running zamiast rebuild — trzeba było ręcznego docker compose up -d --build --force-recreate).

Root cause. deploy-node.sh (~linia 110) w pętli deployu: brak --build w wywołaniu compose. Docker cache'uje obraz po tagu, nie po zawartości src/. compose. Docker cache'uje obraz po tagu, nie po zawartości src/.

Fix — ZROBIONE (2026-07-16, 77defff). deploy-node.sh wywołuje teraz --build warunkowo, gdy serwis ma top-level Dockerfile (test -f services/<svc>/Dockerfile); prebuilt serwisy bez --build (no-op). Zweryfikowane w boju na PIHA: 6 serwisów (node-agent/ha-diag/brain-watchdog/llm-gateway → Building; vikunja/kb-postgres → prebuilt). Follow-up pozostawiony: agent-system ma build w podkatalogach bez top-level Dockerfile — niezarejestrowany przez tę detekcję, osobny task.

Ollama SOLARIA: brak sterownika NVIDII — ZAMKNIĘTE (2026-07-16)

Kontekst. Cutover 2026-07-15 (docs/infra/ollama-solaria-cutover-2026-07-15.md) odkrył, że SOLARIA nie miała zainstalowanego żadnego sterownika NVIDII — nvidia-smi nie istniał na hoście. hosts/solaria/services.yaml opisywał ollama jako "GPU-backed" od dawna, ale to było aspiracyjne — Ollama zawsze szła CPU-only. GPU reservation zakomentowana w services/ollama/docker-compose.yml (f57a01a); item trafił do backlogu jako blokujący fazę mailową embeddingów (moduł 5).

Fix (2026-07-16). Zainstalowany nvidia-driver-595-open z repo dystrybucji (nie stary PPA graphics-drivers dla jammy — zdezaktywowany przez rename na .disabled). RTX 4070 Ti SUPER 16GB, CUDA 13.2, nvidia-smi działa na hoście. nvidia-container-toolkit był już obecny (doinstalowany jako prerequisite przy cutoverze 07-15). GPU reservation przywrócona w compose. Pomiar throughput GPU vs CPU baseline (0.79s/chunk) — patrz jobs/documents-ingest/README.md, sekcja timing.

Status: ZAMKNIĘTE.

Follow-upy pozostawione (osobne taski):

  • Batching wywołań Ollamy — przed fazą mailową (225k kopert). Sekwencyjne wywołania /api/embeddings (nawet na GPU) będą wąskim gardłem przy takiej skali; ocenić równoległość/batch API Ollamy.
  • UNIQUE(envelope_id, chunk_index) bez model w document_chunk (services/kb-postgres/init/002_chunks.sql) — re-embedding innym modelem cicho no-opuje się przez istniejący constraint. Schema change do zrobienia przy fazie 3 (patrz jobs/documents-ingest/README.md, sekcja "Idempotency" kroku 6 embed).

Follow-upy z etapu 0 (truth cleanup, 2026-07-29)

Źródło: sesja merge task/etap0-truth (topologia: status active|dormant, listy serwisów usunięte z topology.yaml — node-level truth only).

  • 40-register.sh emituje stary schemat topologii — szablon bloku node'a w scripts/onboard/steps/40-register.sh nadal zawiera listę services: i nie ma pola status:; wyrównać z nowym schematem node-level-only (inventory/topology.yaml).
  • Historyczny komentarz mqtt_unreachable w observerzescripts/observer/ observer.py:866 wspomina routing mqtt_unreachable -> container_restart usunięty z supervisora (recon D15); sprzątnąć przy najbliższej edycji pliku.