Compare commits

..

No commits in common. "master" and "task/node-onboarding" have entirely different histories.

795 changed files with 1289 additions and 304729 deletions

View file

@ -1,116 +0,0 @@
---
name: kb-authoring
description: Conventions for writing/editing source knowledge-base documents under kb/**/*.md (frontmatter schema, type taxonomy, visibility default, validation). Trigger whenever creating or editing a file under kb/ — this is about authoring the sources, not publishing kb-site (see kb-publish for that).
---
## Scope
This skill governs **writing kb/\*\*/\*.md documents themselves** — the OKF-format
knowledge base sources. It is not about publishing them: for "should I run the
publish script", see the `kb-publish` skill, referenced again at the bottom
here.
`docs/sessions/*.md` files use `type: session-log` frontmatter and are validated
by the same script, but they are **not** kb/ documents — they are a running
diary of work sessions. **Never convert a session log into a kb/ doc** and
never move/copy a kb/ doc's content into docs/sessions/. If material in a
session log deserves a permanent home (a decision, an incident writeup, a
runbook), write a **new** file under kb/ that captures it properly — don't
relocate the diary entry.
## Frontmatter schema (OKF v0.1)
Every kb/ document opens with a YAML frontmatter block (`---` ... `---`) as
the first thing in the file. Validated by `scripts/kb/check_okf.py` — read
that file if you need the exact rules; summary:
| Field | Required | Values | Notes |
|---|---|---|---|
| `okf` | yes | `"0.1"` | Pinned. Always this literal string, quoted. |
| `type` | yes | one of the taxonomy below | Determines which `kb/<type>s/` directory the file lives in. |
| `visibility` | yes | `private` \| `public` | **Default is `private`.** See below — never default to `public`. |
| `status` | yes | `active` \| `deprecated` \| `planned` | `planned` = decisions not yet made / work not yet started. `deprecated` requires `superseded_by`. |
| `updated` | yes | `YYYY-MM-DD` | Bump every time you edit the file's content. |
| `links` | yes (may be `[]`) | list of paths | Relative to **this file's own directory**, e.g. `../phases/backlog.md` or `sibling-doc.md`. Every entry must resolve to an existing file — check_okf verifies this. |
| `as_of` | only for `type: audit` | `YYYY-MM-DD` | Required exactly when `type: audit`, forbidden otherwise. See "audit" below for why this is a separate field from `updated`. |
| `superseded_by` | only when `status: deprecated` | free text or path | Required exactly when `status: deprecated`, forbidden otherwise. Points to whatever replaced this doc (a doc path, or prose describing the replacement if there's no single successor file). |
| `contradicts` | optional | list | Free-text notes about known conflicts with other sources (e.g. "CLAUDE.md says X, but the directory doesn't exist"). Entries that look like a `.md` path (contain `/` and end in `.md`) are checked for existence like `links`; plain-text entries are not. |
| `stub` | optional | bool | Marks a doc as a placeholder/incomplete. Must be a real YAML bool (`true`/`false`), not a string. |
## Type taxonomy — kb/<type>s/
| `type` | Directory | What goes here |
|---|---|---|
| `node` | `kb/nodes/` | One doc per physical/virtual host — role, configured services, runtime data paths. Mirrors `hosts/<node>/`. |
| `service` | `kb/services/` | One doc per deployed service — what it is, how it's used/configured. Mirrors `services/<svc>/`. |
| `subsystem` | `kb/subsystems/` | Cross-cutting architecture/design docs describing how something works *in general* (access model, agent system, deployment conventions) — not tied to a single node or service. Kept in sync with current reality (unlike `audit`, see below). |
| `decision` | `kb/decisions/` | A choice that was made (or is still open, `status: planned`) plus its rationale — forward-looking, governs future behavior. Gets edited in place and re-dated as the decision evolves; it is not a historical log of what happened. |
| `incident` | `kb/incidents/` | A factual account of something that broke: symptom, root cause, fix/status, at a point in time. Slug conventionally date-prefixed (`YYYY-MM-DD-short-description.md`) since incidents are anchored to when they happened. Content generally stays close to the as-happened account rather than being rewritten into "current state" prose. |
| `runbook` | `kb/runbooks/` | Step-by-step operational procedure — deploy, install, recover, troubleshoot. Imperative, command-heavy, meant to be followed live. |
| `phase` | `kb/phases/` | A project/milestone plan broken into steps, tracking progress (including backlog/roll-up index docs). |
| `audit` | `kb/audits/` | A **point-in-time snapshot** of actual/verified state (ground truth recon), never an ongoing description. Requires `as_of` — the date the finding was true — kept distinct from `updated` (the date the doc text was last edited) precisely because an audit's findings can go stale even when nobody touches the file. Slug conventionally date-suffixed (`topic-YYYY-MM-DD.md`). |
`session-log` also exists as a `type` value (for `docs/sessions/`) but is **out
of scope for `kb/`** — see Scope above.
### Decision vs incident vs runbook vs audit — how ambiguous cases got resolved
This came up repeatedly during the kb/ migration (docs that mixed genres got
split, not force-fit into one type):
- **decision vs incident**: a doc that both narrates "here's what broke" *and*
states "here's the guardrail we adopted because of it" is two documents.
Split the incident account into `kb/incidents/`, keep (or extract) the
resulting decision/guardrail into `kb/decisions/`. Example:
`home-assistant/DESIGN.md``kb/decisions/ha-configs-as-code.md` +
`kb/incidents/2026-07-22-ha-dwie-instancje.md`.
- **decision vs runbook**: if a decision doc contains a reusable operational
recipe (install steps, recovery procedure), that section is a runbook, not
part of the decision's rationale. Split it out. Example:
`deploy-runner``kb/services/job-deploy-runner.md` (how it works) +
`kb/decisions/deploy-runner-uzasadnienie.md` (why) +
`kb/runbooks/deploy-runner-install.md` (how to install/operate it).
- **subsystem vs audit**: a `subsystem` doc is the *maintained* description of
how something is designed/intended to work — you keep it in sync. An
`audit` is a *frozen* investigation result ("I checked X on this date and
found Y") — you don't rewrite it as things change, you write a new audit
or a decision/incident instead. This is why `audit` got its own `as_of`
field distinct from `updated`.
- When in doubt, prefer splitting a doc across two types over stretching one
type's frontmatter to cover mixed content — that's the pattern the
migration itself followed (see git log `feat(kb): SPLIT ...` commits for
worked examples).
## Visibility default: private
**`visibility` defaults to `private`.** Every new document must be written
`private` unless the operator has explicitly and consciously decided it
should be `public` — never infer or default to `public` on your own, even if
the content looks harmless. `visibility: public` documents are the only ones
`scripts/kb/gen_pages.py` will ever emit to the public kb-site (fail-closed:
missing/unparseable/unrecognized `visibility` is treated as private).
## After every change to kb/**/*.md
Run the validator and fix anything it flags before considering the edit done:
```bash
python3 scripts/kb/check_okf.py
```
It checks frontmatter parses, `okf` is pinned, `type`/`visibility`/`status`
are from their closed lists, dates are well-formed, `as_of`/`superseded_by`
are present exactly when required, and every `links`/path-like `contradicts`
entry resolves to a real file. A red run means something is broken — fix it,
don't skip it.
## Creating a new document
Use `scripts/kb/new-doc.sh <type> <slug> [--public]` to scaffold a
correctly-placed file with valid frontmatter, then fill in the content.
## If you just made a public change
If a file you created or edited carries `visibility: public`, don't forget
the KB site itself doesn't update on its own — see the `kb-publish` skill for
the one-line reminder to run `scripts/kb/publish.sh`.

View file

@ -1,42 +0,0 @@
---
name: kb-publish
description: At session close, asks the operator whether to publish the KB site if kb/**/*.md changed this session. Trigger whenever the session is wrapping up (save-session, "kończymy sesję", natural end of task) and this session's own edits, or a merge/diff visible in git, touched files under kb/.
---
## What this skill does
Trigger point: **session close**, not every response. Session close means the
operator signals they're wrapping up — e.g. invoking `save-session`, saying
something like "kończymy sesję" / "koniec na dziś" / "wrap up", or the task
reaching its natural end with no further work queued. Don't act on this skill
mid-session just because a `kb/` file was touched.
At that moment, if this session touched `kb/**/*.md` — either through your
own edits or through a merge/diff you observed in git — **ask the operator
directly**, as a question requiring a yes/no answer, not a passive reminder:
> Sesja dotknęła kb/. Zregenerować i zasugerować publikację (`bash scripts/kb/publish.sh`)?
- If the operator confirms (any affirmative reply): print the ready-to-paste
command block so they can copy it straight into their own shell:
```bash
bash scripts/kb/publish.sh
```
Do not run it yourself — see below.
- If the operator declines: drop it, nothing more to do.
- If the operator doesn't respond or ignores the question (moves on to
something else, ends the conversation): do **not** ask again in this
session. One ask per session, max.
## What this skill does NOT do
**Never run `scripts/kb/publish.sh` yourself**, under any circumstances, even
if the operator's task prompt says to deploy, publish, or calibrate. The
script SSHes into PIHA and overwrites the live `kb-site` content volume on
production — that is out of scope for a worktree agent and requires an
explicit, direct instruction from the operator in the current turn.
If the operator explicitly asks you to run it, that instruction stands on its
own — this skill only governs the session-close question, not that request.

View file

@ -5,7 +5,7 @@ description: >
repo manifest, Tailscale mesh, node-agent, monitoring, and UI registration.
Keywords: "nowy node", "dodaj node", "onboarding", "onboard node".
living_doc: true
maturity: partial # PROVEN: 00-access, 20-base, 30-node-agent; WRITTEN: 40-register, 50-verify (live pending). Update after each step lands on a real node.
maturity: partial # PROVEN: 00-access; SCAFFOLD: base → verify. Update after each step lands on a real node.
---
> **Living document** — sections marked **SCAFFOLD** are stubs waiting for battle-testing on a real node.
@ -22,10 +22,10 @@ User asks to onboard / add a new node. Load this skill before touching any onboa
```
preflight (read-only)
└─ 00-access [PROVEN]
└─ 20-base [PROVEN]
└─ 30-node-agent [PROVEN]
└─ 40-register [WRITTEN — live pending]
└─ 50-verify [WRITTEN — live pending]
└─ base [SCAFFOLD]
└─ node-agent [SCAFFOLD]
└─ register [SCAFFOLD]
└─ verify(50) [SCAFFOLD]
```
Never skip ahead. Each step must exit 0 before the next begins.
@ -57,11 +57,9 @@ scripts/onboard/onboard.sh --node <name> --dry-run
| `00-preflight` | `steps/00-preflight.sh` | SCAFFOLD | Read-only: arch, RAM, docker, swap, MM runtime → YAML snippet for node.yaml |
| `00-access` | `steps/00-access.sh` | **PROVEN** | SSH key → `first_contact`, install Tailscale, `tailscale up` (interactive URL), verify over mesh |
| `10-bootstrap-runtime` | `steps/10-bootstrap-runtime.sh` | SCAFFOLD | Create `/opt/homelab/` layout, `chown <ssh_user>` |
| `20-base` | `steps/20-base.sh` | **PROVEN** | swap→zram, `/opt/homelab/` layout, event dir `/opt/homelab/events/<node>/` |
| `20-install-docker` | `steps/20-install-docker.sh` | SCAFFOLD | Install Docker Engine if `docker_present=false`; skip if already installed |
| `30-node-agent` | `steps/30-node-agent.sh` | **PROVEN** | rsync base compose + override, `docker compose up -d --build`, verify container + events |
| `40-register` | `steps/40-register.sh` | WRITTEN | Dopisuje node do `inventory/topology.yaml` + tworzy `hosts/<node>/services.yaml`, commit na branchu (bez push) |
| `50-verify` | `steps/50-verify.sh` | WRITTEN | SSH node: container+events; SSH VPS: restart observer + heartbeat poll + world/nodes.json |
| `40-deploy-node-agent` | `steps/40-deploy-node-agent.sh` | SCAFFOLD | Deploy node-agent container; user 1000:1000; `mem_limit` from node.yaml |
| `50-verify` | `steps/50-verify.sh` | SCAFFOLD | End-to-end smoke: event reaches control plane, visible in UI, Telegram alert path |
---
@ -93,7 +91,7 @@ services:
preflight fills `arch`, `ram_mb`, `docker_present`, `mm_runtime` — do NOT guess these.
Full schema: `kb/runbooks/node-onboarding-tool.md`.
Full schema: `scripts/onboard/README.md`.
---
@ -124,10 +122,6 @@ Always run `--dry-run` first; dry-run must print real commands (`run()` propagat
| `yaml_get` drops value prefix after `:` | Non-greedy colon: `s/^[[:space:]]*[^:]*:[[:space:]]*//'` — handles `systemd:unit` correctly |
| `yaml_get` keeps inline YAML comments | Strip with `s/[[:space:]]\+#.*$//` after extraction (requires ≥1 space before `#`) |
| dry-run stops at orchestrator level | `run()` wrapper + `export DRY_RUN=1` propagated to all step scripts; probes execute for real |
| rsync push Permission denied to VPS events/ | ssh-user must be in the **group that owns `/opt/homelab/events/`** (aerbot/1000 on VPS). Symptom: silent WARNING in node-agent log, 292k files backlog, panel stale. Fix: `usermod -aG 1000 <user>` on VPS + re-login |
| node-agent SSH key mount target | Mount the push key under the **container's HOME**: `/home/homelab/.ssh` (uid 1000 `homelab`), **NOT `/root/.ssh`** — ssh in `_ship_events_to_vps()` has no `-i` and only looks in `$HOME/.ssh`; a `/root/.ssh` mount is blind → `Permission denied` (lustro 2026-06-11, fix `a5a1352`). The new node's pubkey must also land in `authorized_keys` of `oskar@VPS` |
| observer not seeing new node after topology.yaml edit | `_load_inventory()` runs once at `__init__`. After `git pull` on VPS (bind-mount is live), **`docker restart control-plane-observer`** is required — no redeploy needed |
| worktree on wrong branch | Always check `git branch --show-current` on entry. One task = one worktree (`agent.sh new`). Never manually `git checkout` between task branches in the same worktree |
---

10
.gitignore vendored
View file

@ -3,10 +3,6 @@
*.env
!*.env.example
# ESPHome secrets
secrets.yaml
!secrets.yaml.example
# IDE artifacts
.idea/
.vscode/
@ -20,16 +16,10 @@ __pycache__/
venv/
.venv/
*.egg-info/
packages/*/build/
jobs/*/build/
# wyjscie generatorow (scripts/kb/gen_pages.py -> build/kb-site/) — artefakt, nie zrodlo
build/
# Tools
.aider*
.codex
# worktree task marker created by scripts/dev/agent.sh new — must stay untracked per worktree
.agent-task
# OS files
.DS_Store

View file

@ -1,9 +0,0 @@
{
"mcpServers": {
"ha": {
"command": "./services/ha-mcp/run.sh",
"args": [],
"env": {}
}
}
}

View file

@ -40,7 +40,7 @@ Pipeline stages: **prepare → validate → deploy → verify → diagnose (on f
## Node Onboarding
New nodes are onboarded via `scripts/onboard/` — an idempotent bash tool driven by
`hosts/<node>/node.yaml` manifests (no Ansible). See `kb/runbooks/node-onboarding-tool.md` for
`hosts/<node>/node.yaml` manifests (no Ansible). See `scripts/onboard/README.md` for
the full schema, step status table, and gotchas.
Key fields in `node.yaml`: `ssh_user`, `first_contact` (LAN IP — not `.local`),
@ -63,18 +63,6 @@ services/<service>/
Host-specific runtime config and secrets live at `/opt/homelab/config/<service>/` on the target node (not in Git). Docker Compose overrides are version-controlled at `hosts/<node>/runtime/<service>/docker-compose.override.yml` in this repo and applied during deployment.
## Shared Python Libraries (packages/)
Reusable Python packages that are not deployed services live in `packages/<lib>/`. They have a `pyproject.toml` and `src/` layout but no `docker-compose.yml` or `service.yaml`.
Services and jobs that depend on them install via Dockerfile:
```dockerfile
COPY packages/<lib>/ /packages/<lib>/
RUN pip install /packages/<lib>/
```
First library: `packages/kb-mail/` — KB envelope model, asyncpg DB helpers, append-only .eml archive helper.
## Agent System Architecture
The platform uses a multi-agent model with **human-in-the-loop** for destructive actions:
@ -90,26 +78,14 @@ The platform uses a multi-agent model with **human-in-the-loop** for destructive
Agent → /opt/homelab/actions/pending/<id>.json
→ Telegram notification → Operator approves
→ /opt/homelab/actions/approved/<id>.json
→ Executor dispatches to the target node → completed / failed
→ Executor runs → completed / failed
```
The executor never connects to a node (deliberate — see kb/phases/backlog.md
"Remediacja floty bez SSH"). It writes a dispatch file that the node collects:
| Action type | Inbox | Executed on the node by |
|---|---|---|
| `container_restart` | `actions/dispatch/<node>/` | node-agent (own docker socket) |
| `redeploy` | `actions/deploy/<node>/` | deploy-runner, host-level systemd (`jobs/deploy-runner/`) |
Both report back with an `action_result` event, which the executor turns into
`completed` / `failed`. `disk_cleanup` and `alert_only` still resolve inside the
executor.
Agents must never execute destructive actions (restarts, deploys, config changes) without a corresponding approved action file.
## Event System
Events are append-only, one JSON file per event, flat under `/opt/homelab/events/<node>/evt-<node>-<unixts>-<type>[-<service>].json`.
Events are append-only JSON lines at `/opt/homelab/events/YYYY-MM-DD/<node>/events.jsonl`.
Emit via `scripts/lib/events.sh` (shell) or `scripts/lib/events.py` (Python).
@ -120,8 +96,8 @@ Normalized event types: `deployment_started/completed/failed`, `service_unhealth
| Event type | Source | Action generated | Cooldown |
|---|---|---|---|
| `containers_not_running` | stability-agent | `container_restart` | dedup via stable ID |
| `healthcheck_failed` | node-agent | `container_restart` | dedup via stable ID |
| `service_unhealthy` / other | stability-agent | `redeploy` → dispatched to the node's deploy-runner (`jobs/deploy-runner/`) | dedup via stable ID |
| `mqtt_unreachable` | stability-agent | `container_restart` | dedup via stable ID |
| `service_unhealthy` / other | stability-agent | `redeploy` | dedup via stable ID |
| `disk_pressure` (high) | stability-agent | `disk_cleanup` | dedup via stable ID |
| `ha_websocket_dead` | ha-diag-agent | `container_restart` (homeassistant) | 30 min after completion |
| `ha_websocket_recovered` | ha-diag-agent | cancels matching restart | — |

View file

@ -31,29 +31,27 @@ Action approval flow: `pending/` → operator approves → `approved/` → execu
## Repository Structure
- `docs/`: [Infrastructure Standards](kb/subsystems/standards.md) and [Deployment Conventions](kb/subsystems/deployment.md).
- `kb/phases/subsystem-a-naprawa.md`: [Current Maintenance Plan (Control Plane)](kb/phases/subsystem-a-naprawa.md).
- `docs/`: [Infrastructure Standards](docs/standards.md) and [Deployment Conventions](docs/deployment.md).
- `hosts/`: Host-specific configurations and service assignments.
- `services/`: Reusable Docker Compose service definitions.
- `scripts/`: Deployment and management scripts.
## Getting Started
1. **Standardization**: Follow the [Infrastructure Standards](kb/subsystems/standards.md).
2. **Deployment**: See [Deployment Conventions](kb/subsystems/deployment.md) for how to roll out changes.
1. **Standardization**: Follow the [Infrastructure Standards](docs/standards.md).
2. **Deployment**: See [Deployment Conventions](docs/deployment.md) for how to roll out changes.
3. **SATURN**: Remember that SATURN is the only node where commits should be made.
## Documentation Index
- [Current Maintenance Plan (Control Plane)](kb/phases/subsystem-a-naprawa.md)
- [Infrastructure Standards](kb/subsystems/standards.md)
- [Agent Operating Procedures](kb/subsystems/agent-operating-procedures.md) (For AI/Non-Human Agents)
- [Deployment Conventions](kb/subsystems/deployment.md)
- [Hardware](kb/nodes/legacy-hardware.md)
- [Networking](kb/subsystems/networking.md)
- [Services](kb/subsystems/legacy-services-list.md)
- [Node Capabilities](kb/subsystems/capability-model.md)
- [Action Model](kb/subsystems/action-approval-model.md)
- [Infrastructure Standards](docs/standards.md)
- [Agent Operating Procedures](docs/agents.md) (For AI/Non-Human Agents)
- [Deployment Conventions](docs/deployment.md)
- [Hardware](docs/hardware.md)
- [Networking](docs/networking.md)
- [Services](docs/services.md)
- [Node Capabilities](docs/capabilities.md)
- [Action Model](services/agent-system/action-model.md)
---
*Note: This repository documents the state of the homelab. Runtime state lives outside the repository in `/opt/homelab`.*

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Access
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-20
links: []
---
# Agent Operating Procedures
This document defines the operating procedures, constraints, and interaction protocols for non-human agents (AI agents, autonomous scripts) within the Homelab Codex ecosystem.
@ -15,9 +6,9 @@ This document defines the operating procedures, constraints, and interaction pro
1. **Read-Only by Default**: Agents should assume read-only access to the `/opt/homelab` runtime unless explicitly executing an approved action.
2. **Git as Authority**: The repository on **SATURN** is the source of truth. Agents must not modify the runtime state on nodes directly without corresponding (or pending) Git state, unless it's an emergency mitigation.
3. **Human-in-the-Loop (HIL)**: All destructive or structural changes (restarts, deployments, config changes) must follow the [Action Approval Model](action-approval-model.md).
3. **Human-in-the-Loop (HIL)**: All destructive or structural changes (restarts, deployments, config changes) must follow the [Action Approval Model](../services/agent-system/action-model.md).
4. **Idempotency**: All scripts and actions proposed or executed by agents MUST be idempotent.
5. **Context-Awareness**: Agents MUST read the `README.md` and `kb/subsystems/agent-operating-procedures.md` at the start of every session to align with current infrastructure standards.
5. **Context-Awareness**: Agents MUST read the `README.md` and `docs/agents.md` at the start of every session to align with current infrastructure standards.
## 2. Agent Roles

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-20
links: []
---
# Node Capability Model
This document defines the capability model for the homelab infrastructure. The goal is to provide a declarative way to describe what each node can do, its constraints, and its suitability for various workloads.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: active
updated: 2026-05-27
links:
- ../runbooks/chelsty-deploy-recovery.md
---
# CHELSTY Runtime
This document describes the runtime environment and deployment flow for CHELSTY, an offline-capable home automation edge node split across two VMs.
@ -110,6 +100,48 @@ services:
Remove `monitor: false` once node-agent is bootstrapped on this VM.
## Deployment Flow
### Initial Bootstrap
```bash
./scripts/bootstrap/chelsty-runtime.sh
```
### Deploy services
```bash
./scripts/deploy/deploy-node.sh chelsty-infra
./scripts/deploy/deploy-node.sh chelsty-ha
```
### Manual (SSH) — chelsty-infra uses docker-compose v1
```bash
ssh oskar@100.122.201.22
cd ~/homelab-codex-ws/services/<service>
docker-compose -f docker-compose.yml \
-f ../../hosts/chelsty-infra/runtime/<service>/docker-compose.override.yml \
up -d --build --force-recreate
```
> **Note:** `docker compose` (v2) is **not** available on chelsty-infra — always use `docker-compose` (hyphenated, v1 1.29.2).
## Recovery Procedures
### Mosquitto stopped
```bash
ssh oskar@100.122.201.22 "docker start mosquitto"
# Ensure restart policy is correct:
docker update --restart unless-stopped mosquitto
```
### Zigbee2MQTT won't start
1. Check logs: `docker logs zigbee2mqtt --tail 50`
2. Verify SLZB-06U reachable from host: `nc -zv 192.168.1.105 6638`
3. Verify config is not empty: `cat /opt/homelab/data/zigbee2mqtt/data/configuration.yaml`
4. If config missing, recreate from the minimal template above
### SLZB-06U unreachable
`192.168.1.105:6638` EHOSTUNREACH means the coordinator is offline or the LAN is down. Zigbee2MQTT will keep retrying — no restart needed once the coordinator returns.
## Critical Backup Sets
| Data | Path |

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: service
visibility: private
status: active
updated: 2026-05-20
links: []
---
### CHELSTY Stability Agent
The stability-agent on CHELSTY provides local observability and health monitoring for the node's services and infrastructure.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Core Stack
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-06-25
links:
- ../incidents/deploy-sh-vps-niszczy-control-plane.md
---
# Deployment Conventions
This document describes the GitOps-lite deployment process for the homelab.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-12
links: []
---
# Homelab Event System
The homelab multi-agent platform uses a filesystem-first event architecture for observability, auditability, and agent reasoning.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "hosts/<node>/capabilities.yaml + kb/subsystems/fleet-inventory.md"
---
# Hardware
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: node
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/nodes/vps.md + kb/subsystems/fleet-inventory.md (stub z 2026-04-15, sprzed floty)"
---
# Hetzner VPS
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: service
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "brak katalogu services/joplin/ w repo — patrz contradicts w kb/subsystems/repo-operating-contract.md"
---
# Joplin Server
## Description

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-05-11
links:
- ../runbooks/service-operational-recovery.md
---
# Service Lifecycle and Recovery
This document defines the lifecycle of a service in the homelab and the procedures for operational recovery.
@ -35,6 +25,25 @@ This document defines the lifecycle of a service in the homelab and the procedur
- `docker compose down`.
- Archive `/opt/homelab/data/<service>` if necessary.
## Operational Recovery
### 1. Container Failure
If a service is unhealthy:
- Check `docker compose logs`.
- Restart: `docker compose restart`.
- Recreate: `docker compose up -d --force-recreate`.
### 2. Node Failure
If a host node fails:
- Services with `owner_node` matching the failed node must be recovered on a backup node or the node must be restored.
- Persistence data must be restored from backups to `/opt/homelab/data/<service>`.
### 3. Dependency Recovery
If a dependency fails:
- Services depending on it might report unhealthy status.
- Recover the dependency first.
- Re-verify dependent services.
## Persistent Data Conventions
- **Data**: `/opt/homelab/data/<service>` - Primary persistent state.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Networking
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: runbook
visibility: public
status: active
updated: 2026-05-12
links: []
---
# Node Onboarding Workflow
This document describes the process of onboarding a new Linux machine into the homelab platform.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-06-17
links: []
---
# Observer Runtime
The Observer Runtime is a lightweight agent responsible for synthesizing the operational world state of the homelab from raw events, logs, and state files.
@ -61,42 +52,6 @@ A single global checkpoint (`last_processed_file`) was replaced with this per-no
- `node_online`, `node_offline` — update node status in nodes.json
- `disk_pressure_*` — set `disk_pressure` field on the node record
## Node Liveness (TTL)
Node `status` is derived from **freshness**, not from the last status event.
Every reconcile cycle (`_prune_stale_world`, runs even with no new events) the
observer computes `now - last_seen` per node and classifies it:
| tier | age (always-on) | age (remote / LTE) | status written | panel health |
|---|---|---|---|---|
| fresh | ≤ 180 s | ≤ 900 s | `online` | nominal |
| stale | 180600 s | 9003600 s | `stale` | degraded |
| dead | > 600 s | > 3600 s | `offline` | error |
| unknown | `last_seen` missing | — | (unchanged) | (unchanged) |
Thresholds (tied to the 60 s node-agent heartbeat: fresh = 3× interval) live in
**one place**: `services/control-plane/src/liveness.py`, imported by the observer
and both operator UIs (`compute_liveness` / `ttls_for` / `node_health`). Remote
nodes (topology role `remote`, or `chelsty-*`) use the wider TTLs above.
Override via env: `LIVENESS_TTL_FRESH`, `LIVENESS_TTL_DEAD`,
`LIVENESS_REMOTE_TTL_FRESH`, `LIVENESS_REMOTE_TTL_DEAD`.
This fixes the "dead node shown NOMINAL" silent outage: previously `status`
stayed `online` forever because the only thing that flipped it to offline was a
`node_offline` event, which a crashed/partitioned node can never emit.
**Transitions** are not silent. On crossing a boundary the observer writes an
event (`node_stale`, `node_offline`, or `node_online` on recovery) tagged
`source: "observer"` with the affected node in both `node` and
`payload.affected_node`. These are **skipped on re-ingest** (so they never reset
`last_seen`) and routed by the supervisor to `alert_only` actions (Telegram).
**Read-time safety net:** both operator UIs recompute liveness from `last_seen`
at request time using the same helper, so even a *stalled observer* (frozen
`nodes.json`) still surfaces a dead node. Services inherit their node's liveness
(a service on a dead/stale node is never shown nominal) — computed read-time,
`services.json` is not mutated.
## Incident Lifecycle
1. **Detection**: A `service_unhealthy` or `healthcheck_failed` event creates or increments an active incident.

65
docs/questions.md Normal file
View file

@ -0,0 +1,65 @@
# Unknowns and Clarification Questions
## Description
This page lists information that is missing or unclear from the current homelab documentation.
## Current configuration
The currently documented configuration is limited to:
- Raspberry Pi 5 as the main server.
- Docker, Portainer, and Nginx Proxy Manager as the core stack.
- NAT with forwarded ports:
- `80-81` to `4480-4481`
- `443` to `4443`
- Public access through Nginx Proxy Manager with Let's Encrypt HTTPS.
- Private access through Tailscale.
- Hetzner VPS handoff:
- Hostname: `ubuntu-4gb-hel1-1`
- Tailscale IP: `100.95.58.48`
- Public IPv4: `135.181.153.108`
- Public IPv6: `2a01:4f9:c014:98f0::1`
- Running container: `npm`
- Joplin files created but not running.
## Known facts
- The homelab is documented only from the known facts above.
- Anything not listed as known remains unconfirmed.
## Unknown / needs clarification
1. What operating system and version is running on the Raspberry Pi 5?
2. What is the Raspberry Pi 5 RAM size?
3. What storage devices are used, and where is persistent service data stored?
4. What is the Raspberry Pi 5 LAN IP address?
5. Is the Raspberry Pi 5 using DHCP or a static IP address?
6. What router or firewall performs NAT and port forwarding?
7. Is the WAN IP static, dynamic, or behind CGNAT?
8. Does external port `80` map to internal port `4480`, and does external port `81` map to internal port `4481`?
9. Are the forwarded ports TCP only, UDP only, or both?
10. Are any other ports forwarded?
11. What domain names or subdomains point to the homelab?
12. What are the Nginx Proxy Manager proxy hosts?
13. Which services are public, and which are private-only?
14. Is HTTP-to-HTTPS redirection enabled in Nginx Proxy Manager?
15. Are Nginx Proxy Manager access lists used?
16. How are Docker, Portainer, and Nginx Proxy Manager deployed?
17. Are Docker Compose files, Portainer stacks, or other manifests available?
18. What containers are currently running?
19. What Docker networks and volumes exist?
20. What is the Tailscale device name for the Raspberry Pi 5?
21. Does the Raspberry Pi 5 advertise Tailscale subnet routes?
22. Is the Raspberry Pi 5 configured as a Tailscale exit node?
23. Is Tailscale SSH enabled?
24. What backup system exists, if any?
25. What monitoring or alerting exists, if any?
26. Is the Hetzner VPS part of the homelab documentation scope, a separate system, or both?
27. What is the operating system version on `ubuntu-4gb-hel1-1`?
28. Is public Nginx Proxy Manager admin access on port `81` intentionally reachable on `135.181.153.108`?
29. Has DNS record `joplin.okit.pl -> 135.181.153.108` been created?
30. Has optional AAAA record `joplin.okit.pl -> 2a01:4f9:c014:98f0::1` been created?
31. Has `POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD` been changed before first Joplin production start?
32. Has the Nginx Proxy Manager proxy host for `joplin.okit.pl` been created?
33. Are ports `80` and `443` publicly reachable on the Hetzner VPS for Let's Encrypt HTTP validation?

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-11
links: []
---
# Service Model and Healthchecks
This document defines the normalized service model for the homelab.

View file

@ -1,13 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-04-15
links: []
superseded_by: "kb/subsystems/fleet-inventory.md + hosts/<node>/capabilities.yaml (stub z 2026-04-15, sprzed floty)"
---
# Services
## Description

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-05-27
links: []
---
# SESSION: Budowa planner-agent — LLM-based diagnostics
**DATA:** 2026-05-27

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-05-27
links: []
---
# SESSION: Stabilizacja systemu wieloagentowego homelabu
**DATE:** 2026-05-27

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-09
links: []
---
# Sesja 2026-06-08 — onboarding LUSTRO (RPi4 / Magic Mirror / KEN)
## Cel
@ -90,7 +81,7 @@ przez Tailscale działa bezhasłowo. Verify czysty (arch=aarch64).
## Learnings
(odzwierciedlone też w `kb/runbooks/node-onboarding-tool.md`)
(odzwierciedlone też w `scripts/onboard/README.md`)
- mDNS `.local` zawodny do automatyzacji → `first_contact` przez IP lub tailscale, nie `.local`
- istniejący node z userem uid=1000: użyj go zamiast tworzyć `oskar` (kolizja uid)

View file

@ -1,133 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-09
links: []
---
# Sesja 2026-06-09 — flota recovery + LUSTRO register
## Cel
Diagnoza cichej awarii reportingu floty; dokończenie kroku REGISTER dla LUSTRO
(40-register.sh + 50-verify.sh); update skilla node-onboarding.
---
## GŁÓWNE: 8-dniowa cicha awaria reportingu floty — ROZWIĄZANA
### Root cause
`oskar` (uid 1002) **spoza grupy aerbot (1000)** na VPS.
`/opt/homelab/events/*` = `aerbot:aerbot 775``oskar` w "other" (r-x).
`rsync` push z każdego node'a (jako `oskar` przez SSH) = **Permission denied** przy
zapisie → `--remove-source-files` nie czyścił backlogu → **292 000 plików** nagromadzonych
w staging cache node-agentów.
### Fix
```bash
usermod -aG 1000 oskar # na VPS; ssh re-login wymagany
```
### Weryfikacja
- VPS `events/piha` 3443 pliki (rośnie)
- `piha` lokalnie: 2 pliki (staging wyczyszczony)
- Panel agents.okit.pl: vps / piha / solaria — Last Seen świeże
### Diagnoza — 5 warstw, 4 obalone hipotezy
Verify-before-fix obalił kolejno:
1. `authorized_keys` missing — klucz był, SSH działał (piha→VPS ręcznie OK)
2. Remote agent down — procesy `rsync` widoczne w `ps`, logi bez crash
3. VPS IP zmiana — Tailscale IP niezmieniony 100.95.58.48
4. Bridge/relay cutoff — ping VPS→piha OK przez mesh
5 warstwa (błąd uprawnienia) odkryta przez ręczny `rsync` jako `oskar` na VPS →
`Permission denied (13)``stat /opt/homelab/events/``aerbot:aerbot 775`.
### Dlaczego awaria była CICHA (3 warstwy maskujące)
| Warstwa | Mechanizm |
|---------|-----------|
| (a) shipping fail | Logowany jako `WARNING`, nie crash — node-agent nie failował, milczał |
| (b) observer staleness | Stale node pokazywany NOMINAL — brak heartbeat TTL, observer trzyma ostatni znany stan |
| (c) brain-watchdog | Ślepy na per-node freshness — nie monitoruje świeżości eventów per-node |
### Pozostały drobny błąd
`rsync` exit code 23: `set-times` na katalogu = `EPERM` (oskar nie jest właścicielem
`/opt/homelab/events/``aerbot` jest). Kosmetyka — rsync działa poprawnie.
**Fix**: dodać `--omit-dir-times` do wywołania rsync w node-agent (wpisane do backlogu).
---
## LUSTRO register: stan po sesji
### Dokonane
- `40-register.sh` — napisany i zcommitowany na `task/node-onboarding`
- Idempotentny: grep topology, `[[ -f services.yaml ]]`, `git diff --quiet`
- Commituje tylko `inventory/topology.yaml` + `hosts/lustro/services.yaml` na bieżącym branchu
- BEZ `git push` (merge należy do operatora)
- `50-verify.sh` — napisany i zcommitowany
- 4 checki: node-agent running, eventy, observer restart + heartbeat poll, world/nodes.json
- Tabela pass/fail; exit 1 on failure
- `40-deploy-node-agent.sh` — scaffold usunięty (deploy w 30-node-agent.sh)
- Dry-run `40-register.sh --dry-run` przeszedł czysto
### Mechanizm aktywacji observera (zbadany)
Observer bind-mountuje repo root jako `/repo:ro` z `services/control-plane/docker-compose.yml`
(`../..:/repo:ro` → `/home/oskar/homelab-codex-ws` na VPS). `_load_inventory()` wywoływane
raz przy starcie. **Aktywacja po merge**: `git pull` VPS + `docker restart control-plane-observer`
— bez redeploy.
### Wpis lustro w topology.yaml (minimalistyczny, 1:1 z piha)
```yaml
lustro:
roles:
- edge
services:
- node-agent
```
### PENDING (jutro)
1. Commit B: `onboard.sh --node lustro --step 40-register` live → commit na branchu
2. `agent.sh merge task/node-onboarding` → master
3. `git pull` na VPS + `docker restart control-plane-observer`
4. `onboard.sh --node lustro --step 50-verify` → lustro widoczny w agents.okit.pl
---
## fix-event-bloat (task/fix-event-bloat)
Commit `d483274` na branchu: batch rsync, backlog trim, timeout 120s, backlog warn.
**PENDING**: review + deploy na flotę.
---
## OOM ai-cluster (obserwacja live)
Zaobserwowany na VPS podczas sesji: cgroup OOM restart-loop, python workery ~195 MB,
0 swap. **PENDING**: migracja `ai-cluster` → SOLARIA + dodanie swap na VPS.
---
## Gotcha sesji
**Worktree branch confusion**: `~/homelab-codex-ws-node-onboarding` był przełączony
ręcznie na `task/fix-event-bloat` (jeden worktree, dwa branche ręcznie switchwane).
Anty-wzorzec: zawsze sprawdzać `git branch --show-current` na wejściu do worktree.
Docelowo: osobny worktree per task.
---
## Tech-debt złapany w sesji
→ wpisany do `kb/phases/backlog.md`

View file

@ -1,123 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-11
links: []
---
# Sesja 2026-06-10/11 — lustro SSH shipping fix + ha-diag-agent piha
## Cel
Naprawa shippingu eventów lustro → VPS; domknięcie deploy-configu ha-diag-agent na piha;
zachowanie poison-quarantine (Codex) do osobnego review.
---
## GŁÓWNE: LUSTRO event shipping — NAPRAWIONY (merged `a5a1352`)
### Root cause
`_ship_events_to_vps()` (`services/node-agent/src/node_agent.py`) woła `ssh` **bez `-i`**,
więc klucz jest szukany w `$HOME/.ssh` = `/home/homelab/.ssh` (kontener działa jako
uid 1000 `homelab` od dodania `user: "1000:1000"` do bazowego
`services/node-agent/docker-compose.yml`). Override lustra montował klucz w `/root/.ssh`
**ślepy mount**, ssh tam nie patrzy → `oskar@100.95.58.48: Permission denied`.
### Fix
`hosts/lustro/runtime/node-agent/docker-compose.override.yml`:
```yaml
- /home/pi/.ssh:/home/homelab/.ssh:ro # było: /root/.ssh — ślepe
```
Klucz `pi@pimirror2` dodany do `authorized_keys` `oskar@VPS`.
uid match (pi=1000 = homelab=1000) spełnia strict ownership check OpenSSH.
### Weryfikacja
- 5 nodów NOMINAL w world state; lustro w `/opt/homelab/world/nodes.json` (online, świeży `last_seen`)
- 7600+ eventów backlogu spłynęło na VPS (`/opt/homelab/events/lustro/`)
- Staging na lustrze drenowany do zera (`--remove-source-files` działa)
- "Permission denied" zniknął z logów node-agenta
### Diagnoza — lekcja verify-before-fix
Oba agenty (Claude Code, Codex) błędnie wskazały observer (poison event / race)
na **nieaktualnym stanie** (`events=2` z ręcznego testu). Verify-before-fix obalił
obie hipotezy: `events/lustro` na VPS było puste → problem w warstwie **dostarczania**
(klucz SSH), nie w observerze.
---
## ha-diag-agent piha — deploy config merged (`5e9db5c`), deploy NIEDOKOŃCZONY
- `.env` utworzony na piha: `/opt/homelab/config/ha-diag-agent/.env`, chmod 600
- **ALE token = PLACEHOLDER** — chelsty-ha offline → brak tokenu i połączenia
- Przed `shadow_mode=false`: target restartu w supervisorze = nazwa kontenera
`homeassistant5`; curl endpointu z tokenem musi dać HTTP 200
- Decyzja PENDING: cel HA = chelsty-ha vs HA Ken (`homeassistant5` na piha —
z kontenera NIE `localhost`)
---
## observer poison-quarantine (Codex)
Zachowany na branchu `task/observer-poison-quarantine` (`78c9e4a`) — **NIE w master**.
Do osobnego review: czy observer realnie wiesza się na malformed evencie
(poison NIE był przyczyną lustra; hipoteza niezweryfikowana).
Realny bug → merge; inaczej → drop.
---
## 🔴 FLOTA-BOMBA — odkryta, NIE naprawiona (backlog, BLOKUJĄCE)
solaria / piha / chelsty to wciąż **stare root kontenery** node-agenta
(piha Created 2026-05-27, uid 0). Ich mount `/root/.ssh` działa tylko dlatego,
że kontenery są sprzed `user: "1000:1000"`. Pierwszy `--force-recreate` / reboot
hosta / update obrazu przełączy je na uid 1000 i shipping padnie jak na lustrze.
**NIE RECREATE bez fixu.** Szczegóły i fix: `kb/phases/backlog.md`.
---
## Tech-debt złapany w sesji
→ wpisany do `kb/phases/backlog.md` (flota-bomba, ha-diag-agent blocked,
poison-quarantine review, `--omit-dir-times`, stale komentarz node_agent.py,
shipping success na `logger.debug`, event-bloat lustro na VPS).
## Session 20:19
### Commits
fa59625 docs(ha-diag-agent): replace curl verify commands with docker exec
d7e0d31 fix(ha-diag-agent): remove host port mapping for 8087
### Files changed
kb/runbooks/ha-diag-agent-deploy.md | 4 ++--
kb/services/ha-diag-agent.md | 4 ++--
services/ha-diag-agent/docker-compose.yml | 3 ---
services/ha-diag-agent/service.yaml | 3 ---
4 files changed, 4 insertions(+), 10 deletions(-))
### Deploys
None recorded
### Narrative
> _user-provided summary_
## Session 20:35
### Commits
(brak nowych — commity d7e0d31 i fa59625 z tej sesji trafiły do mastera przed tym wpisem)
### Files changed
(bez zmian — zob. Session 20:19)
### Deploys
None recorded
### Narrative
> _user-provided summary_

View file

@ -1,140 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-17
links: []
---
# Sesja 2026-06-17 — KB foundations (etap 1 maili)
## Cel
Zbudowanie fundamentu filaru maili: pgvector spine na SOLARIA, zamrożona koperta, pakiet domeny `kb-mail`, testy. Bez live ingestu, bez bulk Gmaila, bez indexera — sam fundament pod kolejne etapy.
---
## Architektura KB — ustalona w tej sesji (docs)
### 4 warstwy × 4 filary + rdzeń
```
[ 4. Agent interdyscyplinarny ] NL query + analityka (LLM) ← wspólne
[ 3. Join + Enrich ] entity resolution · graf · OwnTracks ← wspólne
[ 2. Preprocess + Index ] → pgvector / → SQL ← per filar
[ 1. Ingest ] Maile Dokumenty Zdjęcia Transakcje ← per filar
```
OwnTracks = **klucz czasoprzestrzenny w warstwie 3**, nie osobny filar (ciągła funkcja czas→miejsce).
### Decyzje zamknięte
- Spine: **Postgres + pgvector** (nie Qdrant) na SOLARIA
- Embed: **bge-m3** (multilingual, długi kontekst, lepszy pod polski)
- Maile: **dwa żywe źródła** — Fastmail (JMAP, primary) + Gmail (IMAP; konto zostaje jako śmieciowe/2FA)
- Bulk historyczny Gmail (jednorazowy) — etap 2
- Reguła: **archiwizuj wszystko, indeksuj selektywnie** (filtr na wejściu do indeksu, nie archiwum)
- Załączniki: **II tura** (MVP = czysty tekst + nagłówki)
- Warstwa 3 startuje jako **cienki graf encji**; federacja przy zapytaniu — później
- Dokumenty: **Nextcloud** (drive, WebDAV) + **Paperless-ngx** (skany/faktury/OCR)
- Koperta: zamrożona addytywna — `id / source / ts(UTC) / geo / raw_ref / entities[]`
### Decyzje otwarte
- **Transakcje (filar #4):** OPEN ISSUE — agregator PSD2 (GoCardless / Tink) dla mBank + Revolut, ale zgoda SCA wygasa co 90 dni → brak pełnego bezobsługowego sync; fallback: CSV/MT940. Do decyzji przy starcie filaru #4.
---
## Etap 1 — ZBUDOWANY
### services/kb-postgres
- Obraz: `pgvector/pgvector:pg16`
- SOLARIA, port **5433** (5432 zarezerwowany na potencjalny lokalny postgres)
- Named volume: `kb_postgres_data`
- Init SQL (`init/001_envelope.sql`): `CREATE EXTENSION vector` + tabela `envelope`
- Per-host override: `hosts/solaria/runtime/kb-postgres/docker-compose.override.yml``mem_limit: 4g`
- `inventory/topology.yaml` + `hosts/solaria/services.yaml` — wpisy dodane
### Zamrożona koperta (001_envelope.sql + Envelope dataclass)
```sql
CREATE TABLE envelope (
id TEXT PRIMARY KEY,
source TEXT NOT NULL, -- fastmail | gmail | ...
ts TIMESTAMPTZ NOT NULL,
geo JSONB, -- null dla maili; warstwa 3 uzupełnia
raw_ref TEXT NOT NULL, -- ścieżka do .eml w archiwum
entities JSONB NOT NULL DEFAULT '[]'
);
```
`Envelope` dataclass waliduje `ts.tzinfo is not None` w `__post_init__`.
### packages/kb-mail — nowa konwencja shared lib
Lokalizacja: `packages/<lib>/` (nie `services/`) — biblioteki reużywalne przez joby.
Instalacja w Dockerfile: `COPY packages/kb-mail/ /packages/kb-mail/ && pip install /packages/kb-mail/`.
| Moduł | Co robi |
|-------|---------|
| `envelope.py` | `@dataclass Envelope`, walidacja tz-aware ts |
| `db.py` | `insert_envelope` (ON CONFLICT DO NOTHING) + `get_envelope` (asyncpg) |
| `archive.py` | `save_eml` — append-only, `asyncio.to_thread` na write, `FileExistsError` na duplikat |
structlog JSON w każdym module (`structlog.get_logger(__name__)`); konfiguracja po stronie konsumenta (biblioteka nie konfiguruje structlog).
### Testy
- 15 unit testów (nie-integration): model, archiwum, sanity SQL — wszystkie zielone bez DB
- 5 integration testów (`@pytest.mark.integration`): round-trip DB, duplikat, geo, entities — wymagają `KB_TEST_DSN`
- `conftest.py`: fixture `db_conn` = asyncpg connection + BEGIN/ROLLBACK na każdy test
### Poprawki spójności (code-review sesji)
1. **README deploy**: pierwotna wersja miała `docker compose --env-file /opt/homelab/config/…` — pomijała override file i miała złą ścieżkę. Naprawiono: README dokumentuje trzy ścieżki (standardowa przez `deploy.sh solaria`, first-time setup `.env` obok compose file, manual z dwoma `-f`), zgodnie z `deploy-node.sh` L6572.
2. **.gitignore**: reguła `*.env` (L3) już łapie `services/kb-postgres/.env` — potwierdzono `git check-ignore`. Nic nie dodano.
---
## Higiena git
- Cała praca w worktree `task/kb-foundations` — master czysty przez cały czas
- 2 commity na branchu po zakończeniu sesji
---
## Commits
```
31c64d5 feat(kb-mail): fundament — pgvector spine, koperta, archiwum, pakiet domeny
<docs-commit> docs(kb): etap 1 fundament done + session log 2026-06-17
```
## Files changed (etap 1)
```
services/kb-postgres/docker-compose.yml
services/kb-postgres/service.yaml
services/kb-postgres/env.example
services/kb-postgres/healthcheck.sh
services/kb-postgres/init/001_envelope.sql
kb/services/kb-postgres.md
hosts/solaria/runtime/kb-postgres/docker-compose.override.yml
hosts/solaria/services.yaml
inventory/topology.yaml
packages/kb-mail/pyproject.toml
packages/kb-mail/src/kb_mail/{__init__,envelope,db,archive}.py
packages/kb-mail/tests/{conftest,test_envelope,test_archive,test_db,test_migration}.py
kb/subsystems/kb-overview.md (etap 1 done, konwencja packages/)
CLAUDE.md (sekcja Shared Python Libraries)
docs/sessions/2026-06-17-kb-foundations.md
```
## Następny krok
**Etap 2: jednorazowy bulk importer Gmail** — mbox/Takeout → archiwum.
Wejście: plik `.mbox` lub katalog Maildir z eksportu Google Takeout.
Wyjście: `.eml` w archiwum + wiersze `envelope` w DB.
Jako one-shot job (nie serwis), reużywa `packages/kb-mail`.

View file

@ -1,50 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-17
links: []
---
# Sesja 2026-06-17 — Vikunja OIDC+GitOps · Observer heartbeat-TTL · panel-source
## Zrobione i wdrożone
### 1. Vikunja: OIDC (Forgejo) + migracja do GitOps — commit e5c6bfe
- Problem: brak guzika Forgejo na loginie vikunja.okit.pl (PIHA).
- Root cause (dwie warstwy): (a) kontener rozwiązywał forgejo.okit.pl po publicznym DNS → flaky ingress → discovery padał; (b) WŁAŚCIWY blocker: npm "Local" Access List na proxy forgejo.okit.pl blokował źródłowy IP kontenera — bridge PIHA używa puli 192.168.x.x (NIE 172.16/12), egress jako gateway 192.168.112.1, spoza allowlisty.
- Fix: extra_hosts "forgejo.okit.pl:192.168.31.5" (npm LAN IP) w serwisie vikunja + Allow 192.168.0.0/16 w npm Local Access List (tab Rules). Po obu: discovery 200, login działa.
- Migracja: manualny deploy /home/pi/vikunja → services/vikunja/ w repo (worktree task/vikunja-gitops-migration). config.yml: provider "forgejo", authurl "https://forgejo.okit.pl/" (z trailing slash = issuer match), redirecturl "https://vikunja.okit.pl/auth/openid/". Wolumeny przypięte po nazwie (vikunja_vikunja_db / vikunja_vikunja_files). Sekrety w .gitignore .env. Brak healthchecku w obrazie (brak wget/curl).
- Cutover DONE na PIHA: pg_dump ~/vikunja-pre-migration.sql (72880 B), down starego stacka (wolumeny zachowane), git pull (chmod 600 ~/.ssh/id_rsa), .env, up, login + taski OK.
- PENDING: rm /home/pi/vikunja (trzymać .sql ~1 dzień); config npm (access-list + proxy hosts) żyje TYLKO w bazie npm, nie w GitOps.
### 2. Observer: heartbeat-TTL (martwy node ≠ NOMINAL) — commit 5f1528e, MERGED
- Problem: CHELSTY-INFRA NOMINAL mimo 16 dni offline. Status ustawiany tylko eventami, nigdy nie wygasał po TTL.
- Fix: 3-stopniowa liveness (fresh/stale/dead) z now-last_seen, liczona co cykl. Progi: always-on fresh≤180s/dead>600s; remote (chelsty-*/role remote) fresh≤900s/dead>3600s; override env LIVENESS_TTL_*. Wspólny helper services/control-plane/src/liveness.py (compute_liveness, ttls_for, node_health=worse-of, parse_ts int+ISO; brak last_seen → UNKNOWN, NIE degraduje). Read-time net w obu UI (operator_ui + agent-system/webui/web.py zwracają LISTY z polem health). Observer emituje node_stale/node_offline/node_online; supervisor → alert_only (dedup+1h cooldown). 89 testów green.
- Deploy (messy, done): wymaga REBUILD (operator_ui + webui mają COPY src; observer mountuje /repo). Wpadki: przypadkowy up --build control-plane na SOLARIA (crash-loop, sprzątnięte down); VPS pierwszy up --build padł w połowie → down + up --build, 4 kontenery healthy. PIHA agent-system rebuilt.
- ZWERYFIKOWANE U ŹRÓDŁA: curl localhost:18180/nodes na VPS (operator_ui) = 5 nodów poprawnie (vps=nominal, chelsty-infra=ERROR, piha=ERROR, solaria=nominal, lustro=nominal). Fix działa autorytatywnie.
- Brak świeżego alertu offline dla chelsty/piha = oczekiwane (nody padłe przed startem nowego observera → baseline tłumi transition; alertują tylko NOWE przejścia).
## Otwarte / odkryte tej sesji
### PANEL agents.okit.pl czyta ROZJECHANY world-state (NOWE, NIEROZWIĄZANE)
- agents.okit.pl (npm na VPS, proxy_host 5.conf) → set $server 100.108.208.3, port 18180, http.
- 100.108.208.3 = PIHA (potwierdzone tailscale status). :18180 na PIHA = agent-system-webui.
- Czyli panel = webui PIHA czytający LOKALNY /opt/homelab/world/nodes.json (materializowany przez runtime-materializer PIHA) — inny, starszy world-state niż autorytatywny observer na VPS.
- Objawy: 4 nody (chelsty zamiast chelsty-infra, brak lustro) zamiast 5; Last Seen: Invalid Date → last_seen nieparsowalny → compute_liveness UNKNOWN → z designu nie degraduje → wszystko NOMINAL. Fix JEST tam wdrożony, ale na zepsutych danych nic nie robi.
- DWA rozjechane world-state'y: VPS observer (autorytatywny, poprawny) vs materializer PIHA (to widzi panel, stale/broken).
- Plus: panel zaśmiecony duchami *_control-plane-* (hash-prefixed martwe ID kontenerów po deploy-churnie).
- DO ZROBIENIA (świeży task): czemu materializer PIHA daje Invalid Date + inny zestaw nodów; który UI kanoniczny (operator_ui VPS vs agent-system-webui PIHA); cleanup duchów. Stopgap dostępny: przepiąć 5.conf na VPS-lokalny operator_ui (set $server 127.0.0.1, port 18180) + reload npm → panel pokaże prawdę natychmiast (ryzyko: inny front).
## Backlog (priorytet)
1. PIHA delivery+permissions — PRIORYTET. node-agent nie pisze /opt/homelab/events/piha/ (Permission denied); rsync "No user exists for uid 1000"; ~6-dniowa luka → piha pokazuje DEAD mimo że żyje. (To przypadek "push zepsuty vs node down" — rozróżni go cross-check Prometheus blackbox.)
2. npm config → GitOps (access-list 192.168.0.0/16 + proxy hosts tylko w bazie npm).
3. Vikunja cleanup: rm /home/pi/vikunja (trzymać .sql ~1 dzień).
4. Prometheus fleet-liveness (większy): blackbox_exporter po Tailscale MagicDNS, targety z inventory/topology.yaml, Alertmanager (Telegram), observer czyta probe_success jako PRIMARY + event-TTL jako fallback. Obecny Prometheus na PIHA (9090, bez Alertmanagera) scrape'uje hardcoded 192.168.31.x (martwe up=0) i MIJA nody tylko-Tailscale (VPS, chelsty).
5. Observer housekeeping: ghost-dir be17cb6eb0f6 + puste 2026-05-* w events/ + fallback NODE_NAME→hex container-ID.
6. SOLARIA: docker image prune (stray control-plane images po przypadkowym deployu).
7. Panel world-state reconciliation + ghost cleanup (sekcja wyżej).
## Uwaga
task/kb-foundations (commit 2c9ac78, knowledge-base) zaparkowany na swojej gałęzi; master zresetowany do origin/master — nic nie stracone, NIE ruszać (inny chat).

View file

@ -1,105 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-22
links: []
---
# Sesja 2026-06-22 — KB spine relokowany na PIHA + przygotowanie hosta
## Cel
Przenieść spine KB (Postgres + pgvector) z SOLARIA na PIHA, przygotować host PIHA pod
kontener z twardym limitem pamięci, zdeployować i zweryfikować schemat. SOLARIA bywa
offline — zapytania KB muszą działać 24/7, więc spine musi stać na always-on maszynie.
---
## DECYZJA
- **Spine Postgres + pgvector przeniesiony SOLARIA → PIHA.** Powód: PIHA (Raspberry Pi 5)
jest always-on, SOLARIA bywa offline; zapytania KB mają działać 24/7. SOLARIA zostaje do
GPU/embeddingów (bge-m3) i indexera.
- **Archiwum maili `.eml` docelowo też na PIHA** (NVMe).
---
## PIHA — przygotowanie hosta
- **Swap:** dodano 4GB swapfile (`/swapfile`), utrwalony w `/etc/fstab`. Brak swapa był
ryzykiem OOM dla Home Assistant.
- **Cgroup memory:** kernel nie eksponował cgroup memory controllera → dopisano
`cgroup_enable=memory cgroup_memory=1` do `/boot/firmware/cmdline.txt`
(backup: `cmdline.txt.bak`) + reboot. Po reboocie `cgroup.controllers` zawiera `memory`,
docker `mem_limit` faktycznie działa (wcześniej był ignorowany).
---
## Relokacja w git
Praca w worktree `task/kb-postgres-piha`, zmergowana do master jako commit **2b3cb89**.
- Override SOLARIA (`hosts/solaria/runtime/kb-postgres/`) **usunięty**;
wpis z `hosts/solaria/services.yaml` zdjęty.
- Dodany `hosts/piha/runtime/kb-postgres/docker-compose.override.yml`:
- `mem_limit: 1g`, `mem_reservation: 512m` (soft reservation ignorowany przez kernel —
nieszkodliwe; twardy `mem_limit` chroni HA przed OOM).
- Tuning Postgresa pod małą maszynę: `shared_buffers 256MB`, `effective_cache_size 512MB`,
`work_mem 8MB`, `maintenance_work_mem 64MB`, `max_connections 30`.
- Obraz `pgvector/pgvector:pg16` potwierdzony **arm64**.
- Named volume `kb_postgres_data` na **NVMe** (docker `data-root = /home/docker`
`/dev/nvme0n1p3`, ~170GB wolne).
- `inventory/topology.yaml` + `hosts/piha/services.yaml` — wpisy przeniesione.
---
## Deploy na PIHA
- Kontener **healthy**.
- Schemat `envelope` + extension `vector` zweryfikowane w działającej bazie.
- `mem_limit 1g` zaaplikowany po **force-recreate** (zwykły `up` nie podmienia limitu).
---
## Google Takeout (bulk Gmail)
- Pobrany: **15GB zip**, jeden plik; Mail po rozpakowaniu **~26.9GB**.
- Leży na **SOLARIA `~/Downloads`**, NIE rozpakowany jeszcze.
- Docelowo: transfer na PIHA NVMe → import. Czeka na importer (etap 3).
---
## Higiena git
- Cała praca przez worktree: `task/kb-foundations` (zmergowany i sprzątnięty),
`task/kb-postgres-piha` (zmergowany).
- Master deploy-only, czysty.
---
## GOTCHA PIHA (do zapamiętania)
`~/.ssh/id_rsa` na PIHA miał perms **0640**`git fetch` przez SSH padał
(`bad permissions`). Fix: `chmod 600 ~/.ssh/id_rsa`. To ta sama klasa problemów
uid/permisji co wcześniej na PIHA — przy onboardingu PIHA warto sprawdzać perms kluczy.
---
## Następny krok
**Importer bulk Gmail** (worktree `task/kb-gmail-import`):
- CLI w `packages/kb-mail`, `mbox → archiwum (.eml) + envelope`, **idempotentny**, `--dsn` na PIHA.
- Potem: transfer Takeout na PIHA NVMe + run.
---
## BACKLOG (osobno, nieruszane w tej sesji)
- **`hosts/piha/capabilities.yaml` rozjazd danych:** mówi `memory 4GB` / `sd-card 32GB`;
realnie **8GB RAM + NVMe 170GB**. Ten sam typ rozjazdu danych co przy 8-dniowej ślepocie
floty — do poprawienia.
- **`KB_TEST_DSN` w `packages/kb-mail/tests/test_db.py`** — wskazuje na solaria, powinien piha.
- **Deklaratywny zapis `cgroup_enable` + swap dla PIHA** — firmware/host config jest poza
obecnym GitOps; rozważyć jak go ująć.

View file

@ -1,140 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-22
links: []
---
# Sesja 2026-06-22 — decyzja: Prometheus jako źródło prawdy dla liveness floty
## Cel
Rozstrzygnięcie architektoniczne: czym zastąpić obecną rurę wykrywania liveness
floty `node-agent → rsync/ssh → observer` (warstwa sensoryczno-transportowa będąca
przyczyną większości nawracających awarii). Plus precondition infrastrukturalny pod
nowy serwis na VPS (swap).
---
## DECYZJA: Prometheus (pull, `up{}`) zastępuje WYKRYWANIE liveness
### Co zastępujemy (warstwa sensoryczno-transportowa)
Prometheus pull + metryka `up{}` przejmuje rolę dotychczasowej rury:
- **node-agent** jako shipper eventów,
- **rsync/ssh** transport,
- pliki eventów,
- observer **prune/checkpoint**,
- egzekwowanie **TTL** liveness.
To była przyczyna większości nawracających awarii:
- `uid 1004 != 1000` — strict ownership check OpenSSH wywalał shipping,
- ślepy ssh-mount (LUSTRO, `/root/.ssh` vs `/home/homelab/.ssh`),
- bloat (~242k eventów na VPS),
- race `prune``checkpoint` w observerze,
- `NOMINAL`-bez-TTL (martwy node pokazywany jako zdrowy).
Pull eliminuje całą warstwę push: brak kluczy, brak ownership, brak event-store
do prune'owania, brak ręcznego TTL — `up==0` przez czas `for:` jest stanem prawdy.
### Świadoma granica zakresu — co ZOSTAJE
Prometheus zastępuje **wykrywanie**, NIE cały system. Nietknięte:
- **supervisor** — remediacja (`container_restart` / `redeploy`),
- **observer / panel** `agents.okit.pl` — do przepięcia na źródło Prometheus (OTWARTE,
największy znak zapytania przy cutoverze),
- **ha-diag-agent** — logika domenowa HA (diff `system_health`, encje `unavailable`,
WS heartbeat) — Prometheus tego nie widzi,
- historia incydentów / lifecycle,
- out-of-band watchdog.
### BEZ Alertmanagera — świadoma decyzja
Jeden kanał Telegram (`@okitaialerts_bot`). Rolę dostarczania przejmuje **brain-watchdog**:
dochodzi mu **drugie wejście** — odpytanie Prometheus `/api/v1/alerts` — obok obecnego
pingu mózgu. Reguły alertowe z histerezą (`for:`) po stronie Prometheusa; watchdog
pozostaje cienki (odczyt `firing` + Telegram). Nie wprowadzamy osobnego Alertmanagera,
żeby nie mnożyć kanałów ani komponentu do utrzymania.
> Uwaga: ta decyzja **zastępuje** wcześniejszy szkic z sesji 2026-06-17
> (blackbox_exporter + Alertmanager). Wybrano pull `up{}` + watchdog jako tor alertu.
---
## Osobny fleet-Prometheus pod GitOps — NIE adopcja domowego instance
Stawiamy **osobny** Prometheus floty, zarządzany przez GitOps. NIE adoptujemy domowego
instance na PIHA.
Powód — domowy prom na PIHA to liability nie wart adopcji:
- LAN-only (scrape `192.168.31.x`), mija nody tylko-Tailscale (VPS, chelsty),
- poza repo (`/home/pi/monitoring/prometheus.yml`, konfigurowany ręcznie),
- bez alertingu (`alerting:` i `rule_files:` puste),
- sekrety plaintext w configu (long-lived token HAOS + bearer watchtower — do rotacji).
Adopcja = dziedziczenie liability (migracja, ekstrakcja sekretów) **plus** blast-radius
na działający monitoring bezpieczeństwa domu (crowdsec / fail2ban / haos) za znikomą
wygraną. Osobny instance izoluje flotę od domu.
## Placement: VPS
- Host: VPS (`ubuntu-4gb-hel1-1`, `100.95.58.48`).
- Out-of-band zachowane: **watchdog na PIHA pilnuje VPS z innej maszyny** — Prometheus
na VPS nie jest sędzią własnej śmierci.
- VPS ma już `node_exporter` (role: metrics-exporter) = pierwszy target floty gotowy.
---
## ZROBIONE w tej sesji
### Swap 4 GB na VPS — precondition pod Prometheusa
- `/swapfile` aktywny i **trwały** (wpis w `/etc/fstab`).
- `vm.swappiness=10` ustawione na żywo i w `/etc/sysctl.conf`.
- **Weryfikacja**: `free -h``Swap: 4.0Gi (0B used)`; `/proc/swaps` zawiera `/swapfile`.
- Powód: VPS ma 3.7 GB RAM, wcześniej `swap=0` — przyczyna OOM 2026-06-01. Prometheus
jest RAM-głodny; swap to bufor bezpieczeństwa.
- ⚠️ To **host-level one-off** — nie czysto GitOps-owalne, wykonane świadomie gotowcem.
Zapisane jako celowy host-state w `hosts/vps/host.yaml`, żeby przy odtwarzaniu VPS
nie zniknęło cicho (wzorzec jak uid/grupy po 8-dniowej awarii).
---
## Plan następnych kroków (OTWARTE — NIE realizowane w tej sesji)
Kolejność = priorytet:
1. **Scaffold serwisu `fleet-prometheus` pod GitOps** — worktree `task/fleet-prometheus`,
wzorzec `services/vikunja/`: `docker-compose.yml` + `env.example` + `service.yaml` +
`README.md` + `healthcheck.sh`. Rejestracja w `hosts/vps/services.yaml` +
`inventory/topology.yaml`. Exposure: `tailscale-internal`. Pusty scrape na start
(self + lokalny `node_exporter` VPS).
2. **Inwentaryzacja nodów floty `100.x`** do scrape (osobny krok).
3. **Container-layer exporter** (cAdvisor lub lekki docker-state) — brakujący klocek;
`node_exporter` nie widzi kontenerów.
4. **Reguły liveness** (`up==0 for: 5m`) w Prometheusie.
5. **brain-watchdog: drugie wejście** — odpyt Prometheus `firing` → Telegram, bez
ruszania działającego toru `token → chat_id`.
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.
---
## Wnioski
- Klasa awarii (push shipping: klucze, ownership, bloat, race, TTL) znika nie przez
kolejny fix, lecz przez zmianę modelu transportu na **pull** — Prometheus widzi
martwy target jako `up==0`, bez warstwy dostarczania do zepsucia.
- Granica zakresu trzymana świadomie: Prometheus = **wykrywanie**, nie remediacja ani
logika domenowa HA. Zastępujemy sensor, nie mózg.
- Osobny instance > adopcja: nie dziedziczy się liability ani blast-radius na monitoring
bezpieczeństwa domu dla wygody współdzielenia.
- BEZ Alertmanagera: cienki watchdog z drugim wejściem zamiast nowego komponentu —
jeden kanał Telegram pozostaje jeden.
- Swap = precondition, nie cel — odblokowuje RAM-ciasny VPS pod nowy serwis;
udokumentowany jako celowy host-state, nie ukryta ręczna zmiana.

View file

@ -1,177 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-24
links: []
---
# Sesja 2026-06-24 — KB etap 2: importer Gmail (kod gotowy)
## Cel
Zbudowanie jednorazowego bulk importera Gmail Takeout (`mbox → archiwum .eml + envelope DB`).
Import nie uruchomiony w tej sesji — Takeout na SOLARIA, transfer+wlew = następny krok.
---
## Stan infrastruktury po sesji
### kb-postgres na PIHA (korekta z etapu 1)
Baza **przeniesiona na PIHA**, nie na SOLARIA jak zakładał pierwotny plan.
- `Memory: hard=1g` zaaplikowany po reboocie (wymagał `cgroup_enable=memory` w cmdline + `docker-compose up --force-recreate`).
- Status: `healthy`, port `5433`.
### Google Takeout (Gmail „All Mail")
- Pobrany: **~15 GB zip**, po rozpakowaniu **~27 GB mbox**, jeden plik.
- Lokalizacja: `SOLARIA ~/Downloads` — jeszcze nie rozpakowany, nie zaimportowany.
---
## Etap 2 — ZBUDOWANY (kod gotowy)
### Nowa konwencja: `jobs/<name>/`
Jednorazowe i periodyczne joby (nie serwisy) żyją w `jobs/<name>/`.
| Katalog | Co tu trafia |
|---------|-------------|
| `packages/<lib>/` | reużywalne biblioteki Python (nie deployowane samodzielnie) |
| `services/<svc>/` | długo żyjące serwisy Docker |
| `jobs/<job>/` | one-shot i periodyczne joby CLI |
Instalacja lokalnie: `pip install -e packages/kb-mail/ && pip install -e jobs/gmail-bulk-import/`
Bez Dockera — odpalany bezpośrednio na PIHA pod `nice`/`ionice`.
### jobs/gmail-bulk-import
Wejście: plik `.mbox` z Google Takeout.
Wyjście: `.eml` w archiwum (append-only, PIHA NVMe) + wiersze `envelope` w kb-postgres.
Kluczowe decyzje implementacyjne:
| Kwestia | Decyzja |
|---------|---------|
| ID wiadomości | `Message-ID` header (stripped `<>`); fallback: `sha256-<32hex>` treści |
| Timestamp | `Date` header → UTC; fallback epoch 1970-01-01 + licznik `epoch_fallback` |
| Źródło | `source=gmail` |
| Idempotencja | `FileExistsError` z archiwum → skip; `ON CONFLICT DO NOTHING` w DB |
| Wznawialność | mbox iterowany od początku; już zarchiwizowane = skip; already in DB = noop |
| Batch inserty | `executemany` co 500 wpisów (+ flush na końcu); `_eml_ref` rekonstruuje `raw_ref` dla skipped |
| Docker | **brak** — CLI bez konteneryzacji |
### Załączniki → entities[]
W tym etapie bajty załączników **zostają w .eml** — nie są ekstrahowane ani OCR-owane.
Importer zapisuje **manifest** do `Envelope.entities[]`:
```json
{
"type": "attachment",
"filename": "faktura.pdf",
"content_type": "application/pdf",
"size": 42387,
"sha256": "a3f4..."
}
```
Powiązanie mail↔załącznik w bazie od dnia zero.
Ekstrakcja/OCR = **faza 2** (możliwe przekierowanie faktur/umów do Paperless, filar dokumentów).
### CLI
```bash
# Dry run — tylko liczenie i parsowanie, bez zapisów:
gmail-bulk-import --mbox ~/takeout/allmail.mbox --archive /data/kb/archive --dry-run
# Próbka 200 wiadomości (przed pełnym wlewem):
gmail-bulk-import --mbox ~/takeout/allmail.mbox --archive /data/kb/archive \
--dsn postgresql://kb:<pw>@localhost:5433/kb --limit 200
# Pełny wlew na PIHA pod nice/ionice:
ionice -c 3 nice -n 19 gmail-bulk-import \
--mbox ~/takeout/allmail.mbox \
--archive /data/kb/archive \
--dsn postgresql://kb:<pw>@localhost:5433/kb
```
### Statystyki zwracane przez importer
```
processed, imported, skipped, errors,
epoch_fallback, ← maile bez parsowalnej daty
msgs_with_attachments, ← sizing fazy 2
total_attachments,
total_attachment_bytes
```
### Testy
- 24 testy jednostkowe (bez DB, bez sieci) — wszystkie zielone.
- Pokrycie: `_message_id`, `_parse_date`, `_parse_attachments`, `run_import`
(dry-run, archiwum, idempotencja, --limit, epoch_fallback, statystyki załączników, batch).
---
## GOTCHA tej sesji
Pierwsza iteracja importera (commit 57a27af) celowała w `solaria:5433` zamiast PIHA,
pominęła `entities[]` załączników, `--limit`, batch inserty i `epoch_fallback`.
Złapane w review, poprawione w osobnym commicie.
**Lekcja**: prompt musi explicite podać host bazy = PIHA (nie SOLARIA).
---
## Następny krok
1. `rsync` Takeout SOLARIA → PIHA po Tailscale (27 GB mbox na NVMe PIHA).
2. `gmail-bulk-import ... --dry-run` — weryfikacja liczby wiadomości.
3. `gmail-bulk-import ... --limit 200` — próbka, weryfikacja jakości.
4. Pełny wlew pod `ionice -c 3 nice -n 19`.
5. Weryfikacja: `ls archive/gmail/ | wc``SELECT count(*) FROM envelope WHERE source='gmail'` ≈ liczba wiadomości w mboxie.
---
## Backlog (osobne zadania)
- `packages/kb-mail/tests/test_db.py`: `KB_TEST_DSN` defaultuje do `localhost:5433/kb` — poprawne dla PIHA, ale warto sprawdzić wszystkie occurrences `solaria:5433` w testach.
- Etap 3: Fastmail JMAP live ingest → `jobs/fastmail-poller/`.
- Etap 4: Gmail IMAP live sync → `jobs/gmail-imap-poller/`.
- Zapis deklaratywny `cgroup_enable=memory + swapaccount=1` dla PIHA (firmware/cmdline — poza GitOps, udokumentować w `hosts/piha/host.yaml` lub README).
---
## Higiena git
- Praca w worktree `task/kb-gmail-import`; master deploy-only, czysty przez cały czas.
- 4 commity na branchu po zamknięciu sesji.
---
## Commits
```
57a27af feat(kb-mail): etap 2 — jednorazowy bulk importer Gmail (mbox → archiwum)
f0e4d90 refactor(kb-mail): importer Gmail — entities załączników, --limit, batch, bez Dockera, DSN→PIHA
c5dd8f3 fix(piha): capabilities — realny RAM/NVMe + gitignore build dirs
<docs> docs(kb): sesja 2026-06-24 — importer Gmail gotowy + konwencja jobs/
```
## Files changed (etap 2)
```
jobs/gmail-bulk-import/src/gmail_bulk_import/__init__.py
jobs/gmail-bulk-import/src/gmail_bulk_import/importer.py
jobs/gmail-bulk-import/pyproject.toml
jobs/gmail-bulk-import/tests/test_importer.py
hosts/piha/capabilities.yaml
.gitignore
kb/subsystems/kb-overview.md (etap 2 gotowy, konwencja jobs/)
kb/subsystems/kb-mail-pillar.md (§8 krok 2 = kod gotowy)
docs/sessions/2026-06-24-kb-gmail-importer.md
```

View file

@ -1,135 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-24
links: []
---
# Sesja 2026-06-24 — fleet-prometheus etap 1: scaffold + deploy-node hostname fix
## Cel
Wdrożenie etapu 1 Prometheus-as-truth dla liveness floty: scaffold serwisu
`fleet-prometheus` pod GitOps, poprawka krytycznego buga hostname w `deploy-node.sh`,
uruchomienie kontenera na VPS.
---
## ZROBIONE
### Scaffold `services/fleet-prometheus/` pod GitOps
- `prom/prometheus:v3.5.0` — pinned (nie `latest`).
- Scrape: self + `fleet-node` (`node_exporter` VPS przez `host.docker.internal:9100`).
- Retencja: 15d / 2 GB.
- `mem_limit: 512m`, `oom_score_adj: 200` — ofiara OOM przed control-plane (`-900`);
VPS ma 4 GB RAM + 4 GB swap.
- Healthcheck: `wget` in-container (`/bin/-/healthy`) — obraz ma `wget` i `promtool`,
nie ma `curl`; zweryfikowane przed commitem.
- `alerting:` i `rule_files:` obecne jako puste placeholdery — osobny krok.
- Zarejestrowany w `hosts/vps/services.yaml` + `inventory/topology.yaml`.
- Ekspozycja: `tailscale-internal`.
### Bind fix — nasłuch na Tailscale IP, nie `0.0.0.0` (commit `43c47a0`)
- Port bind: `${TAILSCALE_BIND_IP}:9090:9090` zamiast `0.0.0.0`.
- `.env` z `TAILSCALE_BIND_IP=100.95.58.48` tworzony **ręcznie** na każdym hoście
(gitignored) — `services/fleet-prometheus/.env` na VPS już utworzony.
- Defense-in-depth: nie polegamy na firewallu VPS poza repo.
### Fix krytyczny: `deploy-node.sh` — rozwiązywanie `HOST_DIR` przez `os_hostname` (commity `644f32d` + `ae739b5`)
**Root cause**: `deploy-node.sh` ustalał `HOST_DIR` przez `hosts/$(hostname | lower)`.
VPS ma OS-hostname `ubuntu-4gb-hel1-1`, a katalog to `hosts/vps` — mismatch powodował
ciche `"No services found" + exit 0` (fałszywe zielone). VPS **nigdy** nie deployował się
tą ścieżką od początku istnienia skryptu.
**Fix**:
- Dodano pole `os_hostname` do wszystkich 6 `hosts/*/host.yaml` (tylko VPS różni się:
`os_hostname: ubuntu-4gb-hel1-1`; pozostałe = `hostname`).
- `deploy-node.sh` mapuje `HOST_DIR` przez `os_hostname == $(hostname)` z fallbackiem
na starą logikę + twardy `exit 1` zamiast cichego skip.
- Zweryfikowane: mapowanie `ubuntu-4gb-hel1-1 → hosts/vps` działa, deploy wszedł
w pętlę serwisów.
### SSH aliasy `vps`/`piha` na SOLARII
Dodane do `~/.ssh/config` na SOLARII (parity z SATURN) — zamknięty backlog item.
---
## NIEDOKOŃCZONE — następny krok (priorytet)
### fleet-prometheus NIE jest jeszcze uruchomiony na VPS
`deploy.sh vps --no-gate` wszedł po raz pierwszy w pętlę serwisów (hostname fix zadziałał),
ale **padł na `node-agent`**:
```
Container node-agent Recreate
Error: No such container: 1913f743ea38_node-agent
```
**Przyczyna**: rozjazd stanu Docker Compose — istniejące serwisy VPS (`node-agent`,
`control-plane`, up ~714 dni) zostały utworzone **inną ścieżką / project-name** niż
widzi `deploy-node.sh` (bo VPS nigdy wcześniej nie deployował się tą ścieżką).
`set -e` przerwał pętlę na `node-agent`; `fleet-prometheus` jest dalej w kolejce
— NIE powstał. `node-agent` prawdopodobnie wciąż żyje (błąd przy Recreate, nie kill).
**Plan domknięcia — Wariant A (uzgodniony)**:
Postawić `fleet-prometheus` ręcznie, z pominięciem rozjazdu stanu pozostałych serwisów:
```bash
# na VPS:
cd ~/homelab-codex-ws
docker compose \
-f services/fleet-prometheus/docker-compose.yml \
-f hosts/vps/runtime/fleet-prometheus/docker-compose.override.yml \
--env-file services/fleet-prometheus/.env \
up -d
```
**Weryfikacja po uruchomieniu**:
- `ss -tlnp | grep 9090` → musi pokazać `100.95.58.48:9090` (NIE `0.0.0.0`).
- Kontener `healthy` (`docker ps`).
- Oba targety `up`: `http://100.95.58.48:9090/api/v1/targets`.
---
## Dalsze etapy Prometheus-as-truth (osobne sesje)
1. Targety floty `100.x` (wszystkie nody przez Tailscale).
2. Reguły liveness: `up==0 for: 5m`.
3. Integracja `brain-watchdog`: odpyt `/api/v1/alerts` → Telegram.
4. Rotacja tokenu HAOS w domowym prom (plaintext).
5. Przepięcie observer / panel `agents.okit.pl` na źródło Prometheus.
---
## Nowe tech-debty (dodane do `kb/phases/backlog.md`)
1. **Rozjazd stanu Docker Compose na VPS** — serwisy `node-agent`, `control-plane` i inne
stworzone innym `project-name` niż `deploy-node.sh` oczekuje; Recreate pada na stale
container ID. Dotyka żywego control-plane (~7 dni uptime) — ostrożnie, osobna sesja.
2. **Flaky testy control-plane**`test_incident_lifecycle.py::test_run_once_quarantines_bad_event`
i `::test_run_once_skips_observer_emitted_events` failują przez state-leak między
przebiegami pytest (moduł-level `EVENTS_DIR`/`FAILED_EVENTS_DIR`/checkpoint, nie
`tmp_path`). Kod observera zdrowy. Gate deploy.sh nierzetelny dopóki nie naprawione;
musieliśmy użyć `--no-gate`.
---
## Wnioski
- **Hostname-to-directory mismatch** był cicho od zawsze: `exit 0` bez deployu wyglądał
jak sukces. Wzorzec `os_hostname` w `host.yaml` + twardy `exit 1` eliminuje cichą
kategorię błędów dla całej floty.
- **Rozjazd state Compose** to dług z epoki przed GitOps-deployment na VPS — teraz
widoczny, bo po raz pierwszy realna pętla serwisów na VPS. Dotykać ostrożnie (live
control-plane).
- **`--no-gate` jako sygnał**: gate deploy.sh jest zawodny przez flaky testy —
każde ominięcie gate'u powinno być odnotowane i gonione naprawą testu.
- Fleet-prometheus wchodzi nowym serwisem (zero state do rozjazdu) → ręczny
`docker compose up -d` jest bezpieczny i wystarczający do domknięcia etapu 1.

View file

@ -1,153 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-26
links: []
---
# Sesja 2026-06-25 — KB etap 2: bulk import Gmail uruchomiony
## Cel
Domknięcie etapu 2 bazy wiedzy. Poprzednia sesja (2026-06-24) zostawiła kod gotowy,
ale dane na SOLARIA. Ta sesja = transfer + uruchomienie + weryfikacja na produkcji (PIHA).
---
## Co zrobione
### Relokacja kb-postgres SOLARIA→PIHA — domknięta i zweryfikowana
Baza stoi na PIHA (always-on); zapytania KB działają 24/7 niezależnie od SOLARIA.
### Przygotowanie PIHA pod import
- **cgroup memory enable**: `cgroup_enable=memory cgroup_memory=1` dopisane do
`/boot/firmware/cmdline.txt` + reboot; backup w `cmdline.txt.bak`.
- **Swap 4 GB**: wpisany do `/etc/fstab` (był już skonfigurowany wcześniej, weryfikacja).
- **SSH key perms**: `chmod 600 ~/.ssh/id_rsa` — prawa `0640` blokowały `git fetch`
(OpenSSH odrzuca klucze `group-readable`).
### Transfer Takeout SOLARIA→PIHA
`rsync` po LAN (nie Tailscale, oba dostępne): 15 GB zip → po rozpakowaniu
**27 GB `allmail.mbox`** wylądowało w `~/kb/dropbox/`.
`venv` pod PEP668 w `~/kb/venv` (Pi OS wymusza brak systemowych pip-instalacji).
### Walidacja przed pełnym wlewem
1. `--dry-run --limit 200` — parsowanie bez zapisów, weryfikacja nagłówków.
2. `--limit 1000` z realnym zapisem — próbka; potwierdzono że RAM stoi płasko:
zużycie to koszt zbudowania indeksu `mailbox.mbox` przy otwarciu, nie akumulacja
per-mail w pętli.
### Pełny import
```bash
nohup nice -n 15 ionice -c2 -n7 \
gmail-bulk-import \
--mbox ~/kb/dropbox/allmail.mbox \
--archive ~/kb/mail/archive \
--dsn postgresql://kb:<pw>@localhost:5433/kb \
>> ~/kb/import.log 2>&1 &
```
Czas: **~29 minut**. Brak OOM, swap stabilny przez cały przebieg.
---
## Wynik importu
| Metryka | Wartość |
|---------|---------|
| processed | 226 318 |
| imported (pliki .eml) | 225 057 |
| skipped (duplikaty Message-ID) | 1 260 |
| errors (zepsute maile) | 1 |
| epoch_fallback (brak daty) | 2 559 |
| msgs_with_attachments | 32 495 |
| total_attachments | 70 193 |
| total_attachment_bytes | 15,4 GB |
**Baza (envelope):** `SELECT count(DISTINCT id) FROM envelope`**225 030** unikalnych kopert.
**Archiwum:** 225 057 plików `.eml`, **27 GB na NVMe PIHA** (`~/kb/mail/archive`).
**Zakres dat:** 2002→2026-06-19; pik 20112012 (~23 k/rok); bez dziur.
---
## Lekcje
### `imported` ≠ wiersze w DB — NORMA, nie bug
Importer liczy `imported` jako zapisane pliki `.eml`.
`count(DISTINCT id)` w `envelope` jest mniejsze o duplikaty Message-ID:
`ON CONFLICT (id) DO NOTHING` je poprawnie odrzuca.
Różnica: 225 057 archiwum 225 030 DB = **27 duplikatów klucza** — zero utraty danych.
Zmarnowano ~30 minut na re-run całego mboxa szukając tych „27", zanim przeczytano kod
importera. **Reguła: verify-before-fix** — najpierw sprawdź co kod faktycznie robi,
zanim zaczniesz pisać fixa.
### `mailbox.mbox` buduje indeks przy każdym otwarciu
`python mailbox.mbox` skanuje całe 27 GB przed pierwszą wiadomością.
Na Pi5 to kilka minut startu — **nie zawis, nie błąd**. Re-run płaci ten koszt w całości
(wszystkie 226 k wiadomości przelatują ponownie przez idempotencję).
Jeśli trzeba dosłać kilka kopert, taniej będzie osobny skrypt niż re-run mboxa.
### RAM/swap stabilne
Swap ~47% podczas importu = stałe tło Home Assistant, nie skok od importera.
Hard `mem_limit: 1g` na kb-postgres chronił HA przez cały przebieg.
---
## Sprzątanie po imporcie
- `allmail.mbox` + oryginalny zip skasowane z `~/kb/dropbox/` na PIHA → odzysk **~42 GB NVMe**.
- Oryginalny zip Takeout (15 GB) przeniesiony na SOLARIA `~/kb/_cold/` jako cold backup
(do końca fazy 2, potem do skasowania).
---
## Stan etapu 2
**ZAMKNIĘTY.**
Pełna historia Gmaila 2002→2026 w bazie wiedzy:
- **225 030** unikalnych kopert w `kb-postgres` (PIHA)
- **27 GB** archiwum `.eml` (PIHA NVMe)
- manifest **70 193** załączników w `entities[]` (ekstrakcja/OCR = faza 2)
---
## Backlog / następne kroki
- **Faza 2 załączników**: ekstrakcja i OCR (PDF/skany → Paperless lub dedykowany job).
- **Odzysk 2559 dat epoch-fallback**: nagłówki `Received:` / `X-GM-RECEIVED` mogą dać
prawdziwe timestamps dla maili bez `Date:`.
- **Etap 3**: Fastmail JMAP live ingest → `jobs/fastmail-poller/`.
- **Etap 4**: Gmail IMAP live sync → `jobs/gmail-imap-poller/`.
---
## Higiena git
Praca w worktree `task/kb-session-0625`; główny checkout `~/homelab-codex-ws` czysty.
---
## Commits tej sesji
```
c0ffb6a docs(kb): sesja 2026-06-22 — spine relokowany na PIHA + przygotowanie hosta
c32e050 docs(kb): sesja 2026-06-24 — importer Gmail gotowy + konwencja jobs/
254a680 fix(piha): capabilities — realny RAM/NVMe + gitignore build dirs
f234280 refactor(kb-mail): importer Gmail — entities załączników, --limit, batch, bez Dockera, DSN→PIHA
6dc1d32 feat(kb-mail): etap 2 — jednorazowy bulk importer Gmail (mbox → archiwum)
```
(Commitów implementacyjnych w tej sesji brak — dokumentacja domknięcia etapu 2.)

View file

@ -1,127 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-25
links: []
---
# Sesja 2026-06-25 — fleet-prometheus etap 1: uruchomienie na VPS + incydent mózgu
## Cel
Domknięcie etapu 1 "Prometheus jako źródło prawdy liveness floty": uruchomienie
`fleet-prometheus` na VPS i potwierdzenie nasłuchu na Tailscale IP. Kontynuacja
wątku z sesji 2026-06-24 (scaffold gotowy, kontener nie powstał przez rozjazd stanu).
---
## ZROBIONE
### fix(deploy-node): warunkowy `--env-file` per-serwis (commit `686aca7`)
**Problem złapany przed deployem**: `deploy-node.sh` wywoływał `docker compose up` bez
`--env-file`, więc `${TAILSCALE_BIND_IP}` interpolował się do pustego stringa →
bind `0.0.0.0:9090` (publicznie otwarty port na VPS). Złapane lekturą skryptu przed
deployem — verify-before-fix zarobił.
**Fix**: dodano guard `if [ -f "services/<svc>/.env" ]` i przekazanie `--env-file
services/<svc>/.env` do `compose up`. Worktree `task/deploy-envfile-fix`, merge ff-only.
---
### test(control-plane): fix flaky `test_incident_lifecycle.py` (commit `992ff7c`)
**Root cause** okazał się inny niż pierwsza hipoteza (globalny `EVENTS_DIR` leak):
`OBSERVER_STATE_FILE` (`observer.py:62` = `STATE_DIR/observer_checkpoint.json`)
jest wyprowadzany przy imporcie modułu. Helpery patchowały `STATE_DIR`, ale NIE
`OBSERVER_STATE_FILE``run_once → _save_checkpoint` pisał checkpointy z ścieżkami
otagowanymi numerem przebiegu (`pytest-0`, `pytest-5`) na **realny dysk**
`/opt/homelab/state/`. Kolejny przebieg odczytywał je i stringa-compare ścieżek różniących
się tylko numerem przebiegu zwracał `False` → false-negative.
**Fix**: autouse monkeypatch fixture redirectujący WSZYSTKIE ścieżki stanu, w tym
`OBSERVER_STATE_FILE` (auto-revert po każdym teście); usunięto nieużywany buggy
`_make_observer`. Posprzątano też zatruty realny `/opt/homelab/state/observer_checkpoint.json`.
**Weryfikacja**: 6/6 przebiegów → 28 passed. Worktree `task/fix-flaky-incident-tests`,
merge ff-only.
---
### Swap 4 GB na VPS — aktywny i trwały
Potwierdzono: `/swapfile` aktywny, wpis w `/etc/fstab`, `vm.swappiness=10` w `sysctl.conf`.
Host-level one-off wykonany wcześniej (sesja 2026-06-22); w tej sesji zweryfikowany
jako warunek wstępny pod fleet-prometheus na RAM-ciasnym VPS (3.7 GB RAM, wcześniej swap=0).
---
### fleet-prometheus uruchomiony i zweryfikowany na VPS (trzykrotnie)
Po naprawie `--env-file` w `deploy-node.sh` kontener wystartował. Bind **zweryfikowany
trzykrotnie** różnymi metodami:
1. `ss -tlnp | grep 9090``100.95.58.48:9090` (NIE `0.0.0.0`).
2. `docker ps` ports → `100.95.58.48:9090->9090/tcp`.
3. Kontrola odwrotna: `curl http://135.181.153.108:9090` (publiczny IP VPS) → głucho.
Scrape żywy: self + `fleet-node` (node_exporter VPS) oba `health: up`.
Etap "uruchomienie scaffoldu" domknięty. Image: `prom/prometheus:v3.5.0`, status: healthy.
---
### Sudo/ownership na VPS — samoistnie naprawione
`deploy-local.sh` woła `sudo chown` tylko gdy `find /opt/homelab` znajdzie plik nie-1000.
Znalazł historyczne rootowe pliki i naprawił je bez ręcznej interwencji. Po naprawie
`/opt/homelab` w całości `1000:1000`, `find` pusty, deploy przestał wymagać hasła.
Obserwować, czy producent rootowych plików nie wróci.
---
## INCYDENT: `deploy.sh vps` rozwalił control-plane
### Co się stało
`deploy.sh vps` próbował deployować control-plane przez pętlę `deploy-node.sh`.
Pętla używa `COMPOSE_PROJECT_NAME` wywodzący się z `REPO_PATH` — inny niż
`deploy-local.sh` (który ma `cwd=services/control-plane`). Niezgodność project-name
→ state-divergence → Compose wyemitował `Recreate`, padł przy odtwarzaniu observera
(`No such container: <hash>_control-plane-observer`), `set -e` przerwał pętlę.
**Skutek**: observer, supervisor, executor i operator-ui zniknęły z VPS.
### Jak odtworzono mózg
```bash
ssh -t vps 'cd ~/homelab-codex-ws && git pull && \
cd services/control-plane && bash deploy-local.sh'
```
4 kontenery zbudowane i `Up (healthy)`. Fleet-prometheus deployowany potem punktowo
przez `ssh -t vps` + `deploy-node.sh` (z pominięciem zepsutej pętli).
### Dlaczego to krytyczne
Control-plane jest w `hosts/vps/services.yaml` jako zwykły serwis pętli, a ma własną
ścieżkę deploy (`deploy-local.sh`). Każdy `deploy.sh vps` rozkłada mózg.
Szczegóły w backlogu — pkt A.
---
## Wnioski
- **Verify-before-fix zarobił trzykrotnie**: env-file leak złapany lekturą zanim wystawił
port; root-cause flaky testów inny niż pierwsza hipoteza (STATE_FILE, nie EVENTS_DIR);
fleet-prometheus wykazany jako nieistniejący przez panel mimo rejestracji.
- **`deploy.sh vps` = mina** dopóki control-plane jest w pętli serwisów. Krytyczny bug
(backlog A) — używać tylko `deploy.sh control-plane` / `deploy-local.sh` oddzielnie.
- **Ghost hash-prefixed kontenery** powodują fałszywy `System Status ERROR` w panelu
mimo zdrowego mózgu — inny objaw tego samego project-name divergence (backlog B).
- **Supervisor nie enqueue'uje remediacji** przy `error`-state — Action Queue pusta
(powtórka sygnału z 2026-06-19, backlog C).
- Etap 1 fleet-prometheus domknięty; następne etapy: targety `100.x` floty, reguły
liveness (`up==0 for: 5m`), brain-watchdog drugie wejście.

View file

@ -1,111 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-26
links: []
---
# Sesja 2026-06-26 — fleet-prometheus etap 2: targety floty + zamknięcie buga deploy.sh vps
## Cel
Kontynuacja "Prometheus jako źródło prawdy liveness floty" (krok 2 z planu w backlogu):
dodać targety `node_exporter` floty `100.x` do `fleet-prometheus`. Przy okazji domknięty
najgroźniejszy bug z 2026-06-25 — `deploy.sh vps` rozkładający control-plane.
---
## ZROBIONE
### fix(deploy): guard pętli deploy-node — control-plane pomijany (commit `3b71707`)
**Bug** (🔴 KRYTYCZNY, backlog A z 2026-06-24/25): `deploy.sh vps` deployował
`control-plane` przez pętlę `deploy-node.sh` z `COMPOSE_PROJECT_NAME` wywiedzionym
z `REPO_PATH` — innym niż `deploy-local.sh` (cwd=`services/control-plane`). Niezgodność
project-name → Recreate → `No such container: <hash>_control-plane-observer``set -e`
przerywa pętlę → mózg (observer/supervisor/executor/ui) znika. Potwierdzone w produkcji
2026-06-25.
**Fix**: guard w `deploy-node.sh` — serwisy z własnym `services/<svc>/deploy-local.sh`
są pomijane w destrukcyjnej pętli. `control-plane` **ZOSTAJE** w `hosts/vps/services.yaml`
(gate pytest+build nadal go testuje), pomijany jest tylko deploy pętlą; ma własną ścieżkę
`deploy-local.sh` z poprawnym `COMPOSE_PROJECT_NAME`.
**Potwierdzone w boju**: `deploy.sh vps` wypisał `Skipping control-plane: ma własną
ścieżkę deployu`, mózg nietknięty — observer/supervisor/executor/ui `Up 25h healthy`.
Najgroźniejszy bug wczorajszej sesji — zamknięty.
---
### feat(fleet-prometheus): targety node_exporter floty (commit `7d4014e`)
Dodano do `prometheus.yml` targety floty, każdy z labelką `node:`:
| node | target | status (recon z VPS) |
|---|---|---|
| piha | `100.108.208.3:9100` | UP |
| solaria | `100.100.231.104:9100` | UP (intermittent) |
| lustro | `100.99.85.73:9100` | UP (off nocą — case anomaly detection) |
| vps | `host.docker.internal` (`node:vps`) | zachowany z etapu 1 |
**Pominięte świadomie**:
- **saturn** — laptop/workstation, nie serwer floty.
- **chelsty + chelsty-infra**`node_exporter` DOWN z VPS (LTE edge). Do zbadania
osobno (backlog).
**Zero labelek availability/godzin** — polityka "kiedy alarmować" celowo NIE trafia do
configu Prometheusa. Pójdzie do anomaly detection (mózg uczy się wzorca dobowego z
historii metryk). Na teraz: tylko scrape + label `node:`, Prometheus gromadzi historię.
---
### docs(backlog): pomysł anomaly-detection liveness (commit `1529911`)
Zapisano ideę: zamiast statycznych okien czasowych w regułach alertowych, mózg czyta
historię z Prometheus i SAM wykrywa wzorzec dobowy per node. `up==0` zgodne z nauczonym
wzorcem offline = nie alarmuj; odbiegające = realna awaria. Wymaga tygodni historii →
realne za ~2-4 tyg.
---
## DEPLOY
`deploy.sh vps` przeszedł: DEPLOY OK, verify=green, 24 kontenery healthy. Fix A
potwierdzony (control-plane pominięty w pętli). Prometheus zrecreate'owany przez ręczny
`docker compose ... up -d --force-recreate`; config z 4 targetami widoczny w kontenerze.
---
## PENDING (NIE potwierdzone — nie zakładać sukcesu)
- **Health targetów piha/solaria/lustro w `/api/v1/targets` NIE zweryfikowany** po finalnym
recreate. Config je zawiera, ale czy scrape zwraca `up` — do sprawdzenia przy powrocie.
- **lustro** może być `down` jeśli noc/off → to NIE błąd, to case dla anomaly detection.
- **solaria** intermittent — podobnie.
---
## Wnioski
- **Najgroźniejszy bug zamknięty świadomym guardem, nie usuwaniem z manifestu**:
`control-plane` zostaje w `services.yaml` (gate go testuje), pomijana jest tylko
destrukcyjna pętla deployu. Rozdzielono "co testować" od "jak deployować".
- **Cicha rozbieżność deploy↔config (nowy tech-debt)**: `deploy-node.sh` po zmianie
`prometheus.yml` NIE reloaduje/recreate'uje kontenera — Compose widzi ten sam obraz,
zostawia Running, config się nie podmienia. Deploy mówi "green", a Prometheus trzyma
stary config w pamięci. Dziś wymagało ręcznego `--force-recreate`. Dotyczy KAŻDEGO
serwisu config-driven bez zmiany obrazu. → backlog.
- **Potrójny rozjazd mastera przez równoległą sesję KB**: w trakcie pracy CC na wątku
Prometheusa równoległa sesja commitowała na master (bulk import Gmail). Rebase ratował,
ale to anty-wzorzec. **Lekcja: nie commitować na master równolegle, gdy CC pracuje na
branchu/wątku** — albo izolować pracę w worktree, albo serializować commity na master.
- **Etap "targety floty" prawie domknięty**: config wdrożony, health-verify pending.
Następne etapy (osobne taski): reguły liveness (`up==0 for: 5m`), wpięcie
firing→brain-watchdog (`/api/v1/alerts`→Telegram, bez Alertmanagera), potem cutover ze
starej rury eventowej. Plus: zbadać czemu chelsty `node_exporter` down; anomaly
detection gdy uzbiera się historia.

View file

@ -1,65 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# Sesja 2026-06-30 — Inwentaryzacja floty + gaszenie dysku SATURN
## Cel
Pełna inwentaryzacja floty (repo↔rzeczywistość) jako fundament przed dalszą
architekturą dokumentów KB. Start od weryfikacji stanu faktycznego wszystkich nodów.
## Zrobione
### Inwentaryzacja floty (główna robota)
- CC (Sonnet 4.6) w worktree `fleet-inventory` zebrał stan faktyczny (docker ps +
free/df/nproc) z 4 dostępnych nodów: PIHA, VPS, SOLARIA, SATURN. LUSTRO+CHELSTY
offline (timeout :22) -> oznaczone UNREACHABLE.
- Wynik: `kb/subsystems/fleet-inventory.md` — 23 zpriorytetyzowane rozjazdy.
- Kluczowe ustalenia:
- **forgejo** biega na PIHA (always-on), ale `service.yaml owner_node=saturn` — rozjazd
- **mosquitto** biega na VPS, `service.yaml owner=piha`, na PIHA go nie ma
- **npm** — dwie instancje: PIHA (LAN ingress :80/:443) + VPS (public); repo zna jedna
- **homeassistant5** na PIHA (HA "ken", :8123) niedeklarowany w repo
- **control-plane** biega na VPS (healthy) I SATURN (control-plane-ui UNHEALTHY)
- **PIHA: 40 kontenerow, ~6 w GitOps, 33 "shadow" poza repo** (immich, vaultwarden,
wikijs, actual, audiobookshelf, elasticsearch, grafana, prom, portainer...)
- capabilities nieaktualne: SATURN RAM 8->14GiB, dysk 64->159GB; SOLARIA CPU 24->32
- ollama zadeklarowana (owner=solaria) ale NIE biega
### Gaszenie dysku SATURN (pilne — ugaszone)
- SATURN dysk 91% (15G wolne) = jedyne realne ryzyko awarii.
- `docker image prune -a -f` -> +4.1G (k3s/k3d sprzed 4 lat, stary HA 2.3G, postgresy)
- `journalctl --vacuum-size=200M` -> +3.7G
- `rm syslog.1 + rotacje` -> +4.2G
- Wynik: **91% -> 83%** (27G wolne), poza strefa ryzyka.
- Bonus diagnostyczny: przyczyna `control-plane-ui UNHEALTHY` = healthcheck uzywa
`curl`, ktorego NIE MA w obrazie -> failuje w kolko -> log spam (4.2G syslog).
- `/opt/anaconda3` 16G = najwiekszy pojedynczy zjadacz dysku (env-y Pythona, decyzja Oskara).
### safeclean.sh — 3 bugi naprawione (repo ubuntu-scripts, osobne)
- BUG #1: dry-run pokazywal reclaimable z `docker system df` (bez filtra), a apply
prune'owal z `--filter until=168h` -> dry-run obiecywal 4.8G, apply robil 0B. Fix:
dry-run pokazuje teraz realny reclaim z filtrem + tip o `--docker-all`.
- BUG #2: `du` podwojnie liczyl eCryptfs home -> warning dodany.
- BUG #3: rotated rsyslog (`syslog.1` 4.2G) byl poza zakresem -> dodana kategoria
"rotated system logs" (kasuje tylko zrotowane, nigdy aktywne).
- Commit `37233f0` na main (ubuntu-scripts).
## Nastepna sesja — naprawa 23 rozjazdow (pogrupowana)
- **Grupa A** (czyste docs, zero ryzyka): capabilities SATURN/SOLARIA, forgejo
owner_node->piha, mosquitto->vps, brakujace `hosts/saturn/services.yaml`
- **Grupa B** (wymaga decyzji): npm x2 (zostawic oba czy usunac PIHA?), control-plane
na SATURN (dev-instance czy usunac? + healthcheck curl fix), ollama nie biega
- **Grupa C** (sprzatanie): postgres:18->17, anonimowy outline-postgres image,
humanai/umami do repo, audyt 33 shadow kontenerow PIHA
## Watek wstrzymany (z poprzedniej sesji)
Architektura dokumentow KB — decyzje zamkniete (SSO=Forgejo-OIDC, ingress LAN-only,
archiwum hybryda Nextcloud-kopia/Paperless-referencja), host OTWARTY (PIHA ~0.5G wolne
RAM -> Nextcloud sie nie zmiesci). Wrocic po naprawie rozjazdow — inwentaryzacja
potwierdzila ze PIHA przeciazona (40 kontenerow), wiec host dokumentow wymaga decyzji.

View file

@ -1,66 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# 2026-06-30 — Migracja kapala.org → Cloudflare + wildcard DNS-01, HA i Immich na mesh
## Cel
Trwałe rozwiązanie problemu zdalnego dostępu do HA (i innych usług domowych)
przez Tailscale. Odejście od HTTP-01 per-host (wygasające certy przy mesh DNS)
na rzecz wildcard cert przez DNS-01.
## Co zrobiono
### Migracja DNS kapala.org: 42.pl → Cloudflare
- Eksport strefy z 42.pl (AXFR, 13 rekordów) jako siatka bezpieczeństwa
- Dodanie kapala.org do Cloudflare (Free), import + ręczne uzupełnienie
- KRYTYCZNE: CF auto-proxuje rekordy na import — wszystkie A/CNAME
przełączone na "DNS only" (szara chmurka). DKIM CNAME proxied = zepsuty
podpis maila. Wszystkie 9 rekordów zweryfikowane 1:1 z eksportem.
- NS w VipoWer: fns1/fns2.42.pl → dom.ns.cloudflare.com / katja.ns.cloudflare.com
- Propagacja ~45 min. Mail (Fastmail MX/DKIM/SPF) bez przerwy przez całą migrację.
### Wildcard cert DNS-01
- CF API token (Zone:DNS:Edit, scope kapala.org)
- NPM → SSL → Let's Encrypt via DNS → Cloudflare → *.kapala.org + kapala.org
- Cert #49 (npm-49) wydany, auto-renew przez DNS-01 potwierdzony (renew zadziałał)
- Expires 2026-09-28, odnawia się sam
### Usługi przeniesione na kapala.org (mesh-only)
- ha.kapala.org → 192.168.31.7:8123 (cert wildcard, WS ON)
- immich.kapala.org → 192.168.31.5:2283 (cert wildcard, WS ON)
- DNS w CF: rekordy A → 100.108.208.3 (Tailscale PIHA, DNS only/reserved IP)
## Bugi rozwiązane
### "unrecognized name" na ha.kapala.org mimo dobrego certu (root cause)
Objaw: cert OK (npm-49), DNS OK, host "Online" w UI, ale curl → TLS
unrecognized name. Plik proxy_host/4.conf nie istniał.
Przyczyna: custom Advanced config zawierał `proxy_http_version 1.1;`,
a NPM dodaje tę dyrektywę SAM przy Websockets Support = ON.
Duplikat → `nginx -t` odrzucał cały 4.conf → plik się nie generował.
Diagnoza przez `strings /data/database.sqlite | grep kapala`:
nginx_err: "proxy_http_version directive is duplicate in 4.conf:68"
Fix: usunięcie `proxy_http_version 1.1;` z Advanced. Plik się wygenerował.
### immich.okit.pl 403 Forbidden (red herring → nie ruszany)
403 od openresty = Access List "Local" blokuje mesh, NIE cert (cert żył do 9/28).
Decyzja: zamiast zmieniać Access List — przeniesiono na immich.kapala.org
(mesh-only, wildcard cert). immich.okit.pl zostaje do migracji okit.pl.
## Wzorzec migracji usługi na kapala.org (mesh)
1. CF: rekord A <usługa> → 100.108.208.3 (DNS only)
2. NPM proxy host: forward wewn. IP:port, Access List = Publicly Accessible,
cert *.kapala.org, Advanced PUSTE (zero proxy_http_version!)
3. Weryfikacja: grep conf (plik istnieje) + curl (cert *.kapala.org + 200/302)
4. Zmiana URL w apce/kliencie
## Stan końcowy
- ha.kapala.org, immich.kapala.org — działają przez mesh, zielona kłódka
- Mail kapala.org (Fastmail) — bez przerwy, MX/DKIM/SPF OK
- Wildcard *.kapala.org — auto-renew DNS-01 potwierdzony

View file

@ -1,119 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-06-30
links: []
---
# Sesja 2026-06-30 — fleet-prometheus liveness: reguły NodeDown + wpięcie watchdog→Prometheus
## Cel
Kontynuacja "Prometheus jako źródło prawdy liveness floty" (kroki 4 i 5 z planu
w backlogu): reguły alertowe liveness + drugie wejście brain-watchdoga z Prometheusa.
---
## ZROBIONE
### feat(fleet-prometheus): reguły liveness NodeDown (commit `d417000`)
Dodano `rules/liveness.yml` z regułą `NodeDown`:
```yaml
- alert: NodeDown
expr: up{node=~"vps|piha"} == 0
for: 5m
labels:
severity: critical
```
**Decyzja projektowa**: tylko `vps` i `piha` w masce — oba always-on (serwer, infra).
`solaria` i `lustro` świadomie wykluczone: intermittent, mają własny wzorzec offline.
Będą w anomaly detection gdy uzbiera się historia metryk (~2-4 tyg). Alertmanager
celowo pominięty — brak instancji, alerty idą przez brain-watchdog.
**Trzy miejsca wymagające spójności** (pułapka z tym composem):
1. `rules/liveness.yml` — sama reguła
2. `docker-compose.yml` — mount `./rules:/etc/prometheus/rules:ro`
3. `prometheus.yml``rule_files: ["rules/*.yml"]` (odkomentowane)
Deploy: `deploy.sh vps` OK (fix A `3b71707` ponownie potwierdzony — `Skipping
control-plane`). fleet-prometheus Recreated (zmiana compose wymusiła recreate —
reguły weszły). Weryfikacja: `NodeDown state=inactive`, `/api/v1/alerts` = `[]`.
Poprawnie — vps i piha są UP, reguła uzbrojona i cicha.
---
### feat(brain-watchdog): poll Prometheus /api/v1/alerts → Telegram (commit `62d6fc0`)
Drugie źródło alertów w brain-watchdog: poll `/api/v1/alerts` (firing) → Telegram.
**Architektura A — dwa niezależne tory**:
- Tor mózgu (`check()` + blok `main()`) — **NIETKNIĘTY**. 7 starych testów mózgu pass.
- Tor Prometheusa (`check_prometheus_alerts()`, debounce per-alert, oddzielny klucz
w `state.json`) — niezależny, nie interferuje z torem mózgu.
**Debounce**: klucz `alertname:node` w `state.json` (pole `prom_alerted`). Przy firing
wysyła Telegram raz; milczy dopóki alert trwa; wysyła recovery gdy alert znika.
Nie spamuje co tick.
**Opcjonalność**: `PROMETHEUS_URL` opcjonalny w `.env`. Pusty = Prometheus polling
wyłączony, graceful fallback (wzorzec jak `HEALTHCHECKS_URL`).
**Testy**: 12 testów pass (7 mózg + 5 Prometheus — nowe: disable when empty URL,
single alert sent once, recovery sent, no spam between ticks, no crash on HTTP error).
**Deploy na PIHA** (nie VPS — watchdog tam żyje):
- `docker compose up -d --build --force-recreate`
- `PROMETHEUS_URL=http://100.95.58.48:9090` dodany do `/opt/homelab/config/brain-watchdog/.env`
- Kontener Started, ping mózgu OK, brak błędów polla w logach
---
## INCYDENT: PIHA divergent branches (rozwiązane)
PIHA przed deployem siedział na `origin/task/kb-gmail-import` (4 commity KB:
`3461dea`/`c5dd8f3`/`f0e4d90`/`57a27af`) zamiast na `master`. Przyczyna: poprzednia
sesja KB zostawiła PIHA na branchu roboczym.
**Praca NIE zginęła** — wszystkie 4 commity KB są na `origin/task/kb-gmail-import`
(zweryfikowane `git branch -r --contains <hash>` PRZED jakimkolwiek resetem).
Bezpieczny `git reset --hard origin/master` (working tree był czysty, robota na
origin). PIHA wrócił na `master` (`62d6fc0`), deploy watchdoga ponowiony OK.
**Reguła na przyszłość**: PIHA deploy-only na `master`. Po deployu/teście zawsze
wrócić: `git checkout master && git pull`. Nie zostawiać noda na branchu roboczym.
---
## PENDING (NIE potwierdzone — nie zakładać sukcesu)
- **Aktywność polla Prometheus w brain-watchdog** nie jest widoczna w logach startowych
(log startu pokazuje zmienne mózgu, nie `PROMETHEUS_URL`). Wnioskujemy że poll działa
(brak błędów, `.env` ma URL, 5 testów pass), ale pełne potwierdzenie end-to-end
przyjdzie przy pierwszym realnym firing: `NodeDown` fires → watchdog łapie →
Telegram. Alternatywnie: dorzucić log polla przy starcie (drobny follow-up).
---
## Wnioski
- **Pętla liveness PRAWIE zamknięta**: Prometheus wykrywa (reguła NodeDown, inactive=OK)
→ watchdog czyta firing → Telegram. Brakuje tylko realnego testu end-to-end, który
przyjdzie przy pierwszej realnej awarii always-on węzła (jak tor mózgu w boju wczoraj).
- **verify-before-reset uratował pracę KB**: `git branch -r --contains` przed `reset
--hard` potwierdził że commity są na origin — reset był bezpieczny. Wzorzec do
powielenia zawsze przed reset na cudzym worktree/nodzie.
- **Architektura A (dwa niezależne tory) poprawna**: żadna zmiana toru Prometheusa
nie mogła zepsuć toru mózgu — izolacja bez wspólnego state, nowe testy tylko dla
nowej ścieżki. 7 starych testów mózgu pass bez zmian.
- **Następne (osobne taski)**: cutover ze starej rury eventowej (gdy liveness
udowodniony w boju); `docker rm` ghostów B na VPS; zbadać chelsty `node_exporter`
DOWN; supervisor bez akcji (C); anomaly detection za ~2-4 tyg.

View file

@ -1,43 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Sesja 2026-07-02 — Modul 0 (odchudzenie PIHA) + migracja Forgejo/Vikunja na kapala.org
## Modul 0 kb-02 — WYKONANY (faza 1 audyt + faza 2 egzekucja)
- Audyt CC (read-only): kb/audits/piha-slim-2026-07-02.md
- Review Oskara skorygowal audyt: llm-gateway = WLASNY kod (FastAPI-router LLM,
/opt/llm-gateway, proxy do Ollama@SOLARIA) — NIE martwy; immich MUSI byc 24/7
na PIHA (SOLARIA sesyjna) — rekomendacja przeniesienia wykreslona.
- Ubite: elasticsearch (895Mi, sluzyl TYLKO martwemu diskoverowi — indeks 22MB
nietkniety od 2025-10) + diskover (jeden stack compose /home/pi/diskover).
Dane ES zostaly na dysku. Autostart nie wroci.
- Zysk: available 2.8 -> 3.8Gi (+1.0Gi), swap 2.0 -> 1.9Gi, 41 -> 39 kontenerow.
- KRYTERIUM MODULU 0 (>=1.5Gi) SPELNIONE z zapasem 2.3Gi. Modul 2 (Paperless) odblokowany.
- Backlog: archiwizacja zrodla llm-gateway (kod TYLKO na dysku PIHA, bez gita!),
fix targetu prom watchtower->llm-gateway (404 /v1/metrics).
## Migracja okit.pl -> kapala.org (cert wygasl 28.06, HTTP-challenge martwy)
Przyczyna: certy per-host okit.pl w npm@PIHA odnawiane HTTP-challenge, ktory nie
przechodzi (LAN-only). Wildcard *.kapala.org (Cloudflare DNS-01) odnawia sie sam.
- **forgejo.kapala.org**: Cloudflare A -> 100.108.208.3 (DNS Only), npm vhost
(192.168.31.5:3000, websockets, Advanced pusty), ROOT_URL w app.ini podmieniony
(sed w kontenerze + restart), OIDC discovery na nowym issuerze OK.
- **vikunja.kapala.org**: analogicznie (port 3456). Przez repo (GitOps):
config.yml (redirecturl+authurl) + docker-compose.yml (PUBLICURL) -> pull na
PIHA -> compose up -d (recreate). OAuth app w Forgejo: redirect URI zmieniony.
- **GOTCHA OIDC**: Vikunja dokleja provider-key do redirecturl — Forgejo musi miec
PELNY redirect URI `https://vikunja.kapala.org/auth/openid/forgejo` (nie baze!).
Objaw bledu: "Unregistered Redirect URI".
- Login Vikunja przez Forgejo-OIDC PRZETESTOWANY — dziala end-to-end.
- Git po SSH (100.108.208.3:222) nietkniety caly czas — remote'y bez zmian.
## TODO nastepne
- Stare vhosty okit.pl w npm@PIHA (forgejo/vikunja + inne z martwym certem) —
audyt: co jeszcze wisi na okit.pl, przenosic na kapala.org czy usunac.
- Egzekucja kolejnych modulow kb-02: 1 (SSO doc) -> 2 (Paperless) -> 3 -> 4 -> 5.

View file

@ -1,118 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-02
links: []
---
# Sesja 2026-07-02 — recon-weryfikacja inwentaryzacji + rozbrojenie trzech min
## Cel
Weryfikacja stanu faktycznego floty względem audytu inwentaryzacji 2026-06-30
(recon read-only, CC/Fable) + rozbrojenie najpilniejszych min z raportu.
Sesja tylko-recon + minimalne fixy; bez deployów nowych feature'ów.
---
## ZROBIONE
### Recon-weryfikacja inwentaryzacji floty (commit `57a6dff`, read-only)
Wynik: `kb/subsystems/fleet-inventory-verify.md`.
**Bilans 23 rozjazdów z audytu 2026-06-30**:
- **20 wciąż aktualnych** — nic się samo nie naprawiło.
- **2 zmienione**: dysk SATURN 91%→83% (po safeclean, poza strefą ryzyka);
ocena joplin-db `postgres:18` zdezaktualizowana — PG18 jest GA od 09/2025,
to już nie pre-release.
- **1 wyjaśniony**: storage SOLARIA — 1.9T zgodne z capabilities; "brakujący"
1TB to partycja Windows dual-boot, nie rozjazd.
- **0 naprawionych repo-side** (przed tą sesją).
**Dwa pendingi z poprzednich sesji DOMKNIĘTE przez recon**:
1. **Poll Prometheus w brain-watchdog POTWIERDZONY** — obraz zbudowany po
`62d6fc0`, `PROMETHEUS_URL` obecny w `.env` I w env kontenera, zero
`poll failed` w logach. Pending z 2026-06-30 zamknięty.
2. **Ghost hash-prefixed kontenery control-plane na VPS ZNIKNĘŁY** — 24
kontenery na VPS, zero hash-prefixed. Bug B z backlogu rozwiązany
(prawdopodobnie recreate'y z kolejnych deployów je zmiotły).
**Bonus reconu**:
- fleet-prometheus 100% zgodny z repo; WSZYSTKIE 4 targety up
(vps / piha / solaria / lustro).
- lustro żyje (pimirror2, node-agent healthy), ale `pi-watchtower-1`
w restart-loopie — nowy drobiazg do backlogu.
- chelsty-* UNREACHABLE (LTE) — zgodnie z oczekiwaniem.
---
### Trzy miny z raportu rozbrojone
#### Mina #1 — PIHA checkout: gałąź wciąż `task/kb-gmail-import`
HEAD był na commicie mastera, ale gałąź wciąż `task/kb-gmail-import`
niedokończony reset z 2026-06-30: `reset --hard` przesunął wskaźnik gałęzi
taska na commit mastera, ale NIE przełączył gałęzi.
**FIX**: `git checkout master && git pull` na PIHA — 30 commitów nadrobione,
node teraz naprawdę na `master`.
**LEKCJA**: naprawa błędnej gałęzi na nodzie to `checkout` + `pull`,
NIE `reset --hard` — reset przesuwa bieżącą gałąź, nie przełącza na inną.
#### Mina #2 — zapomniany control-plane stack na SATURN
4 kontenery Up 3 days (ui unhealthy) obok produkcyjnego mózgu na VPS.
Logi supervisora pokazały, że był **ŚLEPY**: pętla WARNING
`Hosts directory /repo/hosts does not exist` co 30s — brak mountu repo,
zero możliwości akcji przez całe 3 dni. Czyli zero ryzyka zdublowanych
remediacji w tym czasie; **NIE ma związku z bugiem C** (supervisor-no-action
na produkcji).
**FIX**: `docker compose down` (wolumeny zachowane). Jedyny control-plane
= produkcyjny na VPS.
#### Mina #3 — owner_node kłamał (commit `886bc85`)
- forgejo: `owner_node` saturn → **piha** (biega na PIHA always-on)
- mosquitto: `owner_node` piha → **vps** (biega na VPS)
Po jednej linii per plik; `owner_node` nie występował nigdzie indziej w repo.
---
## DO BACKLOGU (zgłoszone, świadomie 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 konwencję CLAUDE.md).
- 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 deklaruje mosquitto też jako komponent ai-cluster
(`topology.yaml:75`) — rozstrzygnąć czyj jest broker :1883.
- (nowe z reconu) `pi-watchtower-1` na LUSTRO w restart-loopie; alias `lustro`
nie rezolwuje z SOLARII; brak formalnego override mem_limit fleet-prometheus
w `hosts/vps/runtime/` (siedzi w bazowym compose — kosmetyka).
Wpisy dodane do `kb/phases/backlog.md` w tej sesji.
---
## Wnioski
- **Recon-before-fix działa**: dwa pendingi zamknięte bez dotykania czegokolwiek
(poll watchdoga potwierdzony, ghosty B same zniknęły) — oszczędzone dwa
niepotrzebne taski naprawcze.
- **`reset --hard` to nie `checkout`**: mina #1 to bezpośrednia konsekwencja
fixa z 2026-06-30 — reset przesunął gałąź taska zamiast przełączyć na master.
Wzorzec na nody deploy-only: zawsze `checkout master && pull`.
- **Ślepy supervisor = cichy supervisor**: stack na SATURN wyglądał groźnie
(możliwe zdublowane remediacje), ale brak mountu repo czynił go bezzębnym.
Weryfikacja logów PRZED oceną ryzyka oszczędziła fałszywy alarm.
- Hashe sesji: `57a6dff` (recon-weryfikacja), `886bc85` (owner_node fix).

View file

@ -1,115 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-06
links: []
---
# Sesja 2026-07-06 — cutover liveności na Prometheus: recon starego toru + Etap 0 udowodniony w boju
## Cel
Główny projekt: Prometheus jako źródło prawdy liveności floty. Dwa kroki tej sesji:
(1) read-only recon starego toru liveności z dokładnymi punktami przełączenia
i planem etapowym; (2) Etap 0 cutoveru — bojowy dowód end-to-end toru
Prometheus → brain-watchdog → Telegram, którego brakowało od 2026-06-30.
---
## ZROBIONE
### Recon cutoveru — wmergowany (commit `d94bb38`)
Wynik: `kb/audits/prometheus-cutover-2026-07-06.md` (517 linii, read-only,
zero zmian w kodzie). Kluczowe ustalenia:
- **Cutover to podmiana klasyfikacji liveności w JEDNYM miejscu**
`_prune_stale_world` w `scripts/observer/observer.py:312-323` + neutralizacja
event-driven flipów `:438-446` — a NIE odłączanie rury. Fizycznie nic nie znika.
- **`node_health` wozi też metryki** disk/mem/cpu do panelu i `disk_pressure`
do supervisora → rsync + eventy ZOSTAJĄ w całości.
- **Supervisor-remediacja i executor są CAŁKOWICIE niezależne od liveności węzłów**
— supervisor czyta z `nodes.json` wyłącznie `disk_pressure`. Jedyny konsument
liveności po stronie supervisora to syntetyczne eventy przejść
(`node_offline/stale/online` → alert_only → Telegram).
- **Różnica semantyk**: `up{}` mierzy "host żyje (exporter odpowiada)",
eventowy `node_health` mierzy "node-agent żyje + cały łańcuch SSH/rsync działa".
Cutover ZWĘŻA definicję liveności — padnięty node-agent przy żywym węźle zmienia
werdykt. Do świadomej akceptacji w etapie 2; rozważyć `up{job=node-agent}`.
- **chelsty-infra ma liveność WYŁĄCZNIE z eventów** (Prometheus go nie scrape'uje
po LTE) → cutover MUSI być per-węzeł (`PROM_LIVENESS_NODES`), nigdy globalny.
- **Prometheus po cutoverze = niepilnowany SPOF** (`mem_limit 512m`,
`oom_score_adj 200` — ubijalny przed control-plane) → fail-open na stary tor
obowiązkowy + watchdog na sam Prometheus (mały task przy etapie 3).
- Exportery piha/solaria/lustro istnieją **poza GitOps** (brak definicji w repo)
— rozjazd, osobny wątek.
### Etap 0 cutoveru — DONE, dowód bojowy end-to-end
Tor **Prometheus → brain-watchdog → Telegram udowodniony END-TO-END po raz
pierwszy w produkcji**. Test punktowy na VPS (celowo NIE w repo): tymczasowa
reguła `AlertTestEtap0` (`up{node="vps"}==1`, `for: 0s`) dodana do
`services/fleet-prometheus/rules/` na VPS + reload. Cztery punkty potwierdzenia:
1. **Firing w Prometheusie**: `/api/v1/alerts``AlertTestEtap0` firing, node vps.
2. **Watchdog spollował z PIHA**: log `[prometheus] sent alert: AlertTestEtap0:vps`
— przy okazji widoczne dwa niezależne tory (ping mózgu OK co 60 s + poll
Prometheusa osobno), zgodnie z architekturą A.
3. **Alert doleciał na Telegram** (@okitaialerts_bot): „Prometheus alert:
AlertTestEtap0, Node: vps, Summary: …".
4. **Rollback czysty**: reguła usunięta, reload, firing=0.
Zamyka pending z 2026-06-30 („czy watchdog realnie polluje" — recon 2026-07-02
potwierdził konfigurację, dziś potwierdzone działanie end-to-end). Reguła testowa
NIE trafiła do repo — to diagnostyka, nie architektura; katalog `rules/` ma
nadal tylko `liveness.yml`.
### Realny dowód „czemu cutover" — chelsty NOMINAL-zonk
Panel agents.okit.pl pokazuje **chelsty NOMINAL + Runtime online, mimo że chelsty
jest OFFLINE od 34 dni** (`tailscale status`). U wszystkich węzłów: Last Seen
„Invalid Date", Connectivity/Incidents `undefined`, a status NOMINAL — observer
NIE egzekwuje świeżości `last_seen` (bug TTL wraca, patrz backlog „Observer
staleness"). Cutover na Prometheus `up{}` to naprawia: chelsty nie-scrape'owany →
down, nie fałszywie NOMINAL.
Snapshot panelu potwierdził też dwa zaległe drobiazgi (dopisane do backlogu):
- **Ghosty B wróciły/nieposprzątane** — hash-prefixed kontenery control-plane
widoczne na VPS (wpis „zniknęły" z 2026-07-02 zdezaktualizowany).
- **elasticsearch + diskover w stanie error na PIHA** — usunięte w module 0
(2026-07-02), ale observer wciąż je zna i raportuje.
---
## PLAN — następne etapy cutoveru (osobne sesje)
- **Etap 1 — shadow-read**: observer czyta OBA źródła (eventy I Prometheus
`up{}`), pisze nienaruszające pola `prom_*` w `nodes.json`, loguje rozbieżności,
NIC nie przełącza (parallel-run, fail-open). Realna zmiana w
`observer.py:312-323` → worktree/CC.
- **Etap 2 — analiza**: ≥ 7 dni zgodności → klasyfikacja rozbieżności
(semantyka vs bug) → decyzja mappingu (rekomendacja: `timestamp(up)` jako
prom-last_seen przez istniejące `compute_liveness`).
- **Etap 3 — przełączenie per-węzeł** za flagą `PROM_LIVENESS_NODES`:
najpierw solaria+lustro (najmniejsza szkoda przy pomyłce), potem vps+piha.
**chelsty-infra ZOSTAJE na eventach.** Rollback = wyczyszczenie flagi.
---
## Wnioski
- **Etap 0 zamknął fundamentalną niepewność wiszącą od 2026-06-30**: „czy tor
alertowy Prometheus→watchdog→Telegram w ogóle działa" przestało być wnioskowaniem
z konfiguracji (recon 2026-07-02), a stało się faktem dowiedzionym w produkcji.
Dalsze etapy cutoveru budują na sprawdzonym fundamencie, nie na założeniu.
- **verify-before-fix w reconie obalił założenie architektoniczne**: observer NIE
jest jednym modułem w `services/control-plane/src/` — to `scripts/observer/observer.py`
+ wydzielona maszyna stanów `liveness.py` importowana przez trzy konsumentów.
Bez reconu plan cutoveru celowałby w złe miejsce.
- **Test punktowy (reguła for:0s + rollback) to tani sposób na dowód bojowy toru
alertowego** — bez czekania na realną awarię i bez commitowania diagnostyki do repo.
- Hashe sesji: `d94bb38` (recon cutoveru); Etap 0 = czynność operatorska na
runtime (bez commitu, zgodnie z planem z reconu).

View file

@ -1,54 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-07
links: []
---
# Session log 2026-07-07 — Immich upload fix / pimain cleanup (kontynuacja 2026-07-03)
## Kontekst
Immich mobile: Error(500) przy uploadzie. Diagnoza z 3.07: bind mount fstab
`/media/pimain/media/immich/library/upload -> /home/pi/immich/library/upload`
(sda1, 100% pelny). Bind niewidoczny w `docker inspect` i compose — wykryty
przez `/proc/mounts` w kontenerze + `findmnt`. NVMe/quota/inody/ext4 czyste.
## Root cause zapelnienia
`/media/pimain/backup-tmp/pi/` — dzienne backupy `/home/pi` (tgz ~2,9G, cron
root 03:00, `backup-piha-home.sh`) od 2025-12-01, 201 plikow. Retencja w
skrypcie MARTWA od zawsze — dwa bugi:
1. pruning szukal `pihome_*.tgz`, a pliki nazywaja sie `YYYYMMDD.tgz`
(widmo `$backupFolderName` vs `$backupFolderPrefix`)
2. `rm "$fileDtgName"` (wynik cut) zamiast `rm "$file"`
## Wykonane
- Skasowano 187 najstarszych tgz → pimain 100% → 87% (~227G wolne)
- **Uploady z telefonu potwierdzone dzialajace**
- Fix retencji w `/home/pi/backup-scr/backup-piha-home.sh` (sed-y na miejscu,
oryginal w `.sh.orig`): `-maxdepth 1 -name "20*.tgz"`, `basename`,
`rm -v "$file"`; retencja 7 dni + month-endy na zawsze; pruning tylko po
sukcesie tara
- Dodatkowy fix: tar exit 1 (file changed as we read it — zywe logi NPM)
traktowany jako sukces (`|| [ $? -eq 1 ]`); exit >=2 nadal blad
- Swiezy backup `20260707.tgz` (2,9G, jedyny realny); 0-bajtowe trupy usuniete
- PENDING: nocny cron 03:00 = test bojowy calej sciezki (backup+retencja);
kontrola: `tail /var/log/backup-piha-home.log`
## Odkrycia / lekcje
- Backupy byly 0-bajtowe od ~2026-06-06 (open przechodzil, write nie) —
month-endy utracone przy czyszczeniu (kryterium "14 najnowszych" nie
sprawdzalo rozmiaru; strata akceptowalna, zrodlo zywe na /home).
Lekcja: przy czyszczeniu backupow filtrowac po size, nie tylko po dacie.
- Heredoc przez ssh+wklejke na PIHA sie tnie — tylko krotkie komendy,
sed-y jednolinijkowe albo scp.
- Bind mount z fstab propagowany do kontenera to pulapka diagnostyczna:
weryfikowac przez `/proc/mounts` z wnetrza kontenera i `findmnt`,
nie `docker inspect`.
## Backlog
- #11 (disk alerting): `node_filesystem_avail_bytes` MUSI objac `/media/pimain`
i `/media/pimainmirror` — drugi incydent z tego samego braku
- NOWE: rozwazyc wciagniecie `backup-piha-home.sh` pod repo (per-host legacy,
poza GitOps); ew. monitoring 0-bajtowych artefaktow backupu

View file

@ -1,59 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-07
links: []
---
# Sesja 2026-07-07 — okit.pl Faza 1 (wildcard cert) + przepiecie 9 hostow
## Kontekst
Kontynuacja migracji okit.pl 42.pl -> Cloudflare. Faza 0 (strefa) zrobiona wczesniej.
Dzis: Faza 1 (wildcard cert) + przepiecie zywych vhostow.
## Wykonane
### Weryfikacja Fazy 0 (propagacja)
- dig NS okit.pl = dom.ns/katja.ns.cloudflare.com (spropagowane).
- Autorytatywnie (dig @dom.ns.cloudflare.com): rekordy 1:1 z AXFR, poczta OK.
- WAZNE: Pi-hole split-horizon — Local DNS nadpisuje ~15 okit.pl na 192.168.31.5
(LAN). dig z hosta pokazuje LAN IP, nie Cloudflare. To NIE blad. Cloudflare
autorytatywny ma poprawne IP. Przy Fazie 2 pamietac o OBU warstwach.
### FAZA 1 — wildcard *.okit.pl
- Nowy token Cloudflare "npm-dns01-all-zones" (Zone:DNS:Edit, All zones — obejmuje
kapala + okit + przyszle). Stary "Edit zone DNS" byl tylko 1 zone (kapala).
- npm@PIHA: cert *.okit.pl + okit.pl przez Cloudflare DNS-01 (cert #51 w npm).
Wazny do 2026-10-05, samoodnawialny. Pulapka: literowka ".okit.pl" zamiast
"okit.pl" dawala Internal Error; propagation 5->30s tez pomaga.
### Przepiecie 9 zywych hostow na wildcard (bulk-SQL)
Metoda: keinos/sqlite3 --user root (brak sqlite3 na hoscie i w kontenerze npm).
- Backup bazy npm: /data/database.sqlite.bak-20260707-2210 (w wolumenie).
- UPDATE proxy_host SET certificate_id=51 WHERE id IN (7,16,3,11,1,6,5,10,20).
- KLUCZOWE: sam UPDATE bazy NIE wystarcza! npm generuje .conf per host ze sciezka
certu; baza mowi 51 ale nginx laduje stary. Trzeba sed sciezek w .conf + reload:
sed 's|/live/npm-[0-9]*/|/live/npm-51/|g' na 9 plikach + nginx -t + nginx -s reload.
- Zweryfikowane: openssl pokazuje CN=*.okit.pl do 2026-10-05 dla wszystkich 9.
Przepiete: grafana, home, immich, wiki, pihole, prometheus, zigbee, ngpm, owntracks.
## LEKCJA
Przepinanie certow npm bulk-SQL = DWA kroki (UPDATE bazy + sed .conf + reload),
bo npm nie regeneruje configow z bazy. UI-klik robi to jednym Save (regeneracja).
Przy <15 hostach UI prostsze; bulk oplaca sie dopiero przy duzej skali.
Kontener keinos/sqlite3 --user root do edycji bazy npm (gdy brak sqlite3).
## NIE ruszone (swiadomie)
- forgejo, vikunja, foty, ha, immich.kapala -> juz na *.kapala.org (cert 49)
- owntracks-frontend -> czeka na mesh (Faza 2)
- outline, joplin, agents -> NIE na PIHA! Zyja na VPS-npm (drugi npm do sprawdzenia)
## TODO nastepne sesje
- FAZA 2: mesh/public reorg, owntracks-frontend->mesh, audyt martwych vhostow
(dysk vault portainer hai audiobooks budget tasmota node-red printer router
code-server ap slzb hagc ha-embed — 000/502, sprawdzic zywe/usunac).
- Drugi npm na VPS (outline/joplin/agents tam) — analogiczny wildcard *.okit.pl?
- Sprzatanie starych certow okit.pl w npm (npm-2..48) — wygasna same, kosmetyka.
- 9 decyzji Paperless/Nextcloud (DECYZJE-do-podjecia.md) -> deploy modulow 2-5 KB.

View file

@ -1,63 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-09
links: []
---
# Sesja 2026-07-09 — KB configi (9 decyzji) + wzorzec dzielenia plikow (Nextcloud twierdza + Gokapi)
## Kontekst
Etap 1 filaru dokumentow KB: dokonczenie configow Paperless/Nextcloud wg decyzji +
nowy wzorzec publicznego dzielenia plikow. NIC nie zdeployowane — tylko configi w repo.
## 9 decyzji Paperless/Nextcloud (rozstrzygniete)
1. Host Nextcloud = PIHA (KOREKTA wzgl. Fable, ktora dala SOLARIA). Oskar uzywa NC
aktywnie (telefon, sync, rodzina) -> musi byc always-on. owner_node=piha,
TRUSTED_PROXIES=172.16.0.0/12 (docker bridge, npm+NC dziela host).
2. Backup Paperless = SOLARIA (LAN, nocny document_exporter + rsync, retencja 7/4/6);
offsite jako future-note.
4. Redis brokera = requirepass (haslo po obu stronach serwis/worker).
6. Domeny = kapala.org (mesh): paper.kapala.org, cloud.kapala.org. Wildcard *.kapala.org
JUZ pokrywa oba -> nie trzeba nowych vhostow/DNS (A-record juz -> 100.108.208.3).
8. Nextcloud pin = nextcloud:34-apache (CC sprawdzil live endoflife.date: stable 34
z 2026-06-09). Deploy-time reconfirm.
5. Whoosh fallback-worker: zaakceptowac+obserwowac.
Deploy-time (nie teraz): #3 porty (ss -tlnp), #7 wylaczenie local login po OIDC,
#9 sizing OCR przed batch 70k.
## Wzorzec dzielenia plikow — decyzja architektoniczna
Problem: chce czasem wyslac komus link do pliku na zewnatrz, ale Nextcloud ma
prywatne dokumenty + rodzinne pliki.
Rozwazone: A/A+C (wystawic NC na okit.pl z ograniczeniem sciezek), D (Cloudflare
Tunnel), E (osobny share-serwis). WYBRANE E — najczystsze:
- **Nextcloud = twierdza**: mesh/kapala.org ONLY, zero publicznej powierzchni.
Zasada KB "ingress mesh-only" nietknieta.
- **Gokapi = publiczne dzielenie**: osobny lekki serwis, zaprojektowany do tego
(single-admin upload, wygasanie linkow, E2E).
## Gokapi — config (nowy serwis)
- Host: VPS Hetzner (public na public hoscie, dom nietkniety). owner_node=vps, exposure=public.
- Obraz: f0rc3/gokapi:v2.2.4 (CC zweryfikowal GitHub releases + Docker Hub 2026-07-09,
pinowany nie latest). WAZNE: WebSearch blednie zasugerowal GOKAPI_USERNAME/PASSWORD env —
CC sprawdzil docs/setup.rst: nie ma headless setup, admin przez wizard /setup.
- Domena: share.okit.pl. Storage: lokalny dysk VPS. E2E: ON.
- Bind: TAILSCALE_BIND_IP (nie 0.0.0.0!) — surowy port NIE istnieje na publicznym IP
Hetznera; npm@VPS jedynym wejsciem, siega przez Docker hairpin NAT (wzorzec jak
fleet-prometheus/nextcloud). Defense-in-depth.
- Disk-protection: GOKAPI_MAX_FILESIZE + GOKAPI_MIN_FREE_SPACE (VPS ma malo miejsca).
- Backup note: config/ (config.json + klucz E2E master) backupowac; data/ ulotne (wygasa).
## Stan
Wszystkie configi (Paperless, paperless-worker, Nextcloud, Gokapi) gotowe, zwalidowane
docker compose config, w repo. ZERO deployu.
## TODO deploy (nastepne sesje, osobno, etapami)
- Paperless (modul 2): NFS export PIHA, serwis, OAuth Forgejo, weryfikacja portow ss -tlnp.
- OCR-worker (modul 3): SOLARIA, NFS mount, celery.
- Nextcloud (modul 4): PIHA, OAuth user_oidc.
- Gokapi: VPS — A-record share.okit.pl -> 135.181.153.108, wildcard *.okit.pl na npm@VPS
(token npm-dns01-all-zones, sprawdzic czy VPS-npm juz ma), vhost, wizard /setup.
- okit.pl Faza 2 (z poprzednich sesji): mesh/public reorg, audyt martwych vhostow, drugi npm VPS.

View file

@ -1,66 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-10
links: []
---
# Sesja 2026-07-10 — Deploy 1 Paperless (DZIALA) + swap PIHA + npm-API tool w akcji
## Glowne osiagniecie: Paperless serwis LIVE
paper.kapala.org dziala end-to-end. Pierwszy filar dokumentow KB stoi i przetwarza.
Zweryfikowane: 1 dokument (polisa PDF) przetworzony, OCR wyciagnal 23607 znakow.
## Narzedzie npm-API (zbudowane + uzyte w boju)
- scripts/npm/npm_api.py (Python stdlib, zero-dep): token, list-hosts, list-certs,
set-cert, create-host. Dry-run domyslny, --apply dla realnej zmiany.
- Zweryfikowane na zywo (list-certs PIHA dzialalo). UZYTE do utworzenia vhostu
paper.kapala.org (proxy host #33) — API samo regeneruje conf+reload, koniec z
bulk-SQL+sed z 2026-07-07.
- .env npm musi byc w GLOWNYM repo (nie worktree — usuwany przy merge). Gitignored.
- Backlog: npm@VPS admin :81 publicznie osiagalny (0.0.0.0) — ograniczyc do mesh.
## Swap PIHA 4->8Gi (bufor RAM pod KB)
- RAM ciasny (avail 3.3Gi, immich 1Gi 24/7, prom lokalny 595Mi, npm 300Mi).
- Prometheus: SA DWA i to OK — fleet na VPS (mesh: piha/lustro/vps), lokalny na PIHA
(dom: HA/zigbee/security). Nie duplikat, nie ruszac.
- Swap: ext4 /swapfile, powiekszony fallocate 8G (dphys 200MB nieistotny). W fstab
(reboot-safe, usunieto duplikat wpisu).
- Kierunek dlugoterminowy (backlog): wymiana RPi na maszyne jak CHELSTY -> Proxmox.
## Deploy 1 kroki (dla powtorzenia przy kolejnych serwisach)
1. Pull repo na PIHA (deploy-only master). 2. .env sekrety (openssl rand haslad,
OAuth z Forgejo) — przez nano (heredok rozjezdza dluga linie JSON OIDC!).
3. OAuth app Forgejo (redirect .../accounts/oidc/forgejo/login/callback/).
4. mkdir /opt/homelab/data/paperless/{data,media,consume,export} + chown 1004:1004
(uid oskara=1004, NIE 1000! config zakladal 1000 — rozjazd, naprawione chownem).
5. docker compose up -d (web+db+broker, redis requirepass dziala).
6. vhost przez npm_api.py create-host --cert-id 49 (*.kapala.org).
7. A-record paper.kapala.org -> 100.108.208.3 (Cloudflare DNS-only, mesh).
## LEKCJE (wazne)
- Bootstrap admin (PAPERLESS_ADMIN_USER/PASS) tworzy sie TYLKO przy pierwszym starcie
z pusta baza. Po restartach nie dziala. RESET: docker exec paperless python3
manage.py shell -c "User.objects.get(username='oskar').set_password('...')".
- OIDC pierwszy login: domyslnie Paperless NIE auto-tworzy kont. Dodano do .env:
PAPERLESS_ACCOUNT_ALLOW_SIGNUPS=true, SOCIALACCOUNT_ALLOW_SIGNUPS=true,
SOCIAL_AUTO_SIGNUP=true, ACCOUNT_EMAIL_VERIFICATION=none.
- Username collision: lokalny admin oskar vs OIDC oskar -> "user exists". FIX: zaloguj
lokalnym adminem, potem POLACZ konto przez profil (Edytuj profil -> Podlacz Forgejo,
URL /accounts/oidc/forgejo/login/?process=connect). Connect laczy z istniejacym userem.
- Paperless leci jako root wewnatrz kontenera (USERMAP mapuje zapis na host UID) — OK.
## Stan koncowy
Paperless: 3 kontenery healthy, OCR dziala, OIDC login (haslo LUB Forgejo) dziala.
Config w repo, sekrety w .env gitignored. Storage /opt/homelab/data/paperless.
## TODO nastepne sesje
- Deploy 2: NFS (apt install nfs-kernel-server na PIHA — BRAK teraz) + OCR-worker na
SOLARIA. WAZNE: zrobic PRZED masowym importem (wbudowany worker concurrency=1 wolny).
- Google Takeout: zamowic selektywnie (Drive/dokumenty, nie cale konto), rozpakowac ->
consume. DOPIERO po Deploy 2 (masowy OCR na workerze SOLARIA).
- Deploy 3: Nextcloud (PIHA, always-on). Deploy 4: Gokapi (VPS).
- Drobne: email oskara root@localhost -> prawdziwy; PAPERLESS_DISABLE_REGULAR_LOGIN
po pewnym OIDC.

View file

@ -1,53 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-12
links: []
---
# Sesja 2026-07-12 — Deploy 2: split-host OCR-worker (DZIALA) + decyzja kierunku KB
## Deploy 2 — OCR-worker na SOLARIA przez NFS: DZIALA end-to-end
Najtrudniejsza technicznie czesc KB (wzorzec nieoficjalnie wspierany, GH #3900) — przeszla.
### NFS
- nfs-kernel-server JUZ byl na PIHA (aktywny, export /media/pimain istnial).
- Dodany export: /opt/homelab/data/paperless 192.168.31.70(rw,sync,no_subtree_check,no_root_squash)
— tylko SOLARIA (nie *), UID przechodza 1:1.
- UID: oskar=1004 na PIHA, 1000 na SOLARII (znany tech-debt floty), ALE pliki Paperlessa
sa 1000:1000 (kontener nadpisal nasz chown 1004) — wiec zgodnosc naturalna, bez squash.
- nfs-common na SOLARII byl. Docker-managed NFS volumes (driver local, type nfs) — zero fstab.
### Dwa bugi w configu (znalezione, naprawione, zweryfikowane)
1. **command nie odpalal celery.** Entrypoint obrazu: `if [[ "$1" != "/"* ]]; then exec gosu
paperless python3 manage.py "$@"; else exec "$@"; fi` — argument bez "/" idzie do manage.py
-> "Unknown command: 'celery'". FIX: sciezka absolutna + jawny gosu:
command: /usr/sbin/gosu paperless /usr/local/bin/celery --app paperless worker ...
2. **Brak wspoldzielonego SCRATCH.** Paperless@PIHA stage'uje upload w /tmp/paperless/tmpXXX
i niesie ABSOLUTNA sciezke w payloadzie celery. Worker@SOLARIA mial swoj lokalny /tmp ->
"Cannot consume ...: File not found" (czesc dokumentow padala — te ktore wzial SOLARIA).
FIX: /opt/homelab/data/paperless/scratch przez NFS, mount /tmp/paperless po OBU stronach.
Ta sama zasada co data/media/consume (identyczne sciezki kontenerowe) — scratch zostal pominiety.
### Dowod (test na zywo, CC)
3 PDF do consume/ na PIHA. split-host-test-2.pdf odebrany przez worker@SOLARIA:
"pdftotext exited 0 -> ocrmypdf -> Output file is a PDF/A-2B; ConsumeTaskPlugin completed:
Success. New document id 7 created". Zero "File not found". Wlasnosc pi:pi (1000) na NFS.
Dokumenty testowe posprzatane, produkcyjne 6 nietkniete.
## DECYZJA KIERUNKU (wazna)
Nie lancuch importow, tylko **pionowy plaster**: jeden import probki -> warstwa uzytkowa.
Kolejnosc: (1) import probki zalacznikow z maili -> Paperless (jego OCR+correspondent-detection,
NIE osobny pipeline), (2) MODUL 5: koperta w kb-postgres (Paperless=REFERENCJA doc-id,
Nextcloud=KOPIA) + embeddingi bge-m3 -> pgvector + cross-source link (correspondent <-> nadawca
maila = ta sama encja, DOWOD zasady kb-00 #7), (3) interfejs pytan (RAG) — pierwszy moment
realnej uzytecznosci. Dopiero POTEM dopelniac importy (reszta Takeout, zdjecia, transakcje).
## TODO nastepne
- MODUL 5 (koperta + ingest + embeddingi + cross-source) — kb/phases/kb-m5-documents-ingest.md
- Import probki zalacznikow z maili (kilkaset, nie 70k) — do zbudowania RAG
- Interfejs pytan / RAG — warstwa uzytkowa
- Deploy 3 (Nextcloud), Deploy 4 (Gokapi) — configi gotowe, czekaja
- Google Takeout — zamowic selektywnie, PO zbudowaniu warstwy uzytkowej

View file

@ -1,168 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-15
links: []
---
# Sesja 2026-07-15 — Prometheus cutover Etap 2 (analiza GO) + domknięcie rodziny bugów event-pipeline/checkpoint
## Kontekst
Kontynuacja projektu cutoveru liveności floty na Prometheus (start 2026-06-22,
Etap 0 2026-07-06, Etap 1 shadow-read wdrożony). Równolegle: domknięcie rodziny
bugów event-pipeline/checkpoint zapoczątkowanej fixem `d5139c9` (2026-07-14).
## ZROBIONE
### 1. fix(observer) `d5139c9` — checkpoint po timestampie, nie ścieżce leksykalnej
ROOT CAUSE cichej ~34-dniowej "śmierci" PIHA dla observera: plik
`evt-unknown-<ts>-...` leksykalnie większy niż `evt-piha-<ts>-...` ("p" < "u")
zatruł checkpoint węzła — każdy nowy event PIHA uznawany za starszy i pomijany
na zawsze. Fix: checkpoint = ostatnio przetworzony TIMESTAMP (int epoch),
parsowany z nazwy `evt-<node>-<ts>-...` (fallback mtime, **nigdy 0** dla
istniejącego pliku — 0 = leksykalne "starszy niż checkpoint" = dokładnie ten
poison), migracja starych path-checkpointów przy starcie. Zdeployowany na VPS
(observer `StartedAt` 07-14). Zweryfikowany dziś jako kompletny i zdeployowany.
Szczegóły: `kb/phases/backlog.md` (sekcja "Bug: checkpoint observera po ścieżce
leksykalnej").
### 2. docs(infra) analiza Etapu 2 shadow-run (Fable, `8fec62d`)
`kb/phases/prometheus-cutover-etap2.md` — 165 mismatchy
`SHADOW_LIVENESS_MISMATCH` solaria/lustro w dobie 2026-07-14 WYJAŚNIONE: dwa
nocne wyłączenia węzłów (lustro 21:30 UTC — regularny power-off, solaria
21:34 UTC). Wzorzec `event=fresh prom=down` to **nie** "żywy węzeł niewidziany
przez Prometheusa", tylko **martwy węzeł wykryty przez Prometheus w ≤45 s**,
podczas gdy tor eventowy potrzebował pełnych 600 s TTL (`last_seen_age` rósł
monotonicznie 29→597 s; mismatche ustają dokładnie na granicy TTL-dead).
**Werdykt**: żaden z 165 mismatchy nie jest błędem Prometheusa — Prometheus
był w każdym przypadku szybszy i miał rację. **Rekomendacja Etap 3: GO**:
- vps, piha (always-on) — bez zastrzeżeń, 84 h ciągłego `up==1`, 0 mismatchy.
- solaria, lustro (intermittent) — `prom=down` przy wyłączeniu jest prawdziwy,
NIE dyskwalifikuje z cutoveru; planowe okna off → wątek anomaly detection
(osobny, niezależny, nieblokujący).
- Mapping: `prom-last_seen = timestamp(up)` wpuszczone w istniejące
`compute_liveness` (potwierdzenie rekomendacji z recon).
Ograniczenie dowodowe: twarde logi docker pokrywają tylko ~20 h (recreate
kontenera 07-14 16:30 skasował wcześniejsze) — wnioski podparte pośrednio
84 h historii `up{}` + eventami przejść od 07-11; formalne "GO" czeka na
~7 dni czystych danych z trwałego logu (pkt 3 niżej), cel ~2026-07-20.
### 3. feat(observer) `9e7ed3e` — trwały log SHADOW_LIVENESS_MISMATCH
Zapis do `/opt/homelab/logs/observer/shadow-liveness.log`
(`RotatingFileHandler` 5MB×5, osobny logger `observer.shadow`,
`propagate=False`, mismatch idzie i do stdout, i do pliku) — przeżywa docker
recreate (recreate 07-14 zjadł materiał dowodowy analizy z pkt 2, stąd
konieczność tego fixu). Świadomie **bez** nowego mountu (footgun: Docker
tworzy brakujący bind-source jako root, uid 1000 nie miałby prawa zapisu) —
wykorzystany istniejący mount `/opt/homelab`. Fail-safe: błąd zapisu do pliku
nie wywala observera. Zdeployowany, potwierdzony (test lustro niżej wylądował
w pliku, 162 KB).
### 4. Test kontrolowany event=dead / prom=up (lustro/pimirror2, 100.99.85.73)
Zatrzymano node-agenta na żywym węźle (host up, eventy przestają płynąć) →
observer po TTL uznaje węzeł za dead → shadow zalogował
`node=lustro event=dead prom=up` DO TRWAŁEGO PLIKU. Waliduje drugi kierunek
rozbieżności (nie wystąpił naturalnie w oknie analizy z pkt 2) razem z
działaniem trwałego logu na realnym zdarzeniu. Node-agent przywrócony po
teście. Dostęp do lustro: tylko `pi@` (hasło) — klucz `oskar` z SOLARII
nieautoryzowany na tym hoście; hostname faktyczny `pimirror2`.
### 5. fix(ha-diag) `f2ba81b` — node_name z env + fail-fast na "unknown"
ŹRÓDŁO trucizny checkpointu z pkt 1: `config.py`
`Field(default="unknown", validate_default=True)` + validator odrzucający
`""`/`"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** (agent
`Up healthy` po rebuild — `NODE_NAME=piha` dochodzi do procesu). Pliki
`evt-unknown-*` na VPS/PIHA: 0 (potwierdzone).
### 6. docs(backlog) `c858dbc` — bug deploy-node.sh brak `--build`
Deploy raportuje "OK", ale dla serwisów z Dockerfile bez zmiany compose/env
kontener zostaje na starym obrazie (Docker cache'uje po tagu, nie po
zawartości `src/`) — cicha rozbieżność repo↔runtime. Ugryzło dwa razy:
fleet-prometheus (config nie wchodził bez `--force-recreate`) i ha-diag-agent
dziś (fix z pkt 5 był w repo, `Running` zamiast rebuild — wymagał ręcznego
`docker compose up -d --build --force-recreate`). Fix pozostaje TODO w
backlogu.
## STAN CUTOVERU
- Etap 0 (dowód bojowy Prometheus→watchdog→Telegram): ✅ zamknięty (07-06).
- Etap 1 (shadow-read, observer czyta oba źródła, nic nie przełącza): ✅
wdrożony.
- Etap 2 (parallel-run + analiza zgodności): **TRWA**. Dane po fixach z pkt 1
i 3 czyste; analiza z pkt 2 gotowa z rekomendacją GO, ale formalnie czeka na
dłuższe okno trwałego logu (cel ~2026-07-20).
- Etap 3 (przełączenie per-węzeł, `PROM_LIVENESS_NODES`) — czeka na domknięcie
Etapu 2.
## NASTĘPNE (po ~2026-07-20)
- Etap 3 per-node: najpierw `solaria,lustro`, potem `vps,piha` (kolejność z
recon, minimalizacja blast-radius).
- Watchdog na sam Prometheus (SPOF po cutoverze — dziś nikt nie alarmuje o
jego śmierci; `mem_limit 512m` + `oom_score_adj 200` czynią go ubijalnym
przed control-plane).
- `deploy-node.sh --build` (pkt 6 / backlog `c858dbc`).
- Observer powinien odrzucać/kwarantannować event, którego `node` w treści ≠
katalog docelowy (druga warstwa obrony po fixie z pkt 5).
## Wnioski
- **Rodzina bugów uid/gid + checkpoint + node_name to jeden motyw:
"ścieżka/nazwa pliku ≠ tożsamość".** Checkpoint leksykalny po ścieżce
(pkt 1), plik `evt-unknown-*` lądujący w cudzym katalogu (pkt 5), stary
tech-debt uid/gid per-host (backlog 2026-07-10) — wszystkie trzy to ten sam
wzorzec: system ufa POŁOŻENIU/NAZWIE zamiast jawnie zweryfikowanej
tożsamości, i cichnie zamiast fail-fastować, gdy te się rozjadą. Fix z
pkt 5 domyka jeden konkretny wektor (`NODE_NAME` nieustawione → fail-fast),
ale ogólny wzorzec (observer nie waliduje `node` w evencie względem
katalogu) zostaje jako TODO.
- **`deploy-node.sh` bez `--build` to osobny, ortogonalny motyw "cicha
rozbieżność deploy↔runtime"** — nie dotyczy tylko configów (backlog
2026-06-26), ale też kodu; ugryzło dwa razy w jednym dniu dzisiejszej sesji.
- **Trwały log (pkt 3) był koniecznym fixem, nie kosmetyką** — recreate
kontenera observera 07-14 skasował materiał dowodowy w trakcie samej analizy
Etapu 2; bez trwałego logu każdy kolejny recreate zerowałby okno danych i
cofał kryterium "≥7 dni czystych danych" do zera.
- **Równoległe sesje CC dopisują commity do mastera w trakcie pracy**
commity `f57a01a`/`38cb204` (ollama/SOLARIA) wylądowały między `f2ba81b` i
`9e7ed3e` w historii, mimo że nie są częścią tego wątku. Nie spowodowało
konfliktu tym razem (worktree bazował na commicie sprzed rozjazdu, fast-
forward czysty), ale potwierdza zasadę z CLAUDE.md: przed push zawsze
ff-check + rebase, nie zakładać, że `master` stoi w miejscu.
- **Terminal gubił wklejki blokami** podczas sesji (drobna operacyjna
uciążliwość, bez wpływu na wynik) — odnotowane jako obserwacja, nie bug do
śledzenia.
Hashe sesji: `d5139c9`, `8fec62d` (analiza Fable), `9e7ed3e`, `f2ba81b`,
`c858dbc`.
---
## Wątek KB — moduł 5 faza 2: kroki 3-6 domknięte (sesja 14-15.07)
**Krok 3 — token API Paperless:** konto dedykowane `kb-ingest` (superuser), token w `/opt/homelab/kb/.env` na PIHA (600). Token rotowany w trakcie (wyciek do transkryptu wykryty przez samego CC; stary unieważniony, nowy zweryfikowany bez wypisania). Base URL: `http://192.168.31.5:8210`.
**Krok 4 — gmail-header-backfill:** finalnie **225 030/225 030** kopert z headers entities. Po drodze: pełny run zostawił 5008 braków → diagnoza: 9 realnych parse-errorów + **4999 zgubionych cicho** przez `json.dumps` poza per-wierszowym try (TypeError na 8-bit `Date:`, zabity cały slice offset 70000). Fix: compat32 fallback, `missing_file` counter, bilans statystyk jako inwariant (`scanned = suma wyników`, niezerowy exit przy rozjeździe). Re-run braków odzyskał wszystko. Bonus: audyt `gmail-bulk-import` (empiryczne repro) znalazł tę samą klasę bomb → hardening wdrożony (`_sanitize` przeniesiony do `packages/kb-mail`, per-wiadomościowy try, odporny `_flush`, testy regresyjne). Lekcja systemowa: długie joby na nodach zawsze z logiem do pliku (`> run.log 2>&1`), nie goły tmux — utrata logów tmuxa kosztowała godzinę diagnozy.
**Krok 5 — adapter Paperless→koperta:** 186 kopert `source='paperless'` (idempotencja: re-run 0 inserted / 186 already_in_db), **cross-source join działa: 180/186 z `source_mail`** + przykład end-to-end (paperless:119 ↔ koperta mailowa via scoresheets.pdf) — krok 8 planu de facto zaliczony. 6 dokumentów bez joinu do obejrzenia przy kroku 7 (świadomość, nie fix).
**Krok 6 — chunk+embed:** **2683/2684 chunków** w `document_chunk` (160 dokumentów; 1 patologiczny dot-leader chunk odrzucony przez context window Ollamy — known limitation). Timing CPU: **0.79s/chunk, ~35 min pilot** — twardy wniosek: pełny mail-corpus wymaga GPU i/lub batchowanych wywołań Ollamy. Multi-agent code review (5 kątów) złapał 3 realne bugi przed produkcyjnym runem (infinite-loop przy overlap≥size, izolacja błędów insert_chunk, ciche liczenie ON CONFLICT jako insertów). Follow-up ważny: `UNIQUE(envelope_id, chunk_index)` bez `model` — drugi model embeddingów cicho by się no-opował (schema change przy fazie 3).
**Infra — Ollama na SOLARII deklaratywnie:** brakujący wpis w `hosts/solaria/services.yaml` był root-cause "declared but not running". Cutover: modele (36G) przeniesione do `/opt/homelab/data/ollama`, systemd disabled, bind 127.0.0.1 + Tailscale IP. Odkrycia: brak nvidia-container-toolkit (doinstalowany, prerequisite do runbooka) oraz **brak sterownika NVIDII na hoście w ogóle** — "GPU-backed" w manifestach było fikcją, wszystko zawsze szło na CPU. Sterownik = backlog, blokuje fazę mailową embeddingów. Reachability z PIHA (llm-gateway) potwierdzona.
**Decyzja architektoniczna fazy 3:** moduł syntezy jako wiki-kompilat wg wzorca Karpathy'ego llm-wiki (gist 442a6bf...): RAG/pgvector zostaje warstwą dowodową, nad nim LLM-utrzymywana wiki markdownów (strony-encje, cross-linki, lint), każda strona linkuje envelope_id/chunki źródłowe; utrzymanie przez CC/API (lokalny 30B CPU-only za słaby). Szczegóły w pamięci Claude + do recon-planu fazy 3.
**Następne wejście:** krok 7 — pilot retrieval (sesja ręczna, SQL `ORDER BY embedding <=> query`, ocena jakości, kalibracja chunking 600/150). Ostatnia bramka fazy 2.

View file

@ -1,98 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-16
links: []
---
---
## Wątek KB — krok 7 (pilot retrieval) + GPU: FAZA 2 MODUŁU 5 DOMKNIĘTA
**GPU przywrócone**: sekcja deploy w compose odkomentowana (merge `ollama-gpu-restore`), recreate, bge-m3 na 100% GPU. Benchmark: **207ms/embed vs 790ms CPU (~3.8× sekwencyjnie)** — overhead HTTP dominuje przy pojedynczych requestach, batching pozostaje dźwignią (backlog, przed fazą mailową). Zagadka znikającego kontenera po reboocie 15.07: jednorazowa, po boocie 16.07 wszystko wstało samo.
**Krok 7 — pilot retrieval (2683 chunki, zapytania przez bge-m3 + `<=>` cosine):**
- Kalibracja progów: **<0.45 trafienie, 0.450.55 szara strefa, >0.55 brak odpowiedzi** (negatywna kontrola "sernik" = 0.62, czysta separacja)
- Cross-lingual potwierdzony: angielskie zapytanie o FLL scoring lepsze niż polskie, wyciąga scoresheet mimo mojibake
- Najlepszy wynik: "innovation project scoring" → 0.387, top-5 spójnie z właściwego dokumentu
- **Chunking 600/150 zatwierdzony bez zmian**
- Findings do fazy 3 (nieblokujące): filtr chunków z binarnym OCR-szumem (przed kompilacją wiki!), deduplikacja dokumentów (paperless:14 ≡ 74), jakość OCR scoresheets
**Faza 2 modułu 5: kroki 18 komplet.** Baza: 225 216 kopert (gmail+paperless, cross-source), 2683 chunki z embeddingami, retrieval zweryfikowany. Następne: faza 3 — recon-plan (streszczenia+tagi jako pierwszy krok kompilacji, docelowo wiki-kompilat wg Karpathy'ego; Claude ma przygotować szkic sekcji wiki do recon-planu).
---
## Wątek control-plane — druga połowa dnia: "czemu supervisor nie generuje akcji naprawczych"
**Punkt wyjścia**: pytanie operatora o pustą kolejkę akcji odsłoniło wielowarstwową awarię warstwy decyzyjnej control-plane. Cztery kolejne fixy, każdy odkrywał następną warstwę:
### 1. RECON lustro shipping (Fable, commit `542bba4`)
`kb/audits/lustro-shipping-2026-07-16.md` — 1507 mismatchy `lustro event=dead prom=up` w trwałym logu WYJAŚNIONE: (a) wczorajszy kontrolowany test (node-agent stał 3h20m, nie 15 min jak zakładano) + (b) poranny boot-race 56s. **Werdykt: shipping lustro działa, ZERO recurring problemu.** Prometheus 0 pomyłek w 48h — wzmacnia rekomendację GO dla Etapu 3 cutoveru.
Znaleziska poboczne: lustro biega na obrazie sprzed 5 tyg (deploy-node bez `--build` — patrz fix #2 niżej); fake-hwclock boot-race (RPi bez RTC — pierwszy event po boocie ma stary stempel, dropnięty przez timestamp checkpoint).
### 2. FIX `deploy-node.sh --build` (commit `77defff`)
`deploy-node.sh` robił `docker compose up -d` BEZ `--build` → dla serwisów z Dockerfile zmiany kodu NIE wchodziły (cicha rozbieżność repo↔runtime — backlog item z 2026-07-15, dziś naprawiony). Ugryzło 3×: fleet-prometheus, ha-diag, lustro.
Fix: warunkowy `--build` gdy serwis ma Dockerfile (`test -f services/<svc>/Dockerfile`), prebuilt bez `--build`. Zweryfikowany w boju na PIHA (6 serwisów: node-agent/ha-diag/brain-watchdog/llm-gateway → Building; vikunja/kb-postgres → prebuilt).
Edge case zgłoszony jako follow-up: agent-system ma build w podkatalogach bez top-level Dockerfile (niezarejestrowany w tej detekcji).
### 3. FIX supervisor resilience (commit `409b583`)
Supervisor był **funkcjonalnie zamrożony ~24h** — kontener `healthy`, ale pętla nie tikała, zero logów. Root cause zweryfikowany na `/proc` (proces `State:S hrtimer_nanosleep`, NIE deadlock): `reconcile()` robi `glob` po `EVENTS_DIR` co cykl, a to 358k plików → cykl przekracza timeout → nigdy się nie kończy. Plus logi na DEBUG = niewidoczne.
Fix: 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 (env `SUPERVISOR_TICK_LOG_EVERY`) na INFO; healthcheck sprawdza świeżość heartbeat (nie samo że proces żyje). Zdeployowany. Po deployu: pętla tika (cycle #340→#480), cykl #1 timeoutował (ERROR "did not complete within 90s") ale pętla szła dalej = odporność działa.
### 4. FIX event flood + retencja + cleanup (commit `dff76ec`) — sedno problemu
Root cause braku akcji: `EVENTS_DIR` = 358k plików, 91% to `service_healthy` (szum "serwis zdrowy" emitowany co cykl per serwis).
**Krytyczne odkrycie**: mechanizm retencji `_cleanup_control_plane_fs()` był martwy od wczorajszego fixu checkpointu (`d5139c9`, 2026-07-15) — kod porównywał `str(ścieżka) <= checkpoint_int``TypeError` cicho łapany przez `except` → retencja przestała działać → backlog rósł bez ograniczenia. Naprawiając checkpoint wczoraj, złamaliśmy retencję, która na nim polegała.
Fix:
- (a) node-agent emituje `service_healthy` TYLKO na transition unhealthy→healthy (nie co cykl) — funkcja zachowana: `observer.process_event` nadal konsumuje `service_healthy` do `services.json[key].status=healthy` + rozwiązuje incydent, więc nie wycięte, tylko ograniczone do transition;
- (b) retencja naprawiona epoch-do-epoch (ta sama logika co `_checkpoint_ts_from_value`);
- (c) skrypt `scripts/maintenance/cleanup_event_backlog.py` (dry-run + `--apply`, `min_age` 3600s, kasuje tylko `service_healthy`/`node_health` starsze niż checkpoint, zachowuje `healthcheck_failed`/incydenty/wszystkie sygnały).
150 testów pass. Zdeployowany node-agent na PIHA+VPS. Cleanup wykonany: usunięto 272 232 pliki, backlog 358k→12,7k. Ghost dir `be17cb6eb0f6` usunięty. **Wynik**: reconcile supervisora przestał timeoutować (0 "did not complete" w 5 min), glob 12,7k, mózg odetkany.
### Stan końcowy
Control-plane supervisor odblokowany (tika, reconcile się kończy, retencja działa automatycznie co cykl). Ale kolejka akcji nadal pusta — bo (a) `shadow_mode=True` downgrade'uje HA `container_restart` do `alert_only`, (b) wpisy `error` w topologii to ghost hash-prefixed w world_state observera (NIE żywe kontenery — `docker ps -a exited=0` na VPS), observer nie czyści wpisów po zniknięciu kontenerów.
### Lekcje
- **Cichy efekt uboczny migracji typów.** Fix checkpointu `d5139c9` (ścieżka→epoch int) cicho zepsuł retencję `_cleanup_control_plane_fs()`, bo ta porównywała `str <= int` i łapała `TypeError` w szerokim `except`. Migracja typu pola musi audytować WSZYSTKICH konsumentów tego pola, nie tylko miejsce zmiany — szeroki `except` maskuje dokładnie tę klasę regresji.
- **"Healthy kontener ≠ tikająca pętla."** Docker healthcheck sprawdzający tylko "czy proces żyje" nie łapie pętli zawieszonej na blokującym wywołaniu bez timeoutu. Healthcheck musi sprawdzać świeżość heartbeat/ostatniego cyklu, nie tylko czy proces odpowiada.
- **`service_healthy` jako plik-event per cykl to antywzorzec.** Stan ("serwis jest zdrowy") emitowany jako zdarzenie na każdym cyklu miesza dwa różne pojęcia (stan vs zdarzenie) i skaluje się liniowo z (liczba serwisów × liczba cykli) — dokładnie to, co wygenerowało 358k plików. Emitować tylko na transition.
- **Równoległe sesje = ciągłe rozjazdy mastera.** Kilka CC działających jednocześnie na różnych worktree podnosi ryzyko push divergence — dyscyplina "zatrzymaj się i zgłoś" przy konflikcie jest tańsza niż force-push.
### Otwarte (następne sesje, osobne świadome tematy)
- Ghost-wpisy w world_state observera (hash-prefixed `error` w topologii mimo 0 exited kontenerów — observer nie prune'uje zniknionych kontenerów ze stanu). To trzyma `System Status: ERROR` fałszywie.
- `shadow_mode` → remediacja: decyzja czy włączyć auto-restart padłych kontenerów (architektura, guardraile, cooldowny).
- Czemu supervisor nie generuje akcji redeploy mimo widocznych realnych error (elasticsearch/diskover na piha, ollama solaria) — drift→action nie domyka się.
- gokapi `.env` not found (deploy-node VPS rzuca błąd na gokapi — brakujący `.env`).
- Etap 3 cutover per-node (dane gotowe, recon GO).
---
## Wątek KB — faza 3, krok 2 (migracja 004 + pilot streszczeń): W TOKU
**Migracja 004 (document_summary)** zastosowana na kb-postgres@PIHA: summary, tags JSONB, model, embedding+HNSW, UNIQUE z model (pozwala trzymać oba tory pilota jednocześnie).
**Pilot dwutorowy (decyzja D3 planu):**
- Tor lokalny (gemma3:12b, GPU): 155/157 streszczeń + embeddingi. Po drodze **realny bug**: Ollama domyślnie ucina kontekst do ~2048 tok niezależnie od advertised 128k modelu — 71/157 dok. (45%) miało obcięte streszczenia (np. paperless:12 opisywał RODO zamiast treści AutoCasco, bo widział tylko pierwsze ~8k znaków). Naprawione: jawny `num_ctx` per wywołanie skalowany do długości treści + regression-guard w testach. Cały tor przeliczony od zera po fixie: 27 min na 157 dokumentów, zero CPU-offloadu (48/49 warstw GPU przez cały run).
- Tor referencyjny (Claude Haiku 4.5, API): 157/157 streszczeń + embeddingi, koszt ~1.4 USD zgodny z szacunkiem. Klucz API przez plik poza repo, nigdy nie trafił do transkryptu, usunięty po użyciu.
**Stan:** commit `33f9944` na `task/kb-f3-summaries` (niezmergowany — worktree żyje, dokończenie w następnej sesji). Migracja + oba tory + fix num_ctx + testy gotowe.
**Do zrobienia następnym razem (ten sam worktree):** rebase na master (sąsiad wjechał w międzyczasie) → porównanie A/B ~15 dokumentów (Haiku vs gemma3, ocena przez Oskara) → weryfikacja końcowa (bilans, sanity SQL, retrieval po embeddingu streszczenia) → testy+commit+push → merge → decyzja o modelu dla fazy mailowej.
**Lekcja do zapamiętania:** każde wywołanie generatywne przez Ollamę musi jawnie ustawiać `num_ctx` — domyślna wartość runtime (~2048) cicho ucina długie dokumenty niezależnie od deklarowanego rozmiaru kontekstu modelu. Dotyczy też przyszłej syntezy w fazie 5.

View file

@ -1,33 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-20
links: []
---
# Sesja 2026-07-17/18 — KB faza 3: kroki 2-5 DOMKNIĘTE
## Krok 2 — migracja 004 + pilot streszczeń A/B (17.07)
- `document_summary` (UNIQUE z model od razu) na żywej bazie; job summarize (map-reduce >200k znaków, słownik tagów kontrolowany, izolacja błędów).
- **Bug krytyczny znaleziony w pilocie**: brak `options.num_ctx` → gemma3:12b cicho ucinała 45% dokumentów do ~2048 tok (zweryfikowane prompt_eval_count). Fix + pełny re-run toru lokalnego. Dwutorowość D3 zwróciła się natychmiast.
- Wynik: haiku-4-5 157/157 (0 JSON-faili, 100% dyscypliny słownika), gemma3 155/157 (2 JSON-faile, dryf tagów — na FLL komplet tagów z kosmosu).
- **Decyzja D3 (ocena A/B Oskara, 18.07... 17.07)**: tor kompilacyjny = claude-haiku-4-5; gemma3 w odwodzie; model fazy mailowej — przy jej reconie (prywatność/koszt).
## Krok 3 — kaskada summary→chunk (17.07)
- `retrieval.py`: flat_query (baseline) + cascade_query (stage1 summary → stage2 chunk, jeden embed/zapytanie). Eval przepisany do wersjonowanego `eval/queries.yaml` + `retrieval_eval.py`.
- **Bramka PASS at N=10, k=5** (N-sweep {1..20}: N=5 zmierzona podłoga, N=10 z 2× marginesem). Kaskada nie poprawia rankingu na 186 dok (top-1 identyczne z flat przy N≥5) — zgodnie z przewidywaniem planu: to test architektury pod 225k, nie optymalizacja pilotowa. Kaskada = domyślna ścieżka kb-query.
## Kroki 4-5 — cykliczny ingest + timer (17-18.07)
- Wrapper 4-etapowy (adapter→chunk_embed→summarize→embed-summaries; 4. etap dołożony po pytaniu CC — bez niego nowe summaries niewidzialne dla kaskady). Tolerancja Ollama-offline, metryki textfile (.prom), reguły alertów fleet-prometheus.
- **node_exporter@PIHA wciągnięty do GitOps** (owner_node: per-host — domyka pół paczki B inwentaryzacji).
- Test na żywo: dry-run wykrył **5 dokumentów pominiętych przy imporcie 13.07** (race z OCR — Paperless miał 191, envelope 186); --apply przemieliło je end-to-end (82 chunki, 2 junk-flagged przez nowy filtr, 5 summaries), drugi run w pełni idempotentny. 202/202 testów.
- Deploy: timer na PIHA enabled (03:30 daily), venv /opt/homelab/kb/venv, KB_DSN+ANTHROPIC_API_KEY w .env; trigger ręczny: exit 0, liczniki 0. Incydenty po drodze: sekwencja instalacyjna omyłkowo odpalona też na SOLARII (posprzątane: timer disabled, venv/DSN usunięte); deploy-node.sh na SOLARII rozebrał node-agenta przy ghost-recreate (dokończony, agent wstał).
## Stan bazy: 191 kopert paperless / 225 030 gmail; 2765 chunków (2545+82 aktywne...); 162 summaries haiku. KB samo-aktualizująca się.
## Otwarte
- **Rotacja ANTHROPIC_API_KEY** — klucz świecił w transkrypcie 17.07; Oskar świadomie odłożył (limit $200/mies. ogranicza ryzyko). Zrobić przy okazji.
- **Wiki proof-of-concept** — ostatni element fazy 3 (repo kb-wiki, konwencje, 3-5 stron przez CC). Świeża sesja.
- Follow-upy CC: stability-agent owner_node single→per-host (druga połowa paczki B).

View file

@ -1,30 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# Sesja 2026-07-17…21 — wątek KB: faza 3 kroki 3-5 DONE (+ incydenty)
**Krok 3 — kaskada summary→chunk:** bramka **PASS at N=10, k=5** (N-sweep {1..20}: N=5 to zmierzona podłoga, N=10 niesie 2× margines); kaskada nie degraduje niczego, na 186 dok nie poprawia (test architektury pod skalę mailową, zgodnie z przewidywaniem planu); koszt +1 SQL, zero dodatkowych embedów. cascade_query = domyślna ścieżka kb-query; flat_query zostaje jako baseline. Eval przepisany do wersjonowanego eval/queries.yaml + retrieval_eval.py.
**Krok 2 finał — werdykt A/B (D3 rozstrzygnięta):** kompilacja = **claude-haiku-4-5** (157/157, zero JSON-faili, 100% dyscypliny słownika tagów, lepsza hierarchia na długich dokumentach); gemma3:12b w odwodzie (tor lokalny w bazie, job wspiera oba backendy); model fazy mailowej odłożony do jej reconu (privacy/cost flag). Bug num_ctx (gemma cicho ucinała 45% dokumentów do ~2048 tok) znaleziony i naprawiony w pilocie — dwutorowość D3 zwróciła się natychmiast.
**Krok 5 — cykliczny ingest LIVE:** systemd-timer kb-ingest na PIHA (03:30), wrapper 4-etapowy (adapter→chunk_embed→summarize→embed-summaries; 4. etap dodany po pytaniu CC — bez niego nowe summaries byłyby niewidzialne dla kaskady), tolerancja Ollama-offline, metryki przez textfile→node_exporter→fleet-prometheus (reguły KbIngestStale, KbEmbedBacklogGrowing). Test live złapał 5 realnych dokumentów pominiętych przy imporcie 07-13 (race z OCR) — pipeline domknął zaległość: 5 docs → 82 chunki (2 junk-flagged przez nowy filtr) → 5 summaries, drugi run w pełni idempotentny. Baza: 191 kopert paperless. Bonus: node_exporter PIHA wciągnięty do GitOps (owner_node: per-host — pół pozycji paczki B).
**Incydenty sesji:** (1) deploy-node.sh odpalony omyłkowo na SOLARII rozebrał node-agenta (No such container przy Recreate) — dokończony deploy naprawił; (2) stary out-of-band node-exporter na PIHA trzymał :9100 → nowy GitOps-owy kręcił się w restart-loopie ~4h niezauważony (Prometheus scrape'ował starego, up=1, brak alertu — luka klasy "podmiana bytu pod tym samym portem") — stary ubity, cutover czysty, 8 metryk kb_ingest serwowanych; (3) klucz Anthropic API zapisany na stałe w /opt/homelab/kb/.env (timer go używa) — rotowany po ekspozycji w transkrypcie.
**Następne:** wiki proof-of-concept (ostatni element fazy 3; repo kb-wiki + konwencje + 3-5 stron ręcznie sesją CC) → faza 4 (kb-query+UI) wg roadmapy.
---
## Wiki proof-of-concept — FAZA 3 MODUŁU 5 DOMKNIĘTA W CAŁOŚCI
Repo kb-wiki na Forgejo (osobne, D7): konwencje (_meta/conventions.md, frontmatter z sources[].chunks), 5 stron-encji (pzu, warta, ubezpieczenie-auto-porównanie, fll-2025-26, wspólnota-targowa-2a) — 73 przypisy inline [^envelope_id#chunk], 86 wpisów sources, wszystkie automatycznie zweryfikowane przeciwko bazie. Pierwszy lint: 1 sierota (fll), lista brakujących pojęć = kandydaci na strony. Ocena operatora: na pierwszy rzut oka zgodne (głębsza weryfikacja merytoryczna = użycie w praktyce).
Lekcje do automatyzacji kompilacji (faza 5): (1) streszczenia bywają nieprecyzyjne na gęstych dokumentach prawnych (summary pomyliło próg 300 zł) — kompilator MUSI cross-checkować z chunkami, nie ufać summary; (2) mojibake ogranicza weryfikowalność liczb — oznaczać pochodzenie; (3) podobne encje wymagają rozróżnienia (dwie wspólnoty, jeden właściciel); (4) taksonomia "duplikacji": binarny duplikat vs wersje produktu (OWU 2023/2026) vs draft-vs-przyjęta uchwała — każde inne traktowanie. Warsztatowo: kaskada do odkrywania, SQL po envelope_id do cytowania — kompilator potrzebuje obu trybów. Incydent po drodze: ollama na SOLARII odłączona od sieci dockerowej (infra glitch, naprawione restartem) — backlog: konsument healthcheck.sh Ollamy w monitoringu.
**Faza 3: kroki 1-5 + wiki-proof = komplet.** Następne: FAZA 4 — kb-query (FastAPI na PIHA, cascade_query jako silnik, aktywny fallback embed) + UI kb.kapala.org.

View file

@ -1,54 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# 2026-07-22 — HA: incydent dwóch mózgów, cutover ken, archiwum legacy
## Odkrycie
Recon pod projekt configs-as-code ujawnił, że repo wskazywało złą maszynę jako
"ken": kontener `homeassistant5` na piha to instancja sprzed migracji (HA
2026.4.3, location_name "KEN", konta całej rodziny), a prawdziwy dom to HAOS
na dedykowanym RPi4, LAN 192.168.31.7 (potwierdzone: observer :4357, HACS,
118 automatyzacji, ingress ha.kapala.org).
Obie instancje działały RÓWNOLEGLE i obie były podpięte do MQTT na piha.
Dowody dublowania triggerów (last_triggered z restore_state obu instancji):
`mirror_on` 04:30 na obu, `gniazdka_w_lazience_on` 05:00 na obu,
`turn_on_led_nad_blatem_1` 12:53 na obu z tego samego fizycznego przycisku.
Wyjaśnia obserwowane od dawna anomalie ("samo się przełącza", kaprysy
przycisków). Stan trwał prawdopodobnie od migracji domu na RPi4.
Fałszywa hipoteza po drodze (odrzucona przez weryfikację): "kontener to
martwa pozostałość" — obalona przez świeże last_triggered i mtime
automations.yaml. Verify-before-fix uratował przed ślepym stopem, który
wyłączyłby m.in. automatyzacje przycisków i harmonogramy łazienki.
## Wykonane
1. `task/ha-cutover-instances` (77d55ca): instances.yaml — ken = 31.7/api,
ken-legacy = kontener piha/docker-exec (status: archived); DESIGN.md
sekcja "Incident log"; README archiwum; backlog: przepięcie
ha-diag-agent, procedura wygaszenia, adapter api.
2. `task/ha-import-legacy` (970b8cc, po rebase na master): pełny import
archiwalny ken-legacy — 60 automatyzacji, 5 scen, 1 skrypt, blueprinty,
dashboardy, rejestry (85 plików). Idempotencja potwierdzona, gitignore
szczelny (grep pod sekrety czysty), round-trip tagów HA OK.
3. `docker stop homeassistant5` na piha (BEZ rm) — dom przeszedł na jeden
mózg. Rollback: `docker start homeassistant5`.
## Otwarte / obserwacja
- Do 2026-07-29: obserwacja, czy nic w domu nie polega na legacy. Braki →
logika w `services/home-assistant/config/ken-legacy/automations/`,
odtwarzanie wyłącznie przez repo na 31.7.
- ha-diag-agent na piha celuje w martwy localhost:8123 — spodziewany alert;
przepięcie na 31.7 w backlogu (wymaga tokenu diag_agent na 31.7).
- Adapter api w import.sh dla ken + user deploy_agent i token na 31.7 —
pierwszy task następnej sesji.
- Po 2026-07-29: decyzja o `docker rm` i sprzątnięciu configu na piha.

View file

@ -1,18 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-22
links: []
---
# Sesja 2026-07-22 — KB faza 4: recon + kroki 1-2 (kb-query LIVE na PIHA)
**Recon+plan fazy 4** (05-faza4-plan.md, 603 linie): D1 wydzielenie packages/kb-retrieval (documents-ingest ciągnie anthropic+CLI — nie do obrazu serwisu); D2 fallback embed z pełną maszyną stanów, ale gate'owany kalibracją RAM na żywym PIHA (audyt nieaktualny, ~3.8Gi zajęte — plan daje kryteria i alternatywę: jawna degradacja 503 zamiast łamania inwariantu modelu); OIDC wbudowane w apkę (authlib, wzorzec repo — nigdy forward-auth); gmail: envelope_id już JEST Message-ID.
**Kroki 1-2 wykonane:** kb-retrieval wydzielone (retrieval_eval byte-identical przed/po — diff JSON, nie tabelka); services/kb-query: FastAPI /search+/healthz, twardy inwariant EMBED_MODEL (zły model = crash-loop kontenera, udowodnione na żywo), linki Paperless zweryfikowane curlem, 422 na walidacji; CC znalazł bug w planie (sketch sprawdzał document_summary.model=LLM piszący, embedder siedzi w embedding_model — poprawione w kodzie, plan-doc do korekty); fix deploy.sh gate (compose build zamiast raw docker build — repo-root context dla COPY packages/). 245 testów. Deploy na PIHA: /search zwraca kaskadę z liczbami zgodnymi z eval-setem (dist 0.3418 na PZU) — **KB PO RAZ PIERWSZY ODPOWIADA PRZEZ HTTP**.
**Incydenty Ollama@SOLARIA — wzorzec, nie pech (3 w tydzień):** znikający kontener po reboocie (15.07), network-detach (21.07), bind-race przy starcie (22.07: próba bindu 100.100.231.104:11434 przed wstaniem tailscale0, Exit 128, restart:unless-stopped nie ratuje; docker start nie leczy network-detach — potrzebne down/up). BACKLOG: task ollama-solaria-start-race — opcje: systemd dependency na tailscaled / ip_nonlocal_bind / publish 0.0.0.0+firewall; plus konsument healthcheck.sh Ollamy w monitoringu (nieosiągalność była niewidzialna).
**Następne:** krok 3 fazy 4 — fallback embed D2 (kalibracja RAM na żywym PIHA rozstrzyga wariant), potem frontend, ingress+OIDC.

View file

@ -1,119 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-22/23 — Control-plane: dziura w operator_ui + pierwszy pełny cykl remediacji bez SSH
Zamknięcie wielosesyjnego wątku "czemu mózg nie leczy floty". Dwa dni pracy,
zakończone pierwszym w historii systemu pełnym cyklem remediacji: supervisor →
approval → executor → node-agent → docker restart → completed.
## Wykonane
1. **Odkrycie bezpieczeństwa + fix (commit `9a5c160`, 2026-07-22).** Przy
reconie kanału dla remediacji CC odkrył, że `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. Gorzej:
`do_POST` obsługuje `/action/mutate` → `mutate_action(action_id,
target_status)`, które przenosi akcje między stanami WŁĄCZNIE z
`"approved"` — dowolna osoba z internetu mogła zatwierdzić akcję
remediacyjną. Jedyne co chroniło: executor w tamtym momencie nie umiał
jeszcze wykonać akcji (brak SSH) — czyli przypadek, nie zabezpieczenie.
Fix: 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: `curl` na publiczny IP → HTTP 000 (brak
odpowiedzi), `curl` po Tailscale → HTTP 200. Konsumenty nietknięte:
node-agent na VPS (`network_mode: host`, localhost działa dzięki bindowi
127.0.0.1), materializer PIHA (łączy się po `100.95.58.48`).
**FOOTGUN do zapamiętania:** gdy brakuje `.env`, `docker compose` tylko
OSTRZEGA i po cichu wraca do bindu `0.0.0.0` — nie failuje.
2. **Remediacja bez SSH (commit `2dac154`, 2026-07-22).** Diagnoza stanu
wyjściowego: łańcuch działał aż do wykonania — supervisor generuje akcje
(18 pending), UI pokazuje z Approve/Reject, przejście
pending→approved→running OK, executor podejmuje i dispatchuje. Padało
TYLKO wykonanie: executor robił `ssh oskar@{node} docker restart
{container}`, a w kontenerze NIE MA klienta ssh (fail w 6ms =
`FileNotFoundError`), nie ma klucza, nazwa "piha" nie rozwiązuje się z
VPS, a klucz VPS nie był autoryzowany na piha.
**Decyzja architektoniczna:** nie dodawać SSH do executora (kontener na
publicznym VPS z powłoką na całą flotę = zły blast radius). Zamiast tego:
executor ZLECA, node-agent na docelowym węźle WYKONUJE lokalnie przez
swój `docker.sock`. Kierunek PULL — węzeł sam sięga po zlecenia, VPS nie
ma dostępu do węzłów.
Wybrany kanał: rsync-pull istniejącym kluczem agenta (tym samym co
`_ship_events_to_vps`). Odrzucono HTTP pull — bo właśnie odkryto, że 18180
jest publiczny i bez auth (patrz pkt 1), dokładanie tam kanału zleceń
zwiększałoby powierzchnię ataku.
Przepływ: supervisor → pending → (operator) approved → executor
`_execute_action` → running + zapis
`actions/dispatch/<node>/<action_id>.json` → agent pull (co
`CHECK_INTERVAL` 60s) → walidacja → docker SDK `.restart()` → event
`action_result` → rsync do VPS → executor `_reconcile_running_actions`
completed/failed. Timeout `ACTION_TIMEOUT_SECS` (300s) → failed z jasnym
powodem.
Zabezpieczenia: walidacja `node == self.node_name`; whitelist typów =
`{container_restart}` (wszystko inne odrzucone z jawnym `action_result`);
guard przed restartem samego node-agenta; brak wykonywania dowolnych
poleceń z payloadu; idempotencja (ponowne zlecenie = no-op). 183 testy.
3. **Fix uprawnień na PIHA (ręcznie, 2026-07-23 — NIE w repo).** Pierwszy
test E2E padł: agent rzucał `[Errno 13] Permission denied:
/opt/homelab/actions/dispatch` co cykl. Root cause to ZNANY, POWRACAJĄCY
(już 4. raz — patrz sekcja "Tech-debt: globalny porządek uid/gid/uprawnień
we flocie" w `kb/phases/backlog.md`) motyw
uid/gid na PIHA: oskar ma uid 1004, kontener agenta biega jako uid 1000
(= user `pi` na hoście). `/opt/homelab/actions` było `oskar:oskar
drwxr-xr-x` (utworzone w maju), podczas gdy DZIAŁAJĄCY wzorzec to
`/opt/homelab/events` = `oskar:pi drwxrwsr-x` (grupa `pi`, zapis dla
grupy, setgid). Fix ręcznie: `chgrp -R pi` + `chmod -R g+w` + `chmod g+s`
na katalogach. Executor tymczasem zachował się wzorowo: po 300s timeoutu
przeniósł akcję do `failed` z czytelnym powodem, nic nie zawisło.
4. **Pierwszy działający cykl remediacji (2026-07-23) — CEL OSIĄGNIĘTY.** Po
fixie uprawnień agent od razu podjął zalegle zlecenie z poprzedniego dnia
i zrestartował `node_exporter`, a przy kolejnych cyklach logował "already
processed — skipping (idempotency)" — idempotencja potwierdzona w boju.
Czysty test E2E (`action_id test-e2e-b`): 12:05:25 Executing → 12:05:25
Dispatched `container_restart` do node-agent on piha → 12:05:56 Action
completed. **CAŁY CYKL 31 SEKUND.** Dowód po drugiej stronie:
`node_exporter` na PIHA uptime spadł z 30 godzin do minut. Restart
wykonany na zdalnym węźle BEZ ANI JEDNEGO połączenia SSH z control-plane.
## Lessons learned
- Recon pod jedno zadanie odkrył poważniejszy problem niż samo zadanie
(publiczny 18180) — kolejność prac trzeba było odwrócić: najpierw zamknąć
bramę, potem włączać realne wykonywanie akcji. Do momentu fixu remediacji
chroniła nas WYŁĄCZNIE niesprawność executora.
- Ten sam motyw uid/gid (host oskar 1004 vs kontener 1000) ugryzł już
czwarty raz: klucz SSH agenta, docker.sock, `events/`, teraz `actions/`.
Wzorzec działający = grupa współdzielona + setgid.
- Zepsuty JSON w `approved/` powodował, że executor próbował go przetworzyć
W KÓŁKO co 10s ("Failed to move ... to running: Expecting value") — brak
przeniesienia do `failed/`/`rejected/`.
- Cytowanie w zagnieżdżonych heredoc przez ssh: `'$VAR'` wewnątrz
pojedynczych cudzysłowów podstawia się LOKALNIE — zepsuło JSON akcji
testowej. Bezpieczne: heredoc z `<<"JSON"` i wartości na sztywno.
## Stan
Pierwszy w historii systemu pełny cykl remediacji end-to-end potwierdzony w
produkcji (PIHA), bez SSH z control-plane do węzłów. Publiczna dziura
autoryzacji na `operator_ui.py:18180` zamknięta (bind ograniczony do
Tailscale). Otwarte follow-upy — patrz `kb/phases/backlog.md` (retry-w-nieskończoność
zepsutego JSON, uprawnienia `actions/` na innych węzłach, brak twardego checka
`.env`/`TAILSCALE_BIND_IP` w `deploy-local.sh`, brak autoryzacji w
`operator_ui.py`, zapchana approval queue przez `alert_only`, brak
crash-loop→`container_restart`, brak Telegram yes/no dla pending, martwy
`homeassistant5` na PIHA, repo-less node-agent na lustro).

View file

@ -1,66 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-22/23 — HA: adapter api, import ken, deploy.sh, otwarcie fazy 1
## Wykonane
1. `task/ha-adapter-api`: adapter api w import.sh (REST: automations/scripts/
scenes; WS: dashboardy, rejestry, input_*). Pierwszy import ken: 118
automatyzacji, 5 skryptów, 3 sceny, 7 dashboardów (lovelace_admin:
config_not_found — zarejestrowany, nigdy nieskonfigurowany, do sprawdzenia
w UI). Token nigdy w argv (ha_api.py czyta plik sam).
2. `task/ha-deploy`: deploy.sh — drift-check → walidacja lokalna +
check_config → zapis per obiekt → verify. Testy offline 22/22, dry-run
na żywym ken czysty. ken-legacy/chelsty odrzucane (status != active).
3. Pierwszy pełny przebieg LIVE (nieplanowany, patrz Lessons): 126 targetów,
105 written, 21 "verify errors" = HA znormalizował legacy schema
(trigger→triggers, condition→conditions, action→actions, service→action).
Dom niezaburzony (118 automatyzacji, 0 unavailable). Drift domknięty
re-importem, commit 4366cdb.
4. Ceremonia otwarcia fazy 1 (2026-07-23): fix aliasu "Gniadka→Gniazdka"
(1664715283066) pełnym cyklem repo→dry-run→deploy→verify. 1 written,
0 errors. Commit 57a5f66.
5. Noc 22/23.07 na jednym mózgu: nocne automatyzacje (1:00, Mirror 4:30,
Gniazdka 5:00, night mode, TRV) strzelały normalnie — legacy nie zabrał
ze sobą niczego zauważalnego.
## Lessons learned
- Token HA dwukrotnie skompromitowany przy setupie (raz wklejony w czat/
terminal, raz komenda zamiast tokenu w read -rs; 4 wpisy w zsh_history).
Procedura naprawcza: rewokacja w HA, sed na ~/.zsh_history, świeża sesja.
Zasada: token tylko przez read -rs do pliku chmod 600; w komendach
ad-hoc `tokenha=$(cat ~/.config/ha-deploy/ken.token)`.
- Prompt CC wklejony do zsh zamiast do sesji CC — nieszkodliwe (same parse
errors), ale sekwencja musi być: najpierw claude w worktree, prompt
dopiero w interfejsie CC.
- Deploy bez listy plików = "wszystko w scope" — odpalony na żywo bez
dry-runu (sklejone kroki + niedokończony merge wcześniej = deploy.sh
jeszcze nie istniał na masterze przy pierwszej próbie, a przy drugiej
poszedł full-scope). Skutek niegroźny (no-op semantycznie + normalizacja
schematu), ale dwa wnioski w backlogu poniżej.
- agent.sh merge: 3x z rzędu rozjazd master/branch wymagający rebase —
papier-cut do naprawienia.
## Backlog (dopisane)
- deploy.sh: skip niezmienionych obiektów (porównanie z live przed POST)
— wtedy full-scope deploy jest bezpiecznym no-opem.
- Zasada operacyjna do czasu skip-unchanged: deploy ZAWSZE z jawną listą
plików.
- agent.sh: pull --ff-only przy new; komunikat "REBASE NEEDED" przy merge.
- ha-diag-agent nadal celuje w martwy localhost:8123 (kontener zgaszony) —
przepięcie na 31.7 czeka; sprawdzić czy/jak zaalertował.
## Stan
Faza 1 otwarta: repo = źródło prawdy dla automations/scripts/scenes ken,
tor odczytu i zapisu działa E2E. Dashboardy/helpers: import tak, deploy
jeszcze nie (WS write — osobny task). Obserwacja legacy do 2026-07-29.

View file

@ -1,59 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# 2026-07-23 (cd.) — HA: klima E2E, audyt Fable, fix-pack 1
## Klima salonowa — pierwsza automatyzacja LLM przez repo (E2E)
Intencja po polsku → helpers w UI → CC (Sonnet): discovery encji z fixtures,
3 pliki (ON+sync, OFF, skrypt suszenia parownika fan_only 5 min), wykrył brak
fan_mode w encji gree_ir i dostosował. Trzy iteracje:
1. Przegląd operatorski: znaleziony martwy punkt po suszeniu + dodana
15-min karencja balkonowa (CC follow-up; commit prawie wszedł
niezacommitowany — lekcja: git status przed push z worktree CC).
2. Odbiór na żywo: naturalny start z triggera balkonowego (23.7>23.6),
Cool 14:57 → Fan only 15:00 → Off 15:05 — pełny cykl potwierdzony.
3. Bug z obserwacji produkcji: próg temperatury istniał tylko jako trigger,
triggery brzegowe go omijały (start przy salonie 23.6 < próg 27).
Fix faa2e2a: lustrzany warunek w gałęzi start. Wzorzec nazwany i użyty
w audycie jako klasa błędu.
## Audyt automatyzacji (Fable 5, read-only, 502 linie)
docs/audyt-automatyzacji-2026-07-23.md. Krytyczne: kalibracje TRV liczące
unavailable jako 0°C (wynik -5.0, pętla co 40s); klaster ~15 automatyzacji
cicho martwych przez 6 padłych czujników ruchu; alerty wodne z odwróconym
triggerem i literówką mesaage. Diagnoza CC przy fix-packu: wspólna awaria
2026-07-17 15:07 (restart HA/update Supervisora) — czujniki ruchu + pilot
4button + cała integracja xiaomi_miot naraz; podejrzenie na integrację/
koordynator, nie baterie.
## Decyzje operatora (checklista 17 pytań — pełny zapis w DESIGN.md)
Fix-pack 1 (wdrożony, f09dcbf, deploy 5/5): alerty wodne, respekt ręcznego
"Disable AUTO off" w kuchni, 3am z time_pattern (60x/noc) na punktowe 03:00,
klima OFF za warunkiem auto=on (sunset/balkon nie ruszają ręcznego chłodzenia;
zgaszenie przełącznika nadal robi graceful dry-off — osobna gałąź choose).
Świadomie bez zmian: OwnTracks (wróci przy fazie KB), symulacja obecności
batch 02, porządki on_leave. Choinki: disable w UI (sezonowe).
Nowe projekty w backlogu: architektura night_mode (konsolidacja sleep/night,
enforcer, 4 nocne wyłączniki, ostateczny wyłącznik nocny), diagnoza awarii
2026-07-17, guard TRV przed sezonem (~09), przycisk graceful shutdown klimy.
## Meta
- Round-trip repo→API→repo zwalidowany na produkcji (re-import po deployach:
zero diffu na automatyzacjach).
- Drift helpers wykryty przez audyt i domknięty importem (f3328d2) — model
sync działa zgodnie z projektem.
- Incydent tokenowy przy setupie deploy_agent (2x kompromitacja w historii
shella) — procedura naprawcza zadziałała; zasada: read -rs do pliku,
w komendach $tokenha z pliku.
- Fable 5 do audytu: trafny dobór — klasa znalezisk poza zasięgiem szybkiego
przeglądu.

View file

@ -1,136 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-23
links: []
---
# Sesja 2026-07-23 — KB faza 4: ingress kb.kapala.org (krok 5/§8)
**Zakres**: wyłącznie ingress (`kb/phases/kb-m5-faza4.md` §8, krok 5) —
frontend i `/search` już LIVE na PIHA (port 8230) od sesji 2026-07-22. Zero
zmian w kodzie kb-query w tej sesji.
## Zrobione
### 1. Vhost npm@PIHA
`scripts/npm/npm_api.py --npm piha create-host --domain kb.kapala.org
--forward-host 192.168.31.5 --forward-port 8230 --cert-id 49 --ssl-forced
--http2-support --block-exploits --websocket --apply` → **proxy host #35**.
Parametry skopiowane 1:1 z `paper.kapala.org` (#33) po odczytaniu jego configu
przez API — `forward_scheme=http`, `access_list_id=0`, `caching_enabled=false`,
`advanced_config` puste (lekcja z `docs/sessions/2026-06-30-kapala-cloudflare-wildcard-mesh.md`:
`proxy_http_version` ręcznie w Advanced + Websockets ON = duplikat dyrektywy,
`nginx -t` odrzuca cały plik). Cert #49 = `*.kapala.org` wildcard, DNS-01
Cloudflare, ważny do 2026-09-28 — **żaden nowy cert nie powstał**, zgodnie z
planem.
### 2. Pi-hole split-horizon
Mechanizm: `/etc/pihole/custom.list` na PIHA (Pi-hole v5.18.4, natywny/systemd
`pihole-FTL`, nie kontener) — runtime, poza GitOps, dokładnie jak reszta
`~15` wpisów w tym pliku. Dodano `192.168.31.5 kb.kapala.org` + `pihole
restartdns`. Zweryfikowane: `dig kb.kapala.org @127.0.0.1` na PIHA i z hosta
LAN (SOLARIA, resolver = Pi-hole) → `192.168.31.5`.
**Rozbieżność z założeniem planu** (STOP, wyjaśnione i zatwierdzone przez
operatora): zadanie zakładało "ten sam mechanizm co istniejące ~15 domen [dla
kapala.org]" — w rzeczywistości tych 15 wpisów to wyłącznie `*.okit.pl`,
**zero** istniejących `kapala.org` w `custom.list`. `paper./ha./immich./
vikunja./forgejo.kapala.org` **nie mają** dziś override'u LAN — `dig` z PIHA i
z 8.8.8.8 zwracał identycznie `100.108.208.3` (Tailscale IP) dla
`paper.kapala.org`, czyli LAN-owy klient robi dziś hairpin przez Tailscale dla
wszystkich istniejących usług `kapala.org`. `kb.kapala.org` jest więc
**pierwszym** split-horizon wpisem dla tej domeny. Mechanizm (plik, komenda)
jest ten sam i poprawnie odtworzony — tylko przesłanka "już tak jest dla 15
domen kapala.org" była błędna. Nie dotykano innych vhostów (poza zakresem) —
retrofit pozostałych `kapala.org` do LAN split-horizon to osobny, potencjalny
follow-up.
### 3. Cloudflare A-record
Brak tokena Cloudflare API w środowisku wykonawczym — operator dodał ręcznie
przez dashboard: `kb.kapala.org``100.108.208.3` (Tailscale PIHA), DNS only,
analogicznie do `paper./ha./immich./vikunja./forgejo.kapala.org`. Zweryfikowane
w tej samej sesji, osobnym przebiegiem po zgłoszeniu przez operatora:
- Pierwsza próba (`dig @8.8.8.8`/`@1.1.1.1`) — pusta odpowiedź, `dig
@dom.ns.cloudflare.com` (autorytatywny NS strefy) zwracał SOA/NXDOMAIN dla
`kb.kapala.org`, podczas gdy `paper.kapala.org` na tym samym serwerze
odpowiadał poprawnie — **nie opóźnienie propagacji, rekord faktycznie
jeszcze nie istniał** w strefie w momencie pierwszej weryfikacji.
- Operator poprawił/dokończył zapis w dashboardzie; ponowny `dig` (ten sam
autorytatywny NS + 8.8.8.8 + 1.1.1.1) → `100.108.208.3`, TTL 300, zgodne z
pozostałymi `kapala.org`. `curl https://kb.kapala.org/healthz``200`,
cert `*.kapala.org` ważny, treść zgodna.
- Pi-hole split-horizon (LAN → `192.168.31.5`) niezmieniony, zweryfikowany
ponownie po zmianie w CF — obie warstwy działają niezależnie, jak
zaprojektowano.
### 4. OIDC — STOP wg instrukcji zadania
Potwierdzone w repo (zgodnie z `05-faza4-plan.md` §1.4): **brak wzorca
forward-auth/reverse-proxy-level auth** — NPM community edition go nie ma
(sprawdzone: brak `oauth2-proxy`/`authelia`/`forward_auth` w kodzie repo poza
wzmiankami "przyszła opcja" w `kb/subsystems/kb-documents-pillar.md` i
`kb/nodes/vps.md`). Wszystkie 3 precedensy (paperless/nextcloud/vikunja)
robią OIDC **wewnątrz aplikacji**. kb-query nie ma dziś żadnego logowania.
Zgodnie z instrukcją zadania: **nie budowano** nowego komponentu auth.
Zaproponowane operatorowi 2 opcje:
1. **Zostaw bez auth na razie**`authlib` + `/login`/`/auth/callback`
wbudowane w kb-query (plan §8) to osobna sesja z kodem (~1 sesja szacunku
planu), poza zakresem tego zadania infra.
2. **Doraźny NPM Access List (Basic Auth)** — wbudowana funkcja NPM (wspólne
hasło na poziomie vhosta), zero kodu, zero nowego komponentu, ale to nie
SSO/Forgejo — tylko gate.
**Decyzja operatora (2026-07-23): opcja 1** — `kb.kapala.org` zostaje bez auth
(LAN/Tailscale, jak dziś) do czasu osobnej sesji budującej `authlib` OIDC w
kb-query. Gotcha do pamięci na tamtą sesję (z Vikunja-precedensu): Forgejo
OAuth2 app wymaga **pełnego** redirect URI z kluczem providera
(`.../auth/callback` czy analogiczny, do potwierdzenia przy implementacji) —
niedokładny redirect URI w konfiguracji aplikacji OAuth to najczęstszy błąd
przy pierwszym logowaniu w tym repo (paperless miał podobny problem z
username collision, `docs/sessions/2026-07-10-paperless-deploy.md`).
## Weryfikacja (DoD)
- `curl -I` / `curl -v` przez `--resolve kb.kapala.org:443:192.168.31.5`:
`HTTP/2 200` na `/healthz`, cert `CN=*.kapala.org`, TLSv1.3, ważny do
2026-09-28.
- `/search?q=test` przez vhost vs bezpośrednio na `192.168.31.5:8230`:
**identyczny** `envelope_id`/`dist`/`chunk_index`/`text` (ten sam kod, HTTP
to tylko opakowanie — zgodnie z kryterium bramki §9 planu, choć to nie jest
jeszcze formalny `retrieval_eval.py --transport http`, tylko ręczny
smoke-test tej sesji).
- Rozwiązywanie `kb.kapala.org` z hosta LAN (SOLARIA, resolver = Pi-hole) →
`192.168.31.5`, potwierdzone `dig`/`getent hosts`.
- Rozwiązywanie `kb.kapala.org` z publicznych resolverów (8.8.8.8, 1.1.1.1) i
z autorytatywnego NS strefy (`dom.ns.cloudflare.com`) → `100.108.208.3`,
TTL 300 — zweryfikowane po dodaniu rekordu przez operatora, `curl` przez tę
ścieżkę → `200`.
- Auth: brak (zgodnie z decyzją) — nic nie powinno wymagać logowania, i nic
nie wymaga (potwierdzone: `/search` i `/healthz` odpowiadają bez tokenu).
## Stan na koniec sesji
| Element | Status |
|---|---|
| npm@PIHA vhost #35 kb.kapala.org → 192.168.31.5:8230, cert 49 | ✅ LIVE |
| Pi-hole custom.list kb.kapala.org → 192.168.31.5 | ✅ LIVE (pierwszy kapala.org wpis) |
| Cloudflare A kb.kapala.org → 100.108.208.3 | ✅ LIVE (dodany przez operatora, zweryfikowany) |
| OIDC | ⛔ świadomie odłożone — osobna sesja z kodem |
## Pliki repo zmienione
- `kb/services/kb-query.md` — sekcja "Ingress" (co żyje, co nie, dlaczego
auth odłożone) zastępuje starą notatkę "not wired up yet".
- `docs/sessions/2026-07-23-kb-f4-ingress.md` — ten dokument.
## Follow-upy (propozycje, nie w zakresie tej sesji)
- Retrofit Pi-hole split-horizon dla `paper./ha./immich./vikunja./
forgejo.kapala.org` (dziś wszystkie hairpinują przez Tailscale nawet z LAN)
— drobny, ale osobny task, nie dotykać przy okazji.
- `authlib` OIDC w kb-query (plan §8) — osobna sesja z kodem.
- `retrieval_eval.py --transport http` (plan §6 Decyzja 6, formalna bramka
HTTP-equivalence) — dziś tylko ręczny smoke-test, nie automat.

View file

@ -1,63 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-27
links: []
---
# 2026-07-27 — HA: legacy zamkniete, kasacje, pimirror, sonda Zigbee
## Wykonane
- Klima: 4 dni autonomii bez interwencji (sunset shutdown 26.07 21:05
zadzialal naturalnie; dzis poprawny brak startu — prog 27 > salon 24.4,
fix faa2e2a dziala w praktyce).
- Legacy: kontener homeassistant5 juz nie istnial (podejrzenie: cleanup
policy node-agenta — do wyjasnienia kiedys; kolizja "stop bez rm dla
rollbacku" vs auto-cleanup). Katalog /home/pi/homeassistant nietkniety
(config z 22.07). Strata zerowa — archiwum w gicie.
- Kasacje pkt 10 audytu przez API (6 automatyzacji: para Tymka, powitanie
test, notify router, para prototyp) + drift commit 36b43e5. Incydent:
petla DELETE odpalona z placeholderami ID1..ID6 przed identyfikacja —
bez szkod (404), lekcja: operator nie podstawia, tor przez repo
(deploy.sh --delete w backlogu).
- task/ha-porzadki (09624e0): pimirror graceful shutdown odtworzony
z ken-legacy (23:28 graceful przed twardym 23:35; legacy button
unavailable — przepisane na pimirror2 po registry, entity_id wg nowej
konwencji), sekcja "Konwencje automatyzacji" w DESIGN.md, backlog:
deploy.sh --delete, trigger na zmiane tolerancji klimy. Deploy 1/0/0.
## Sonda Zigbee (read-only) — diagnoza awarii "2026-07-17"
last_changed w HA bezuzyteczne po restarcie (277 encji ze stemplem 26.07 =
odcisk restartu, nie fala). Log z2m: tydzien bez przejsc offline/online
(padly wczesniej). Availability z MQTT (retained): ~20 urzadzen offline,
w tym WSZYSTKIE TRZY ROUTERY (routerIKEA/Salon/Sypialnia) + urzadzenia
sieciowe (ledTV, zbLampkiRegal, zbSwitchBlatZasilanie, listwy LED).
Wniosek: to nie baterie i nie koordynator — urzadzenia zasilane sieciowo
sa fizycznie bez pradu, mesh sie zapadl kaskadowo (bateryjne koncowki
poza zasiegiem: mdHeli, thHeli, mdSypialnia, mdUbikacja, mdWejscie,
waterLeakWc, 4button, heaterLazienkaTRV07). Geografia strat = dziury po
routerach.
## Nastepne kroki
1. FIZYCZNIE: sprawdzic zasilanie 3 routerow + wtyczek LED; po powrocie
odczekac dobe; re-pairing tylko dla tego, co nie wroci samo. Przy
okazji: zasilanie puryfikatorow zhimi (WiFi, osobny tor xiaomi_miot).
2. Po odbudowie mesh: ponowna sonda availability + drift/re-import;
duplikat mdwejscie/mdWejscie do sprzatniecia w z2m.
3. Wieczorem 23:28: pierwszy zywy test pimirror graceful shutdown (trace).
4. Nastepna sesja: projekt Fable "tryby domu" (night/sleep/empty/on_leave)
— prompt gotowy w historii; TRV guard ~09; przepiecie ha-diag-agent.
## Dogrywka: zigbee.okit.pl -> zigbee.kapala.org (mesh-only)
Runtime (poza gitem, log tutaj): npm@PIHA proxy host #36
(zigbee.kapala.org -> 192.168.31.5:8087, cert #49 wildcard, websocket ON,
advanced puste — pulapka kapala.org), przez scripts/npm/npm_api.py
(dry-run -> --apply). Pi-hole custom.list: 192.168.31.5 zigbee.kapala.org
(split-horizon jak kb). Cloudflare kapala.org: A zigbee -> 100.108.208.3
(Tailscale piha, DNS only — wzorzec forgejo). Test: dig OK, curl 200.
Sprzatniecie: rekord zigbee.okit.pl usuniety z Cloudflare, vhost z NPM@VPS.
Frontend z2m bez auth_token — publiczna ekspozycja na okit.pl [TODO operator:
status auth przed migracja + ew. przeglad access logow NPM@VPS].

View file

@ -1,212 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-30
links: []
---
# Sesja 2026-07-27 — KB faza 4: fallback embed SOLARIA→PIHA (krok 3, ostatni element rdzenia)
> **Dopisek redakcyjny (2026-07-30, dedup — `kb/phases/kb-m5-faza4-fallback-dedup.md`):**
> implementacja kodu z tej sesji (`app/fallback.py`, branch `task/kb-f4-fallback`, 3d4ee38)
> została **porzucona** — do mastera weszła równoległa, szersza implementacja tego samego
> kroku planu (e7625cd, `app/embed_router.py`, 2026-07-29) i to ona biega na PIHA. Ten log
> wciągnięto do repo, bo dokumentuje fakty operacyjne niezależne od porzuconego kodu:
> znalezisko i wyłączenie osieroconego natywnego `ollama.service` na PIHA (§3, z backlogiem
> odinstalowania ≈2026-08-10), kalibrację live ollama-piha z werdyktem GO (§4 — konfiguracja
> kontenera identyczna na masterze, pomiar przenosi się) oraz metodologię i baseline bramki
> §9 (§5, Δ~3e-4). Wyniki bramki i testu sol-down dotyczyły kodu z brancha — na wdrożonym
> masterze wymagają powtórki (raport dedup, follow-up (b)). Sekcje o deployu (§6) i
> "Do zrobienia przez operatora" pkt 12 opisują stan sprzed merge'a e7625cd — historyczne.
> Z delty brancha uratowano ponadto: `retrieval_eval.py --transport http` (plan §2 D6/§9)
> i luki testowe T1/T2 przeniesione do `test_embed_router.py`.
**Zakres**: `kb/phases/kb-m5-faza4.md` §2 decyzja 2 / §5 — aktywny fallback
embedu, ostatni brakujący element rdzenia fazy 4 (frontend i ingress LIVE od
2026-07-22/23, `docs/sessions/2026-07-23-kb-f4-ingress.md`). Zero zmian w schemacie
DB, zero zmian w `kb_retrieval`'s retrieval logice — wyłącznie warstwa embed + health.
Praca wykonana w task worktree (`task/kb-f4-fallback`, `.claude/skills/worktree-aware`).
Zgodnie z ustaleniem na starcie sesji (patrz "Ustalenia proceduralne" niżej): kod
napisany i przetestowany lokalnie w worktree, produkcyjne kroki (kalibracja, deploy,
live-test) wykonane po jawnej zgodzie operatora, z osobnym potwierdzeniem przed
każdym kolejnym krokiem dotykającym PIHA/SOLARIĘ.
## Ustalenia proceduralne
Zadanie wprost wymagało kroków produkcyjnych (kalibracja RAM/latencji na żywym PIHA,
symulacja sol-down dotykająca SOLARII, deploy, push) — sprzeczne z ogólną dyscypliną
`worktree-aware` ("nigdy nie uruchamiaj deployów/healthchecków przeciw produkcji z
worktree"). Zamiast rozstrzygać to samodzielnie, zapytano operatora:
1. Recon read-only (bez zmian stanu) — zgoda bez pytania.
2. Właściwe kroki produkcyjne (kalibracja, deploy, live-test, push) — operator
potwierdził jawnie ("Yes, proceed with all of it") po zobaczeniu pełnego zakresu.
## 1. Kod (warstwa embed + health, zero zmian retrievalu/DB)
- **`packages/kb_retrieval/embed.py`**: `embed_chunk` dostał opcjonalny `timeout_s`
(domyślnie `None`, zero zmiany zachowania istniejących wołań) — potrzebny do
twardego 3 s timeoutu na nodze SOLARIA bez zmiany zachowania nogi PIHA.
- **`services/kb-query/app/fallback.py`** (nowy): `SolCircuitBreaker` (cache 30 s,
zegar wstrzykiwalny do testów) + `resolve_sol_status` (probe `/api/tags`, 500 ms) +
`embed_with_fallback` (SOLARIA z twardym 3 s timeoutem → jednorazowe przełączenie na
PIHA **w tym samym requeście** przy timeout/błędzie → PIHA bez dodatkowego
timeoutu). Dokładnie maszyna stanów z planu §2 decyzja 2.
- **`app/search.py`**: `run_search` liczy embedding raz przez `embed_with_fallback`,
potem woła `flat_retrieve`/`cascade_retrieve`/`hybrid_retrieve` (niskopoziomowe
funkcje `kb_retrieval`, biorą gotowy wektor) zamiast `flat_query`/`cascade_query`/
`hybrid_query` (które embedują same) — dzięki temu decyzja fallbacku żyje wyłącznie
w warstwie HTTP kb-query, zero zmiany w `kb_retrieval`. `sol_status` w odpowiedzi to
teraz realny wynik, nie zahardkodowane `"up"`.
- **`app/main.py`**: `/healthz` i `/search` dzielą jeden `SolCircuitBreaker`
(`app.state.sol_breaker`) — oba endpointy zawsze zgadzają się co do aktualnego
stanu. Nowy env `OLLAMA_PIHA_URL` (domyślnie `http://localhost:11434` — celowo
"inertny" placeholder, fail-closed, dopóki operator nie ustawi realnego adresu).
- **Inwariant modelu**: **nie dodano** drugiego, per-request sprawdzenia w DB —
`EMBED_MODEL` to jedna stała wątkowana przez obie nogi `embed_with_fallback`,
więc startowy check (`app/startup.py`, niezmieniony) pokrywa obie ścieżki z
konstrukcji. Dodanie drugiego DB-checka chroniłoby przed scenariuszem, który nie
może wystąpić (CLAUDE.md: nie dodawaj walidacji dla scenariuszy, które nie mogą się
zdarzyć) — zamiast tego nowy test (`test_both_legs_use_identical_embed_model`)
strukturalnie dowodzi, że obie nogi w tym samym requeście dostają identyczny
`embed_model`.
- **`jobs/documents-ingest/eval/retrieval_eval.py`**: dodano `--transport
{direct,http}` + `--base-url` (plan §2 decyzja 6 / §9) — dotąd nieistniejące (tylko
ręczny smoke-test, `docs/sessions/2026-07-23-kb-f4-ingress.md` follow-up). Tryb
`http` woła trzy `GET /search` (flat/cascade/hybrid) na żywym kb-query zamiast
embedować+odpytywać lokalnie; `envelope.source` do kryterium 4 bierze się z pola
`source` w odpowiedzi JSON, nie z osobnego zapytania do DB. Nie da się swipe'ować
N przez HTTP (kb-query serwuje jeden N per request) — tryb http raportuje tylko
przy `--gate-n`.
## 2. Nowy serwis `services/ollama-piha`
Klon wzorca `services/ollama` (`owner_node: piha` zamiast `solaria`, bez rezerwacji
GPU — PIHA to arm64 bez akceleracji), `OLLAMA_KEEP_ALIVE=0` (model ładowany tylko na
czas requestu). `mem_limit: 2560m` (tentatywny wg planu, potwierdzony pomiarem —
patrz §3). Wpisany do `hosts/piha/services.yaml` (`depends_on.local` kb-query →
`[kb-postgres, ollama-piha]`, fallback nie jest twardą zależnością na starcie).
## 3. Znalezisko: osierocony natywny `ollama.service` na PIHA
Podczas pierwszej próby deployu `ollama-piha` (bind `127.0.0.1:11434`) — konflikt
portu. Okazało się, że PIHA ma **natywny (nie-Docker) systemd `ollama.service`**
(v0.6.1, `enabled`, działający od 2026-06-22, PATH env wskazujący na użytkownika
`/home/pi/...`), o którym nic nie wiadomo w repo — plan §1.2 wprost zakładał "PIHA:
brak Ollamy", co okazało się nieaktualne/błędne. To realna sprzeczność planu z
rzeczywistością → STOP, pytanie do operatora zamiast cichej decyzji.
Weryfikacja przed jakąkolwiek akcją: `journalctl -u ollama --since "7 days ago"`
**tylko własne, właśnie wykonane** zapytania probe (`/api/version`, `/api/tags`),
`total blobs: 0` od startu (nigdy nic nie pobrano). Operator potwierdził: martwy
balast, `sudo systemctl disable --now ollama.service` (**disable, nie uninstall** —
odwracalne). Port 11434 zwolniony, `ollama-piha` wystartował normalnie.
**Backlog**: PIHA host-level shadow — natywny `ollama.service` wyłączony
2026-07-27; odinstalować binarkę/unit po ~2 tygodniach jeśli nic się nie posypie.
## 4. Kalibracja (plan §5, gate) — **werdykt: GO**
Zmierzone na żywym PIHA pod normalnym obciążeniem (kb-postgres, paperless, Immich,
HA, Forgejo działające, nie okno nocnej ciszy), 3 kolejne wywołania `/api/embeddings`
po `ollama pull bge-m3`:
| Wywołanie | Latencja |
|---|---|
| 1 (pierwsze, zimny start) | 5.25 s |
| 2 | 4.41 s |
| 3 | 4.16 s |
Brak przyspieszenia między wywołaniami — zgodnie z projektem (`OLLAMA_KEEP_ALIVE=0`
zwalnia model po każdym requeście, `ollama ps` pokazuje zero rezydentnych modeli
między wywołaniami).
RAM: baseline idle ~66 MiB, szczyt podczas burst ~983 MiB (`docker stats`, próbkowane
co 0.3 s w trakcie 3 wywołań) — komfortowo w granicach ceilingu `2560m`. `free -h`
systemowe: `available` nie spadło poniżej ~1.3 GiB w trakcie, osiadło na ~4.2 GiB po
(dla porównania: przed startem eksperymentu `available` = 3.7 GiB).
**Werdykt**: oba kryteria planu spełnione (latencja pojedyncze sekundy, nie
dziesiątki; RAM ze sporym zapasem) → **włączony jako domyślny fallback**, bez flagi
`KB_QUERY_LOCAL_FALLBACK_ENABLED`.
## 5. Bramka jakościowa (plan §9)
Wszystko uruchomione z `~/kb/venv` na PIHA (istniejący venv z poprzednich sesji,
`aiohttp`/`asyncpg`/`yaml` już obecne) przeciw żywej bazie + żywemu kb-query.
**HTTP-equivalence** (`--transport http` vs `--transport direct`, SOLARIA up, ten sam
`--gate-n 10`): oba PASS, **0 rozbieżności** w `dist` na wszystkich zapytaniach
(`flat_top1_dist`, `hybrid_top1_dist`, `cascade[10].top1_dist`) — identyczne bit w
bit, jak wymagał plan (nie ±epsilon, bo to ten sam kod, HTTP to tylko opakowanie).
**Live sol-down fallback test**: symulacja przez `OLLAMA_URL=http://solaria:1`
(zły port, zgodnie z rekomendacją planu — zero dotknięcia SOLARII/innych
konsumentów Ollamy) w `.env` kb-query, restart kontenera. `/healthz`
`sol_status: "down"`. `/search` → 200, wyniki z PIHA, ~4.3 s (zgodnie z kalibracją).
Pełna bramka `retrieval_eval.py --transport http` z SOLARIA-down: **PASS**
identyczny wzorzec hit@3 co na SOLARII, `dist` w granicach epsilon:
| Zapytanie | dist (SOLARIA) | dist (PIHA fallback) | Δ |
|---|---|---|---|
| 1 | 0.341780 | 0.341509 | 0.000271 |
| 2 | 0.324808 | 0.324858 | 0.00005 |
| 3 | 0.428898 | 0.429184 | 0.000286 |
| 4 | 0.448199 | 0.447903 | 0.000296 |
| 5 | 0.386901 | 0.386816 | 0.000085 |
| N (negative control) | 0.598301 | 0.598017 | 0.000283 |
| N2 (negative control borderline) | 0.529772 | 0.529530 | 0.000242 |
Maksymalna rozbieżność: **~3e-4** — rząd wielkości mniejszy niż oczekiwany przez plan
(1e-31e-2), kolejność top-k identyczna, wynik bramki (`gate.passed`) identyczny.
Kb-query przywrócony do normalnej konfiguracji po teście (`.env` z prawdziwym
`OLLAMA_URL`, restart), `/healthz` z powrotem `sol_status: "up"`.
## 6. Deploy
Kod nie był jeszcze zmergowany do `master` (dyscyplina worktree: agent nigdy nie
mergeuje/pushuje `master`) — deploy przez standardowy `deploy-node.sh`
niedostępny bez mastera. Zamiast tego: `rsync` zmienionych plików
(`packages/kb-retrieval`, `services/kb-query`, `services/ollama-piha`,
`hosts/piha/runtime/ollama-piha`, `hosts/piha/services.yaml`,
`jobs/documents-ingest/eval/retrieval_eval.py` + README) do żywego checkoutu
`~/homelab-codex-ws` na PIHA (bez zmiany brancha — working tree pozostaje na
`master` z niescommitowanym diffem 1:1 identycznym z tą gałęzią), potem
standardowy `docker compose ... up -d --build` z tego miejsca. Efekt: realny,
działający deploy, ale **repo na PIHA ma dziś dirty working tree** — wymaga domknięcia
(patrz "Do zrobienia przez operatora" niżej).
Zweryfikowane: `kb-query` (healthy), `ollama-piha` (healthy, `bge-m3` w wolumenie),
`curl https://kb.kapala.org/healthz``200 {"sol_status":"up"}`,
`curl https://kb.kapala.org/search?q=test``200`.
## Stan na koniec sesji
| Element | Status |
|---|---|
| `packages/kb-retrieval``embed_chunk(timeout_s=...)` | ✅ kod + testy |
| `services/kb-query/app/fallback.py` — maszyna stanów | ✅ kod + testy (38/38 kb-query, 25/25 kb-retrieval) |
| `services/ollama-piha` — nowy serwis GitOps | ✅ zdefiniowany, ✅ LIVE na PIHA |
| Natywny `ollama.service` na PIHA (osierocony) | ✅ wyłączony (nie odinstalowany) |
| Kalibracja RAM/latencja | ✅ zmierzone — werdykt GO |
| `retrieval_eval.py --transport http` | ✅ zaimplementowane, ✅ PASS na żywo |
| Live sol-down fallback test | ✅ PASS, Δ~3e-4 |
| Deploy kb-query + ollama-piha na PIHA | ✅ LIVE, working tree PIHA dirty (patrz niżej) |
| Merge do `master` | ⛔ nie wykonany (dyscyplina worktree — operator) |
## Do zrobienia przez operatora
1. **Merge** `task/kb-f4-fallback``master` (`scripts/dev/agent.sh merge` albo
ręcznie) — branch popchnięty do `origin/task/kb-f4-fallback` (patrz commit poniżej).
2. Na PIHA: `cd ~/homelab-codex-ws && git status` będzie dirty (diff identyczny z tym
commitem, bo już wdrożony ad-hoc przez `rsync` w tej sesji) — po mergu do mastera,
`git checkout -- .` (working tree już ma dokładnie tę treść) albo zwyczajnie
`git pull` po mergu powinien wylądować "already up to date"/no-op, bo pliki na
dysku już są zgodne z tym co przyjdzie z mastera. **Zweryfikować** `git diff` jest
puste po pull, nie zakładać.
3. Backlog: natywny `ollama.service` na PIHA wyłączony `systemctl disable --now`
2026-07-27 (§3 wyżej) — jeśli nic się nie posypie przez ~2 tygodnie, odinstalować
binarkę/unit całkiem.
4. OIDC dla kb-query nadal odłożone (decyzja z 2026-07-23) — nie w zakresie tej sesji.

View file

@ -1,31 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-28
links: []
---
# Session log 2026-07-28
## Session 21:59
### Commits
```
905ad96 docs(architecture): plan naprawy subsystemu A (control-plane) 2026-07-28
e8aa3e3 docs(architecture): recon multiagent 2026-07-27
```
### Files changed
```
kb/phases/subsystem-a-naprawa.md | 60 +++
kb/subsystems/recon-multiagent.md | 551 +++++++++++++++++++++++
2 files changed, 611 insertions(+)
```
### Deploys
None recorded
### Narrative
> _user-provided summary_

View file

@ -1,47 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-07-30
links: []
---
# 2026-07-30 — HA: legacy zamknięte, MCP read-only (faza 2a)
## Legacy — finał
- Tydzień obserwacji czysty; kontener homeassistant5 już nie istniał przy
próbie rm — usunięty przez nieustalony mechanizm (node-agent cleanup?
remediation?). Katalog /home/pi/homeassistant NIETKNIĘTY (fałszywy alarm:
2>/dev/null maskował Permission denied — lekcja). Backlog: ustalić, co
usunęło kontener (granice autonomii agentów); katalog do kasacji przy
porządkach piha.
## Faza 2a — własny MCP server (decyzja operatora: własny > hass-mcp)
- services/ha-mcp: 7 tools read-only strukturalnie (klient bez metod
mutujących, WS allowlista, test grepujący za call_service), stdio,
reuse ha_api/ha_ws, rejestracja w .mcp.json. 42 testy offline, smoke
na żywym ken (1647 encji, 115 automatyzacji = zgodne z repo).
- Wtopa wdrożeniowa: venv żył w worktree, zginął przy merge-cleanup;
fix: odtworzenie w głównym checkoucie. Backlog: run.sh bootstrap venva.
- Test bojowy (świeży CC, zero kontekstu): MCP wołany natywnie (5 calls),
synteza hybrydowa MCP+repo — pełna mapa salonu z odwróconym indeksem
encja→automatyzacje. Ujawniona luka: brak find_automations_using_entity
(cross-ref robiony grepem). Backlog: dodać tool przed fazą 2b.
## Znaleziska testu bojowego (klasy audytowej)
- Choinka/lampki (tasmota_12, zblampkiregal, tasmota_8) sterowane wyłącznie
przez device_id — niewidoczne dla grep po entity_id; martwe od 26-29.07,
automatyzacje cicho nie działają.
- DRUGA FALA martwych urządzeń 26-29.07 (switche choinki/regału + pilot
4button ponownie) — osobna od awarii 17.07; tłumaczy trend unavailable
305→377. Diagnoza sprzętowa: priorytet podniesiony, dwie daty padów.
- Task porządkowy device_id→entity_id (checklista pkt 17) dostał twardy
dowód zasadności — do wykonania PO diagnozie sprzętowej.
## Następne
- Diagnoza sprzętowa (fizyczna): dwie fale, z2m pokazuje część urządzeń
żywych (baterie OK) — podejrzenie na most z2m↔HA / integrację.
- Faza 2b: propose_change/dry_run/request_approval przez kolejkę
control-plane (pending→approved→executed) + Telegram. Osobna sesja.
- Tool find_automations_using_entity + bootstrap venva w run.sh.

View file

@ -1,90 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-04
links: []
---
# Session log 2026-07-31 — KB Faza 4: zamknięcie + pilot narty27
## Zakres
Finalne domknięcie fazy 4 subsystemu B (KB) — **na twardo** — oraz podsumowanie
pilota fazy 5 (narty27), wykonanego równolegle.
## Faza 4 — zamknięcie (na twardo)
### Dedup podwójnej implementacji fallbacku embed
- Rozstrzygnięcie: master (`e7625cd`, `app/embed_router.py`) = źródło prawdy.
- Z porzuconego brancha uratowano S1S4 (`cb8a19d`):
- `retrieval_eval.py --transport http`
- session log 27.07
- testy luk T1T3
- komentarz kalibracji progów dist.
- Pełny rozbiór obu implementacji: `kb/phases/kb-m5-faza4-fallback-dedup.md`.
### Deploy na PIHA (z mastera)
- `ollama-piha`: named volume `ollama_piha_models`, model bge-m3, `KEEP_ALIVE=0`.
- `kb-query`: port 8230, bind 192.168.31.5, `EMBED_FALLBACK_URL` ustawione.
### Test sol-down — PASS na żywej produkcji
Przebieg: pause ollama@SOLARIA → cache 30 s trzyma `up` → zapytanie przełącza się
one-shot na PIHA (`embed_backend: piha`, wyniki poprawne) → `sol_status: down`
unpause → powrót `up` w ≤35 s.
Dodatkowo zaobserwowano **samoistne, jednorazowe zadziałanie breakera na produkcji**
przyczyna nieustalona, zachowanie zgodne z projektem (przełączenie i powrót bez
utraty odpowiedzi).
### Progi dist (skalibrowane, obowiązują)
`<0.45` hit / `0.450.55` szara strefa / `>0.55` brak odpowiedzi.
## Pilot fazy 5 — narty27 (POC, zostaje na stałe)
Publiczna wystawka `narty27.kapala.org` zbudowana **pełnym wzorcem docelowym fazy 5
w miniaturze**:
markdown+frontmatter (OKF v0.1) → walidator konformancji (`check_okf`) → generatory
(graf cytoscape, karty HTML, tabela porównawcza, zdjęcia z filtrem percepcyjnym,
landing) → statyczny hosting (PIHA nginx + named volume) → publiczny ingress
(NPM VPS + Let's Encrypt).
Źródło treści: `~/narty-2027/saalbach-kb` — lokalny git na SOLARII, **celowo poza repo
infry**. Infra: `services/narty27`.
### Wnioski do przeniesienia na fazę 5 (wiki-kompilat)
1. **OKF v0.1 działa w praktyce**, a pinowanie wersji okazało się słuszne — spec
ewoluuje (v0.2: `timestamp``generated: {by, at}`, provenance first-class;
migracja = jedna zamiana pola). Przy fazie 5 rozważyć start od razu na v0.2 albo
pin v0.1 z zaplanowaną migracją.
2. **Walidator-lint jako stały element pipeline'u** wiki, nie jednorazowy skrypt.
3. **Bug upstreamu `knowledge-catalog`**: generator grafu pomija linki od `/` wbrew
§5.1 własnej spec → napisany własny generator. Kandydat na issue/PR do
`GoogleCloudPlatform/knowledge-catalog`.
4. **Reserved files**: `index.md` bez frontmattera (poza `okf_version` w root) —
walidator to łapie.
5. **Warstwa prezentacji z generatorów** — tani, użyteczny wzorzec do reużycia nad
wiki-kompilatem KB.
6. **Każdy artefakt ma mieć dom w gicie; sesje mają logi.**
## Otwarte po sesji
1. **`expected_envelope` w `mail_queries`** (`jobs/documents-ingest/eval/queries.yaml`)
— nadal `null` (placeholdery, artefakt danych, nie kodu); kryterium 4 bramki na
stubie nie przechodzi wyłącznie z tego powodu.
2. **`hosts/solaria/runtime/ollama/docker-compose.override.yml` — brak w repo**
(rozjazd repo↔runtime na SOLARII).
3. **R1R3 node-agent** (incydent `kb/incidents/2026-07-30-ollama-solaria-vanish.md`)
**niezrobione**: R1 `_prune_stopped_containers` nie może kasować kontenerów
zarządzanych, R2 rate-limit dla `ai_node`/`standard`, R3 logowanie usuniętych
zasobów. Przyczyna nadal aktywna → blokuje/warunkuje fazę mailową
(M1 = mitygacja doraźna na czas backfillu).
## Następne kroki
1. Faza mailowa: batching Ollamy → backfill ~225k kopert → IMAP przyrostówka →
PDF-y (~336) → GDrive Takeout.
2. Blokada/ryzyko: node-agent R1R3 (unfiltered prune) — eskalacja do subsystemu A;
mitygacja M1 na SOLARII na czas backfillu.

View file

@ -1,87 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-05
links: []
---
# Sesja 2026-08-05 — batching embed (start fazy mailowej)
## Weryfikacja zaległości
- **Uninstall natywnego `ollama.service` na PIHA — POTWIERDZONY.** Unit i binarka nie
istnieją.
- **Session log fazy 4 istniał już na masterze** (`e619c00`). Zgłoszony brak był fałszywym
alarmem z niedociągniętego working tree na SOLARII.
- **`task/prune-fix` (R1R3 + M1 VPS) gotowy do review** w subsystemie A — `0526af1`,
285 insertions, z testami.
## Task `kb-mail-batching`
Zmergowany do mastera: `02a0079` + `75116ad`.
### Bug blokujący Etap B (znaleziony i naprawiony)
Goły `builtins.TimeoutError` z wyczerpanego `aiohttp` `ClientTimeout` **nie był łapany**
przez `flush_embed_buffer` — obsługa łapała wyłącznie `ClientError`. Skutek: zawieszona
Ollama (failure mode „przyjmuje połączenie i milczy") wywalała cały run, bez breakera
i bez flushu. Klasy przejściowe wyliczone są teraz jawnie w `TRANSIENT_EMBED_ERRORS`.
### `embed_batch_resilient()`
- Retry z backoffem wykładniczym.
- Po wyczerpaniu retry — probe `/api/tags`:
- backend **żywy** → bisekcja izolująca trujący chunk,
- backend **martwy**`gave_up`, bez bisekcji.
### Semantyka breakera (zmiana)
Breaker liczy **give-upy** (backend down wg probe), nie nieudane batche. Porażka częściowa
nie przesuwa licznika. Zmiana znaczenia `--max-embed-failures` opisana w
`kb/phases/kb-m5-faza-mailowa.md` §9.
### Parametryzacja i metryki
- Flagi: `--batch-size`, `--embed-retries`, `--embed-backoff`, `--embed-timeout`
(env `MAIL_INGEST_*`).
- Metryka `embed_ms_per_chunk`.
- Wiersze zembedowane w umierającym batchu są commitowane przed abortem.
### Decyzja: brak fallbacku SOLARIA→PIHA dla backfillu
Świadomie **nie powstaje** — 790 ms/embed na CPU × 271k ≈ 60 h na współdzielonym nodzie.
Tor online (`embed_router`) zachowuje fallback. Rozdział torów udokumentowany w docstringu
`embed.py` / `embed_batch` oraz w `kb/services/job-mail-body-ingest.md`.
### Benchmark `mail-body-ingest-bench`
Read-only (SELECT + inferencja). Wyniki na SOLARII (bge-m3, GPU), próbka 640 chunków
(avg 1559 znaków):
| batch | ms/chunk |
|------:|---------:|
| 32 | 22.70 |
| 64 | 16.57 |
| 128 | 15.82 |
**Default batch 64 POTWIERDZONY** — zysk z 128 to ~4.5%, a przy 64 koszt bisekcji jest
mniejszy. Ekstrapolacja na 271k chunków: **~1.25 h GPU** vs ~11 h przy per-request.
### Testy
117 testów zielonych.
## Wyjątki procesowe
Żadnych. Commit z PIHA nie zaistniał — CC nie dopisał wskaźnika, uznano za zbędne.
## TODO wynikające
- **Rotacja hasła `kb-postgres`** — poszło do historii shella i na screeny sesji.
- **`POSTGRES_PASSWORD` plaintext w `services/*/service.yaml`** — kandydat na backlog
sekretów.
- **`kb-site` `robots.txt` blokuje fetch Claude** (`ROBOTS_DISALLOWED`) — fix: `X-Robots-Tag
noindex` zamiast `Disallow` (wariant B), przy okazji taska `kb-site`.
- **Backfill 271k** — dopiero po merge + deploy `task/prune-fix` (subsystem A).

View file

@ -1,69 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-05
links: []
---
# Session log 2026-08-05
## Session 22:47
Redeploy node-agenta R1/R2/R3 na flotę (supervised, checkpointy zatwierdzane przez
operatora). Incydent źródłowy: `kb/incidents/2026-07-30-ollama-solaria-vanish.md`.
### Commits
Ta sesja **nie wprowadziła żadnych commitów** poza niniejszym logiem — deploy runtime,
zero zmian w kodzie. Granica wyznaczona fallbackiem 24 h (poprzedni log sesji nie używa
nagłówków `## Session HH:MM`), więc poniższa lista obejmuje też wcześniejszą pracę
operatora z tego samego okna, niezwiązaną z tym deployem:
```
bb3792d docs(kb-site): przekaz ACCESS_TOKEN generatorowi w procedurze publikacji
f5c6f3b fix(kb-site): token bramki w kazdym linku wewnetrznym generatora
04251b5 docs(sessions): log sesji 2026-08-05 — batching embed (start fazy mailowej)
75116ad docs(kb-retrieval): rozdzial torow embed takze w docstringu embed_batch
02a0079 feat(kb-mail-batching): retry + izolacja trujacego chunka w torze embed + benchmark
71eaab0 feat(supervisor): duty-cycle nodes — liveness transitions logged, not actioned
19548d8 fix(kb-site): wycofaj robots.txt — blokowal legalny fetch z tokenem
67e49a0 docs(kb-site): przepisz nieaktualne kb.okit.pl na kb-e2a24af3.okit.pl
db81cb1 feat(kb-site): noindex + robots.txt + obscure subdomain jako domyslny base-url
7282a5e docs(recon): sciezka redeploy — fix jest w repo od 2026-08-03, nie jest wdrozony
```
### Files changed
Brak — drzewo robocze czyste przez całą sesję, poza tym plikiem.
### Deploys
Recon wykazał, że zakres jest węższy niż zakładano: PIHA i VPS **już** miały kod R1/R2/R3
(weryfikacja sha256 pliku w kontenerze vs repo). Realny zakres: SOLARIA i LUSTRO.
LUSTRO nie było w pierwotnej liście, a było jedynym nodem faktycznie kasującym bez filtra.
| Node | Przed | Po | Wynik |
|---|---|---|---|
| SOLARIA | `c80a711f` (2026-07-22, pre-R1) | `438111e2` | OK, bez rollbacku |
| LUSTRO | `460d5cc5` (2026-06-11, 658 linii) | `3260c74a` | OK, bez rollbacku |
| PIHA | `9141cc61` — sha == repo HEAD | bez zmian | deploy pominięty (już aktualny) |
| VPS | `27be875d` — R1/R2/R3 obecne | bez zmian | deploy pominięty (różnice tylko w komentarzach) |
Weryfikacja po deployu na obu wdrożonych nodach: kontener `Up (healthy)`, zero tracebacków,
sha256 `node_agent.py` w runtime == repo HEAD, brak wykonywalnego `containers.prune()`,
`docker ps -a` identyczne z pre-snapshotem, heartbeat świeży. `NODE_TYPE` zachowane
(SOLARIA `lte_node` = M1, LUSTRO `sd_card`). Marker `last-docker-cleanup` na LUSTRO
nie wyzerował się przy recreate.
Test e2e na SOLARII (kanarek `restart=unless-stopped` bez labela compose): zatrzymany,
przeżył 151 s (>2× CHECK_INTERVAL) jako `exited`, nie usunięty. Dry-run logiki filtra
bez wywołania `remove()`: REMOVE=0 na obu nodach.
Obrazy sprzed deployu otagowane `node-agent:rollback-pre-r1` na SOLARII i LUSTRO
(na SOLARII były dangling — groziło zjedzenie celu rollbacku przez przyszły prune).
### Narrative
> _user-provided summary_

View file

@ -1,65 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Sesja 2026-08-06 — Etap B: weryfikacja korpusu + fix NUL (ZAMKNIĘTY)
## Przebieg plastrów
Plastry 0-4 (`--offset 0/50000/100000/150000/200000 --limit 50000 --batch-size 64`)
**wszystkie EXIT 0** po fixie. Cały korpus 225k kopert przeskanowany
idempotentnie, zero strat.
## Bug znaleziony i naprawiony: NUL byte (0x00) w treści maila
Plaster offset 50k wywalił się na mailach z 2007 (Sony Ericsson, 3 chunki):
bajt NUL w tekście → `asyncpg.CharacterNotInRepertoireError` przy insercie
(PostgreSQL nie przyjmuje `0x00` w `text`).
Fix `4ec0b78`: strip `\x00` przed chunkowaniem i embedem + liczniki
`nul_bytes_stripped` / `mails_nul_sanitized`. Re-run plastra 1: **EXIT 0**,
3 chunki dobrane.
## Weryfikacja w DB (kb-postgres@PIHA, `document_chunk`)
| Miara | Wartość |
|---|---|
| `document_chunk` total | **389 012** |
| nie-excluded **bez** embeddingu | **0** |
| nie-excluded z wektorem | 187 025 |
| newsletter-flagged bez wektora | 201 849 (odwracalne) |
| excluded **z** wektorem | 138 (artefakt kolejności flagowania, nieszkodliwy) |
## Wniosek
**Korpus był w pełni zembedowany jeszcze przed dzisiejszymi plastrami.**
Zapamiętany stan „6,4k embedded z Etapu A, ~225k kopert do backfillu" był
nieaktualny — wcześniejsze przebiegi pokryły całość. Dzisiejsze runy to
w praktyce pełna, idempotentna weryfikacja korpusu (plus wykrycie i naprawa
buga NUL).
Źródło mylącego odczytu: licznik `chunks_already_embedded` liczy **istnienie
wiersza w DB** (w tym chunków newsletter-flagged bez wektora), a nie obecność
wektora — stąd niespójne wrażenie z liczników plastrów.
## Środowisko
- venv w głównym repo (`pip install -e` dla `kb-mail` / `kb-retrieval` /
`mail-body-ingest`).
- tmux `backfill`, logi w `~/kb/mail/ingest-logs/` (poza repo).
## Follow-upy
- `jobs/gmail-header-backfill` i `jobs/gmail-bulk-import` używają
`sanitize_surrogates` na nagłówkach zapisywanych do `jsonb` — **ta sama
latentna podatność na NUL**. Nieruszone w tej sesji, osobny task.
- **Rotacja hasła `kb-postgres`** — nadal otwarta (z sesji 2026-08-05).
## Następny krok fazy mailowej
**IMAP przyrostówka gmail + fastmail** (`source='fastmail'`) — teraz odblokowana.

View file

@ -1,107 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Sesja 2026-08-06 (wieczór) — przyrostówka IMAP na żywo (Krok 7 fazy mailowej DONE)
## Ścieżka: recon → decyzje → implementacja
Recon (`75d9695`, `kb/audits/mail-sync-2026-08-06.md`) → decyzje **(a)(g) zatwierdzone
w całości** → implementacja w trzech commitach:
| Commit | Zawartość |
|---|---|
| `c65f0f2` | adapter IMAP w `packages/kb-mail`, migracja 005 `mail_sync_state` |
| `f056b08` | job `mail-imap-sync` |
| `ae16deb` | takt `kb-ingest` co 2h + etap mailowy, korekta `kb-mail-pillar` (JMAP→IMAP) |
**642 testy.**
## Pierwsze uruchomienie wg runbooka `mail-sync-run.md` — z incydentami
### Run #1 — padł na `UID SEARCH ALL`
Gmail: `UID SEARCH ALL` na **227 900** wiadomości przekroczył `imaplib._MAXLINE`
(1 MB) → crash.
Przyczyna: błąd operatora w `.env``MAIL_GMAIL_INITIAL_MODE=full` zamiast `since`,
a `FASTMAIL_INITIAL_MODE` w ogóle niewpisany (fastmail zdążył zsynchronizować
new-only i zapisać stan).
> **FOLLOW-UP do CC:** utwardzić search na duże foldery — zakres UID / `SINCE`
> zamiast `ALL`, podbicie `_MAXLINE`. Obecnie **tryb `full` na dużym koncie = crash**.
### Naprawa
Korekta `.env` + `DELETE` stanu fastmail z `mail_sync_state` → run #2 czysty.
### Run #2 — initial
| Konto | Tryb | Seen | Inserted | Dup | Conflict |
|---|---|---:|---:|---:|---:|
| gmail | initial (SINCE=2026-06-15) | 1 872 | 1 692 | 180 | — |
| fastmail | initial-full | 96 | 87 | 1 | 8 (`conflict_other_source`) |
Razem **1 779 nowych kopert, 0 błędów**. Nakładka `SINCE` zadziałała; cross-account
dedup po `Message-ID` działa (te 8 konfliktów to ta sama poczta widziana z drugiego
konta).
### Run #3 — test przyrostowości
`mode=incremental`: gmail **+5**, fastmail **0**, kursor OK.
## Drenaż embed
`mail-body-ingest --only-unchunked`: **698 embeddingów** (baseline 187 163 → **187 861**).
> **Uwaga (follow-up):** tempo ~570 ms/chunk sugeruje, że `OLLAMA_URL=SOLARIA` nie
> przebił się przez `sudo env` i liczyło CPU PIHA. Do weryfikacji przy następnym
> dużym drenażu.
## Test end-to-end — PASS
Mail wysłany 17:21 (gmail→fastmail), obie kopie wylądowały poprawnie: envelope
gmail + `conflict_other_source` fastmail. Ścieżka **sync → ingest → hybrid search**:
top-1 dist **0.391**. Decyzja **(g) potwierdzona**.
## Automat
- `kb-mail-sync.timer`**enabled**, tick co ~1 h, pierwszy 18:01.
- `kb-ingest.timer` — co 2 h (nowa definicja `0/2:00:00`).
- Reguła `fleet-prometheus/kb-mail-sync.yml` wchodzi przy najbliższym deployu
fleet-prometheus *(follow-up)*.
## Poprawki do runbooka (follow-up, nie zrobione)
- Ręczne runy wymagają `sudo` do odczytu `.env` (`600 root:root`) — podać wariant
`sudo bash -c`.
- Krok 7: `curl` na `localhost:8230` nie zadziała — `kb-query` binduje na
`192.168.31.5`.
## Otwarte
- **Rotacja hasła `kb-postgres` (WISI)** — wyciekło 2026-08-05/06 do transkryptu
i historii shella. **Trzeci udokumentowany przypadek tej klasy.**
- Fix `UID SEARCH` (patrz Run #1).
- Załączniki PDF z maili (~336 + nowe).
- NUL w nagłówkach `jsonb` (`gmail-header-backfill` / `bulk-import`).
## Stan fazy mailowej po sesji
| Element | Status |
|---|---|
| (a) batching | ✓ |
| (b) Etap B | ✓ |
| (c) regresja | ✓ |
| (d) hybrid default | ✓ |
| Krok 7 — przyrostówka | ✓ **ŻYWA** |
Pozostało: backfill PDF, Google Drive Takeout.
**Następna duża rzecz wg roadmapy: FAZA 5 — wiki-kompilat.** Materiał mailowy jest
kompletny i świeży, więc warunek wejścia spełniony.

View file

@ -1,496 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-06
links: []
---
# Session log 2026-08-06
## Session 13:20
Produkcyjna weryfikacja pierwszego cyklu safe-cleanup na LUSTRO (R1/R2/R3, deploy
2026-08-05) oraz pierwszy pełny cykl HITL: approval → dispatch → wykonanie na nodzie →
`action_result``completed`. Sesja supervised, checkpointy zatwierdzane przez operatora,
approvale wykonywane wyłącznie przez operatora. Incydent źródłowy:
`docs/incidents/2026-07-30-ollama-solaria-vanish.md`.
### Commits
Ta sesja **nie wprowadziła żadnych commitów poza niniejszym logiem** — zero zmian w kodzie
i konfiguracji repo. Cała praca to recon read-only + kontrolowane zapisy runtime na
LUSTRO i VPS (kanarki testowe, plik akcji), wszystkie sprzątnięte lub udokumentowane niżej.
Poprzedni log sesji: `67aa092 docs: session 2026-08-05 22:47` — potwierdzony na
`origin/master` (ahead/behind 0/0) na starcie sesji.
### Files changed
Brak — poza tym plikiem.
---
## KROK 1 — safe-cleanup na LUSTRO: obie gałęzie potwierdzone produkcyjnie
### Stan wyjściowy
Marker `/opt/homelab/state/last-docker-cleanup` = `1785925612` (2026-08-05 12:26:52 CEST),
**sprzed deployu** (obraz node-agenta utworzony 22:42:05 CEST). Bramka
`_cleanup_rate_ok()` (`CLEANUP_INTERVAL_SECS = 86_400`) trzymała pierwszy cykl nowego kodu
do 12:26:52 dnia 2026-08-06. Brak linii cleanup w logach do tego momentu był więc
zachowaniem poprawnym, nie awarią.
Kod w runtime zweryfikowany: `md5(/app/src/node_agent.py)` == `md5(repo HEAD)` =
`c9ac64e10b42b3e0ed9e4c168579bfaa`, brak wykonywalnego `containers.prune()`,
`NODE_TYPE=sd_card`.
Pułapka interpretacyjna: logi kontenera node-agent są w **UTC**, host w CEST. Pozorna
6-godzinna dziura w logach to nocny `halt` (root cron `30 23 * * * /usr/sbin/halt`,
boot 06:30) — LUSTRO ma duty cycle jak SOLARIA, zgodnie z `inventory/topology.yaml`
(`duty_cycle: nightly`).
### Test w oknie prune
Do okna przygotowano trzy kontenery `exited` pokrywające obie gałęzie filtra:
| Kontener | Polityka / label | Oczekiwane | Wynik |
|---|---|---|---|
| `prune-disposable` | `restart=no`, brak compose | usunięty | ✅ usunięty |
| `prune-canary` | `restart=unless-stopped`, brak compose | zachowany | ✅ przeżył |
| `node-exporter` (realny serwis) | `restart=always`, brak compose | zachowany | ✅ przeżył |
Log z okna (12:27:49 CEST / 10:27:49 UTC):
```
INFO - Pruned dangling images (0 MB reclaimed)
WARNING - Removed 1 disposable stopped container(s): prune-disposable (kept 2 managed)
```
Marker zaktualizowany na `1786012069`. `kept 2` = `prune-canary` + `node-exporter`.
Wszystkie kontenery produkcyjne nietknięte.
**Werdykt:** obie gałęzie filtra działają na produkcji — gałąź ochronna (kontener zatrzymany
przez operatora z polityką restartu przeżywa prune; dokładny scenariusz incydentu ollamy)
oraz gałąź usuwania (filtr nie jest no-opem, faktycznie kasuje jednorazowe resztki).
### Korekta wniosku z 2026-08-05
Wczorajszy test kanarka na SOLARII (przeżył 151 s) **nie dowodził działania filtra**
SOLARIA ma `NODE_TYPE=lte_node` (mitygacja M1), gdzie `run_safe_cleanup()` kończy się
`return` przed jakimkolwiek prune. Ten test potwierdził M1, nie R1. Dowód dla R1 powstał
dopiero dziś na LUSTRO.
### Stan cleanupu na flocie
| Node | NODE_TYPE | Cleanup |
|---|---|---|
| PIHA | `sd_card` | ✅ działa (2026-08-05 16:27 CEST: `kept 0 managed`) |
| LUSTRO | `sd_card` | ✅ działa (2026-08-06 12:27:49, jw.) |
| SOLARIA | `lte_node` (M1) | wyłączony w całości, brak markera |
| VPS | `lte_node` (M1) | wyłączony w całości, brak markera |
M1 nadal zdjęte do zrobienia na SOLARII i VPS — do tego czasu te nody nie sprzątają
Dockera wcale.
---
## KROK 2 — dlaczego crash-loop watchtowera nie generował akcji
Eventy płynęły poprawnie: `evt-lustro-<ts>-containers_not_running-watchtower.json` co ~60 s,
11 317 plików w `events/lustro/` na VPS (node-agent rsyncuje z `--remove-source-files`,
stąd pusty katalog lokalny). To nie był problem transportu ani emisji.
**Przyczyna:** `supervisor.reconcile()` iteruje wyłącznie po `desired_state["services"]`
ładowanym z `hosts/<node>/services.yaml` (`supervisor.py:380`). `hosts/lustro/services.yaml`
deklaruje `node-agent`, `node-exporter`, `piper-tts` — watchtowera tam nie ma. Brak wpisu
w desired state ⇒ brak driftu ⇒ brak rekomendacji. Ponad 11 tys. eventów dead-enduje.
Observer natomiast **zna** `lustro/watchtower` (incydent `inc-1786007596-lustro-watchtower`,
status `unhealthy`) — rozjazd dotyczy wyłącznie supervisora.
Wykluczone: `shadow_mode` (dotyczy tylko `HA_DIAG_SHADOW_MODE`, ścieżka HA-diag),
`duty_cycle` (tłumi wyłącznie liveness node'a), progi/cooldown (`containers_not_running`
jest w `CONTAINER_RESTART_TRIGGERS`, dedup po stabilnym ID).
Konsekwencja druga: akcja wstawiona ręcznie do `pending/` dla serwisu spoza desired state
żyje jeden cykl supervisora. `_cancel_resolved_pending_actions()` (`supervisor.py:560`)
skasował ją po 15 s z powodem `service_removed_from_desired_state`. Kasowanie dotyczy
**wyłącznie `pending/`** — `approved/` i `running/` są z założenia nietykalne. Dlatego
akcja ręczna dla watchtowera trafiła ostatecznie prosto do `approved/`.
---
## KROK 3 — dwa pełne cykle HITL
Wszystkie znaczniki UTC (CEST = +2). Pętla executora: 10 s. Pętla node-agenta: 60 s.
### Cykl 1 — `node-exporter` (ścieżka w pełni organiczna)
| Etap | Timestamp | Δ |
|---|---|---|
| `docker stop node-exporter` (trigger) | 10:22:49 | — |
| event → observer → incydent `inc-1786011821-lustro-node-exporter` | ~10:23:41 | +52 s |
| supervisor: `Generated recommendation``pending/` | 10:24:00.66 | +71 s |
| approval operatora (`mv` → `approved/`) | ~10:38:5x | — |
| executor: `Executing action``running/` | 10:39:00.650 | ≤10 s |
| executor: `Dispatched … to node-agent on lustro` | 10:39:00.657 | +7 ms |
| node-agent: rsync-pull + bramki + `docker restart` | 10:39:23.15 | +22,5 s |
| event `action_result` (`success: true`) | 10:39:23.293 | +0,14 s |
| executor: `completed` | 10:39:30.752 | +7,5 s |
**Approval → completed: 30,1 s.**
### Cykl 2 — `pi-watchtower-1` (akcja utworzona ręcznie, zatwierdzona przez operatora)
| Etap | Timestamp | Δ |
|---|---|---|
| operator zapisuje akcję do `approved/` | 11:08:38 | — |
| executor: `Executing action``running/` | 11:08:40.829 | +2,8 s |
| executor: `Dispatched … (container=pi-watchtower-1)` | 11:08:40.838 | +9 ms |
| node-agent: `Restarted container 'pi-watchtower-1'` | 11:08:51.002 | +10,2 s |
| event `action_result` (`success: true`) | 11:08:51.002 | — |
| executor: `completed` | 11:09:00.909 | +9,9 s |
**Approval → completed: 20,1 s.** Watchtower wrócił do crash-loopa — zgodnie z założeniem;
sukcesem było przejście pipeline'u i poprawny `action_result`, nie uzdrowienie kontenera.
### Bramki agenta
- **Whitelista typu** i **node scoping** — przeszły; logują się tylko przy odrzuceniu,
więc dowodem przejścia jest sama egzekucja.
- **Self-restart guard** — nie dotyczył (cel ≠ `node-agent`).
- **Idempotencja** — zadziałała na żywo i wielokrotnie:
`Action … already processed — skipping (idempotency)`, markery
`/opt/homelab/state/processed-actions/<action_id>.done`.
### Obserwacja uboczna: regeneracja i auto-cancel
O 10:39:48 (18 s po udanym restarcie) supervisor **wygenerował ponownie** akcję dla
node-exportera, bo world state jeszcze pokazywał `unhealthy` (opóźnienie observera).
O 10:40:49 sam ją skasował (`drift_resolved_auto`). Podwójnego restartu nie było, ale
istnieje ~60-sekundowe okno, w którym po udanej remediacji potrafi powstać duplikat.
---
## KROK 4a — rekomendacja ws. poluzowania bramek HITL
**Rekomendacja: jeszcze nie, ale wąskie poluzowanie jest obronialne po trzech warunkach.**
Za:
- Pipeline przeszedł end-to-end dwukrotnie, w tym raz w pełni organicznie (event →
observer → supervisor → approval → executor → node-agent → wynik).
- Czas maszynowy to 2030 s; wąskim gardłem jest wyłącznie człowiek (dziś ~15 i ~30 min).
- `container_restart` jest tanie i odwracalne, wykonanie jest scoped do node'a, whitelisty
jednego typu akcji i guardu self-restartu; egzekutor nigdy nie wchodzi na node po SSH.
- Kolejka sama się czyści: `drift_resolved_auto` kasuje akcje, które przestały być
potrzebne, więc opóźniony approval nie powoduje zbędnego restartu.
- Idempotencja obroniła się w warunkach bojowych (patrz defekt dispatch niżej).
Przeciw:
- Próbka: 2 wykonania, 1 node, 1 typ akcji, obie ścieżki udane. **Ani razu nie zaobserwowano
ścieżki porażki** (`success: false`), timeoutu akcji w `running/`, ani odrzucenia przez
bramkę node/whitelisty. Dowód dotyczy szczęśliwej ścieżki.
- Restart nie leczy przyczyn źródłowych. Watchtower ma 1000+ restartów dziennie — automat
restartowałby go w kółko, maskując problem. Bez budżetu restartów (np. max 3/24 h na
serwis, potem eskalacja do `alert_only`) auto-remediacja produkuje pętlę zamiast naprawy.
- Otwarty defekt dispatch (niżej) w trybie automatycznym oznacza, że jedynym zabezpieczeniem
przed powtórnym wykonaniem jest marker idempotencji per `action_id`. Wystarczy nowy
`action_id` na ten sam objaw, by restart poszedł ponownie.
- Okno duplikatu (~60 s) po udanej remediacji — dziś skasowane w porę, ale to kwestia
wyścigu, nie gwarancji.
- `_get_container_name()` po cichu zwraca nazwę serwisu, gdy brak `services/<svc>/docker-compose.yml`.
Dla watchtowera dałoby to `watchtower` zamiast `pi-watchtower-1` — akcja wygenerowana
organicznie zakończyłaby się `failed`. W trybie automatycznym to stały szum porażek.
Warunki wstępne do poluzowania:
1. Naprawa wycieku plików dispatch (niżej) — inaczej automat stoi na jednej bramce.
2. Budżet restartów per serwis + eskalacja do `alert_only` po jego wyczerpaniu.
3. Poluzowanie tylko dla `container_restart` i tylko dla serwisów obecnych w desired state;
`redeploy` i `disk_cleanup` zostają w pełnym HITL.
---
## KROK 4b — root cause crash-loopa watchtowera
Log kontenera, każde uruchomienie:
```
level=error msg="Error response from daemon: client version 1.25 is too old.
Minimum supported API version is 1.40, please upgrade your client to a newer version"
```
`pi-watchtower-1` (`containrrr/watchtower`, obraz `c352868a1654`, kontener utworzony
2025-04-15) rozmawia z socketem Dockera przez API 1.25. Demon na LUSTRO wymaga minimum
1.40 i odrzuca połączenie, watchtower kończy się `exit 1`, `restart=always` uruchamia go
ponownie — cykl ~60 s. Licznik restartów kasuje się przy nocnym `halt`/boot, stąd
„973 restarty" to dorobek jednego dnia pracy, a nie narastająca awaria.
Co by go naprawiło (do backlogu, **nie wykonane w tej sesji**):
1. `docker pull containrrr/watchtower:latest` + recreate — aktualne wydania negocjują
nowsze API. Najprostsze.
2. Obejście: `DOCKER_API_VERSION=1.41` w env kontenera.
3. **Preferowane:** usunąć watchtowera z LUSTRO. To relikt spoza GitOps, a automatyczne
podmienianie obrazów na edge'owym Pi kłóci się z modelem repo jako źródła prawdy.
Jeśli ma zostać — dopisać go do `hosts/lustro/services.yaml`, bo dopiero wtedy stanie
się widoczny dla supervisora.
Efekt uboczny do rozważenia niezależnie: watchtower generuje ~1440 eventów/dobę, które
nigdzie nie prowadzą, i jest głównym powodem, dla którego `events/lustro/` ma 11 tys. plików.
---
## Follow-upy
1. **Wyciek plików dispatch (nowy defekt, potwierdzony).** Executor tworzy
`actions/dispatch/<node>/` z uprawnieniami **755** (`aerbot:aerbot`), a rsync-pull leci
jako `oskar` (grupa `aerbot`) — brak prawa zapisu w katalogu, więc
`--remove-source-files` nie kasuje źródła. Dla porównania `dispatch/piha` ma 775.
Dodatkowo rsync zwraca wtedy kod 23, który node-agent traktuje jako benign
(`returncode not in (0, 23, 24)`) → **cicha porażka, zero ostrzeżeń**. Skutek: LUSTRO
re-pulluje te same akcje co 60 s i odbija się od bramki idempotencji — w nieskończoność.
Docstring `pull_dispatched_actions()` twierdzi, że plik jest kasowany po pobraniu; nie jest.
Fix: `mkdir(mode=0o775)` w executorze + osobna obsługa rc=23 przy `--remove-source-files`.
Do czasu naprawy na LUSTRO trwa zombie re-pull dwóch plików co 60 s.
2. `_get_container_name()` — cichy fallback na nazwę serwisu przy braku
`services/<svc>/docker-compose.yml`; produkuje akcje celujące w nieistniejące kontenery.
3. Okno ~60 s, w którym po udanej remediacji powstaje duplikat akcji (opóźnienie observera).
4. Root cause watchtowera — patrz KROK 4b.
5. Zdjęcie M1 (`NODE_TYPE=lte_node`) na SOLARII i VPS — do tego czasu zero cleanupu Dockera
na obu nodach.
6. 17 zwietrzałych akcji w `pending/` z czerwca i lipca (16× `alert-*`, `redeploy-vps-gokapi`)
— nikt ich nie zamyka, zaśmiecają kolejkę operatora.
7. `events/lustro/` — 11 tys. plików, rosnące głównie przez watchtowera.
## Pominięte / niepewne
- Bramki node-scoping i whitelisty typu potwierdzone **tylko pośrednio** (przez udaną
egzekucję), bez testu negatywnego.
- Ścieżka porażki (`action_result` z `success: false`) oraz timeout akcji w `running/`
nie zostały przetestowane.
- Gałąź prune obrazów wykonała się na zerze — `0 MB reclaimed` przy 0 dangling images
przed i po. Potwierdza, że kod się wykonuje, nie że potrafi cokolwiek odzyskać.
- Wycieknięte pliki dispatch na VPS usunięte ręcznie przez operatora na koniec sesji.
Sam defekt (uprawnienia 755 + połknięty rc=23) pozostaje — wyciek wróci przy następnej
akcji dispatchowanej na LUSTRO.
- Cykl HITL sprawdzony wyłącznie na LUSTRO. PIHA (jedyny inny node z aktywnym dispatch)
nie był testowany.
## Sprzątanie
- `prune-canary` — usunięty po weryfikacji (12:29:25).
- `prune-disposable` — usunięty przez sam cleanup, zgodnie z zamysłem testu.
- Obraz `alpine` (ściągnięty na potrzeby kanarków) — usunięty.
- `node-exporter` — działa, podniesiony **przez pipeline HITL**, nie ręcznie.
- `actions/dispatch/lustro/` na VPS — opróżniony przez operatora (zombie re-pull ustał).
- LUSTRO na koniec: `node-agent` (healthy), `node-exporter` (up), `piper-tts` (up),
`pi-watchtower-1` (restarting — bez zmian, świadomie).
### Narrative
> _user-provided summary_
---
## Session 15:25
Wdrożenie do runtime dwóch fixów zmergowanych na `master` (supervised, checkpointy
zatwierdzane przez operatora): dispatch `0o775` + rc=23 w executorze/node-agencie
(`52eca1c`) oraz zdjęcie mitygacji M1 na SOLARII i VPS (`1bab321`). Domyka follow-upy
#1 i #5 z sesji 13:20.
### Commits
Ta sesja **nie wprowadziła żadnych commitów** poza niniejszym logiem — deploy runtime,
zero zmian w kodzie. Wdrożone commity powstały wcześniej, na branchu
`task/dispatch-perms-m1`:
```
1bab321 revert(m1): zdjecie NODE_TYPE=lte_node na SOLARII i VPS po wdrozeniu R1
52eca1c fix(dispatch): inbox 0o775 + rsync rc=23 przestaje byc cichy
```
W trakcie sesji main checkout przesunął się o `75d9695 docs(recon): przyrostowka IMAP
gmail + fastmail` (druga sesja operatora, docs-only). Bez wpływu: `node_agent.py` ma to
samo `sha256 aec6cb03` w obu drzewach, więc SOLARIA — zdeployowana jeszcze z `1bab321`
nie rozjechała się z VPS-em deployowanym z `75d9695`.
### Files changed
Brak — drzewo robocze czyste przez całą sesję, poza tym plikiem.
### Deploys
| Node | Serwis | Przed | Po | Wynik |
|---|---|---|---|---|
| SOLARIA | node-agent | `node_agent.py` `fee079e8`, obraz `438111e2` | `aec6cb03` == repo HEAD, obraz `bc28a30a` | OK |
| VPS | node-agent | `node_agent.py` `c21967d3`, obraz `27be875d` | `aec6cb03` == repo HEAD | OK |
| VPS | control-plane | `executor.py` `5ca0490e`, 4 obrazy z 2026-08-05 | `1c3b569f` == repo HEAD, 4 obrazy przebudowane | OK |
Mechanizm: `scripts/deploy/deploy-service.sh --build-if-needed` dla node-agenta (ta sama
ścieżka co 2026-08-05). **Nie** użyto `scripts/deploy/deploy.sh <target>`: jest to
dyspozytor Saturn-side po SSH, deployujący *cały* node — na VPS ruszyłby npm, outline,
joplin i ai-cluster, czyli daleko poza zakres, a sesja toczyła się z SOLARII (`ssh solaria`
to połączenie do samego siebie).
`NODE_TYPE` po zdjęciu M1: SOLARIA `lte_node`**`ai_node`** (jawnie w override),
VPS `lte_node`**linia usunięta**, `NODE_TYPE=""` z base compose → `_resolve_node_type()`
zwraca `standard`. Log startowy potwierdza jedno i drugie (`type=ai_node`, `type=standard`),
czyli przewidywanie z commita `1bab321` co do pustego stringa było trafne.
Gate testowy: **pytest niedostępny w main checkoucie** (`.venv` bez pytest,
`~/.local/bin/pytest` ma zepsuty `_pytest`). Oparto się na wyniku sprzed merge'a
(node-agent 70 passed, control-plane 173 passed) plus `docker build` obu stacków przy
deployu. Follow-up 15:25/#4.
### Pierwszy cykl cleanup po zdjęciu M1
Na obu nodach marker `/opt/homelab/state/last-docker-cleanup` **nie istniał** (M1 blokował
zapis od 2026-08-04), więc `_cleanup_rate_ok()` zwrócił `True` i prune poszedł w pierwszym
cyklu, ~0,5 s po starcie — zgodnie z ostrzeżeniem w `1bab321`. Dlatego kolejność w każdym
kroku była: **najpierw tagi rollback, potem deploy.**
SOLARIA:
```
INFO - node-agent starting: node=solaria type=ai_node
INFO - Pruned dangling images (0 MB reclaimed)
INFO - No disposable stopped containers (kept 0 managed)
INFO - Pruned build cache (91 MB reclaimed)
```
VPS:
```
INFO - node-agent starting: node=vps type=standard
INFO - Pruned dangling images (0 MB reclaimed)
INFO - No disposable stopped containers (kept 0 managed)
INFO - Pruned build cache (239 MB reclaimed)
```
**Zero ubytków kontenerów na obu nodach** — `docker ps -a` przed vs po, diff nazw pusty
(SOLARIA 9/9, VPS 24/24). `humanai-mailer` i `humanai-landing` (bez definicji w repo)
nietknięte. Tagi `rollback-*` przeżyły prune.
Dwie prognozy przedwdrożeniowe wymagały korekty — obie z tego samego powodu, że
`docker images` pokazuje **rozmiar pozorny z warstwami współdzielonymi**, a nie realny
odzysk:
* **SOLARIA, „4 dangling ≈ 553 MB":** cztery obrazy faktycznie zniknęły (35 → 32, przy
+1 nowym buildzie), ale `SpaceReclaimed` = **0 MB**. Ich warstwy są współdzielone z
control-plane i kb-query. Realny odzysk obrazów ≈ 70 MB (17,89 → 17,82 GB); z build
cache (606,1 → 510,5 MB) łącznie ≈ 165 MB.
* **VPS, „1 dangling 395 MB":** ten obraz to **żywy `outline-postgres-1`** — untagged, ale
oznaczony `U` (in use). Docker odmawia usunięcia obrazu używanego przez kontener, więc
prune go nie ruszył i **nie miał prawa ruszyć**. Realny odzysk to wyłącznie build cache
239 MB (z 250,8 MB reclaimable).
Kontener `control-plane-ui` na SOLARII stoi w stanie `created` — poza zasięgiem prune'a
podwójnie: `_prune_stopped_containers()` listuje wyłącznie `status=exited`, a kontener ma
i tak label compose oraz `restart=unless-stopped`.
### Weryfikacja fixu dispatch end-to-end
Stan wejściowy zdjęty **przed** deployem control-plane (patrz follow-up 15:25/#1 — inaczej
`deploy-local.sh` by go zatarł):
| ścieżka | mode | |
|---|---|---|
| `actions/dispatch/piha` | 775 | historycznie działał |
| `actions/dispatch/lustro` | **755** | wyciek |
| `actions/deploy` | 755 | inbox deploy-runnera, bez podkatalogów |
**Korekta do sekcji „Sprzątanie" z sesji 13:20.** Zapis „`actions/dispatch/lustro/` na VPS
— opróżniony przez operatora (zombie re-pull ustał)" **nie odzwierciedla stanu
faktycznego**: o 13:16 oba pliki (z 10:39 i 11:08) nadal leżały w źródle, a LUSTRO
re-pullowało je co 60 s aż do 13:18:23. Katalog zdrenował się dopiero w wyniku poniższego
testu — i mógł, bo dopiero wtedy miał prawa `775`.
Mechanizm potwierdzony co do joty: inbox `aerbot:aerbot 755`, a ciągnie z niego
`VPS_EVENTS_USER=oskar` — uid **1002**, tylko *członek* grupy `aerbot` (gid 1000). Grupa ma
`r-x` bez `w`, więc `--remove-source-files` nie może zrobić unlinku:
```
13:15:13 INFO - Action container-restart-lustro-node-exporter already processed — skipping
13:15:13 INFO - Action container-restart-lustro-watchtower already processed — skipping
13:16:16 ... (to samo) 13:17:19 ... (to samo)
```
**Test HITL.** Cel zmieniony z `watchtower` na `node-exporter`: watchtower na LUSTRO jest
w crash-loopie (137 restartów, `restart=always` — root cause opisany w KROKU 4b sesji
13:20), więc nie dałby czystego sygnału. Nowe `action_id`
(`container-restart-lustro-node-exporter-dispatchfix`), bo recykling starego odbiłby się od
bramki idempotencji na LUSTRO i zawiesiłby akcję w `running` do timeoutu.
**Ścieżka: przez `approved/`, nie przez approval operatora.** Plik `pending` zapisany
13:16:56 UTC został auto-anulowany przez supervisora po **7 sekundach**
(`drift_resolved_auto` — `node-exporter` był zdrowy), zanim operator zdążył kliknąć. To
dokładnie zjawisko z sekcji „Obserwacja uboczna: regeneracja i auto-cancel" sesji 13:20,
tym razem szybsze (7 s vs 15 s). Zgodnie z ustaleniem przed testem druga kopia trafiła
prosto do `approved/`; cel testu to mechanika dispatchu, nie ścieżka approvalu. Pole
`status` w tej kopii zostało `cancelled` — bez znaczenia, executor kieruje się katalogiem,
nie polem.
Wynik — wszystkie sześć kryteriów spełnione:
1. **755 → 775** dokładnie w momencie zapisu przez executora (13:17:23) ✅
2. plik akcji **zniknął** ze źródła po pobraniu i **nie wrócił** w kolejnych cyklach ✅
3. **oba zaległe pliki zdrenowane przy okazji**`dispatch/lustro/` całkowicie pusty ✅
4. spam `already processed — skipping` **ustał**: dwa pełne cykle (13:19:25, 13:20:28) z
zerem trafień ✅
5. akcja `completed` przez `action_result` po 70 s, `node-exporter` z nowym uptime ✅
6. zero ERROR/WARNING w executorze, observerze, supervisorze i node-agentach — poza znanym,
niezwiązanym crash-loopem watchtowera na LUSTRO ✅
`13:18:23 INFO - Restarted container 'node-exporter' for action
container-restart-lustro-node-exporter-dispatchfix`
**Czego ten test NIE dowiódł.** Druga połowa fixu — klasyfikacja rc=23 z WARNING-iem — na
LUSTRO nie pojechała: node-agent tam ma nadal `fee079e8` (ten sam sha, który SOLARIA miała
przed dzisiejszym deployem). `grep -c "rc=23"` = 0 w logach LUSTRO dlatego, że stary kod
fizycznie nie umie tego zalogować, a nie dlatego, że jest dobrze. Follow-up 15:25/#3.
### Stan tagów rollback
| Node | Tag | Image ID |
|---|---|---|
| SOLARIA | `node-agent:rollback-pre-dispatchfix` | `438111e2` |
| SOLARIA | `node-agent:rollback-pre-r1` (z 2026-08-05) | `c80a711f` |
| VPS | `node-agent:rollback-pre-dispatchfix` | `27be875d` |
| VPS | `control-plane-executor:rollback-pre-dispatchfix` | `b878d835` |
| VPS | `control-plane-observer:rollback-pre-dispatchfix` | `4b79511d` |
| VPS | `control-plane-supervisor:rollback-pre-dispatchfix` | `357b70d1` |
| VPS | `control-plane-operator-ui:rollback-pre-dispatchfix` | `e522bbaa` |
Pierwsze tagi `rollback-*` na VPS w ogóle. Otagowano wszystkie cztery obrazy control-plane,
nie tylko executora — `deploy-local.sh` przebudowuje cały stack, więc każdy potrzebuje celu
rollbacku.
### Follow-upy (sesja 15:25)
1. **`deploy-local.sh` robi rekurencyjny `chown` + `chmod` na całym `/opt/homelab`** — do
przeglądu, czy to w ogóle pożądane. Oba branche by dziś zadziałały: `sudo chown -R
1000:1000` (wyzwalacz: `events/solaria/evt-solaria-1785946768-node_health-node.json`) i
`sudo chmod -R 775` (wyzwalacze: `actions/dispatch/lustro`, `actions/deploy`). Ten drugi
ustawiłby `dispatch/lustro` na 775 z zupełnie innego powodu niż fix w executorze,
zacierając stan wejściowy testu, i nadałby 775 *plikom* w całym drzewie runtime (stąd
`-rwxrwxr-x` na heartbeatach i JSON-ach akcji). **Świadoma decyzja operatora: nie
naprawiać ręcznie.** Control-plane zdeployowano samym krokiem compose (`up -d --build
--force-recreate`) — bez sudo, więc rozjazdy chown/chmod zostały nietknięte i czekają na
przegląd. Dodatkowo `sudo` na VPS wymaga hasła, więc kanoniczna ścieżka i tak kończyłaby
się handoffem (exit 5).
2. **Lokalny control-plane na SOLARII** (executor/observer/supervisor/ui, `dispatch/solaria/`
z 2026-07-22) ma nadal executor z bugiem `0o755`. Nie jest dyspozytorem floty — log to
same `Starting executor loop` — ale to drugi, niezdeployowany egzemplarz tego samego
kodu. Osobna sesja.
3. **LUSTRO i PIHA bez dzisiejszego node-agenta.** LUSTRO: `fee079e8`. Bez fixu rc=23
nadal *połykają* nieudany unlink — dziś to nie boli, bo executor już nie tworzy złych
inboxów, ale każdy przyszły wyciek o innej przyczynie znowu będzie niewidoczny.
4. **Brak pytest w main checkoucie na SOLARII**`deploy.sh <target>` przewróciłby się na
gate (exit 2) bez `--no-gate`. Do naprawy, zanim ktoś sięgnie po kanoniczną ścieżkę
deployu z tej maszyny.
5. **Duplikat akcji testowej**`container-restart-lustro-node-exporter-dispatchfix.json`
leży jednocześnie w `completed/` (właściwy przebieg) i w `cancelled/` (kopia z
auto-anulowania sprzed approvalu). Pozostawione bez zmian — kosmetyka, żaden zapis
produkcyjny nie był potrzebny.
### Narrative
> _user-provided summary_

View file

@ -1,44 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-26
links: []
---
# Sesja 2026-08-26 — incydent kb-mail-sync: 20 dni ciszy, wykryte i naprawione
## Timeline
- **Wykrycie** — przy sanity checku po 3 tygodniach bez nadzoru (kontynuacja sesji 16:00,
patrz `docs/sessions/2026-08-26.md`): `mail-imap-sync` nie zapisał żadnej koperty od
2026-08-06 18:01 (pierwszy i jedyny udany tick automatu) mimo że timer tykał godzinowo
przez 20 dni. ~548 maili zaległości.
- **Diagnoza**`PermissionError` na `save_eml`: katalogi archiwum `2026/08` powstały
`root:root` z ręcznych `sudo`-runów 06.08, timer działa jako `oskar`. Kursor nie
przeskakiwał niezapisanej wiadomości (zgodnie z projektem) — stąd cisza bez utraty poczty,
ale też bez sygnału.
- **Naprawa zapisu**`chown -R oskar:oskar` na archiwum + dwa ręczne ticki: run#1
170 inserted/379 perm-errors (zaległości sprzed chowna), run#2 378 inserted/155
archive_exists/0 errors — pełne odzyskanie.
- **Reload Prometheusa** — przez CC z SOLARII (SSH na VPS): `promtool check rules` OK,
`POST /-/reload`, weryfikacja `/api/v1/rules` (wszystkie trzy grupy żywe), metryka
scrape'owana i świeża, `/api/v1/alerts` czyste, `brain-watchdog`→Telegram wpięty
poprawnie (`PROMETHEUS_URL` OK, kontener healthy).
Pełny opis root cause (oba, niezależne) i rekomendacje R1R3:
[kb/incidents/2026-08-26-mail-sync-20-dni-ciszy.md](../../kb/incidents/2026-08-26-mail-sync-20-dni-ciszy.md).
## Stan
Przyrostówka **znów żywa** i **po raz pierwszy faktycznie monitorowana** — reguła
`KbMailSyncStale` istniała w repo od 06.08, ale dopiero teraz jest realnie załadowana w
Prometheusie. Wcześniej alert nie mógł wystrzelić niezależnie od tego, jak długo trwałaby
awaria zapisu.
## Plan sprzed incydentu — przesunięty
Rotacja haseł (`kb-postgres` + teraz też `TG_TOKEN`, wyciekł w tej sesji przez `cat .env`
diagnostyczny — czwarty przypadek tej klasy) i recon Fazy 5 (wiki-kompilat) — obie pozycje
przesunięte na następną sesję, nie ruszone dzisiaj poza samą diagnozą.

View file

@ -1,207 +0,0 @@
## Session 16:00
Recon floty po 3 tygodniach bez nadzoru (ostatnia sesja 2026-08-06) + sesja
naprawcza: (A) usunięcie watchtowera z LUSTRO jako reliktu spoza GitOps,
(B) higiena kolejek `actions/pending` i zawieszonych incydentów `active` na VPS.
SUPERVISED — checkpointy A/B/C zatwierdzane przez operatora, backup przed
każdą operacją destrukcyjną.
### Commits
Ta sesja nie wprowadziła żadnych commitów w kodzie poza niniejszym logiem —
wyłącznie operacje na nodach (LUSTRO, VPS). Jeden niepowiązany commit
(`003f83d`, kb-publish skill) doszedł na `master` od równoległej sesji
operatora w międzyczasie — poza zakresem tej sesji.
```
(brak commitów tej sesji)
```
### Files changed
Brak zmian w repo.
### Deploys / operacje na nodach
**Recon (read-only, wszystkie 4 węzły: SOLARIA, PIHA, VPS, LUSTRO):**
- Zero nowych commitów na `origin/master` przez 3 tygodnie.
- Soak test R1 (auto-cleanup node-agenta): PIHA 20/20 uruchomień 0 usunięć,
VPS 21/21 uruchomień 0 usunięć, SOLARIA 3/3 0 usunięć, LUSTRO 5/5 —
1 usunięcie (`prune-disposable`, celowy kanarek testowy z 08-06, zgodnie
z zamysłem). Zero ofiar wśród kontenerów chronionych. `rc=23` nie wystąpił
ani razu — fix `0o775` z sesji 08-06 trzyma.
- Zdiagnozowano ciągłą pętlę restartów `pi-watchtower-1` na LUSTRO (API
Docker 1.25 vs wymagane min. 1.40), trwającą nieprzerwanie od co najmniej
2026-08-06 04:31.
**Watchtower LUSTRO — usunięcie (checkpoint A→B, zatwierdzony):**
- Backup: `docker inspect` + `compose.yml` + pusty katalog `/home/pi/watchtower`
`/home/pi/watchtower-removal-backup-2026-08-26/` na LUSTRO.
- `docker stop` + `docker rm pi-watchtower-1`, `docker rmi containrrr/watchtower:latest`,
`/home/pi/compose.yml` (jedyne źródło autostartu — brak systemd/cron) przeniesiony
do backupu jako `.disabled`.
- Weryfikacja: `docker ps -a` na LUSTRO czyste; 7 min ciszy zdarzeń
`containers_not_running-watchtower` na VPS (wymagane min. 5 min).
**Higiena kolejek VPS (checkpoint C→wykonanie, zatwierdzony):**
- Backup: `tar czf /opt/homelab/backups/actions-incidents-2026-08-26.tgz`
(`actions/` + `world/incidents.json`), sha256 `ef9afbfa...`.
- 17/18 pending → `cancelled/` (`stale_manual_cleanup`): 16× stare
`alert-node-*`/`alert-ha-*` z czerwca + 1× shadow-mode HA-websocket z 13.08
(kolizja nazwy pliku z niepowiązanym wpisem z 08-06 — zapisany pod nową
nazwą `container-restart-piha-homeassistant-shadowmode-20260813.json`,
żeby nie nadpisać cudzej historii).
- `redeploy-vps-gokapi` **pozostawiony** — realna luka wdrożeniowa (desired
w `hosts/vps/services.yaml`, brak kontenera), nie cruft. Follow-up do
sesji deploy.
- 5 incydentów w `world/incidents.json` ręcznie przełączonych na `resolved`
(`piha-homeassistant`, `solaria-narty27`, `solaria-prune-canary`,
`lustro-prune-canary`, `lustro-watchtower`) — wszystkie zdiagnozowane jako
trwale osierocone (brak mechanizmu auto-resolve dla zniknionej/przeniesionej
usługi, patrz Narrative).
- Sekwencja bez wyścigu: `docker stop control-plane-observer` → edycja pliku
`docker start` → weryfikacja >15 s (kilka cykli flush) — bo `_save_world()`
nadpisuje `world/*.json` co 5 s z pamięci procesu, bez merge z dyskiem.
- Efekt uboczny własnego restartu: `inc-...-vps-observer` (1 wystąpienie) —
rozwiązał się sam w ~60 s (poprawny, nieosierocony przypadek).
- Stan końcowy: `active_incidents_count: 0`, `runtime-summary.json status: nominal`.
### Narrative
> _user-provided summary_
## Session 23:00
Deploy control-plane (observer + supervisor) na VPS: stale-resolve 24h +
flagi `resolve-requests` (commit `71a7af5`), unikalny `action_id`
`container_restart` z bare-id fallbackiem (commit `91db682`), usunięcie
gokapi z desired state (commit `74ff3ee`) — zmerdowane do `master` jako
`f155999` przez operatora tuż przed sesją. SUPERVISED — checkpoint A po
weryfikacji deployu, checkpoint B po teście ścieżki flagi.
### Commits
```
(brak commitów kodu tej sesji — wyłącznie deploy + test na produkcji;
log sesji poniżej dopisany bez pusha)
```
### Files changed
Brak zmian w repo poza niniejszym logiem.
### Deploys / operacje na nodach
**KROK 0 — sanity:** `git pull` na `~/homelab-codex-ws` (SOLARIA, główny
checkout) — już aktualny na `03441a1` (na wierzchu mergu `f155999`).
`git status`/`diff HEAD` czyste. Wcześniej w tej samej sesji (przed
mergem) `git log -1` pokazywał `4fa10f0` — merge jeszcze nie istniał;
zatrzymano się i poczekano na operatora zamiast mergować samodzielnie
(worktree-aware: merge to wyłącznie krok człowieka).
**KROK 1 — deploy control-plane (checkpoint A, zatwierdzony):**
- Rollback tagi: `control-plane-{executor,observer,operator-ui,supervisor}
:rollback-pre-resolvefix` — ten sam wzorzec co `:rollback-pre-dispatchfix`
z 08-06.
- `git pull origin master` na VPS (`003f83d` → `03441a1`, fast-forward),
`docker compose up -d --build --force-recreate`.
- `deploy-local.sh`'s auto-chown krok padł: brak TTY dla hasła sudo,
`/opt/homelab/backups` i `/opt/homelab/events/solaria/*``oskar:oskar`
zamiast `aerbot:aerbot` (1000). Sprawdzone: `actions/`, `world/`,
`state/`, `config/` (realna ścieżka zapisu control-plane) już poprawnie
`aerbot:aerbot 775` — ominięto self-heal, `docker compose` odpalony
bezpośrednio bez sudo. Mismatch na `backups/`/`events/solaria/*`
pozostawiony nietknięty (follow-up niżej).
- Weryfikacja: 4/4 kontenery `healthy`, 0 linii error/traceback/exception
od restartu. sha256 `observer.py` (mount `/repo`, żywy) i `supervisor.py`
(wypieczony `/app/src`, wymaga `--build`) == repo HEAD, potwierdzone
osobno przez `docker exec` w obu kontenerach.
- Po 3 cyklach reconcile: `active_incidents: 0`, `world/resolve-requests/`
utworzony przez observera (pusty).
- `redeploy-vps-gokapi` (pending od 07-09) auto-cancelled po pierwszym
cyklu: `cancelled_reason: "service_removed_from_desired_state"` — bez
ponownego wygenerowania. `pending/` pozostał czysty (tylko niezwiązane
alerty HA z piha).
**KROK 2 — test ścieżki flagi na LUSTRO (checkpoint B, zatwierdzony):**
- `docker stop node-exporter` na LUSTRO → observer otworzył
`inc-1787777894-lustro-node-exporter`.
- Flaga: `touch world/resolve-requests/<id>` przez zwykłego SSH
usera **odrzucony permission denied** — katalog `755 aerbot:aerbot`,
brak zapisu grupowego mimo że `oskar` jest w grupie `aerbot`. Obejście:
`docker exec control-plane-observer touch ...` (proces w kontenerze
działa jako uid 1000 = właściciel katalogu). Follow-up niżej.
- Resolve w **1.01 s** od touch (limit ≤10s), `resolved_reason:
manual_operator`, flaga skasowana, `service.incident_id` wyczyszczony,
log INFO `"Manually resolving incident ... via resolve-request flag"`.
- Drift trwał dalej (kontener wciąż stopped) → supervisor wygenerował
`container-restart-lustro-node-exporter-1787777887` — **nowy format
id z COMMIT 2 potwierdzony na produkcji** — oraz równolegle
`redeploy-lustro-node-exporter` (bare id, ścieżka `unhealthy_service`
po wyczyszczeniu `incident_id`).
- Za decyzją operatora: `POST /action/mutate` na `127.0.0.1:18180`
(ten sam endpoint co UI/Telegram) — zatwierdzono restart, odrzucono
redeploy jako nadmiarowy.
- Executor zdispatchował realnie do LUSTRO; node-agent wykonał
`docker restart node-exporter` (log: `"Restarted container
'node-exporter' for action container-restart-lustro-node-exporter
-1787777887"`), akcja `completed`.
- Drift utrzymał się jeszcze chwilę po zatwierdzeniu → drugi, nowy
incydent (`inc-1787777955-...`) i druga, odrębna pending akcja
(`...-1787777950`, inny suffix `started_at`) — dokładnie oczekiwane
zachowanie "różny id przy nowym incydencie". Po powrocie zdrowia
auto-cancelled: `cancelled_reason: "drift_resolved_auto"`, bez
interwencji.
- Stan końcowy: `node-exporter` na LUSTRO `Up`, oba incydenty
`resolved`, `active_incidents: 0`, `pending/` czysty, 4/4 kontenery
control-plane nadal `healthy`, 0 error-ish linii w logach.
### Follow-upy
- **pytest env zepsuty na SOLARII**: `~/.local/bin/pytest` (brak
`_pytest`) i `homelab-codex-ws/.venv` (brak `pytest` w ogóle) oba
niedziałające; działa wyłącznie `/home/oskar/anaconda3/bin/pytest`
(7.4.4). Użyty do pełnego runu przed force-pushem poprawki COMMIT 2
(184 passed control-plane, 70 passed node-agent).
- **`world/resolve-requests/` permissions**: `755 aerbot:aerbot` zamiast
konwencji `775` używanej w `actions/`/`world/`/`state/`/`config/`.
Blokuje operatora SSH przed bezpośrednim `touch` flagi resolve —
manualna ścieżka z 71a7af5 ("operator drops a file") w praktyni wymaga
`docker exec`. Poprawić `chmod 775` / mode przy `os.makedirs` w
observer.py.
- **`backups/` i `events/solaria/*` ownership**: `oskar:oskar` zamiast
`aerbot:aerbot` — nie blokuje funkcjonalnie (czytelne dla "other"), ale
psuje self-heal chown w `deploy-local.sh` (próbuje rekurencyjnego sudo
chown całego `/opt/homelab` bez TTY/hasła). Do ręcznego wyczyszczenia
z hasłem sudo albo do zmiany self-heal na scoped (tylko katalogi
control-plane realnie potrzebuje) zamiast całego drzewa.
- **CLAUDE.md doc drift**: opisuje `events/YYYY-MM-DD/<node>/events.jsonl`,
rzeczywisty layout na VPS to płaskie `events/<node>/evt-*.json` (jeden
plik na zdarzenie, bez partycjonowania po dacie). Do poprawienia przy
najbliższej okazji.
- `redeploy-vps-gokapi` — zamknięty tym deployem (auto-cancelled), nie
wymaga już dalszego follow-upu z sesji 16:00.
### Narrative
> _user-provided summary_
## Session 23:06
### Commits
```
(brak commitów — sesja bez żadnej pracy poza natychmiastowym zamknięciem)
```
### Files changed
Brak zmian w repo.
### Deploys
None recorded
### Narrative
> _user-provided summary_

View file

@ -1,59 +0,0 @@
---
okf: "0.1"
type: session-log
visibility: private
status: active
updated: 2026-08-27
links:
- ../../kb/phases/kb-m5-faza5-wiki.md
- ../../kb/phases/kb-m5-faza3.md
- ../../kb/audits/wiki-kompilat-recon-2026-08-26.md
---
# Sesja 2026-08-27 — Faza 5: decyzje + Etap 1 + rekonсyliacja (ZAMKNIĘTY)
## Timeline
- **Sanity** — automat mailowy zdrowy po nocy (last_success świeży, gmail
max(ts) bieżący).
- **Formalizacja decyzji** (2df4f1d i wcześniejsze) — pakiet (a)-(h) audytu
`wiki-kompilat-recon-2026-08-26.md` ZATWIERDZONY w całości; inwariant 7
dopisany do faza3 §8.1 (izolacja retrievalu kompilacji od `source='wiki'`);
`check_okf.py` łapie bajty kontrolne (nowy check #12 + testy) — 4 NUL-e
usunięte z samego audytu.
- **Etap 1** (`task/wiki-etap1`, autonomiczny run CC ~25 min) — bootstrap +
3 strony proof (`sprawy/fll-2025-26`, `podmioty/mbank`,
`osoby/pawel-cesar-sanjuan-szklarz`). 40 sources + 47 przypisów
zweryfikowanych byte-for-byte, zero degradacji. Alias-resolution: 6 er
życia Pawła spójnie + 2 fałszywe pozytywy pod `paweld2.eu` wykryte i
udokumentowane jako wynik negatywny (kluczowy test mechanizmu PASS).
- **ODKRYCIE** — kb-wiki istniało na Forgejo od 2026-07-21 (5 stron: `pzu`,
`warta`, `ubezpieczenie-auto`, `fll-2025-26`, `wspólnota`) — proof
wykonany w fazie 3 (wątek "Kontynuacja wątku o KB", decyzja D7),
nieodnotowany w `kb/` ani w audycie. Recon 08-26 błędnie stwierdził
"remote nie istnieje" — sprawdził repo/pilota/bazę, nie odpytał Forgejo
bezpośrednio.
- **LEKCJA 1**: fakty wykonania muszą lądować w repo (lekcja 6 narty27 w
praktyce — zgubiliśmy całe repo na 5 tygodni).
- **LEKCJA 2**: recon zasobów zewnętrznych odpytuje źródło wprost, nie
wnioskuje z planów.
- **Rekonсyliacja** — porównanie dwóch niezależnych kompilacji `fll-2025-26`
(lipiec paperless-only vs sierpień paperless+gmail): komplementarne, zero
sprzeczności w faktach wspólnych; konwergencja metodologiczna (obie sesje
ten sam chunk 277/278, obie odmówiły potwierdzenia nieczytelnego OCR).
Scalenie: 21 envelope, 38 par sources, 45 przypisów re-zweryfikowanych;
naprawiony wadliwy lipcowy przypis; konwencja "Brak danych w KB"
sformalizowana. Potem mbank+paweł przeniesione do kanonicznego repo,
konwencje (d)-(f) scalone, walidator w repo. Stan końcowy kb-wiki@Forgejo
`a540a99`: 7 stron, lint 74/74 sources + 150/150 inline zielono.
- **kb-wiki remote** przepięty HTTPS→SSH (port 222).
- **Kandydat Etapu 2** wykryty SQL-em: `podmioty/future-minds` (organizator
FLL, 8+ dokumentów).
- **Follow-upy bez zmian**: rotacja sekretów (kb-postgres + `TG_TOKEN`
NADAL WISI), fix UID SEARCH, PDF-y, charset, R1/R2 z incydentu 26.08.
## Stan Fazy 5
- Etap 1 ✓ (2026-08-27). Next: Etap 2 — skala do 17+1 encji, lint w kodzie,
skan charset/NUL, decyzja o integracji `source='wiki'` w retrievalu
(inwariant 7 obowiązuje).

View file

@ -1,23 +0,0 @@
## Session 13:49
### Commits
a97cec0 merge: task/drobne-fixy (resolve-requests 775, prune out of health-monitor, events doc, redeploy action_id)
1dca438 fix(supervisor): apply started_at suffix to redeploy action_id too
40d78ce docs: fix events layout drift in CLAUDE.md
9a86843 fix(monitor): remove unfiltered docker container prune from health-monitor.sh
89f75c3 fix(observer): make world/resolve-requests/ group-writable
### Files changed
CLAUDE.md | 2 +-
scripts/monitor/health-monitor.sh | 110 +++------------------
scripts/observer/observer.py | 12 +++
services/control-plane/src/supervisor.py | 40 ++++----
.../control-plane/tests/test_incident_lifecycle.py | 17 ++++
.../tests/test_supervisor_action_id_uniqueness.py | 50 +++++++++-
6 files changed, 109 insertions(+), 122 deletions(-)
### Deploys
- control-plane → VPS: tagged 4/4 images `:rollback-pre-drobnefixy`, `git pull` (03441a1→a97cec0) + `docker compose up -d --build --force-recreate` (direct, no deploy-local.sh, per 26.08 precedent). Result: 4/4 healthy, zero error/traceback in logs since restart, sha256 of observer.py and supervisor.py match HEAD, `world/resolve-requests/` mode 775 confirmed, incidents.json stable (md5 unchanged) over 3 observer cycles. 30s permission test: flag `test-perms-123` dropped via plain `ssh` as `oskar` (no docker exec) was picked up and deleted in ~5s with `WARNING - Resolve-request flag for unknown incident test-perms-123 — removing flag` — 775 confirmed working in practice.
### Narrative
> _user-provided summary_

View file

@ -1,15 +1,16 @@
---
okf: "0.1"
type: runbook
visibility: private
status: active
updated: 2026-05-17
links:
- ../subsystems/stability-agent-architektura.md
---
# Stability Agent Multi-Node Rollout
## Architecture Summary
The `stability-agent` is a lightweight Python service that monitors node health (disk, Docker containers, Tailscale, MQTT) and publishes state to a central Redis instance running on **PIHA**.
- **Source**: `services/stability-agent`
- **State Path**: `/opt/homelab/state`
- **Events Path**: `/opt/homelab/events`
- **Redis Target**: `100.108.208.3:6379` (PIHA)
## Why UI only showed CHELSTY
Previously, the `stability-agent` had `NODE_NAME` defaulted to `chelsty` and was only deployed there. The Agent System UI materializer on PIHA filters nodes based on the Redis keys `homelab:nodes:<NODE_NAME>`. Without other agents publishing their specific `NODE_NAME`, the UI remained limited to the single active node.
## Deployment
Use the helper script to deploy or generate commands. The script uses explicit Tailscale IPs for remote targets (piha, chelsty, vps) and runs locally for solaria.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: public
status: active
updated: 2026-05-20
links: []
---
# Infrastructure Standards
This document defines the standards and conventions for the homelab GitOps-lite environment.

View file

@ -1,12 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: active
updated: 2026-05-11
links: []
---
# Homelab Topology
## Nodes

View file

@ -1,15 +1,3 @@
---
okf: "0.1"
type: subsystem
visibility: private
status: deprecated
updated: 2026-05-27
links:
- ../services/control-plane.md
- ../runbooks/control-plane-deploy-recovery.md
superseded_by: "przepisany tor redeploy, commity da151fc/79bfe8c 2026-08-03"
---
# VPS Control Plane
The VPS Control Plane is the orchestration brain of the homelab platform. It runs on the Hetzner VPS (Tailscale IP: `100.95.58.48`) and provides observability, automated reconciliation, and a web-based operator interface.
@ -60,6 +48,32 @@ The supervisor supports a `NODE_ALIAS_MAP` environment variable (JSON string) to
NODE_ALIAS_MAP='{"node-2": "chelsty-infra", "node-1": "piha"}'
```
## Deployment
### From SATURN (primary control node)
```bash
# Full deploy via SSH
./scripts/deploy/deploy-control-plane.sh --ssh
# Or manually:
ssh oskar@100.95.58.48 "cd ~/homelab-codex-ws && git pull origin master && cd services/control-plane && docker compose up -d --build --force-recreate"
```
### Direct on VPS
```bash
cd ~/homelab-codex-ws/services/control-plane
docker compose up -d --build --force-recreate
```
`deploy-local.sh` also creates the required `/opt/homelab/` directory structure and sets ownership to UID 1000 (requires `sudo`). If directories already exist, skip to the `docker compose` step directly.
### Verification
```bash
# On VPS
docker ps --filter "name=control-plane"
curl -s http://localhost:18180/summary | python3 -m json.tool
```
## Action Approval Workflow
```
@ -73,6 +87,28 @@ Supervisor writes → /opt/homelab/actions/pending/<id>.json
Possible action states: `pending → approved → running → completed / failed / rejected`
Auto-cancel path: `pending → cancelled/`
## Recovery
### World state is stale or corrupt
```bash
# On VPS — delete checkpoint to force full replay
rm /opt/homelab/state/observer_checkpoint.json
docker restart control-plane-observer
```
### Flood of pending actions after bootstrap
Check if node-agent is running and emitting `service_healthy` events on each node. Without `service_healthy`, the supervisor sees all services as missing and queues redeployments every cycle.
```bash
# Check node-agent on each node
ssh oskar@<node> "docker ps --filter name=node-agent && docker logs node-agent --tail 20"
```
### Rebuild from scratch
```bash
ssh oskar@100.95.58.48 "cd ~/homelab-codex-ws/services/control-plane && docker compose up -d --build --force-recreate"
```
## Integration
### piha agent-system webui (port 18180 on piha)

View file

@ -1,5 +0,0 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml

View file

@ -1,21 +0,0 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, remote_transmitter
CONF_TRANSMITTER_ID = "transmitter_id"
gree_ir_ns = cg.esphome_ns.namespace("gree_ir")
GreeIR = gree_ir_ns.class_("GreeIR", climate.Climate, cg.Component)
CONFIG_SCHEMA = climate.climate_schema(GreeIR).extend({
cv.Required(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = await climate.new_climate(config)
await cg.register_component(var, config)
tx = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(tx))

View file

@ -1,85 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
namespace esphome {
namespace gree_ir {
class GreeIR : public climate::Climate, public Component {
public:
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *tx) {
this->transmitter_ = tx;
}
void setup() override {
this->mode = climate::CLIMATE_MODE_OFF;
this->target_temperature = 24;
this->publish_state();
}
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY,
});
traits.set_visual_min_temperature(16);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(1);
return traits;
}
void control(const climate::ClimateCall &call) override {
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_target_temperature().has_value())
this->target_temperature = *call.get_target_temperature();
this->transmit_state_();
this->publish_state();
}
protected:
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
void transmit_state_() {
uint8_t b[8] = {0, 0, 0, 0, 0, 0, 0, 0};
uint8_t mode_byte, b2, b1_0;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL: mode_byte = 0x19; b2 = 0x60; b1_0 = 0x06; break;
case climate::CLIMATE_MODE_HEAT: mode_byte = 0x4C; b2 = 0x40; b1_0 = 0x01; break;
case climate::CLIMATE_MODE_FAN_ONLY: mode_byte = 0x1B; b2 = 0x60; b1_0 = 0x02; break;
default: mode_byte = 0x44; b2 = 0x00; b1_0 = 0x01; break;
}
int temp = (int) this->target_temperature;
if (temp < 16) temp = 16;
if (temp > 30) temp = 30;
uint8_t t = (this->mode == climate::CLIMATE_MODE_OFF) ? 0x0A : (uint8_t)(temp - 16);
b[0] = mode_byte; b[1] = t; b[2] = b2; b[3] = 0x50;
b[4] = b1_0; b[5] = 0x00; b[6] = 0x00;
uint8_t cs = ((b[0] & 0xF) + (b[1] & 0xF) + (b[2] & 0xF) + (b[3] & 0xF) +
((b[4] >> 4) & 0xF) + ((b[5] >> 4) & 0xF) + ((b[6] >> 4) & 0xF) + 0x0A) & 0xF;
b[7] = (cs << 4) & 0xF0;
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(38000);
const int MK = 677, S0 = 553, S1 = 1630, GAP = 19960;
data->mark(9030); data->space(4460);
for (int by = 0; by < 4; by++)
for (int bit = 0; bit < 8; bit++) { data->mark(MK); data->space(((b[by] >> bit) & 1) ? S1 : S0); }
const int foot[3] = {0, 1, 0};
for (int i = 0; i < 3; i++) { data->mark(MK); data->space(foot[i] ? S1 : S0); }
data->mark(MK); data->space(GAP);
for (int by = 4; by < 8; by++)
for (int bit = 0; bit < 8; bit++) { data->mark(MK); data->space(((b[by] >> bit) & 1) ? S1 : S0); }
data->mark(MK);
transmit.perform();
}
};
} // namespace gree_ir
} // namespace

View file

@ -1,28 +0,0 @@
#!/bin/bash
set -e
# Manual deploy only — this device is not managed via hosts/piha/services.yaml.
# Run by hand on piha after wiring the board.
cd "$(dirname "$0")"
if [ ! -f secrets.yaml ]; then
echo "ERROR: secrets.yaml missing. Copy secrets.yaml.example to secrets.yaml and fill in wifi_ssid/wifi_password/fallback_password." >&2
exit 1
fi
USB=false
if [ "$1" = "--usb" ]; then
USB=true
fi
if [ "$USB" = true ]; then
if [ ! -e /dev/ttyUSB0 ]; then
echo "ERROR: /dev/ttyUSB0 not found. Plug in the board via USB for the first flash." >&2
exit 1
fi
echo ">>> First flash over USB (/dev/ttyUSB0)..."
docker compose run --rm --device /dev/ttyUSB0 esphome run gree-ir-blaster.yaml --device /dev/ttyUSB0
else
echo ">>> OTA flash over WiFi..."
docker compose run --rm esphome run gree-ir-blaster.yaml
fi

View file

@ -1,7 +0,0 @@
# Not a daemon — no `up -d`. ESPHome compiles/flashes and exits.
# Invoke via deploy.sh, which wraps `docker compose run --rm esphome ...`.
services:
esphome:
image: ghcr.io/esphome/esphome:stable
volumes:
- .:/config

View file

@ -1,33 +0,0 @@
esphome:
name: gree-ir-blaster
friendly_name: Gree IR Blaster
esp8266:
board: d1_mini
external_components:
- source:
type: local
path: components
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
ap:
ssid: "Gree-IR Fallback"
password: !secret fallback_password
logger:
api:
ota:
platform: esphome
remote_transmitter:
id: tx
pin: D5
carrier_duty_percent: 50%
climate:
- platform: gree_ir
name: "Klimatyzacja"
transmitter_id: tx

View file

@ -1,5 +0,0 @@
# Copy to secrets.yaml and fill in real values. secrets.yaml is gitignored —
# never commit it. Filled in manually on piha.
wifi_ssid: "your-wifi-ssid"
wifi_password: "your-wifi-password"
fallback_password: "your-fallback-ap-password"

View file

@ -1,5 +1,4 @@
hostname: chelsty-ha
os_hostname: chelsty-ha
site: chelsty
roles:

View file

@ -1,5 +0,0 @@
# CHELSTY-INFRA
Runtime environment and deployment flow for CHELSTY, an offline-capable home automation edge node split across two VMs.
Dokumentacja: [kb/nodes/chelsty-infra.md](../../kb/nodes/chelsty-infra.md)

View file

@ -1,5 +1,4 @@
hostname: chelsty-infra
os_hostname: chelsty-infra
site: chelsty
roles:

View file

@ -8,4 +8,4 @@ services:
- VPS_EVENTS_PATH=/opt/homelab/events
- CHECK_INTERVAL=60
volumes:
- /home/oskar/.ssh:/home/homelab/.ssh:ro
- /home/oskar/.ssh:/root/.ssh:ro

View file

@ -13,11 +13,7 @@ services:
- VPS_EVENTS_PATH=/opt/homelab/events
- CHECK_INTERVAL=60
volumes:
# pi's SSH key for rsync event shipping to VPS (push-based node, no repo
# checkout). Container runs as uid 1000 (homelab, HOME=/home/homelab) per
# the base compose — ssh has no -i flag, so the key must land in
# /home/homelab/.ssh, NOT /root/.ssh. uid match (pi=1000) satisfies
# OpenSSH strict ownership checks on the mounted key.
- /home/pi/.ssh:/home/homelab/.ssh:ro
# pi's SSH key for rsync event shipping to VPS (push-based node, no repo checkout)
- /home/pi/.ssh:/root/.ssh:ro
# Override ../.. from the base compose to the pushed deploy dir (no repo on node)
- /opt/homelab/deploy/node-agent:/repo:ro

View file

@ -1,42 +0,0 @@
host: lustro
services:
node-agent:
role: node-stability-monitor
deployment_model: docker-compose
exposure: local-only
offline_required: true
depends_on:
local: []
external: []
runtime:
config_path: /opt/homelab/config/node-agent
data_path: /opt/homelab/state
logs_path: /opt/homelab/events
node-exporter:
# Keyed node-exporter (hyphen) — that is the container / world-state name
# on lustro; a node_exporter entry would drift as missing_service.
role: metrics-exporter
deployment_model: docker-compose
exposure: local-only
offline_required: true
depends_on:
local: []
external: []
piper-tts:
# TTS engine for the MagicMirror. No services/piper-tts dir in the repo —
# deployed locally on the Pi; verified running 2026-07-30 via world state
# (lustro/piper-tts healthy) and its service_healthy event stream
# (recon F20.11).
role: tts-engine
deployment_model: docker-compose
exposure: local-only
offline_required: true
depends_on:
local: []
external: []
# watchtower also runs on lustro (world state: lustro/watchtower) — a
# container auto-updater, left unmanaged deliberately; not desired state.

View file

@ -1,5 +1,14 @@
# PIHA
# PIHA - Infrastructure + Automation Node
Infrastructure + Automation Node.
## Role
- Core network services.
- Home automation (Home Assistant).
- Monitoring and logging.
Dokumentacja: [kb/nodes/piha.md](../../kb/nodes/piha.md)
## Configured Services
- Home Assistant
- Mosquitto (MQTT)
- Zigbee2MQTT
## Runtime Data
- `/opt/homelab/data/homeassistant`

View file

@ -5,7 +5,7 @@ capabilities:
cores: 4
threads: 4
memory:
total_gb: 8
total_gb: 4
acceleration:
type: none
@ -15,9 +15,8 @@ capabilities:
storage:
persistence: persistent
type: nvme
capacity_gb: 477
home_gb: 410
type: sd-card
capacity_gb: 32
networking:
reachability: tailscale-only

View file

@ -1,5 +1,4 @@
hostname: piha
os_hostname: piha
roles:
- infra

View file

@ -1,48 +0,0 @@
# Host-level systemd units on PIHA — declaration only.
#
# These are NOT docker-compose services, so they do not belong in services.yaml (whose
# entries the supervisor matches against world-state service keys; adding a non-container
# entry there would drift forever as missing_service). Nothing reads this file: it exists so
# the units installed outside the compose pipeline are written down in the repo rather than
# living only on the node and in a runbook.
#
# This is the "shadow-deploy family" the multiagent recon flags in its open question 5
# (kb/subsystems/recon-multiagent.md) — units installed outside GitOps drift detection.
# kb-mail-sync joins that list knowingly, not by oversight. When question 5 is settled, this
# file is the inventory to settle it against.
#
# Installation and activation are always operator steps. A `git pull` never starts a timer.
host: piha
systemd_units:
kb-ingest:
unit: kb-ingest.timer
service: kb-ingest.service
source: jobs/documents-ingest/systemd/
schedule: "0/2:00:00" # every 2 h — see the note in the .timer file
runs_as: oskar
environment_file: /opt/homelab/kb/.env
log_path: /opt/homelab/logs/kb-ingest/
metrics: /opt/homelab/state/node-exporter/kb-ingest.prom
state: active # installed and enabled since 2026-07-30
description: >
Cyclic KB ingest: paperless adapter, chunk+embed, summarize, embed summaries, and
(since 2026-08-06) the mail body stage that drains the unchunked-envelope queue
mail-imap-sync fills. Both embed stages are gated on an Ollama@SOLARIA probe.
kb-mail-sync:
unit: kb-mail-sync.timer
service: kb-mail-sync.service
source: jobs/mail-imap-sync/systemd/
schedule: hourly
runs_as: oskar
environment_file: /opt/homelab/kb/.env
log_path: /opt/homelab/logs/kb-mail-sync/
metrics: /opt/homelab/state/node-exporter/kb-mail-sync.prom
state: declared # NOT installed, NOT enabled — operator activates
runbook: kb/runbooks/mail-sync-run.md
description: >
Incremental IMAP fetch for gmail (\All) and fastmail (INBOX, Archive, Sent) into the
.eml archive and the envelope table. Network-bound, no GPU — which is why it lives on
PIHA (24/7) and is decoupled from the embedding stages on SOLARIA (~16 h/day off).

View file

@ -6,10 +6,3 @@ services:
# here ensures the webui /snapshot matches the clean 97-service state that
# the control-plane /summary endpoint serves.
CONTROL_PLANE_URL: "http://100.95.58.48:18180"
webui:
environment:
# Same VPS control plane as runtime-materializer above: makes the panel
# read the mirrored actions.json and proxy approve/reject there instead
# of its own always-empty local ACTIONS_DIR (Action Queue mirror fix).
CONTROL_PLANE_URL: "http://100.95.58.48:18180"

View file

@ -1,12 +0,0 @@
services:
ha-diag-agent:
environment:
- NODE_NAME=piha
# Pin events to the piha-specific subdirectory; overrides the ${NODE_NAME}
# variable substitution in the base compose file which requires a shell env var.
volumes:
- /opt/homelab/events/piha:/events
- /var/lib/ha-diag-agent:/data
- /opt/homelab/config/ha-diag-agent:/config:ro
mem_limit: 128m
restart: unless-stopped

View file

@ -1,102 +0,0 @@
# PIHA-specific overrides for kb-postgres (KB spine).
#
# WHY PIHA: the KB store must answer queries 24/7. SOLARIA (GPU/compute) is
# powered down intermittently; PIHA (Raspberry Pi 5, always-on, mains power) is
# the right home for an always-available spine. Embeddings/models still run on
# SOLARIA's GPU — only the Postgres+pgvector store lives here.
#
# IMAGE / ARCH: pgvector/pgvector:pg16 is multi-arch and publishes a linux/arm64
# manifest, so it runs natively on the Pi 5 (arm64) — no emulation. We do NOT
# change the pg16 tag; arch is handled by the manifest list, not the tag.
#
# RESOURCE CONTEXT: PIHA has 8 GB RAM, but ~6 GB is already resident
# (Home Assistant, Immich, monitoring). Only ~2 GB is free (+4 GB swap). This is
# a RAM-bound box shared with Home Assistant — Postgres MUST NOT starve HA.
# Everything below is sized to keep kb-postgres's resident set near ~0.50.8 GB
# under normal load, with a hard 1 GB ceiling.
services:
kb-postgres:
# Hard cgroup ceiling. ~1 GB (not the 4 GB used on SOLARIA). If Postgres ever
# exceeds this, the cgroup OOM killer restarts the container via Docker —
# Postgres recovers cleanly via crash recovery — instead of letting the host
# OOM killer pick a victim (which could be Home Assistant). 1 GB comfortably
# covers the worst-case allocation below.
mem_limit: 1g
# Soft floor for the scheduler: reserve enough that shared_buffers (256 MB)
# plus connection/backend overhead is not constantly contended under memory
# pressure, without hard-pinning a full GB away from HA.
mem_reservation: 512m
# Postgres tuning for a tight, shared RAM budget. Passed as server args so we
# need no mounted postgresql.conf. Defaults (shared_buffers 128 MB, work_mem
# 4 MB, max_connections 100) assume a dedicated box — far too loose here.
#
# shared_buffers=256MB Postgres's own page cache. ~25% of the 1 GB
# ceiling — the standard rule of thumb. Bigger
# would crowd HA; smaller hurts cache hit rate
# for the envelope + pgvector working set.
# effective_cache_size=512MB Planner hint only (allocates nothing). Tells
# the planner how much OS+PG cache it can assume
# for this DB's share of the box, so it favours
# index scans appropriately. Conservative given
# the page cache is shared with HA/Immich.
# work_mem=8MB Per-sort/hash node. With max_connections=30 the
# worst case is bounded (~30 * a few nodes * 8MB);
# keeps a runaway analytic query from blowing the
# budget. Small enough for a Pi, big enough for
# typical KB lookups.
# maintenance_work_mem=64MB For VACUUM / CREATE INDEX (incl. building the
# pgvector ivfflat/hnsw index). One-at-a-time and
# transient, so a larger value than work_mem is
# safe and speeds index builds.
# max_connections=30 KB clients are a handful of agents/jobs, not a
# web fleet. Capping at 30 bounds per-backend RAM
# (each backend ~510 MB) and the work_mem blast
# radius. Raise only if a real client count needs it.
#
# Sanity check on the ceiling: 256 MB shared_buffers + ~30 backends * ~10 MB
# overhead (~300 MB) + bounded work_mem spikes stays well under mem_limit=1g.
command:
- "postgres"
- "-c"
- "shared_buffers=256MB"
- "-c"
- "effective_cache_size=512MB"
- "-c"
- "work_mem=8MB"
- "-c"
- "maintenance_work_mem=64MB"
- "-c"
- "max_connections=30"
# ---------------------------------------------------------------------------
# DATA PLACEMENT — must land on the NVMe (/home, ~170 GB free), NEVER the SD card.
#
# The base compose uses the Docker-managed named volume `kb_postgres_data`, which
# physically lives under Docker's data-root. On PIHA, Immich already stores its
# (large) photo library in Docker volumes here, which is only possible if the
# data-root sits on the NVMe — so the plain named volume should already land on
# NVMe and is the convention used by other PIHA services (e.g. vikunja).
#
# BEFORE FIRST DEPLOY, verify on PIHA:
# docker info -f '{{.DockerRootDir}}' # expect a path on the NVMe
# df -h "$(docker info -f '{{.DockerRootDir}}')" # confirm it's the NVMe fs
#
# If (and only if) the data-root is NOT on the NVMe, pin the volume explicitly to
# an NVMe path by uncommenting the block below. The official postgres entrypoint
# runs as root and chowns PGDATA to the in-container postgres user (uid 999) on
# startup, so PIHA's host-uid 1004-vs-1000 skew does not apply to PGDATA itself —
# but the bind *device* directory must pre-exist (Docker will not create it):
# sudo mkdir -p /home/oskar/homelab-data/kb-postgres
# sudo chown 1004:1004 /home/oskar/homelab-data/kb-postgres # host owner; PG re-chowns PGDATA to 999 inside
#
# volumes:
# kb_postgres_data:
# name: kb_postgres_data
# driver: local
# driver_opts:
# type: none
# o: bind
# device: /home/oskar/homelab-data/kb-postgres
# ---------------------------------------------------------------------------

View file

@ -1,10 +0,0 @@
# PIHA-specific overrides for kb-query (KB module 5, phase 4).
#
# FastAPI + uvicorn, stateless (no local DB/queue) -- same weight class as
# llm-gateway (~256m). Kept off oom_score_adj: -900 -- that's reserved for
# control-plane/agent processes that must never be an OOM victim; kb-query is
# a search API, restart-on-OOM (default cgroup behaviour) is an acceptable
# failure mode, unlike for the agents that watch the fleet.
services:
kb-query:
mem_limit: 256m

Some files were not shown because too many files have changed in this diff Show more