Commit graph

421 commits

Author SHA1 Message Date
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
oskar d42fbe8d8a docs: backlog — narty27 exposure private w kontrakcie vs publiczne w praktyce
Trzeci wpis ze znalezisk sesji 2026-07-31: services/narty27/service.yaml
i README deklaruja exposure private ("no npm vhost, no public ingress"),
a narty27.kapala.org jest publiczne (NPM VPS host #14, cert LE #38).
Pole exposure czytaja agenci — rozjazd kontrakt<->rzeczywistosc.

Fix jako osobny task: najpierw weryfikacja konsumentow pola exposure
(observer/supervisor/deploy), dopiero potem zmiana service.yaml + README.
Pliki serwisu swiadomie nietkniete w tym commicie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:39:52 +02:00
oskar 6dc419c640 docs: backlog — bug grafu knowledge-catalog + brakujacy override ollama@SOLARIA
Dwa wpisy do sekcji Aktywne, oba ze zrodlem w sesji 2026-07-31:

- upstream GoogleCloudPlatform/knowledge-catalog: generator grafu pomija
  linki od "/" wbrew §5.1 wlasnej spec; obejscie = wlasny generator
  w pilocie narty27, fix = issue/PR do upstreamu
- hosts/solaria/runtime/ollama/docker-compose.override.yml brak w repo,
  mimo ze ollama jest w hosts/solaria/services.yaml — konfiguracja
  zywego kontenera niewersjonowana (wzorzec: hosts/piha/runtime/ollama-piha)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:39:52 +02:00
oskar e619c00874 docs: session log 2026-07-31 — KB faza 4 zamknieta + pilot narty27
Uzupelnia log o fakty brakujace w pierwszej wersji:
- OKF v0.2: migracja = jedna zamiana pola (timestamp -> generated {by,at})
- upstream bug: GoogleCloudPlatform/knowledge-catalog, generator grafu
  pomija linki od / wbrew §5.1 wlasnej spec
- landing w lancuchu generatorow (bylo: "strona linkow")
- nowa sekcja "Otwarte po sesji": expected_envelope w mail_queries,
  brak hosts/solaria/runtime/ollama/docker-compose.override.yml,
  R1-R3 node-agent (incydent 2026-07-30-ollama-solaria-vanish)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 14:39:52 +02:00
oskar 45fc21e18a docs(kb): session log F4 final + pilot narty27 2026-08-04 14:39:52 +02:00
oskar 79bfe8ceef feat(control-plane): host-side deploy runner — fix broken redeploy path 2026-08-03 19:23:16 +02:00
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
oskar 4c457a8006 merge: solaria gid-fix docs + recon appendix 2026-08-03 16:48:42 +02:00
oskar 3e42218868 feat(npm): create-cert — zamawianie certow Let's Encrypt przez API
Brakowalo certu dla narty27.kapala.org (host 14 na npm@VPS) — 443 zwracalo
tlsv1 unrecognized name. Proby POST /api/nginx/certificates z meta
{letsencrypt_email, letsencrypt_agree} konczyly sie 400 "data/meta must NOT
have additional properties".

Zweryfikowane w GET /api/schema oraz w /app/internal/certificate.js kontenera
(NPM 2.14.0): meta ma additionalProperties:false i nie zna pol email/agree.
Certbot dostaje --agree-tos na sztywno, a -m <email> z konta uzytkownika NPM.
Poprawny payload to {provider, domain_names, meta:{dns_challenge:false}}.

- create-cert --domain (powtarzalne), HTTP-01, dry-run domyslnie + --apply
- POST /nginx/certificates jest synchroniczne (reload + certbot + reload),
  wiec ma wlasny timeout 120 s zamiast globalnych 15 s; po nim polling
  GET /nginx/certificates/<id> az do expires_on, z czytelnym bledem po czasie
- gdy POST padnie na timeoucie klienta, cert jest odszukiwany po domenach
  (certbot moze wciaz pracowac po stronie serwera)
- set-cert: przyjmuje takze --host-id/--cert-id obok pozycyjnych i umie
  ustawic --ssl-forced przy tej samej zmianie
- README: schemat payloadu, skad bierze sie email, przyklad dla narty27

Cert #38 wystawiony (expires 2026-11-01), podpiety pod host 14 z ssl_forced;
https://narty27.kapala.org/viz.html -> 200, http -> 301.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 16:07:21 +02:00
Oskar Kapala 756e8454a5 docs: add link to maintenance plan in README.md
Co-authored-by: Junie <junie@jetbrains.com>
2026-08-03 13:03:58 +02:00
oskar 2e13a7d1bb feat(narty27): statyczny hosting viz.html na PIHA (nginx + named volume)
Nowy serwis services/narty27 — nginx:alpine serwujacy jeden self-contained
plik viz.html z named volume narty27_content (:ro). Port 8240 to nastepny
wolny w bloku 82x0 na PIHA (8210 paperless, 8220 nextcloud, 8230 kb-query).

Content jest personal: zyje wylacznie w volume narty27_narty27_content i w
zrodle na SOLARII (~/narty-2027/saalbach-kb/viz.html). Nigdy w repo, bez
backup joba, bez bindu pod /opt/homelab/data.

Procedura aktualizacji w README uzywa kontenera-pomocnika (alpine z volume
rw), NIE `docker cp` — przy mount :ro docker cp zwraca "mounted volume is
marked read-only", zarowno dla dzialajacego jak i zatrzymanego kontenera
(zweryfikowane empirycznie 2026-07-31). Zapisywane sa dwie kopie tego samego
pliku: viz.html (nazwa kanoniczna) + index.html (zeby golny root dzialal).

Walidacja: docker compose config OK (volume rozwija sie do
narty27_narty27_content), YAML parse OK, bash -n healthcheck.sh OK.
Bez zywego deployu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-31 16:38:55 +02:00
oskar f0f426ef55 docs(sessions): 2026-07-30 legacy final, MCP faza 2a, druga fala awarii 2026-07-30 20:19:46 +02:00
oskar ffe2a0c35b docs(incidents): root-cause zniknięcia ollama@SOLARIA — node-agent prune bez filtrów co 60 s
Kontener nie padł ani nie zniknął przy starcie: usunął go własny node-agent.
19 s po operatorskim `docker stop ollama` cykl cleanupu wywołał
`containers.prune()` bez filtrów — Docker kasuje każdy kontener nie-running,
ignorując restart policy i labele compose.

Dowód: ten sam cykl logu node-agent, dwie kolejne linie —
  16:45:33,528 WARNING Container exited: ollama (restart=unless-stopped)
  16:45:33,563 INFO    Pruned stopped containers (57 MB reclaimed)
57 MB to jedyna niezerowa wartość SpaceReclaimed w całym dniu (reszta 0 MB).

Mina uzbroiła się dzień wcześniej: prune istnieje od 01b7758, ale na SOLARII
node-agent nie miał dostępu do docker.sock (GID 996 vs 999) do czasu ddae57c.

Wykluczone: remediation pipeline (dispatch/solaria pusty, whitelist tylko
container_restart), stability-agent (zero ścieżek usuwających), config compose
(AutoRemove=false, brak --rm, brak compose down), cron/systemd (brak prune).

To NIE jest ollama-solaria-start-race — tamte dotyczyły startu kontenera.
Tu problem jest w cleanupie node-agenta i dotyczy każdego serwisu: ai_node
(solaria) i standard (vps) prune'ują bez rate-limitu, sd_card raz na 24 h.
`docker stop` jest obecnie na 4 z 6 nodów operacją destrukcyjną.

Fix świadomie niezaimplementowany — rekomendacje R1-R4 + mitygacja M1 w §7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 20:19:06 +02:00
oskar 3474744607 docs(recon): node-agent vs stability-agent — decide-support merge/retire
Decide-support recon for the two per-node sensors. Finding-first, sections 1-5
per brief, ends with DECISION NEEDED (no recommendation).

Headline: stability-agent's filesystem event channel is structurally unreadable
by the control plane, not merely mis-tagged. Recon D15 named one break; there
are five, stacked, any one fatal:
  1. wrong subtree   — events/<date>/<node>/events.jsonl vs events/<node>/
  2. wrong extension — observer.py:912 / supervisor.py:605 glob **/*.json
  3. wrong format    — observer json.load()s the whole file; JSONL is not one
  4. never shipped   — node_agent.py:788 rsyncs only events/<node>/
  5. details vs payload key, plus service=None (D15)

D15 is stale on master but still true in production: fix f92e161 (2026-07-29) is
merged, the running solaria image was built 2026-05-17, and 100% of the 5,056
deployed events sampled carry service=None. Moot regardless — breaks 1-4 mean
the observer never opens the file. All six event types are inert, not just
containers_not_running.

Counterweight: stability-agent is the sole writer of the Redis keys the
agent-system WebUI renders (4 node, 130 service, 31,458-entry stream, refreshed
during the recon). node-agent has no Redis code. Retiring it blanks that UI.

Runtime evidence over the retained window:
  node-agent      50,773 events -> 1 incident
  stability-agent  7,317 events -> 0 incidents (structurally zero since
                                  first deploy 2026-05-17)

Also found: deploy-local.sh validates 'chelsty' but the repo has
hosts/chelsty-infra/, so the only override carrying MQTT_HOST has never been
applied (stale homelab:nodes:chelsty key from 2026-06-02); scripts/lib/events.sh
writes to the same dead path; CLAUDE.md:100 documents the dead format as
canonical; recon B7 is stale — stability-agent IS in hosts/{solaria,vps}
services.yaml, but deploy-node.sh:93 skips it.

Read-only recon: no runtime mutations, no config changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 20:07:56 +02:00
oskar 0650eb857a feat(ha-mcp): read-only MCP server (faza 2a)
Own minimal MCP server exposing the live state of the HA instances in
services/home-assistant/instances.yaml to Claude Code over stdio — the
phase-2 "MCP read-only" gate in services/home-assistant/DESIGN.md.
Operator decision 2026-07-30: build our own rather than adopt hass-mcp,
so the tools reuse scripts/ha/lib/{ha_api,ha_ws}.py (one token-handling
story for the whole HA toolchain) and can answer from the repo and from
instances.yaml, which a generic server cannot.

Seven tools, all read-only, default instance `ken`: list_entities,
get_state, get_areas, find_entities_by_description, read_automation,
list_automations, instance_status.

Read-only by construction, not by policy: REST goes through ha_api.Client
(get/get_raw_text only — no POST method exists), WebSocket commands are
checked against a three-entry *_list allowlist before being sent, and
read_automation reads services/home-assistant/config/<instance>/ rather
than /api/config. Tests assert all three, including a grep guard that
fails if requests.post/call_service ever appears in the package. The
write path stays repo + scripts/ha/deploy.sh.

Details that follow from how this instance actually behaves:

- unavailable is never silent — every entity view carries unavailable +
  unavailable_since, every list a count. The 2026-07-23 audit traced ~15
  silently dead automations to conditions sitting on dead sensors.
- chelsty-ha (status: offline in instances.yaml) is answered from the
  file, never dialed — no 5s timeout for a known-offline LTE site.
- areas come from the WS registries (entity area_id > device area_id) with
  a storage-export fallback; area_source/area_note say which was used and
  what the offline export cannot resolve.
- PL->EN fuzzy matching, since the house is Polish and the entity_ids are
  transliterated English: "czujnik temperatury salon" ->
  sensor.thsalon_temperature, each hit explaining why it matched.
- 5s timeouts and errors returned as {"error": ...} inside a normal tool
  result — a missing token or an unreachable instance never crashes the
  server or hangs the agent.

Registered for Claude Code in the repo-root .mcp.json (new file) as `ha`,
via services/ha-mcp/run.sh (prefers the venv, falls back to system
python3). The mcp SDK lives in services/ha-mcp/.venv — rationale for venv
over --break-system-packages is in the README.

Tests: 42 offline (no network, no HA, no token) + a live read-only smoke
against ken — HA 2026.7.2, 1647 entities, 377 unavailable, 115
automations, 13 areas.
2026-07-30 16:47:27 +02:00
oskar cb8a19de83 test(kb-query): luki T1–T3 z test_fallback.py + status kalibracji ollama-piha (salvage S3+S4)
T1: one-shot switch przy timeoutcie mid-embed (ścieżka asyncio.wait_for,
dotąd nieprzetestowana — scenariusz 'SOLARIA wisi'). T2: breaker zostaje
'down' po mid-embed failure — kolejne requesty w oknie TTL idą prosto na
fallback bez probe'a. T3: noga fallbacku nie dziedziczy twardego timeoutu
primary. T4 pominięty (semantyka granicy TTL identyczna, wg raportu).
Pytest kb-query: 42/42 PASS.

S4: pomiar kalibracji 2026-07-27 (peak ~983 MiB, GO) dopisany do override'u
i sekcji Calibration w README — master mówił dotąd 'Confirm/trim after live
calibration'; konfiguracja kontenera identyczna z mierzoną, pomiar się
przenosi. Raport dedup: status zaktualizowany na 'salvage wykonany'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:40:24 +02:00
oskar 4b92924332 docs(sessions): wciągnięcie loga 2026-07-27 kb-f4-fallback z porzucanego brancha (salvage S2)
Implementacja kodu z tej sesji porzucona na rzecz e7625cd (dopisek redakcyjny
na górze pliku), ale log jest jedynym zapisem faktów operacyjnych: osierocony
natywny ollama.service na PIHA wyłączony 2026-07-27 (backlog odinstalowania
≈2026-08-10), kalibracja live ollama-piha z werdyktem GO (peak ~983 MiB,
~4.2–5.3 s/embed) i baseline bramki §9 (HTTP-equivalence 0 rozbieżności,
sol-down Δ~3e-4 — do powtórki na masterze, raport dedup follow-up (b)).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:40:24 +02:00
oskar 71eb264448 feat(eval): retrieval_eval --transport http — bramka §9 przez żywe /search kb-query (salvage S1)
Cherry-pick 1:1 z porzucanego brancha task/kb-f4-fallback (3d4ee38) wg decyzji
z raportu dedup (docs/kb/modules/05-fallback-dedup-raport.md): plan §2 D6/§9
wymaga trybu HTTP-equivalence, a master go nie miał — e7625cd nie tknął tego
pliku, patch aplikuje się czysto i woła wyłącznie GET /search (pola
envelope_id/dist/source zgodne z odpowiedzią mastera). Live-PASS 2026-07-27
na kodzie brancha; smoke na masterze: CLI + pełny przebieg http przeciwko
stubowi /search (raport i werdykt bramki generują się poprawnie).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:40:24 +02:00
oskar 8ab262d38f docs(kb): raport dedup fallbacku embed — e7625cd vs 3d4ee38, klasyfikacja delty i plan salvage
Recon read-only przed salvage (moduł 5 faza 4 §2/§5): embed_router.py na
masterze pokrywa 100% zachowań fallback.py z porzucanego brancha i dodaje
weryfikację modelu per backend, mapowanie 503/500 i tryb bez fallbacku.
Do uratowania: --transport http w retrieval_eval.py (plan §2 D6/§9, czysty
cherry-pick), session log 2026-07-27 (kalibracja GO, osierocony ollama.service),
luki testowe T1/T2, wynik kalibracji do override/README ollama-piha.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 16:40:24 +02:00
Oskar Kapala 473bf8e5ad chore(solaria): remove planner-agent from desired state — legacy ai-cluster, retirement per RECON; unblocks deploy gate (no litellm/redis on saturn)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-30 15:58:24 +02:00
oskar 59cb3a4fbc docs(solaria): note verified docker gid 996; recon appendix on undeployed fix
The group_add fix itself already landed on master (ddae57c) but the running
container still has GroupAdd=[999] — deploy pending from main checkout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:44:17 +02:00
oskar 615b6ab2a8 docs(architecture): Etap 0 changes appendix in recon; telegram-frontend pattern in legacy doc
RECON appendix summarizes the whole etap-0 cleanup (topology dormant status,
hosts reconciliation, dead-code removal, legacy decision), carries the
runbook line for the stale chelsty pending action
(rm /opt/homelab/actions/pending/redeploy-chelsty-infra-ha-diag-agent.json —
verified the only chelsty-targeted pending file 2026-07-30), and lists
discrepancies found but out of scope: lustro watchtower, stale solaria/chelsty
world-state keys, piha naming-drift duplicate keys, gokapi still down with a
stuck pending redeploy, outline/joplin/umami still lacking services/ dirs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:36:29 +02:00
oskar 89454ea7e1 feat(hosts): reconcile desired state with reality on vps, piha, saturn, lustro
Etap 0 truth cleanup (recon B5/B6/F20), all facts re-verified 2026-07-30 via
read-only ssh (docker ps, world/services.json, events store):

- vps: add stability-agent, npm, outline, joplin-server, umami (all running
  and healthy in world state). joplin keyed joplin-server to match the
  world-state key — an aggregate 'joplin' entry would drift as
  missing_service forever. humanai-mailer/humanai-landing documented as
  unmanaged: true + monitor: false (hand-run, no compose labels — supervisor
  must not queue dead-end redeploys for them). ai-cluster deliberately gets
  NO entry: legacy, retiring — comment points at ai-cluster-LEGACY.md.
- piha: comment block enumerating the ~28 known unmanaged containers plus the
  host systemd mosquitto; bringing them in is a later stage (recon OQ5).
- saturn: create services.yaml with explicit empty list (dev workstation /
  orchestrator, nothing monitored — closes recon F20.4).
- lustro: add node-exporter (hyphenated key = actual container name) and
  piper-tts (runs locally on the Pi, no services/ dir); watchtower noted as
  deliberately unmanaged.

All YAMLs parse (yaml.safe_load); control-plane suite 147 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:36:23 +02:00
oskar 7cb0ab6034 docs(topology): lustro daily duty cycle — nightly power-off, expected liveness cycles
Verified 2026-07-30 in the vps event store: lustro's node_offline events fire
once a day, ~24 h apart (~23:30 local), matching solaria's scheduled power-off
pattern. Documented so daily stale/offline/online cycles read as expected
behavior, not faults.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-30 15:36:13 +02:00
oskar fbf165fbea fix(supervisor): route healthcheck_failed to container_restart
healthcheck_failed incidents fell through to redeploy, which is broken as
wired (executor calls deploy-node.sh with arguments it ignores, at a path
that does not exist in the container) — so 3376 healthcheck_failed events
dead-ended with no working remediation (recon D14/D15). A container restart
plausibly heals a failing healthcheck and rides the executor path that
actually works; redeploy returns to the map once etap 2 fixes the executor.

service_unhealthy / deployment_failed / missing_service stay on redeploy —
theoretical until etap 2, kept so drift remains visible in pending actions
(noted in comments). CLAUDE.md routing table updated to match; stale
mqtt_unreachable example in the observer's trigger_type comment refreshed.

Tests: trigger-type recognition and the end-to-end observer→supervisor
reconcile test parametrized over both container_restart triggers, with an
assertion that no redeploy action is also generated. Full control-plane
suite: 147 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:24:01 +02:00
oskar f92e161ec6 fix(stability-agent): tag containers_not_running events with compose service
The aggregate containers_not_running event carried service=None, which the
observer skips when building service state and incidents — stability-agent's
flagship signal never opened an incident (recon D15). Emit one event per
non-running container instead, tagged with the compose service name from the
com.docker.compose.service label (same pattern as node-agent's
_canonical_container_name fix from May), falling back to the container name
with Docker's stale-state hash prefix stripped; never crashes on unlabeled
containers. 'created' compose tracking artifacts are skipped — they are not
running services and would open fake incidents now that the event is
actionable.

Adds the service's first test suite covering the label-extraction helper.
Smoke-run performed with runtime paths redirected (no docker build, authoring
only): main loop runs, service names resolve on live solaria containers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:22:07 +02:00
oskar ddae57c842 fix(solaria): node-agent group_add for host docker gid 996
Base compose assumes Debian-default docker gid 999; on SOLARIA the docker
group is 996, so node-agent hit 'Docker unavailable: Permission denied' on
the socket and reported no containers (recon A2/E19). Same per-host
override pattern as piha (123) and lustro (991). Verified read-only on the
host: getent group docker -> docker996:oskar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:20:08 +02:00
oskar 670cb71c99 docs(backlog): follow-upy z etapu 0 — 40-register.sh stary schemat topologii, komentarz mqtt_unreachable w observerze
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar 07224f26da docs(claude): drop mqtt_unreachable row from supervisor routing table
Follows the supervisor change: mqtt_unreachable never becomes an
incident (recon D15), so the documented container_restart routing was
false. Table now matches code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar d7d9c0f6f7 docs(architecture): ai-cluster decommission decision (legacy, not migrated)
Bus codex/* dead since 2026-06-09 (recon C9). Records what survives into
subsystem B (patterns only: task bus, worker roles, enforced command
allowlists, preview mode), that task/ai-cluster-solaria stays unmerged
as documentation, and that the vps containers get stopped in a separate
supervised session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar d03288e5b3 fix(supervisor): drop dead mqtt_unreachable restart trigger
The observer never creates incidents with trigger_type=mqtt_unreachable,
so the CONTAINER_RESTART_TRIGGERS branch for it could never fire (recon
D15). stability-agent keeps emitting the event; it just never becomes an
incident. Tests: 145 passed.

SERVICE_NAMES in ai-cluster's service_ops_worker.py (the other dead
constant from the plan) is NOT touched: that code is legacy-frozen in
the unmerged task/ai-cluster-solaria worktree and nothing on this branch
references it (verified by grep — only the recon and plan docs mention it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar e67868f5da chore(deploy): remove dead deploy-role.sh
The script composes from roles/$ROLE/compose/docker-compose.yml, but no
roles/ directory exists anywhere in the repo (recon B7) and nothing
references the script. Dead since the roles concept was abandoned.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar d8f65c3034 fix(hosts): align manifests with observed reality (recon B6/B7/C8/F20.5)
- hosts/solaria/services.yaml: add stability-agent and node_exporter
  (running on solaria since deployment, absent from desired state —
  recon B6); add planner-agent with monitor:false (legacy ai-cluster
  family, decision pending, do not remediate)
- services/stability-agent/service.yaml: owner_node chelsty (not a real
  node) -> per-host, same convention as node_exporter/ha-diag-agent; it
  runs on vps, piha, solaria (F20.5)
- services/mosquitto/: mark NOT DEPLOYED / legacy in service.yaml and
  README — the manifest matches nothing that runs (vps broker = legacy
  ai-cluster stack, piha broker = host systemd package, chelsty has its
  own runtime config; recon C8). Kept until the MQTT topology decision
  (recon open question 7); not deleted yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar aa8276963c feat(topology): node status active|dormant + dormant handling in control plane
topology.yaml (per its own rule that hosts/*/services.yaml are
authoritative, recon F20.1):
- add status field; chelsty-infra + chelsty-ha -> dormant (site hardware
  down since ~2026-06-01, revival planned)
