homelab-codex-ws/docs/backlog.md
oskar 1cd6401adb mitigation(solaria): M1 — NODE_TYPE=lte_node wyłącza prune node-agenta
Mitygacja tymczasowa z incydentu 2026-07-30-ollama-solaria-vanish (§7, M1),
na czas backfillu embed (faza mailowa KB). Na SOLARII node-agent robił
niefiltrowany `docker container prune` co cykl (60 s), kasując również
kontenery z `restart: unless-stopped` zatrzymane świadomie przez operatora.

Zweryfikowane w kodzie (services/node-agent/src/node_agent.py, linie zgodne
z incydentem): `self.node_type` jest czytane wyłącznie w run_safe_cleanup()
(648, 654) i w dwóch liniach logu (250, 1103). `lte_node` daje wczesny return
w run_safe_cleanup — monitoring, eventy, dispatch akcji bez zmian.
_cleanup_control_plane_fs jest bramkowane node_name == VPS, nie node_type.
stability-agent nie prune'uje — node-agent był jedynym źródłem.

Ścieżka deployu zweryfikowana: deploy-service.sh:100 składa
${HOST_DIR}/runtime/${SERVICE}/docker-compose.override.yml, a HOST_DIR to
hosts/<node> w obu wywołaniach (deploy-node.sh:102 operatorskie,
deploy-runner.sh:170 agentowe). Dowód, że plik nie jest martwy: działający
kontener na SOLARII ma NODE_TYPE=ai_node, co występuje wyłącznie w tym pliku.
`docker compose config` na złożeniu daje NODE_TYPE=lte_node, group_add 999+996
zachowane, projekt "node-agent" (bez zmiany nazwy projektu).

Skutek uboczny: lte_node pomija CAŁY cleanup, więc dangling images i build
cache też nie są sprzątane — pilnować miejsca na dysku SOLARII.

Zdjąć po wdrożeniu R1 na nodzie → przywrócić NODE_TYPE=ai_node.
Nie zdeployowane — deploy po stronie operatora.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:49:22 +02:00

