Compare commits
No commits in common. "master" and "task/node-onboarding" have entirely different histories.
master
...
task/node-
|
|
@ -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`.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
10
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"mcpServers": {
|
||||
"ha": {
|
||||
"command": "./services/ha-mcp/run.sh",
|
||||
"args": [],
|
||||
"env": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
CLAUDE.md
34
CLAUDE.md
|
|
@ -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 | — |
|
||||
|
|
|
|||
24
README.md
24
README.md
|
|
@ -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`.*
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 |
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 | 180–600 s | 900–3600 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
65
docs/questions.md
Normal 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?
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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_
|
||||
|
|
@ -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` L65–72.
|
||||
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`.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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ąć.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 ~7–14 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.
|
||||
|
|
@ -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 2011–2012 (~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.)
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.45–0.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 1–8 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.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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).
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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].
|
||||
|
|
@ -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 1–2 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-3–1e-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.
|
||||
|
|
@ -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_
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 S1–S4 (`cb8a19d`):
|
||||
- `retrieval_eval.py --transport http`
|
||||
- session log 27.07
|
||||
- testy luk T1–T3
|
||||
- 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.45–0.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. **R1–R3 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 R1–R3 (unfiltered prune) — eskalacja do subsystemu A;
|
||||
mitygacja M1 na SOLARII na czas backfillu.
|
||||
|
|
@ -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` (R1–R3 + 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).
|
||||
|
|
@ -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_
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -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 20–30 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_
|
||||
|
|
@ -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 R1–R3:
|
||||
[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ą.
|
||||
|
|
@ -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/*` są `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_
|
||||
|
|
@ -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).
|
||||
|
|
@ -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_
|
||||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
|
@ -1,12 +1,3 @@
|
|||
---
|
||||
okf: "0.1"
|
||||
type: subsystem
|
||||
visibility: private
|
||||
status: active
|
||||
updated: 2026-05-11
|
||||
links: []
|
||||
---
|
||||
|
||||
# Homelab Topology
|
||||
|
||||
## Nodes
|
||||
|
|
@ -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)
|
||||
5
hardware/esp/ir-ac-ha-integration/.gitignore
vendored
5
hardware/esp/ir-ac-ha-integration/.gitignore
vendored
|
|
@ -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
|
||||
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
hostname: chelsty-ha
|
||||
os_hostname: chelsty-ha
|
||||
site: chelsty
|
||||
|
||||
roles:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
hostname: chelsty-infra
|
||||
os_hostname: chelsty-infra
|
||||
site: chelsty
|
||||
|
||||
roles:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
hostname: piha
|
||||
os_hostname: piha
|
||||
|
||||
roles:
|
||||
- infra
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.5–0.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 ~5–10 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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -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
Loading…
Reference in a new issue