- add lustro as a full active node (runs node-agent, ships events, F20.11)
- drop per-node service lists (vps list contradicted hosts/vps, F20.1;
  piha/solaria lists were stale too, F20.8/F20.9) — node-level truth only
- deployment.mode pull -> push: every deploy script SSH-pushes from
  saturn (F20.10)

Dormant semantics in code:
- observer (scripts/observer/observer.py): loads status from topology;
  _prune_stale_world skips dormant nodes — last-known world state stays
  frozen, no node_offline/node_stale/node_online events emitted
- supervisor (services/control-plane/src/supervisor.py): reloads dormant
  set each reconcile; dormant hosts' services excluded from desired state
  (existing pending actions auto-cancel via
  service_removed_from_desired_state), disk_cleanup skipped, node/HA
  events from dormant nodes not routed to alerts

Tests: services/control-plane/tests/test_dormant_nodes.py (9 cases);
full suite 145 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar 72788429e2 docs(architecture): ARCHITEKTURA.md — standing decisions 2026-07-28
One page of binding architecture decisions per the 2026-07-27 recon:
two subsystems (A keep-it-running / B do-the-work), node roles incl.
solaria's by-design duty cycle and dormant chelsty site, ai-cluster
declared legacy (bus dead since 2026-06-09), approvals stay HITL with
Telegram as the etap-2 channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:15:55 +02:00
oskar e7625cd322 feat(kb): aktywny fallback embeddingów SOLARIA→PIHA dla kb-query (faza 4 Krok 2)
Ostatni krok fazy 4 KB (plan §2 Decyzja 2, §5): kb-query przestaje być martwe
przez ~16 h/dobę, gdy SOLARIA (GPU) śpi — zapytania embeduje wtedy lokalna
Ollama CPU na PIHA (wolniej: ~790 ms+ vs ~207 ms na GPU, ale działa).