1355 lines
76 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Tech-debt backlog
Centralny tracker tech-długu i znanych usterek. Wpisy ze sesji — dodawaj z datą i kontekstem.
---
## M1 aktywna na SOLARII — cleanup node-agenta wyłączony do czasu R1 (2026-08-04)
**Data**: 2026-08-04
**Źródło**: `docs/incidents/2026-07-30-ollama-solaria-vanish.md` (§7, M1).
**Stan**: w `hosts/solaria/runtime/node-agent/docker-compose.override.yml` ustawiono
`NODE_TYPE=lte_node` (było `ai_node`) na czas backfillu embed (faza mailowa KB).
`lte_node` powoduje wczesny return w `run_safe_cleanup()`, więc na SOLARII nie działa
niefiltrowany `docker container prune` kasujący zatrzymane kontenery w ≤60 s —
łącznie z tymi, które mają `restart: unless-stopped` i zostały zatrzymane świadomie.
`self.node_type` jest czytane wyłącznie w `run_safe_cleanup()` i dwóch liniach logu,
więc monitoring, eventy i dispatch akcji działają bez zmian.
**Skutek uboczny**: `lte_node` pomija CAŁY cleanup — na czas mitygacji nie są sprzątane
dangling images ani build cache. Pilnować miejsca na dysku SOLARII.
**Nie zdeployowane** — zmiana jest tylko w repo (branch `task/m1-prune-mitigation`);
wchodzi przy najbliższym redeployu node-agenta na SOLARII.
**Zdjąć po**: wdrożeniu R1 (filtrowanie prune po restart policy / labelu compose) na tym
nodzie — wtedy przywrócić `NODE_TYPE=ai_node`. R1R3 w toku po stronie subsystemu A.
---
## 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 ssh**
`subprocess.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
### Bug upstreamu `GoogleCloudPlatform/knowledge-catalog`: generator grafu pomija linki od `/`
**Data**: 2026-07-31
**Źródło**: pilot fazy 5 — narty27 (`docs/sessions/2026-07-31-kb-f4-final-narty27.md`,
sekcja „Wnioski do przeniesienia na fazę 5", pkt 3)
**Problem**: generator grafu z `knowledge-catalog` pomija linki zaczynające się od `/`
(ścieżki absolutne), wbrew §5.1 **własnej spec** OKF, która je dopuszcza. Efekt: część
krawędzi grafu po prostu nie powstaje — cicho, bez ostrzeżenia. Rozbieżność
implementacja↔spec po stronie upstreamu, nie naszej konfiguracji.
**Obejście (zastosowane)**: własny generator grafu (cytoscape) w pilocie narty27 —
nie używamy generatora upstreamu.
**Fix**: zgłosić issue/PR do `GoogleCloudPlatform/knowledge-catalog` (minimalny repro:
dokument z linkiem `/foo` → brak krawędzi w wyjściu grafu, mimo §5.1). Jeśli upstream
naprawi — rozważyć powrót z własnego generatora przy fazie 5 (wiki-kompilat), żeby nie
utrzymywać własnego kodu bez potrzeby.
---
### `hosts/solaria/runtime/ollama/docker-compose.override.yml` — brak w repo (rozjazd repo↔runtime)
**Data**: 2026-07-31
**Źródło**: sesja 2026-07-31 (`docs/sessions/2026-07-31-kb-f4-final-narty27.md`,
sekcja „Otwarte po sesji" pkt 2)
**Problem**: `ollama` jest zadeklarowana w `hosts/solaria/services.yaml` (rola
`llm-inference`, exposure `private` = bind na `TAILSCALE_BIND_IP` + loopback), ale
`hosts/solaria/runtime/` zawiera wyłącznie `node-agent` i `stability-agent` — override
dla `ollama` **nie istnieje w repo**. Host-specific konfiguracja żywego kontenera
(rezerwacja GPU, bind, env) nie jest zatem wersjonowana: repo nie opisuje tego, co
faktycznie biega na SOLARII. Ta sama klasa rozjazdu repo↔runtime co przy węzłach
repo-less — dowolny redeploy z repo może wystawić serwis inaczej, niż działa dziś.
Wzorzec docelowy istnieje obok: `hosts/piha/runtime/ollama-piha/docker-compose.override.yml`.
**Fix**: zrekonstruować override z żywego kontenera na SOLARII (`docker inspect` →
bind/porty/GPU/env/limity) i zacommitować jako
`hosts/solaria/runtime/ollama/docker-compose.override.yml`; zweryfikować, że deploy
z repo daje kontener identyczny z obecnym **zanim** ktokolwiek zrobi redeploy.
---
### `services/narty27`: `exposure: private` w `service.yaml` + README vs faktyczna publiczna ekspozycja (NPM VPS + LE)
**Data**: 2026-07-31
**Źródło**: pilot fazy 5 — narty27 (`docs/sessions/2026-07-31-kb-f4-final-narty27.md`);
znalezione przy porządkowaniu backlogu po tej sesji
**Problem**: kontrakt serwisu deklaruje `private``services/narty27/service.yaml:5`
(`exposure: private # LAN/Tailscale only; no npm vhost, no public ingress`) i to samo
w `services/narty27/README.md` — podczas gdy `narty27.kapala.org` jest **publiczne**
(NPM na VPS + cert Let's Encrypt). Pole `exposure` steruje traktowaniem ekspozycji
przez agentów (patrz „Discovery Entry Points for Agents" w CLAUDE.md — `service.yaml`
jest kontraktem operacyjnym, z którego agent czyta, jak zarządzać serwisem), więc
rozjazd kontrakt↔rzeczywistość jest tu groźniejszy niż zwykła nieaktualna
dokumentacja: agent podejmie decyzję na podstawie pola, które kłamie.
**Fix (osobny task)**: (1) **najpierw** zweryfikować, co realnie konsumuje pole
`exposure` (observer / supervisor / ścieżka deployu) — dopiero to pokaże, czy poza
dokumentacją zmiana coś przestawia; (2) potem poprawić `service.yaml` + README na
`public`, z komentarzem wskazującym NPM VPS host **#14** i cert LE **#38**.
---
### `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/<id>.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 `.env``docker 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
`error``System 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-observer``set -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 -h``Swap: 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:20``node_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.py``SystemExit(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`/`tesseract` → `ConsumeTaskPlugin 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 observerze** — `scripts/observer/
observer.py:866` wspomina routing `mqtt_unreachable -> container_restart`
usunięty z supervisora (recon D15); sprzątnąć przy najbliższej edycji pliku.