solaria (powered off ~16 h/day by design) and lustro (nightly display
power-off) generated node_offline/node_stale/node_online alerts on every
daily cycle. Six of them have sat in actions/pending/ since 2026-06-17/18,
unapproved. Because an unapproved pending action suppresses its own dedup
ID indefinitely (recon D14, supervisor.py pending/approved/running check),
those stale alerts also meant a *real* future outage on either node would
generate nothing at all.
Suppression is data-driven from inventory/topology.yaml, not a hardcoded
node-name check:
- topology.yaml: new `duty_cycle` (+ `duty_cycle_reason`) on solaria and
lustro, mirroring the existing dormant/dormant_reason shape. vps and piha
deliberately do not carry it — an offline 24/7 node is a real incident.
- supervisor: _load_dormant_nodes() -> _load_node_policy(), loading both
dormant_nodes and duty_cycle_nodes from one topology read. dormant
behavior is byte-for-byte unchanged.
- supervisor: one guard in _route_node_event. Duty-cycle liveness events
are logged at INFO and return; no action is written.
duty_cycle is deliberately NOT dormant. A duty-cycle node stays fully
active: its services are still reconciled (missing_service -> redeploy),
its disk pressure still generates disk_cleanup, and its ha_* events still
route. Only the liveness alert is suppressed. Regression tests pin all
three.
Fail-loud: an unreadable topology leaves both sets empty, which disables
suppression and lets alerts through. A broken topology must never silently
mute the fleet.
Accepted trade-off: a genuine permanent outage of solaria or lustro no
longer alerts. It stays visible in the operator UI (which computes liveness
independently at read time) and in the event feed. An "offline longer than
the expected window" escalation is the natural follow-up and needs a
schedule in the topology field rather than a bare marker.
Tests: 169 passed in services/control-plane/tests (was 157; +12).
Runtime deployment is deliberately NOT part of this commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"User-agent: * / Disallow: /" odbijalo nie tylko wyszukiwarki, ale kazdego
klienta respektujacego robots.txt (m.in. fetch asystentow AI) — takze takiego,
ktory znal token dostepu. Token mial WPUSZCZAC znajacych go, a robots.txt ich
WYPYCHAL.
Dzialalo tez przeciwko wlasnemu celowi: crawler zablokowany przed pobraniem
strony nigdy nie widzi meta noindex, a wyszukiwarka i tak potrafi wylistowac
sam URL, ktorego nie wolno jej bylo pobrac.
Ochrona przed indeksowaniem zostaje bez zmian: <meta name="robots"
content="noindex, nofollow"> w <head> kazdej strony (scripts/kb/gen_pages.py).
Plik mial jedna linie polityki, wiec bez niej tracil sens — usuniety razem
z bind-mountem w compose (katalog static/ zniknal jako pusty).
Test: docker compose config (exit 0, zostaje tylko named volume);
gen_pages.py --check — CZYSTO, 0 trafien.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dociagniecie do zmiany DEFAULT_BASE_URL z 24afb49 — po niej repo w szesciu
miejscach dalej podawalo stary adres.
- kb/runbooks/kb-site-deploy.md: wszystkie wystapienia + rekord Cloudflare
(Name: kb -> kb-e2a24af3). Runbook jest visibility: private, wiec slug
moze stac wprost.
- services/kb-site/{README.md,service.yaml,env.example,docker-compose.yml}
- hosts/piha/services.yaml: komentarz przy exposure
kb/services/kb-site.md swiadomie nietkniety — dokument publiczny, slug tam
nie wchodzi (opisuje adres jako "non-obvious subdomain").
check_okf.py exit 0, gen_pages.py --check exit 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Warstwa "nie daj sie przypadkiem znalezc" dla publicznej wystawki KB:
- gen_pages.py: <meta name="robots" content="noindex, nofollow"> w <head>
kazdej generowanej strony (page_shell, wiec takze index).
- gen_pages.py: DEFAULT_BASE_URL -> https://kb-e2a24af3.okit.pl. Slug musi
zgadzac sie z rekordem DNS i vhostem w npm@PIHA (runbook kb-site-deploy).
- services/kb-site: static/robots.txt (Disallow: /) montowany ro na
/usr/share/nginx/html/robots.txt. Plik nie jest dokumentem KB, wiec jedzie
z repo, a nie z wolumenu podmienianego przy kazdej publikacji.
- kb/services/kb-site.md: sekcja "Access" — token w query paramie na warstwie
nginx/NPM (sekret zyje tylko w NPM, nie w repo) + obscure subdomain +
noindex. Explicit: to obscurity, nie kontrola dostepu — token w URL laduje
w access logach, historii przegladarki i naglowku Referer.
Bramka publikacji bez zmian: gen_pages.py --check exit 0 (22 wyciszone
whitelista, jak dotad).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recon read-only zlecony pod teze "executor odpala deploy-node.sh z argumentami,
ktore skrypt ignoruje, na sciezce nieistniejacej w kontenerze". Teza byla
prawdziwa dla mastera do 2026-08-03; dzis nie jest. 79bfe8c + da151fc zastapily
to dispatchem do jobs/deploy-runner/ (systemd na hoscie wezla).
Realny stan: luka deployowa w trzech miejscach naraz — kontener executora
zbudowany 2026-07-22 (wciaz stary kod), /opt/homelab/actions/deploy/ nie istnieje
na VPS, deploy-runner nie jest zainstalowany na zadnym wezle. Zero akcji
kiedykolwiek osiagnelo stan terminalny (completed 0 / failed 0); jedyne dwa
action_result to reczne testy z 2026-07-23, nie remediacje z incydentu.
Zweryfikowane wzgledem fbf165f: healthcheck_failed idzie do container_restart,
nie do redeploy. Zywe incydenty w world state maja wylacznie trigger_type
containers_not_running / healthcheck_failed — czyli zaden nie generuje redeployu;
dzialaja tylko dryfy missing_service (2 pending).
Najostrzejszy problem projektowy: jedyny zywy emiter service_unhealthy
(node_agent.py:1104-1115) ma zahardkodowane service="control-plane", a
control-plane ma wlasne deploy-local.sh — deploy-service.sh:93-96 konczy sie
exit 3, wiec runner odmowi. Po wdrozeniu fixu redeploy sterowany incydentem
nadal nie wykona sie ani razu.
Ubocznie: w obrazie executora nie ma binarki ssh, wiec disk_cleanup
(executor.py:392-400) jest martwy tym samym defektem. Osobny task.
Dokument konczy sie sekcja FIX SHAPE — pieciopunktowa lista decyzji do podjecia,
bez rekomendacji.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docker na PIHA wyczerpal domyslne pule adresowe (~30 zywych stackow,
"all predefined address pools have been fully subnetted"), wiec
docker-compose nie mogl zalozyc kb-site_default i serwis nie wstawal.
Deklaracja networks: [proxy] na serwisie wylacza domniemana siec
domyslna i podpina kontener pod istniejacy bridge "proxy"
(192.168.0.0/20, tworzony poza tym stackiem) — zero nowych podsieci.
Bez wplywu na ruch: npm@PIHA siedzi na nginxproxymanager_default i
trafia do kb-site po opublikowanym porcie hosta 8250, nie po tej sieci.
Walidacja: yaml.safe_load + asercje ksztaltu (bez testu na PIHA).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Decyzja operatora 2026-08-04: /opt/homelab to standardowa sciezka deploy rootu
homelaba, ta sama na kazdym wezle, opisana wprost w publicznej czesci modelu
(standards, service-model, observer, event-system). Nie ujawnia sekretow ani
topologii, wiec zostaje na stronie publicznej.
scripts/kb/check_whitelist.txt: wpis `/opt/homelab` z uzasadnieniem i data.
Zakres wyjatku jest waski — sprawdzone, ze wycisza wylacznie warianty
`/opt/homelab/...`; `/home/oskar/...`, `/opt/other/...`, adresy RFC1918 i porty
dalej zapalaja czerwone.
gen_pages.py: load_whitelist() obcina komentarz `#` w dowolnym miejscu linii,
nie tylko na jej poczatku — wyjatek ma stac obok uzasadnienia, a nie osobno.
Zaden ze skanowanych wzorcow nie zawiera `#`, wiec obciecie jest bezpieczne.
kb/runbooks/kb-site-deploy.md §2: zapisany aktualny stan bramki (exit 0, 22
trafienia wyciszone) zamiast opisu decyzji do podjecia.
Po zmianie: python3 scripts/kb/gen_pages.py --check -> CZYSTO, exit 0.
kb/services/kb-site.md (type: service, visibility: public) — opis samej
wystawki: co jest publikowane (regula fail-closed na `visibility`), jak
renderowane sa odnosniki do dokumentow nieopublikowanych, struktura builda,
stopka z commitem, bramka --check. Napisany tak, zeby sam przechodzil --check:
zero adresow, portow i sciezek hosta — szczegoly operacyjne siedza w runbooku.
kb/runbooks/kb-site-deploy.md (type: runbook, visibility: private) — pelna
procedura: deploy na PIHA, generacja + kontrola wyciekow jako bramka
publikacji, podmiana tresci w wolumenie helperem alpine (wzorzec z
services/narty27/README.md, rozszerzony z pliku na drzewo), rekord A w
Cloudflare + Pi-hole local DNS (split-horizon), proxy host i cert przez
scripts/npm/npm_api.py, weryfikacja, rutynowa aktualizacja jednym lancuchem
&&, tabela rollback/typowe problemy.
Dwie rzeczy zapisane wprost, bo latwo je przeoczyc:
- kolejnosc DNS -> NPM host -> cert (HTTP-01 wymaga dzialajacego vhosta),
- `rm -rf /content/*` przed rozpakowaniem — bez tego dokument przelaczony z
public na private zostaje w wolumenie i dalej jest serwowany.
Runbook notuje tez aktualny wynik --check (22 trafienia /opt/homelab w
dokumentach public) jako decyzje do podjecia przed pierwsza publikacja:
wyczyscic zrodla albo swiadomie wpisac wyjatek do whitelisty.
services/kb-site/ — nginx:alpine serwujacy wolumen kb-site_content
(:ro, /usr/share/nginx/html), pelny layout z CLAUDE.md: docker-compose.yml,
service.yaml, README-wskaznik, env.example (swiadomie pusty — brak sekretow),
healthcheck.sh. Wpis w hosts/piha/services.yaml.
Port hosta 8250, NIE 8240 z opisu zadania: 8240 jest juz zajete przez narty27
(services/narty27/docker-compose.yml). Blok statyczny PIHA to 8210 paperless,
8220 nextcloud, 8230 kb-query, 8240 narty27 -> 8250 to nastepny wolny.
Uzgodnione z operatorem.
exposure: public — w odroznieniu od narty27 ta wystawka ma byc dostepna z
internetu przez vhost npm@PIHA kb.okit.pl; bind :8250 jest upstreamem proxy,
nie punktem wejscia.
Tresc jest czystym artefaktem repo (wyjscie scripts/kb/gen_pages.py), zyje
wylacznie w wolumenie kb-site_kb-site_content — bez binda pod /opt/homelab/data
i bez zadania backupu: odtworzeniem jest regeneracja z kb/.
Sprawdzone lokalnie: docker compose config -q, bash -n healthcheck.sh,
yaml.safe_load na obu manifestach.
Tryb --check nie generuje niczego: skanuje build/kb-site/**/*.html linia po
linii i przerywa z exit 1 na pierwszym zestawie trafien. Wzorce:
ip-rfc1918 192.168./10./172.16-31.
ip-tailscale 100.64-127.
ip-public-v4 reszta poprawnych adresow v4 (loopback, link-local, multicast,
broadcast i pule dokumentacyjne RFC 5737 sa neutralne)
ip-v6 adresy z "::" albo >=4 grupami (3 grupy to zwykle godzina)
port :NNNN w zakresie 1024-65535
path-host /home/... i /opt/...
token-hex ciagi hex >=32 znakow
token-b64 ciagi base64-podobne >=40 znakow mieszajace cyfry i litery
(sciezki absolutne odsiane — raportuje je path-host)
Raport: plik:linia [wzorzec] trafienie, na koncu licznik per wzorzec.
scripts/kb/check_whitelist.txt — swiadome wyjatki, na start PUSTY (same
komentarze z opisem formatu). Wpis to `<fragment>` albo
`<sciezka strony>|<fragment>`; fragment dopasowuje sie jako podciag trafienia,
wiec jeden wpis `/opt/homelab` wycisza wszystkie warianty.
Uruchomione lokalnie na 11 wygenerowanych stronach: 22 trafienia, wszystkie
path-host (/opt/homelab/... w dokumentach public), zero IP, portow i tokenow.
Whitelist zostaje pusta — decyzja co z tymi sciezkami zrobic nalezy do
operatora (patrz kb/runbooks/kb-site-deploy.md).
scripts/kb/gen_pages.py — kb/**/*.md -> build/kb-site/ (index.html pogrupowany
per type + strona na dokument). Renderer markdown na samej bibliotece
standardowej, wzorowany na ~/narty-2027/saalbach-kb/gen_pages.py; parser
frontmattera wspoldzielony z check_okf.py, zeby lint i generator widzialy
frontmatter tak samo.
Kwalifikacja fail-closed: publikowany jest wylacznie dokument z jawnym
`visibility: public`. Brak frontmattera, niepoprawny YAML, brak pola albo inna
wartosc = private. Linki do dokumentow nieopublikowanych nie sa renderowane jako
linki — zostaje etykieta z dopiskiem [private]; render_inline ma druga bramke
(linkuje tylko http/mailto/kotwice/.html), wiec martwy odnosnik nie ma jak
przeciec na strone.
BASE_URL jest parametrem (--base-url, domyslnie https://kb.okit.pl) i trafia do
<link rel="canonical">. Stopka kazdej strony: data generacji + git rev-parse
--short HEAD.
check_okf.py: EXCLUDE_DIRS = ("build",) — wyjscie generatora nie jest zrodlem
i nie podlega lintowi. build/ dopisany do .gitignore.
Uruchomione lokalnie: 150 dokumentow kb/, 10 public, 140 pominietych.
Liczba przepisana z README sprzed circuit breakera; 1764052 dodalo 4 testy
(trip po N kolejnych, reset po sukcesie, 0 wylacza, flush threadingu przy
abortcie) i zaktualizowalo licznik w sekcji Tests, ale nie w DoD.
Potwierdzone przebiegiem: 55 passed w jobs/mail-body-ingest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Weryfikacja 0-odwolan z 01db57a liczyla tylko podzbior prefiksow — poza nim
zostalo 13 wskaznikow w plikach niemarkdownowych i w prozie dokumentow:
docs/kb/modules/05-faza3-plan.md -> kb/phases/kb-m5-faza3.md (2x systemd)
docs/kb/modules/05-faza4-plan.md -> kb/phases/kb-m5-faza4.md (kb-query app.js)
docs/kb/modules/DECYZJE-*.md -> kb/decisions/kb-dokumenty-otwarte.md
docs/backlog.md (npm panel admina) -> kb/decisions/backlog-aktywne.md
docs/backlog/ (uid/gid floty) -> kb/decisions/backlog-uid-gid-flota.md
docs/kb/modules/0X-*.md -> kb/phases/kb-m*.md
docs/incidents/2026-07-30-*.md -> kb/incidents/ (3x node-agent)
docs/architecture/RECON-multi*.md -> kb/subsystems/recon-multiagent.md
jobs/deploy-runner/README.md -> kb/services/job-deploy-runner.md
Wyjatek zamierzony: `docs/kb/modules/05-faza3-pilot-streszczen.md` w §11 planu
fazy 3 to nazwa artefaktu, ktory nigdy nie powstal — przepiety na docelowa
konwencje (kb/phases/kb-m5-faza3-pilot-streszczen.md), zeby przyszly plik
wyladowal w nowym drzewie, a nie w skasowanym katalogu.
Weryfikacja: skan po 67 sciezkach zmigrowanych w tej galezi (git grep -F na
kazdej) = 0 trafien poza docs/sessions (logi historyczne, celowo nietkniete);
0 martwych linkow markdown na 190 plikach; check_okf.py 190/190 ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Stub z 2026-04-15 (65 linii) z pierwszego reconu, gdy homelab byl jednym
RPi5. Wszystkie "Unknown / needs clarification" dawno odpowiedziane przez
kb/subsystems/fleet-inventory.md i hosts/<node>/capabilities.yaml.
Kasowany, a nie migrowany, bo niesie publiczne IPv4/IPv6 i Tailscale IP
VPS-a przy zerowej wartosci merytorycznej — migracja oznaczalaby
przeniesienie tych danych do KB bez zadnego zysku.
Zero odwolan w repo. Tresc pozostaje w historii gita.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Naprawa kontraktu CLAUDE.md §Service Structure (opcja b). Migracja do KB
zabrala README z katalogow serwisow i hostow, przez co 0/26 katalogow
services/ spelnialo wymagany layout. Wskazniki przywracaja nawigacje,
nie duplikujac tresci.
31 wskaznikow, jednolity format, dokladnie 5 linii:
# <nazwa>
<jedno zdanie opisu>
Dokumentacja: [kb/...](../../kb/...)
Opis nie jest pisany od zera — wyciagany z kb-doca: pierwsze pelne zdanie
pierwszego akapitu (sklejane z zawinietych linii, ciete tylko tam, gdzie
backticki i nawiasy sa zbilansowane), a dla node'ow czlon tytulu H1 po
myslniku. Dla ha-mcp opis z H1, bo pierwszy akapit zaczyna sie od markera
statusu. Wiodace markery "**Status: ...**" sa zdejmowane.
26 x services/<svc>/README.md, 5 x hosts/<node>/README.md.
WYJATEK services/home-assistant/config/ken-legacy/README.md: pelne
ostrzezenie "historical archive, do not deploy" przywrocone doslownie
z historii (odzyskane z drzewa sprzed migracji) + link do kb-doca.
Ostrzezenie musi stac tam, gdzie chroni — w katalogu archiwum, nie tylko
w KB. Odwolanie do services/home-assistant/DESIGN.md przepiete na
kb/decisions/ha-configs-as-code.md + kb/incidents/2026-07-22-ha-dwie-instancje.md.
check_okf.py: POINTER_GLOBS + is_pointer() wykluczaja wskazniki ze scope'u
lintu. Wskazniki celowo NIE maja frontmattera OKF — to nawigacja, nie
dokumenty KB. Wykluczenie zapisane wprost, zeby poszerzenie SCOPE nie
zaczelo ich nagle walidowac.
Bez wskaznikow: hosts/chelsty-ha/ i hosts/lustro/ — nie maja dokumentow
w kb/nodes/ (luka odnotowana juz w reconie etapu 1). Utworzenie ich
wymagaloby napisania nowej dokumentacji, czyli wyjscia poza konwersje.
Lint: 190/190 ZGODNE. Weryfikacja 822 plikow: 0 martwych linkow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rozstrzygniecie 1. Monolit mieszal cztery typy OKF. Rozbity PER TYP po
granicy sekcji `##`:
10 x decision — pozycje backlogu (w tym backlog-aktywne 28 KB
i backlog-zamkniete 12 KB, ktore zostaja calosciami)
7 x incident — bugi/awarie dotad wtopione w backlog: cutover HA ken,
checkpoint observera, ha-diag-agent node=unknown,
deploy-local ghost-kontenery, paperless-worker config,
deploy-node nie przebudowuje obrazu, ollama bez sterownika
2 x phase — HA configs-as-code, monitoring floty Prometheus
kb/phases/backlog.md zostaje jako cienki indeks (type: phase, status: active):
oryginalna preambula + wygenerowany spis linkow do wszystkich 19 elementow.
23 przychodzace odwolania zostaja przepiete na ta sciezke w grupie 7.
NIE rozbijano po `###` (38 pozycji w "Aktywne" + 13 w "Zamkniete" = 51
plikow). Rozstrzygniecie mowi "rozbij per typ", a nie per pozycja;
rozdrobnienie do 51 plikow rozerwaloby czytelnosc backlogu.
Kontrola: preambula + 19 sekcji == oryginal z HEAD (multizbior niepustych
linii). Tresc pozycji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
czujniki-2026-07-30 (node-agent vs stability-agent)
lustro-shipping-2026-07-16 (event=dead prom=up, 1507 mismatchy)
prometheus-cutover-2026-07-06 (recon starego toru livenesci)
piha-slim-2026-07-02 (audyt odchudzania PIHA)
vps-stacki-2026-07-27 (audyt niezarzadzanych stackow na VPS)
ODSTEPSTWO OD RECONU — swiadome. Recon typowal te 5 plikow jako SPLIT
(audit+decision / audit+incident / audit+phase). Rozstrzygniecie 2 wprowadza
typ `audit` z polem as_of i mapuje kazdy z nich na JEDNA sciezke
kb/audits/<obszar>-<data>.md. Audyt jest spojna migawka stanu z konkretna
data — rozbicie go na "ustalenia" i "rekomendacje" rozerwaloby ten kontekst
i wymagaloby redakcji tresci, czego etap 2 zabrania. Zostaja w calosci.
Efekt: 29 SPLIT-ow z reconu realizowane jako 24 (10 service+runbook,
14 wielotypowych), 5 zamienionych na caloscowe dokumenty type: audit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/kb-overview.md — architektura 4 warstwy x 4 filary, zasady
przekrojowe, kolejnosc projektow, stan per zrodlo, konwencje katalogow
kb/decisions/kb-log-decyzji.md — sekcja "Decyzje — zamkniete vs otwarte"
Tresc sekcji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/control-plane.md — status: deprecated,
superseded_by: "przepisany tor redeploy, commity da151fc/79bfe8c 2026-08-03"
kb/runbooks/control-plane-deploy-recovery.md — Deployment + Recovery
Rozstrzygniecie 3: dokument NIE jest odswiezany, tylko oznaczony jako
nieaktualny. Ostatnia zmiana tresci 2026-05-27, czyli przed przepisaniem
toru redeployu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/service-lifecycle.md (visibility private wg rozstrzygniecia 6)
kb/runbooks/service-operational-recovery.md — Operational Recovery
Tresc sekcji nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/subsystems/deployment.md — konwencje deployu
kb/incidents/deploy-sh-vps-niszczy-control-plane.md — sekcja
"ZNANY BUG — deploy.sh vps niszczy control-plane (2026-06-25)"
UWAGA: "Recovery Workflow" to ### zagniezdzone w "Staged Deployment
Framework". Split mechaniczny tnie wylacznie po ##, a wyciagniecie tego
fragmentu wymagaloby przebudowy tresci — zostaje w dokumencie glownym.
Do rozwazenia jako osobny runbook w etapie redakcyjnym.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/decisions/ha-configs-as-code.md — decyzje projektowe HA configs-as-code
kb/incidents/2026-07-22-ha-dwie-instancje.md — sekcja "Incident log":
dwie instancje HA sterujace domem rownolegle po migracji
Incydent byl dotad wtopiony w dokument decyzyjny; teraz jest adresowalny
jako osobny wpis type: incident. Tresc nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/job-deploy-runner.md (How it works now)
kb/decisions/deploy-runner-uzasadnienie.md (What was broken)
kb/runbooks/deploy-runner-install.md (Install per node, Operating it, Tests)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/paperless.md (Stack, OIDC, Storage i backup, RAM na PIHA)
kb/decisions/paperless-split-ocr.md (split OCR serwis@PIHA + worker@SOLARIA)
kb/runbooks/paperless-cutover.md (cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/nextcloud.md (Stack, WebDAV dla ingestu, Storage i backup)
kb/decisions/nextcloud-host-piha.md (decyzja: host = PIHA)
kb/runbooks/nextcloud-cutover.md (OIDC przez Forgejo + cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/gokapi.md (Stack, Backup, Rejestracja w repo)
kb/decisions/gokapi-storage-e2e-siec.md (storage lokalny zamiast S3,
szyfrowanie E2E, bind tylko na Tailscale)
kb/runbooks/gokapi-cutover.md (cutover checklist)
Tresc sekcji nietknieta; kontrola multizbioru linii == oryginal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
kb/services/fleet-prometheus.md (Stack, Placement & exposure, Data, Next steps)
kb/decisions/fleet-prometheus-osobny-od-prom.md ("Why separate from the home prom")
kb/runbooks/fleet-prometheus-deploy.md (Configuration, Verify)
Wzajemne links. Tresc sekcji nietknieta; kontrola: multizbior niepustych
linii czesci == oryginal z HEAD.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Wzorzec mechaniczny: sekcje deploy/verify/install/testy wycinane do
kb/runbooks/<serwis>-*.md, reszta zostaje dokumentem type: service.
Wzajemne `links` w obie strony. Tresc sekcji nietknieta — przenoszone
doslownie, dodany wylacznie naglowek H1 nowego runbooka.
kb-query, paperless-worker, planner-agent, ha-diag-agent, ollama-piha,
narty27, home-assistant, ha-mcp, job-gmail-header-backfill, job-mail-body-ingest.
Weryfikacja: dla kazdego pliku multizbior niepustych linii
(main + runbook) == oryginal z HEAD. Zero zgubionych, zero dodanych.
Recon szacowal 13 splitow service+runbook; faktycznie 2-typowych jest 10,
pozostale 5 (paperless, nextcloud, gokapi, fleet-prometheus, deploy-runner)
sa 3-typowe i ida osobno jako splity wielotypowe.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rozstrzygniecie 5: katalogi z kodem, ktore nie mialy README, dostaja stub
(frontmatter + stub: true + opis jednozdaniowy). NIE pisana pelna dokumentacja.
kb/services/: control-plane, node-agent, brain-watchdog, node-exporter,
job-gmail-bulk-import, pkg-kb-mail, pkg-kb-retrieval.
Kazdy stub podaje zrodlo opisu (service.yaml / docstring / CLAUDE.md /
pyproject.toml). Dla gmail-bulk-import i kb-retrieval zrodla brak — stub
mowi o tym wprost zamiast zmyslac opis.
Dodatkowo kb/subsystems/repo-operating-contract.md — wskaznik na CLAUDE.md
(rozstrzygniecie 4). CLAUDE.md zostaje w korzeniu jako zywa konfiguracja
narzedzia; kb-doc niesie pole contradicts: brak katalogow services/joplin,
services/outline, services/ai-cluster deklarowanych w sekcji
"Repo-managed services on VPS". Kontekst PR2 feat/vps-service-migration,
swiadomie nienaprawiane.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nowy typ `audit` (rozstrzygniecie 2) — migawka stanu z pola `as_of`,
nie opis stanu biezacego.
monitoring-coverage-2026-07-14, ha-automatyzacje-2026-07-23.
Pozostale 5 audytow/reconow jest wielotypowych — wychodza w grupie SPLIT-ow.
git mv + frontmatter, tresc nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jedyny jawny incident-doc w repo. Pozostale incydenty siedza wtopione
w docs/backlog.md, services/home-assistant/DESIGN.md i lustro-shipping-recon
— wychodza w grupie SPLIT-ow.
git mv + frontmatter, tresc nietknieta.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Zaadaptowany z ~/narty-2027/saalbach-kb/check_okf.py. Tamten sprawdzal
wylacznie obecnosc frontmattera i niepuste `type`. Tutaj dochodza reguly
tego repo: okf przypiete do "0.1", type/visibility/status z zamknietych
list, updated/as_of jako YYYY-MM-DD, as_of wymagane wylacznie dla
type: audit, superseded_by wymagane wylacznie dla status: deprecated,
stub jako bool, links rozwiazywalne wzgledem katalogu dokumentu.
Zakres walidacji: kb/ + docs/sessions/. Reszta repo (CLAUDE.md, README.md,
.claude/skills/) lezy poza baza wiedzy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tolerancja pojedynczego nieudanego batcha jest słuszna, przeżycie martwej Ollamy
już nie. Parse archiwum jest jednowątkowy i wyprzedza GPU, więc przy Etapie B
(~212k kopert bez --since) padnięta Ollama przemieliłaby resztę korpusu z
prędkością parse'u, oznaczając każdy chunk jako chunks_errors — bez ani jednego
zapisu, ale kosztem ~2 h przebiegu do powtórzenia. Znany tryb awarii
Ollama@SOLARIA jest totalny (zniknięcie kontenera / network-detach, 4 incydenty,
§1.4/§7), nie częściowy, więc próg z kolejnych porażek trafia w niego od razu.
--max-embed-failures N (domyślnie 5, 0 wyłącza) → EmbedBackendUnavailableError
i exit 2, odrębny od exit 1 (który pełny korpus osiąga legalnie na pojedynczych
parse_errors — §1.5). Licznik zeruje się po udanym batchu, więc kryterium jest
"kolejnych", nie "łącznie". Przy abortcie dopychane są zaległe wpisy
entities[type=threading]: nie zależą od Ollamy, są idempotentne, a ich odtworzenie
oznaczałoby ponowny odczyt tych samych 27 GB. Nowy licznik embed_batch_failures
jest wyłącznie diagnostyczny — równania bilansu bez zmian.
Plan §9: dopisane decyzje operatora do Etapu B (plastry po 50k, breaker, pominięty
dry-run całości, hybrid default poza zakresem) + nota jak czytać exit 1 vs exit 2.
Testy: 4 nowe (trip po N kolejnych, reset po sukcesie, 0 wyłącza, flush threadingu
przy abortcie); 55 passed mail-body-ingest, 25 passed kb-retrieval. Smoke:
--limit 5 dry-run na żywym kb-postgres@PIHA — bilans domknięty, zero zapisów,
zero wywołań Ollamy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tabela §12 wyglądała jak lista TODO mimo nagłówka „Kroki 0-4 WYKONANE" — to
wprowadzało w błąd przy planowaniu kolejnych tasków (założono zbędną pracę nad
batchingiem, zrobionym już w 51998fd). Każdy wiersz dostaje kolumnę Stan
(WYKONANE / OTWARTE) i dowód: commit, plik lub weryfikacja na żywej bazie.
Stan ustalony z dowodów w repo/gicie, nie z nagłówka:
- Kroki 0-3: commity 348ce10, 51998fd, ad0ef40, a640cf1 + istniejące pliki/testy
- Krok 4: §7 + potwierdzenie na żywej bazie (33 871 chunków gmail = Etap A)
- Krok 5: §8 PASS, commity 56f64e9 / bce635c / 71eb264
- Kroki 6 i 7: OTWARTE — brak commitów, brak artefaktów, wolumen bazy to nadal
wyłącznie Etap A
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R1 (critical): _prune_stopped_containers no longer calls containers.prune().
The unfiltered API call removes EVERY non-running container, ignoring
RestartPolicy and compose labels — that is what deleted ollama@solaria 19 s
after an operator `docker stop`. No filter argument can fix it (`until` filters
on creation time, not stop time). Replaced with explicit enumeration over
containers.list(all=True, filters={"status": "exited"}), skipping anything with
restart policy unless-stopped/always/on-failure or a com.docker.compose.project
label. A stopped managed service is recorded operator intent and now belongs to
the module's NEVER TOUCHED list; only one-off leftovers are removed. Dangling
image and build cache prune are unchanged.
R2 (high): _sd_card_rate_ok → _cleanup_rate_ok, applied to every cleanup-
eligible node type. ai_node and standard pruned every 60 s (1440/day, "0 MB
reclaimed" in practically every cycle) with no rate limit at all; they now share
sd_card's CLEANUP_INTERVAL_SECS (24 h) and mark the cleanup timestamp.
R3 (medium): removals are named in the log — WARNING with the container names
when the list is non-empty, INFO otherwise. The old line reported megabytes
only, which is why ollama's deletion looked identical to every no-op cycle.
Tests: services/node-agent/tests/test_safe_cleanup.py (22 cases) — the ollama
regression (exited + unless-stopped survives, prune() never called), all three
restart policies, compose-label protection, disposable-leftover removal, sweep
continues past a failed remove, WARNING-level naming, and the rate limit for
ai_node/standard/sd_card plus lte_node still doing nothing.
Refs docs/incidents/2026-07-30-ollama-solaria-vanish.md §7 R1-R3.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
node-agent calls docker containers.prune() with no filters every
CHECK_INTERVAL (60 s on VPS). The Docker API removes EVERY non-running
container regardless of RestartPolicy or compose labels — this destroyed
ollama@solaria on 2026-07-30 (19 s after an operator `docker stop`).
On VPS the blast radius is worse: humanai-mailer and humanai-landing have
no compose definition in this repo (recreated by hand from `docker inspect`),
so a pruned container there is an irreversible loss of the only config source.
self.node_type is read only by run_safe_cleanup() (node_agent.py:648,654) and
two log lines (250, 1103). Verified additionally for VPS: the control-plane
filesystem rotation and health probe in run_once() are gated on
`node_name == VPS_NODE_NAME`, not node_type — so they keep running. Monitoring,
event shipping and action dispatch are likewise unaffected.
Temporary — remove once R1 (explicit-enumeration prune) is deployed.
Refs docs/incidents/2026-07-30-ollama-solaria-vanish.md §7 M1.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
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>