Nowy serwis services/ollama-piha (GitOps, owner_node: piha):
- ollama/ollama:latest (arm64 natywnie), OLLAMA_KEEP_ALIVE=0 — model zwalnia
  RAM natychmiast po każdym wywołaniu (spike, nie rezydent; PIHA dzieli 8 GB z HA)
- bind wyłącznie 127.0.0.1 + LAN_BIND_IP (192.168.31.5), nigdy 0.0.0.0/Tailscale
- named volume ollama_piha_models (NVMe data-root) zamiast bind-mounta — obraz
  biega jako root w kontenerze i bind łamałby wzorzec uid PIHA (oskar=1004,
  kontenery uid 1000, setgid pi)
- override hosts/piha/runtime/ollama-piha: mem_limit 2560m (wartość startowa
  z planu, do potwierdzenia kalibracją na żywo), świadomie bez mem_reservation
- pull bge-m3 to jawny, ręczny krok deployu (README) — obraz nie ma modeli

kb-query — maszyna stanów fallbacku (app/embed_router.py):
- health-check SOLARII (GET /api/tags, timeout 1.5 s) z cache 30 s — zero
  sondowania per request; po powrocie SOLARII ruch wraca na GPU w ≤30 s
- primary up → embed na SOLARII z twardym timeoutem 3 s; błąd W TRAKCIE
  zapytania = jednorazowe przełączenie (krok 3b planu): status down na 30 s
  i TO SAMO zapytanie leci na fallback — user nie widzi błędu SOLARII
- primary down → embed prosto na ollama-piha (bez twardego timeoutu: CPU +
  zimny load modelu to legalnie pojedyncze sekundy)
- 503 tylko gdy oba backendy padłe (lub fallback nieskonfigurowany)
- inwariant modelu, druga połowa: każdy backend weryfikowany raz, leniwie przy
  pierwszym użyciu, że /api/tags zawiera EMBED_MODEL (bge-m3 — ta sama wartość
  co startowy check przeciw document_chunk.model/document_summary.embedding_model);
  niezgodność = ERROR log + 500, nigdy ciche liczenie dystansów między
  różnymi przestrzeniami embeddingów; leniwie, bo śpiąca SOLARIA nie może
  blokować startu serwisu
- odpowiedź /search: nowe pole embed_backend ("solaria"|"piha") + sol_status
  wg realnego świata routera (UI już renderuje down jako "offline (fallback
  embed)"); log INFO backend=... elapsed_ms=... per zapytanie
- /healthz: sol_status przez cache routera (spójny widok z routingiem) +
  fallback_status (żywa, tania sonda /api/tags)

Konfiguracja spójnie przez env (compose + env.example + service.yaml + README):
EMBED_PRIMARY_URL (zastępuje OLLAMA_URL), EMBED_FALLBACK_URL (pusty = brak
fallbacku, zachowanie sprzed kroku 2), EMBED_{PRIMARY,FALLBACK}_NAME,
EMBED_HEALTH_TTL_S/EMBED_HEALTH_TIMEOUT_S/EMBED_PRIMARY_TIMEOUT_S.

Testy: 39 pass (14 nowych w test_embed_router.py: cache TTL, failover w trakcie
zapytania, powrót po TTL, oba padłe, mismatch modelu na primary i fallbacku,
tag "bge-m3:latest" vs "bge-m3"); docker build + smoke (importy + uvicorn do
guardu KB_DSN) OK; compose config OK dla obu stacków.

Deploy (Oskar, na PIHA z mastera po merge):
  cd ~/homelab-codex-ws && git pull
  # 1. ollama-piha
  cp services/ollama-piha/env.example services/ollama-piha/.env
  docker compose -f services/ollama-piha/docker-compose.yml \
    -f hosts/piha/runtime/ollama-piha/docker-compose.override.yml \
    --env-file services/ollama-piha/.env up -d
  docker exec ollama-piha ollama pull bge-m3     # ręczny krok, obowiązkowy
  services/ollama-piha/healthcheck.sh
  # 2. kb-query (dopisać fallback do istniejącego .env)
  echo 'EMBED_FALLBACK_URL=http://192.168.31.5:11434' >> services/kb-query/.env
  docker compose -f services/kb-query/docker-compose.yml \
    -f hosts/piha/runtime/kb-query/docker-compose.override.yml up -d --build
  services/kb-query/healthcheck.sh
  # (deploy-node.sh też podniesie oba serwisy z hosts/piha/services.yaml,
  #  ale pull bge-m3 i .env pozostają ręczne)
Weryfikacja: testy A/B/C w services/kb-query/README.md (backend=solaria przy
SOLARII online; backend=piha przy symulacji offline; powrót na GPU w ≤30 s).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 19:01:29 +02:00
oskar e02c925ffb docs: session 2026-07-28 21:59 2026-07-28 21:59:26 +02:00
oskar 905ad96fa8 docs(architecture): plan naprawy subsystemu A (control-plane) 2026-07-28
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 21:51:14 +02:00
oskar e8aa3e3a09 docs(architecture): recon multiagent 2026-07-27 2026-07-27 22:24:39 +02:00
oskar 50c4b2024a docs(sessions): zigbee.kapala.org przez tailnet/LAN, okit.pl sprzatniete 2026-07-27 18:52:15 +02:00
oskar 25fca9e29e docs(sessions): 2026-07-27 legacy rm, kasacje, pimirror, sonda Zigbee (routery bez pradu) 2026-07-27 18:28:22 +02:00
oskar 09624e008e feat(ha/ken): porzadki po audycie — pimirror z archiwum, konwencje, backlog
Odtworzone graceful shutdown pimirror (audyt pkt 14, jedyna realna strata
migracji): nowa automatyzacja "Pimirror: graceful shutdown przed odcieciem
zasilania" (1785164185794.yaml) wciska przycisk shutdown przed twardym
cieciem zasilania o 23:35, dajac istniejacemu ACK-flow ("Magic Mirror OFF on
ACK") szanse zamknac Pi grzecznie zamiast zawsze odcinac prad na sile.
Zaadaptowane wzgledem legacy: entity_id zamiast device automation, oryginalna
encja button.rpi_pimirror_* jest dziś unavailable — uzyto aktualnej
button.rpi_pimirror2_* (potwierdzone w storage-export/ken).

Spisana konwencja na przyszlosc (audyt pkt 17) w DESIGN.md: entity_id zamiast
device automations, alias PL z prefiksem funkcjonalnym, description z
"managed-by: repo" — bez hurtowej migracji istniejacych automatyzacji.

Backlog: deploy.sh --delete (kasacje wspolnym torem dry-run/LIVE zamiast
recznych curl DELETE) i brakujacy trigger na zmiane input_number.klima_salon_tolerancja
w automatyzacji ON klimy.

Walidacja: normalizator round-trip (byte-identical), 4 pakiety testow offline
(normalize/split/deploy_api/import_api) pass, dry-run deploy.sh na zywym ken
(check_config valid, brak driftu) — bez LIVE deployu.
2026-07-27 17:51:39 +02:00
oskar 9eb3500b1e docs(infra): audyt niezarzadzanych stackow VPS — mapa przed konsolidacja do GitOps
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 17:49:37 +02:00
oskar 36b43e5c9c drift(ken): kasacje po audycie pkt 10 — para Tymka, powitanie test, notify router, para prototyp 2026-07-27 16:51:30 +02:00
oskar b7fe813576 docs(kb-query): confirm Cloudflare A-record for kb.kapala.org is live
Operator added the A-record manually; verified against Cloudflare's own
authoritative NS (which briefly still returned NXDOMAIN — not a propagation
lag, the record simply wasn't saved yet on first check) plus 8.8.8.8/1.1.1.1.
Both DNS layers (Cloudflare public/Tailscale + Pi-hole LAN override) and the
vhost now confirmed working end-to-end.
2026-07-23 18:38:34 +02:00
oskar fbe81f9bf5 docs(kb-query): ingress kb.kapala.org live — npm vhost + Pi-hole DNS, OIDC deferred
Runtime steps (not in Git, logged here): npm@PIHA proxy host #35
(kb.kapala.org -> 192.168.31.5:8230, cert #49 *.kapala.org wildcard, same
pattern as paper./vikunja.kapala.org); Pi-hole custom.list split-horizon
entry added and verified (first kapala.org entry in that file — the other
kapala.org vhosts turned out to have no LAN override at all, a plan
assumption that didn't hold). Cloudflare A-record left for the operator (no
API token available here). OIDC intentionally not built: confirmed no
forward-auth pattern exists anywhere in this repo, and building authlib
OIDC into kb-query is real service code out of scope for an infra-only
task — operator decided to leave kb.kapala.org without auth for now.
2026-07-23 18:38:34 +02:00
oskar 8f307f45aa docs(sessions): 2026-07-23 klima E2E, audyt Fable, fix-pack 1 2026-07-23 18:11:51 +02:00
oskar f09dcbf10d fix(ha/ken): fix-pack 1 po audycie — alerty wodne, respekt manual, 3am punktowo, klima OFF za auto
Zmiany wyłącznie w repo (config/ken/ automations), deploy LIVE robi operator.

- 1752086407230 "dry in Kuchnia": trigger moist -> not_moist (kopiuj-wklej z
  "moist in Kuchnia", "sucho" nigdy nie przyjdzie).
- 1752085965483 "dry in Lazienka": literówka mesaage -> message (skrypt
  notify_email_ntfy dostawał niezdefiniowane message, akcja się wykładała).
- 1764190493305 "after 15 minutes anyway": dodany warunek stanu
  automation.turn_off_lights_in_kuchania == on, żeby respektować ręczne
  "Disable AUTO off" (double-click) zamiast unieważniać je po 15 minutach.
- 1700832676138 "unconditionally at 3am": time_pattern hours: '3' (60
  odpaleń/noc) -> punktowy trigger time: 03:00:00, zgodnie z intencją aliasu.
- 1784804668795 "Klima salon: wyłącz...": dodany warunek
  input_boolean.klima_salon_auto == on dla gałęzi sunset/balkon (automat
  ubija tylko chłodzenie, którym sam zarządza — ręczne chłodzenie przy
  zgaszonym auto zostaje nietknięte). Trigger auto_off (zgaszenie
  klima_salon_auto) ma osobną gałąź choose bez tego warunku — graceful
  shutdown suszenia parownika działa nawet gdy przełącznik właśnie zgasł.
  Zweryfikowano na żywym ken: helpery klimy istnieją (input_boolean.
  klima_salon_auto=off, input_number.klima_salon_temp_docelowa=24.0,
  input_number.klima_salon_tolerancja=3.0).

DESIGN.md: nowa sekcja "Decyzje operatora po audycie 2026-07-23" — co
zaimplementowane, co świadomie odłożone (enforcer sleep mode + konsolidacja
nocnych wyłączników -> projekt night_mode; OwnTracks / Leave batch 02 / alerty
on-leave -> bez zmian).

docs/backlog.md: cztery nowe wpisy w Aktywne — guard TRV przed sezonem
grzewczym, przycisk graceful shutdown klimy na dashboard, diagnoza wspólnej
awarii sprzętowej 2026-07-17 (czujniki ruchu + pilot 4button + xiaomi_miot),
projekt architektura night_mode.

Walidacja: round-trip scripts/ha/lib/normalize.py na wszystkich 5 zmienionych
plików (stabilny), 4 zestawy testów offline scripts/ha/tests/*.sh (wszystkie
PASS), dry-run scripts/ha/deploy.sh ken --dry-run per plik na żywym ken
(wszystkie: check_config valid, brak driftu, brak abortu).

Choinkowe automatyzacje (audyt 4.2) celowo nietknięte w plikach — disable to
stan runtime, nie config; operator wyłączy je przez UI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:17:38 +02:00
oskar bce635c270 fix(eval): mail_hit@3 criterion 4 + N2 threshold, gate PASS after Etap A
Bug: hit_at_3 returned None for kind=mail_hit rows (expected_envelope is
always null for them -- operator supplies query text, not a Message-ID), so
criterion 4 could never count a hit and read 0/5 despite hybrid distances of
0.25-0.42. Fixed with mail_hit_at_3: hit iff the top-3 distinct hybrid
envelopes include a mail-sourced one (envelope.source lookup via
fetch_envelope_sources, since hybrid_retrieve overwrites source to "hybrid"
on merge) under HIT_THRESHOLD. Also added a per-query no_answer_threshold
override in queries.yaml for criterion 3.

N2 ("piaskownica plastikowa") investigation: after Etap A added ~34k mail
chunks, N2's top-1 neighbor dropped to dist 0.5298 (< the 0.55 bar). Content
check showed it's a ski-school reservation newsletter (Rossignol ski sizes)
-- a semantic false-positive collision, not a real corpus match. M5, which
the operator had added assuming a genuine piaskownica mail existed, itself
misses (dist 0.5585) -- confirming there's no such mail in the corpus. M5
dropped; N2's pass bar lowered to 0.50 with a note documenting the collision.

Gate result on the live DB post-Etap A (Etap A: 13 009 mails scanned -> 33 871
new gmail chunks, 6 398 embedded / 27 473 newsletter-flagged, balanced +
idempotent on rerun; Ollama incident #4 during the run required a compose
force-recreate, not just restart -- root-cause task ollama-solaria-start-race
stays in backlog): all 4 criteria PASS (5/5 flat hits held, cascade hit@3 5/5
vs flat 4/5, negative controls above their bars, mail hit@3 4/4 after
dropping M5). Plan doc updated with the numbers and verdict table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 17:06:49 +02:00
oskar fc5c698898 fix(mail-body-ingest): html_to_text loses entire body on unclosed void elements
The naive LIFO tag stack pushed every start tag and popped on any end tag by
position, not name. Real email HTML almost always writes void elements
(<meta>, <br>, <img>, ...) without a self-closing slash -- e.g. <head><meta
charset=...><meta name=viewport ...></head> pops a "meta" frame for the
literal </head>, leaving skip_depth stuck at 1 for the rest of the document.

Caught live during the faza-mailowa Etap A dry-run spot-check (plan §7,
Krok 4 calibration step) against real archived mail: a genuine HTML-only
promotional email extracted to '' entirely. Re-running the Etap A dry-run
after the fix dropped body_empty from 3253/13300 (24.5%) to 291/13300
(2.2%) -- the bug was silently discarding real content from a meaningful
slice of HTML-only mail.

Fix: void elements are never pushed onto the stack (so they can never
desync it); every other closing tag searches backward for its matching
open tag and truncates the stack from there, which also self-heals other
malformed nesting instead of only the void-tag case. 3 new regression
tests reproduce the exact <head><meta>...</head> pattern, br/hr line
breaks, and a stray unmatched closing tag.
2026-07-23 17:06:49 +02:00