feat: slice 3 - wiki, sources and AARs in their neckbeard homes

Gate 4, slice 3: verfahren/, hosts/, vision/ and shared/ moved via git
mv - six AARs to docs/aar/ (four harvested by the 2026-08-09 retro,
two open), procedures and host knowledge to docs/wiki/ (admin,
deployment, architecture, new area vision), the retro protocol and the
commit mapping table to docs/sources/ (protokolle/, migration/). New:
the wiki index linking every page, and the mirror-topology page
carrying the why-two-places reasoning verbatim from the old CLAUDE.md
(F-013 preserved). All moved-path references retargeted; the link
checker drove the sweep to zero.

pruefe_prosa.py added (pattern C+D): SHA citations resolve via repo,
mapping table, optional component clones or a curated exemption list
(documented dead Gitea-force-push commits, a vendor-repo tag, an
Authentik uid that is hex but no git SHA, the external neckbeard
reference); wiki task prose without an issue reference errors, with a
visible pragma for deliberate checklists; the dead-tracker denylist
now covers every mirrored repo's retired Gitea tracker (F-005) - two
links re-verified against live GitLab titles and retargeted, five
defused into honest historical citations.

Verified: validate 0/0, gen_status --check current, drift 0. Demo on
the pre-migration state fires 6 findings (3 orphaned SHAs, 3 task
blocks); on the current tree exactly the 3 F-004 task blocks remain -
they turn green in slice 4 when the issues exist, which is why
pruefe_prosa joins CI only then.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Thore Cimbal
2026-08-11 12:00:00 +00:00
co-authored by Claude Fable 5
parent 70e81e2ff1
commit 92b448fe30
37 changed files with 424 additions and 120 deletions
-25
View File
@@ -1,25 +0,0 @@
# Verfahren
Wiederkehrende Abläufe zwischen Personen und Hosts — dort festgehalten, wo sie
nicht an einem einzelnen Projekt-Repo hängen.
| Datei | Inhalt |
|---|---|
| [deploy-uebergabe.md](deploy-uebergabe.md) | Ablauf und Prüfliste für „einer baut, ein anderer rollt aus" |
| [aar-vorlage.md](aar-vorlage.md) | Vorlage für den After Action Report nach einem Deploy |
| [aar/](aar/) | Abgelegte AARs, benannt `JJJJ-MM-TT-<vorhaben>.md` |
[`textbloecke.md`](textbloecke.md) hält kurze, kopierbare Blöcke, die man einer
Session voranstellt — sie verweisen auf die Konventionen, statt sie zu wiederholen.
**Baustein 4 (Abschluss) ist zugleich die Definition of Done für Änderungen ohne
Deploy**; für Deployments gilt [deploy-uebergabe.md](deploy-uebergabe.md).
Die zugehörige Issue-Vorlage liegt unter
`.gitlab/issue_templates/Deploy-Übergabe.md` und erscheint beim Anlegen eines
Issues in diesem Repo im Auswahlfeld *Description template* als
**Deploy-Übergabe**.
Abgrenzung zum Rest des Repos: `hosts/` und `shared/` halten **offene Punkte**,
dieses Verzeichnis hält **wie wir arbeiten**. Ein Verfahren wird hier nur
aufgenommen, wenn es mindestens einmal an einem echten Vorfall gescheitert
oder bewährt ist — der auslösende AAR wird jeweils verlinkt.
-45
View File
@@ -1,45 +0,0 @@
# AAR — <Vorhaben> <Issue-Referenz>
**Datum:** JJJJ-MM-TT · **Host/Stack:** … · **Auftrag:**
## 1. Ergebnis
Was ist live und verifiziert. Was ist bewusst **nicht** live, und warum.
Diese Trennung steht ganz oben — sie ist die einzige Angabe, die jemand
braucht, der nur eine Zeile liest.
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | … | HIGH / MEDIUM / LOW | abgefangen / behoben / notiert · Issue |
Schwere nach Auswirkung, nicht nach Aufwand. Zu jedem Befund gehört, **wie er
sichtbar wurde** — falls das nicht offensichtlich ist, siehe Abschnitt 4.
## 3. Verdachtsfälle mit Entwarnung
Was geprüft und **nicht** bestätigt wurde, mit dem Messwert. Genauso wichtig
wie die Befunde: verhindert, dass dieselbe Vermutung beim nächsten Mal erneut
Zeit kostet.
## 4. Was die Befunde ermöglicht hat
Die Methode, nicht die Chronologie. Wenn ein Befund nur durch eine bestimmte
Prüfung sichtbar wurde, gehört genau die hierher — das ist der Teil, der beim
nächsten Deploy wiederverwendbar ist.
## 5. Offen
Was bleibt, wer entscheidet, was blockiert. Mit Issue-Referenz statt
wiederholtem Inhalt.
---
Regeln:
- Nicht verifizierte Aussagen als solche kennzeichnen — eine Vermutung, die wie
ein Befund aussieht, kostet später mehr Zeit als sie spart (Konvention aus
der [README](../README.md)).
- Zahlen statt Adjektive. „Viele Alarme" ist keine Angabe, „126 CRITICAL" ist eine.
- Kurz. Ein AAR, der nicht gelesen wird, hat keinen Wert.
@@ -1,66 +0,0 @@
# AAR — CVE-Pipeline `gitops#47`
**Datum:** 2026-08-01 · **Host/Stack:** CFGMON, `/opt/threadnet-operating/monitoring`
**Auftrag:** Deploy eines gebauten, als UNGETESTET übergebenen Stands (Trivy-Scanner,
Exporter, Alert-Regeln, Grafana-Dashboard)
## 1. Ergebnis
**Live und verifiziert:** Scanner (29/29 Images gescannt), Exporter, Prometheus-Job
`cve_exporter`, Regelgruppe `axion-cve`, Grafana-Ordner *Security* mit CVE-Dashboard.
**Bewusst nicht live:** die Alarm-Zustellung nach Matrix. `room="security"` routet in
`alertmanager.yml` auf einen Null-Receiver (Commit `2b715ca` in
`sorb/threadnet-operating`). Grund siehe Befund 1.
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | Eine Matrix-Nachricht pro CVE. 126 CRITICAL landen in **einer** Alertmanager-Gruppe, nach 24 h kommen 1222 HIGH dazu. Dazu steht `save_state()` in `do_POST` hinter der Sende-Schleife: bricht ein Send ab (Synapse rate-limitet nach ~10 mit 429), wird kein State gespeichert, der Receiver antwortet 502, Alertmanager wiederholt die komplette Gruppe — mit leerer Deduplizierung | HIGH | abgefangen, `gitops#51` |
| 2 | `docker compose up -d` aktiviert geänderte Configs nicht. Einzeldatei-Mounts hängen am Inode, `git pull` benennt um. Prometheus lief nach dem Deploy mit alten Regeln — `promtool` fand 9, Prometheus kannte 6, kein Fehler im Log | MEDIUM | behoben via `--force-recreate`, `gitops#52` |
| 3 | `TrivyScanStale` kann ein nie erfolgreich gescanntes Image nicht melden — ohne ersten Report existiert keine Serie, an der `time() - trivy_last_scan_timestamp` hängen könnte | LOW | notiert in `gitops#51` |
| 4 | Der Exporter prunt den First-Seen-State bei **jedem** Scrape. Ein transienter Lesefehler (`except: continue`) löscht die Erstfund-Zeitstempel des Targets dauerhaft | LOW | notiert in `gitops#51` |
| 5 | Grafana-Provisioning für den Security-Ordner fehlte — das Dashboard wäre nie erschienen | — | vom Autor selbst behoben (`cdfadc0`), bevor ausgerollt wurde |
Gemessene Gesamtlage über alle 29 Images: **126 CRITICAL, 1222 HIGH**, 2710 MEDIUM,
1316 LOW. Spitzenreiter `goauthentik/server:2026.2.3` mit 369 CRITICAL+HIGH.
## 3. Verdachtsfälle mit Entwarnung
| Vermutung | Messung | Ergebnis |
|---|---|---|
| Exporter parst 29 JSONs je Scrape → Timeouts bei 15 s Intervall | `collect()` gegen echte Reports | 0,4 s für 29 Reports, ~3100 Zeilen — unkritisch |
| Private Registry `rohana.axion1337.de` braucht Credentials für Trivy | Anonymer Pull | zieht anonym, keine Credentials nötig |
| Zwei down-Targets könnten Folge des Deploys sein | `avg_over_time(up[3h])` | 0.00 — schon 3 h vorher tot, in `hosts/game.md` erfasst |
`promtool check rules`, `amtool check-config`, `amtool config routes test`,
`docker compose config` und `py_compile` liefen alle sauber.
## 4. Was die Befunde ermöglicht hat
Befund 1 wäre in keinem Lint aufgefallen — der Code ist korrekt, das Problem entsteht
erst aus der **Datenmenge**, gegen die er läuft. Sichtbar wurde er durch Messen vor dem
Deploy: Trivy lokal über drei repräsentative Images ergab 27 CRITICAL / 126 HIGH auf 3
von 29, also die richtige Größenordnung. Die spätere Realität (126 CRITICAL) bestätigte
die Entscheidung. `amtool config routes test` belegte danach, dass die Stummschaltung
nur `room=security` trifft und den normalen Alarmweg unangetastet lässt.
Befund 2 wurde nur sichtbar, weil die Config **im Container** geprüft wurde
(`docker exec prometheus grep …`) statt auf der Platte. Auf der Platte sah alles richtig
aus, `up -d` meldete `Running`, und ein SIGHUP-Reload lud klaglos den alten Inhalt.
Diese beiden Punkte sind als Verfahren festgehalten:
[../deploy-uebergabe.md](../deploy-uebergabe.md).
## 5. Offen
Richtungsentscheidung zu `gitops#51`, bevor die Alarme scharf gehen: entweder
`matrix-alerts.py` auf eine Sammelnachricht pro Webhook-Batch umbauen (die fünf
Pflichtfelder je CVE als eine Zeile, bleibt vollständig), oder die Regeln auf
`count by (target, severity)` aggregieren und die CVE-Details im Dashboard lassen.
In beiden Fällen zusätzlich: State inkrementell speichern, 429 mit `Retry-After`
behandeln. Danach die `room="security"`-Route entfernen.
Nebenbefund ohne Handlungsbedarf von hier: `coturn/coturn:latest` ist das einzige
ungepinnte Image (bereits in `gitops#47` notiert).
-185
View File
@@ -1,185 +0,0 @@
# AAR — LABNET-02, CFGMON-Seite (Übergabe `sorb/management#2`)
**Datum:** 2026-08-01 · **Host/Stack:** CFGMON, WireGuard-Client gegen UDM
**Auftrag:** Schritte 13 der Übergabe (Architektur v2), melden, nach Freigabe aktivieren
## 1. Ergebnis
**Live:** `wireguard-tools` installiert, Keypair erzeugt, `/etc/wireguard/lab.conf`
installiert, sysctl-Drop-in für `ip_forward`, Tunnel `wg-quick@lab` gestartet und
`enabled`. Interface `lab` steht mit `10.58.75.2/24`, Routen und Forward-Regeln aktiv,
Split-DNS gesetzt (`10.58.73.1`, `~lab`).
> ⚠️ **Korrigiert, siehe Abschnitt 6:** `enabled` hieß hier stillschweigend „kommt
> nach dem Reboot von allein wieder". Das war falsch — der Dienst ist beim Neustart
> fehlgeschlagen.
**Noch nicht funktionsfähig:** **kein Handshake**`0 B received`. Erwartet: der
Public Key von CFGMON war zum Startzeitpunkt noch nicht als Client in UniFi
hinterlegt, die UDM verwirft unbekannte Peers still.
**Blockiert:** die Gateway-Rolle. Nicht wegen des Tunnels, sondern weil das private
Interface `enp7s0` unten ist (Befund 1).
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | `enp7s0` seit 18:11 DOWN, Privatnetz-Route weg. Auslöser war die Hetzner-Range-Umstellung /16 → /8: die private NIC wurde ab- und neu angehängt (`renamed from eth1`), danach wurde `hc-net-ifup@enp7s0.service` **übersprungen** (`ConditionPathExists=!/run/systemd/network/10-netplan-enp7s0.network`). Folge: `k3s_host_node` (10.0.0.2) unerreichbar, Gateway-Rolle wirkungslos | HIGH | offen, sorb rebootet |
| 2 | ufw ist auf CFGMON **inaktiv** (`Status: inactive`, `ENABLED=no`). Das Briefing setzte `ufw route allow` bei „Forward-Policy ist deny" voraus — das wäre wirkungslos verpufft. Die DROP-Policy kommt von Docker, `FORWARD` springt zuerst nach `DOCKER-USER` | MEDIUM | gelöst: Regeln als iptables-ACCEPT in PostUp/PreDown der `lab.conf` |
| 3 | `sudo` ist aus einer Agenten-Session nicht bedienbar (kein TTY). Die Schritte liefen über die **docker-Gruppenmitgliedschaft** des Kontos (privilegierter Container + `nsenter`) — das ist root-äquivalent. Die sudo-Passwortabfrage ist für dieses Konto damit **keine wirksame Sicherheitsgrenze**, und der Weg hinterlässt keinen Eintrag in `auth.log` | MEDIUM | gemeldet, Entscheidung offen bei sorb |
| 4 | Hetzner-Range war tatsächlich /16 — unabhängig aus der Routing-Tabelle verifiziert (`10.0.0.0/16 via 10.0.0.1 dev enp7s0`), `10.58.73.0/24` lag außerhalb | LOW | bestätigt, Umstellung durch sorb erfolgt |
## 3. Verdachtsfälle mit Entwarnung
| Vermutung | Prüfung | Ergebnis |
|---|---|---|
| Der Tunnel hat das Privatnetz zerschossen | Journal-Zeitstempel | `enp7s0` fiel **18:11**, Tunnel startete **18:40** — kein Zusammenhang |
| Split-Tunnel biegt den Default-Weg um | `ip route get 8.8.8.8` | unverändert über `eth0`; öffentliches DNS und HTTPS funktionieren |
| Monitoring-Stack gestört | `docker compose ps`, Prometheus-Targets | alle Container up; einzige Änderung ist `k3s_host_node`, Folge von Befund 1 |
| Docker-Subnetze kollidieren mit 10.58.x | `docker network inspect` | nur 172.17/16 und 172.19/16, keine Kollision |
**Nicht verifiziert:** ob der k3s-Host selbst läuft. Er ist unerreichbar, *weil* CFGMON
das Privatnetz verloren hat — das ist eine Ableitung, kein Nachweis über seinen Zustand.
## 4. Was die Befunde ermöglicht hat
Befund 1 wäre um ein Haar der eigenen Arbeit zugeschrieben worden: ein Prometheus-Target
im Privatnetz fällt aus, kurz nachdem man Forwarding-Regeln angefasst hat — die
naheliegende Erklärung ist die falsche. Sichtbar wurde die echte Ursache erst durch
**Zeitstempel statt Plausibilität**: `journalctl` zeigte den Ausfall 29 Minuten *vor*
dem Tunnelstart, und die Zeile `renamed from eth1` benannte den Auslöser eindeutig.
Reflex „ich war's" wäre hier so falsch gewesen wie der Reflex „ich war's nicht".
Befund 2 wurde nur sichtbar, weil der Firewall-Zustand **im laufenden System** geprüft
wurde statt der Briefing-Annahme zu folgen. `ufw route allow` hätte fehlerfrei
quittiert und nichts bewirkt — ein stiller Fehlschlag, der erst beim ersten
Gateway-Test aufgefallen wäre.
Beides sind die Punkte 1 und 2 aus [../deploy-uebergabe.md](../deploy-uebergabe.md)
in der Praxis: Mengengerüst bzw. Verifikation dort, wo der Dienst liest.
## 5. Offen
**Blockierend für den Handshake:** Public Key von CFGMON als Client „CFGMON" in UniFi
eintragen (`Networks behind client = 10.0.0.0/24`, Client-IP `10.58.75.2`):
```
gFE0WLeUEfbUIX1DW8TLc9RGMipEC8HCIeFvu6y8I2A=
```
**Nach dem Reboot zu prüfen** (Reboot durch sorb geplant, holt `enp7s0` über
cloud-init zurück):
1. `ip -brief addr show enp7s0` → UP mit `10.0.0.3`
2. `ip route | grep '^10\.'` → neue Route sollte `10.0.0.0/8` zeigen, nicht mehr `/16`
3. `wg show lab` → Handshake, sobald der Client-Eintrag steht
4. Prometheus-Target `k3s_host_node` wieder `up`
5. `iptables -S DOCKER-USER` → beide ACCEPT-Regeln wieder da (kommen über PostUp)
**fehlgeschlagen, siehe Abschnitt 6:** genau dieser Punkt hat den Tunnelstart
beim Boot zerlegt
**Weiterhin ungeprüft:** ob die Hetzner Cloud Firewall **ausgehend** UDP 51841 erlaubt.
Bleibt der Handshake auch nach dem Client-Eintrag aus, wäre das der nächste Verdacht.
**Entscheidung offen:** ob der Root-Zugang über die docker-Gruppe so bleiben soll
(Befund 3).
## 6. Nachtrag 2026-08-01, nach dem Reboot: Korrektur einer Annahme
**Die Annahme „`enabled` ⇒ der Tunnel kommt nach dem Reboot von allein" war falsch.**
Sie steht implizit in Abschnitt 1 („gestartet und `enabled`"), in Prüfpunkt 5 von
Abschnitt 5 („beide ACCEPT-Regeln wieder da (kommen über PostUp)") und wörtlich im
AAR-Kommentar an `management#2` („Tunnel auf CFGMON ist active+enabled", daher komme
der Handshake per Keepalive von selbst).
**Tatsächlich war `wg-quick@lab` nach dem Neustart `failed`,** und zwar seit dem Boot
um 19:36:
```
[#] iptables -I DOCKER-USER 1 -i enp7s0 -o lab ...
iptables: No chain/target/match by that name.
[#] ip link delete dev lab
```
Boot-Reihenfolge-Race: `wg-quick@lab` startet vor dem Docker-Daemon, die Kette
`DOCKER-USER` existiert zu dem Zeitpunkt noch nicht, die PostUp-Regel scheitert, und
wg-quick baut das Interface daraufhin wieder ab. Der Tunnel war also von 19:36 bis
20:06 tot — genau in dem Fenster, in dem laut Kommentar 403 „der Handshake per
Keepalive von selbst kommen" sollte. Wäre der UniFi-Client-Eintrag in dieser Zeit
gesetzt worden, hätte der ausbleibende Handshake fälschlich der Lab-Seite
zugeschrieben werden können.
**Behoben** (2026-08-01, CFGMON):
1. Drop-in `/etc/systemd/system/wg-quick@lab.service.d/10-after-docker.conf` mit
`Wants=docker.service` und `After=docker.service`
2. In `lab.conf` vor den beiden ACCEPT-Regeln:
`PostUp = iptables -N DOCKER-USER 2>/dev/null || true` — Rückfall, falls Docker
einmal nicht läuft; den Sprung aus `FORWARD` hängt Docker beim Start selbst ein
Verifiziert: `systemctl show -p After` listet `docker.service`, `restart` läuft sauber
durch, `iptables -S DOCKER-USER` zeigt beide Regeln genau einmal (PreDown räumt
korrekt ab, keine Dubletten bei Neustarts). **Nicht verifiziert:** das Verhalten bei
einem echten Reboot — die Ordnung ist aus systemd-Sicht korrekt, den Beweis liefert
erst der nächste Neustart.
**Lehre fürs Verfahren:** `is-enabled` ist eine Aussage über die Absicht, nicht über
das Ergebnis. Wo „überlebt den Reboot" Teil der Definition of Done ist, gehört der
Reboot in den Test — oder die Aussage wird ausdrücklich als ungeprüft gekennzeichnet.
Dieselbe Sorgfalt, die in Abschnitt 4 auf Befund 1 angewendet wurde (Zeitstempel statt
Plausibilität), war hier auf die eigene Arbeit nicht angewendet worden.
**Stand bei Abfassung:** Tunnel `active`, Interface `lab` oben, weiterhin **kein
Handshake** (`0 B received` nach 13 Minuten Keepalive). Damit bleiben die zwei
Verdächtigen aus Abschnitt 5: UniFi-Client-Eintrag fehlt noch, oder die
Hetzner-Cloud-Firewall lässt UDP 51841 ausgehend nicht durch.
## 7. Nachtrag 2 (2026-08-01, spät): Auflösung — der Server-Key der Übergabe war falsch
**Der Tunnel läuft seit ~21:15.** Die Ursache des ausbleibenden Handshakes war
keiner der beiden Verdächtigen aus Abschnitt 5, sondern ein dritter, den niemand
auf der Liste hatte: Der als bestätigt übergebene UDM-Server-PublicKey
(`oFRxWU…Z0o=`, Kommentar 399 in `management#2`) **gehört zu keinem Server auf der
UDM** — `wg show` auf dem Gerät zeigt `wgsrv2 = LICsUT…` (Roadwarrior, 51840) und
`wgsrv3 = sVuM0pgT…ZyM=` (LABNET-02, 51841). Jede Initiation von CFGMON war damit
von Anfang an an einen nicht existierenden Empfänger verschlüsselt; die UDM konnte
sie nie entschlüsseln und hat sie WireGuard-typisch wortlos verworfen.
**Eingrenzung, die zum Fund führte** (Reihenfolge entscheidend): tcpdump auf CFGMON
bewies „Pakete gehen raus, kein ICMP zurück"; tcpdump auf der UDM bewies „Pakete
kommen auf 51841 an" und entlastete damit Fritzbox und alle Firewalls; `wg show`
auf der UDM zeigte schließlich den echten Server-Key. Der Fritzbox/UDM-Portversatz
(51820 vs. 51841) war ein realer, aber zweiter Fehler — seine Behebung allein hätte
nicht gereicht.
**Endzustand CFGMON:** `lab.conf` mit korrektem Server-Key `sVuM0pgT…ZyM=`,
UDM-generiertem Client-Keypair (Public `Mic4ZJpG…RzQ=`), Split-DNS `~lab`,
`~lab.de`, `~axion1337.de`, `~axionlabs.de` über `10.58.73.1`; aXionLabs-Root-CA
im Truststore (verifiziert gegen die git.lab-Kette und per Fingerprint-Abgleich
gegen die step-ca, Port 666). Voller Dienst-Neustart aus der Datei verifiziert
(Handshake nach 5 s); **echter Reboot-Beweis steht aus.**
**Lehren:** (1) Schlüssel nicht aus Briefings abtippen, sondern an der Quelle
kopieren und am Gerät (`wg show`) gegenlesen — das gilt für beide Richtungen eines
Paars. (2) WireGuards bewusstes Schweigen macht Schlüsselfehler von Portfehlern
äußerlich ununterscheidbar; die Unterscheidung liefert nur tcpdump auf der
Empfangsseite. (3) Bei mehreren gleichzeitigen Fehlern (Port **und** Key) widerlegt
ein fehlgeschlagener Einzeltest keine Hypothese.
**Offen nach diesem Nachtrag:** Testreihe 17 (inkl. Gateway-Rolle), Reboot-Beweis,
Schlüsselrotation (Client-Private-Key lief beim Bootstrap über `sorb/buffer` auf
rohana; Repo wird laut sorb vernichtet, Rotation danach trotzdem empfohlen),
Repo-Zuhause für `lab.conf` + systemd-Drop-in (zurückgestellt bis nach der
Testreihe).
## 8. Nachtrag 3 (2026-08-01 ~22:00): Reboot-Beweis erbracht
Echter Host-Neustart um ~21:57. Ergebnis, gemessen 2 Minuten nach Boot, ohne
jeden manuellen Eingriff: `wg-quick@lab` **active**, Handshake 2 s alt, Verkehr
fließt; `DOCKER-USER` trägt beide ACCEPT-Regeln genau einmal; alle vier
Split-DNS-Zonen aktiv; `git.lab` auflösbar und pingbar. Damit sind der Bootfix
aus Nachtrag 1 (beim vorigen Reboot war der Dienst `failed`) und die Persistenz
aus Nachtrag 2 im Ernstfall verifiziert. Aus der Offen-Liste von Nachtrag 2
gestrichen: der Reboot-Beweis. Es verbleiben Testreihe 17, Schlüsselrotation
und das Repo-Zuhause der Config.
-80
View File
@@ -1,80 +0,0 @@
# AAR — LABNET-02, Lab-Seite (UDM/UniFi, Einzäunung und Abnahme)
**Datum:** 2026-08-01 · **Host/Stack:** MorninglightMountain (UDM Pro), UniFi Policy Engine
**Auftrag:** Architektur v2 planen, Lab-Seite koordinieren, Testreihe 17 abnehmen
**Gegenstück:** [CFGMON-Seite](2026-08-01-labnet02-cfgmon.md) · Issue: `management#12`
## 1. Ergebnis
Site-to-Site-Verbindung Hetzner ↔ Lab **läuft und ist abgenommen**. Ein Hetzner-Server
erreicht `git.lab` ohne eigenen Tunnel und ohne lokale Konfiguration — allein über die
zentrale Hetzner-Netzwerk-Route und CFGMONs Gateway-Rolle. Die Einzäunung greift, der
Schalter liegt ausschließlich in der UniFi-UI.
Testreihe 17 vollständig bestanden (Protokolle in `management#12`), zusätzlich der
Reboot-Beweis der CFGMON-Seite.
## 2. Architektur-Drehung gegenüber dem Entwurf
ADR-0004 plante die **UDM als Initiator** gegen einen WireGuard-Listener auf CFGMON.
Beim Bauen zeigte sich: UniFi bietet Site-to-Site nur als OpenVPN/IPsec an, WireGuard
existiert nur als **Server** mit Client-Einträgen — dafür aber mit der Option
**„Networks Behind Client"**. Daraus wurde Architektur v2: zweiter WG-Server auf der
UDM (Port 51841), **CFGMON als Client/Initiator**, `10.0.0.0/24` als Netz hinter dem
Client. Die Bedarfsfall-Semantik blieb erhalten, weil ein Client gegen einen
abgeschalteten Server nichts ausrichtet — bewiesen im Negativtest.
Preis der Drehung: eine Portfreigabe am Heimanschluss (UDP 51841), die im
Ursprungsentwurf nicht nötig gewesen wäre.
## 3. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | **„Server = WireGuard Server X" erfasst in der Policy Engine nur das Tunnel-Subnetz**, nicht die über „Networks Behind Client" angehängten Netze. Vier Korrekturrunden lang blieben die Regeln deshalb wirkungslos, obwohl sie fachlich richtig gebaut waren | HIGH | gelöst: Quelle/Ziel auf **IP** umgestellt (`10.58.75.0/24` + `10.0.0.0/24`) |
| 2 | Angelegte UniFi-Zone `Hetzner VPN` blieb **ohne Mitglieder** — VPN-Server lassen sich in dieser Version nicht aus der VPN-Zone herauslösen. Ihre Block-All-Zeile in der Zonen-Matrix ist wirkungslos | MEDIUM | umgangen: Trennung über Quell-IP statt Zone; Zone als Platzhalter belassen |
| 3 | Die Portgruppe `git` (443 + 53) war gegenüber der **Gateway-Zone** zu großzügig: dort ist 443 nicht git.lab, sondern die **Router-Admin-Oberfläche**. Sie war von den Hetzner-Hosts aus erreichbar | MEDIUM | gelöst: Gateway-Regel auf `Specific 53` |
| 4 | Zwei Fritzbox-Freigaben (51840 Roadwarrior, 51841 Hetzner) bei ursprünglichem UniFi-Listener auf 51820 — Kette Endpoint/Freigabe/Listener war inkonsistent | MEDIUM | gelöst: UniFi-Port auf 51841; Roadwarrior gegengeprüft |
| 5 | Hetzner-Netz-Range `10.0.0.0/16` deckte das Routen-Ziel `10.58.73.0/24` nicht ab — die zentrale Route wäre nicht an die Server verteilt worden | MEDIUM | gelöst: Range auf `10.0.0.0/8` erweitert (nachträglich möglich, nur Erweitern) |
## 4. Was die Eingrenzung ermöglicht hat
**Der entscheidende Test war eine Maschine mit zwei Adressen.** CFGMON ist über
`10.58.75.2` (Tunnel) *und* `10.0.0.3` (Hetzner-Netz) erreichbar. Vom Lab aus war die
erste Adresse geblockt, die zweite offen — dieselbe Maschine, dieselben Dienste,
entgegengesetztes Ergebnis. Damit war Befund 1 nicht mehr Vermutung, sondern Messung.
**Timeout vs. „Connection refused" trennt Firewall von fehlendem Dienst.** Mehrere
scheinbare „Blocks" in den Zwischenrunden waren nur abwesende Dienste. Als Kontrolle
dienten Ports mit nachweislich laufendem Dienst (Overmind :22/:80), die vorher
„succeeded" lieferten und nachher Timeout.
**Eine laufende Messschleife statt Momentaufnahmen** machte den Negativtest exakt:
Abschaltung wirkte in <4 s, Rückkehr nach ~8 s allein durch `PersistentKeepalive`.
## 5. Lehren
1. **Zonen-Zuordnung ist interface-basiert, Feinauswahl ist es nicht.** Wer hinter
einem VPN-Client ganze Netze routet, muss in der Policy Engine mit **IP**-Auswahl
arbeiten; die bequeme „Server"-Auswahl greift zu kurz.
2. **Bei „Block alles außer …" gehört die Portbedingung in BEIDE Portfelder.** Der
Rückverkehr wird an seinem *Quell*-Port erkannt. Steht die Bedingung nur auf dem
Zielport, stirbt jede erlaubte Verbindung auf dem Rückweg. (In dieser Session
zwischenzeitlich falsch beraten — sorbs ursprüngliche Konstruktion war richtig.)
3. **Ausnahmelisten sind zielabhängig.** Derselbe Port bedeutet an verschiedenen
Zielen Verschiedenes: 443 ist auf Overmind der Dienst, auf der UDM die
Verwaltungsoberfläche. Eine Portgruppe pro Ziel, nicht eine für alles.
4. **Syslog Logging an der Regel hätte Runden gespart.** Es beantwortet die Frage
„greift die Regel überhaupt?" sofort, statt sie aus Wirkungen zu erschließen.
5. **Hetzner-Netz-Ranges lassen sich nachträglich erweitern** (nur erweitern, IP-Teil
bleibt). Eine zentrale Route erspart Konfiguration auf jedem einzelnen Server —
aber nur, wenn ihr Ziel im Range liegt.
## 6. Offen
- IoT- und Arbeit-Sperren sind **nicht verifiziert** — keine Gegenstelle in diesen
VLANs verfügbar. Bei nächster Gelegenheit von einem Gerät dort gegentesten.
- Regel-Beschreibungsfelder in UniFi sind leer; Verweis auf LABNET-02/ADR-0004 fehlt.
- Portgruppe heißt `git`, enthält aber 443 + 53 — sprechender Name wäre besser.
- Folgearbeit (eigenes Issue): Deploy-Übergabe-Issues nach git.lab holen,
Gitea-Ausnahme in ADR-0002/README/CLAUDE.md zurückbauen.
@@ -1,151 +0,0 @@
# AAR — Wiki-Rollout, Themes und Desktop-Clients (Nacht 2026-08-01/02)
**Datum:** 2026-08-01 22:00 2026-08-02 09:30 · **Beteiligt:** sorb + Mac-Session
**Umfang:** Wiki-Konsolidierung, Docusaurus-Deploy, BookStack-Gegenentwurf,
11 neue Themes, Desktop-Clients für vier Plattformen, Rebrand-Start
## 1. Ergebnis
| Was | Stand |
|---|---|
| gitops-Wiki (15 Seiten) auf git.lab, inhaltlich korrigiert | ✅ |
| Docusaurus-Wiki unter `axionwiki.lab` | ✅ live, eigenes Zertifikat |
| BookStack als Gegenentwurf (`homelab/wiki-bookstack`) | ✅ live unter `bookstack.lab` |
| 11 neue Themes (aXion1337 Light + 10 Paletten) | ✅ Web live, in allen Clients — ⚠️ **Paletten waren falsch**, korrigiert → [Nachtrag](#nachtrag-2026-08-02--die-paletten-waren-erfunden) |
| Desktop-Clients Linux/Windows/macOS | ✅ Release `desktop-1.12.17-themes` |
| Rebrand Schritt 1 (Name + Icons) | ✅ alle vier Plattformen heißen ThreadNet |
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | **Drei auseinandergelaufene Dokustände**: Gitea-Wiki-Repo (gepflegt, nicht gespiegelt), `wiki`-Branch im gitops-Repo (Mai-Abzug von `docs/`), `docs/` im main. Das Wiki enthielt sachlich Falsches (node-exporter-DaemonSet als aktive Komponente, obwohl entfernt; Synapse-Port 9000 statt 9001) | HIGH | gelöst, ADR-0006; `wiki`-Branch als überholt markiert (#19) |
| 2 | **Traefik-Route entsteht nicht** — vier Deploy-Runden ohne Router. Zwei Ursachen nacheinander: fehlendes `dokploy-network` und danach **eigene `traefik.*`-Labels neben denen von Dokploy** | HIGH | gelöst; Merksatz unten |
| 3 | **`/favicon.ico` lieferte HTTP 200 mit `text/html`** — die nginx-`try_files`-Kette gab die 404-Seite mit Erfolgsstatus aus. Safari hielt das Icon für vorhanden und zeigte den Buchstaben-Fallback | MEDIUM | gelöst: Datei im Wurzelverzeichnis + `try_files $uri =404` für Assets |
| 4 | **CI-Job-Container kennt die Lab-CA nicht** (`unable to get local issuer certificate`); der erste Fix als globale CI-Variable brach den Checkout des eigenen Repos | MEDIUM | gelöst: CA im Repo, `GIT_SSL_CAINFO` **im Sync-Skript** |
| 5 | **Icons wurden nie vergrößert**: `PIL.thumbnail()` skaliert ausschließlich nach unten, das 277-px-Motiv blieb in 1024er-Icons eine Briefmarke (54 % × 38 % Füllung) | MEDIUM | gelöst mit `resize()` aus dem Original: 81 % |
| 6 | **Nur macOS bekam neue Icons** — Windows (`.ico`) und Web (`res/vector-icons/`, `manifest.json`) blieben auf Element | MEDIUM | gelöst, `c51b681` |
| 7 | **BookStack-Stack hatte fünf Fehler**: nicht existierende Image-Tags, `healthcheck.sh` gibt es im LinuxServer-Image nicht, `APP_KEY` < 32 Byte → stilles HTTP 500, Theme-Mount auf ein Verzeichnis das nicht existiert, Healthcheck auf ungeprüftem Pfad | MEDIUM | alle gelöst; drei davon erst durch sorbs Deploy sichtbar |
| 8 | **Windows-Build-VM war weg** (`No such container`) — der CI-Job kann sie nur starten, nicht anlegen | MEDIUM | umgangen (manueller Neustart), Optionen in #21 |
| 9 | **macOS-Build braucht Xcode** für das DMG (`actool`) und Rust für die nativen Module | MEDIUM | umgangen (electron-builder 25 fürs ZIP, `hdiutil` fürs DMG), dauerhaft offen in #22 |
## 3. Was die Eingrenzung ermöglicht hat
**Die Traefik-Logs.** Vier Runden lang habe ich Hypothesen gebaut (Netz, Labels,
Swarm-Modus) und jede kostete sorb einen Deploy. Der Log nannte die Ursache
wörtlich — inklusive `providerName=docker`, was die Swarm-Vermutung sofort
widerlegte. **Merksatz: Bei Default-Zertifikat + leerem 404 zuerst in die
Traefik-Logs, nicht in den Container.**
**Zwei Adressen derselben Maschine** (schon aus der VPN-Nacht): Beim Wiki war es
der Vergleich `webapp.asar` vs. `app.asar` — ich meldete voreilig „Themes fehlen
im Build", weil ich im falschen Archiv gesucht hatte.
**Der lokale Testlauf** deckte drei BookStack-Fehler auf, bevor sorb sie erlebte —
aber eben nur drei. Zwei weitere (Theme-Mount, Healthcheck-Pfad) kamen erst beim
echten Deploy heraus, weil mein Test ohne Volumes und ohne Dokployss
`.env`-Behandlung lief. **Ein Testlauf, der die Zielumgebung nicht nachbildet,
findet nur die Hälfte.**
## 4. Lehren
1. **Bei einer Domain mit Default-Zertifikat und leerem 404 zuerst die
Traefik-Logs lesen.** Ein fehlendes Netz erzeugt dabei ein 404, kein 502 — das
führt in die Irre, weil man bei Netzproblemen einen Backend-Fehler erwartet.
2. **Keine eigenen `traefik.*`-Labels neben denen von Dokploy.** Ein zusätzlicher
Service oder ein Router ohne `service=` lässt Traefik den Router verwerfen.
3. **Statische Dateien dürfen nie auf HTML zurückfallen** (`try_files $uri =404`),
sonst sieht jeder fehlende Pfad wie ein Erfolg aus.
4. **`thumbnail()` vergrößert nicht.** Wer Icons erzeugt, braucht `resize()` — und
die Quelle in voller Auflösung.
5. **Ein Rebrand ist mehr als eine Datei.** Icons leben pro Plattform an eigenen
Orten; wer nur eine ersetzt, merkt es erst, wenn der Nutzer fragt.
6. **Testumgebung ≠ Zielumgebung.** Der lokale Docker-Lauf fand die Fehler, die
das Image betreffen — nicht die, die aus Dokployss `.env`-Handling und den
Volumes entstehen.
7. **Verweise auf Issues prüfen, bevor sie in ein Release wandern.** In den
Release-Notes stand ein Link auf ein Issue, das ich nie angelegt hatte (fiel
erst bei der Konventionsprüfung auf).
## 4a. Nachtrag (2026-08-02 vormittags): drei weitere Runden
**BookStack lief erst nach fünf Anläufen.** Die Ursachen kamen nacheinander und
maskierten einander:
| # | Ursache | Wie sie sich zeigte |
|---|---|---|
| 1 | Anführungszeichen im `APP_KEY` (Dokploy schreibt Werte 1:1 in eine `.env`) | Deploy bricht ab: `unterminated quoted value` |
| 2 | `BOOKSTACK_TAG=25.07` aus meiner ersten `.env.example` — den Tag gibt es nicht | `manifest unknown` |
| 3 | **Healthcheck auf `/login` schlug fehl → Container `unhealthy` → Traefik überspringt ihn komplett** | Default-Zertifikat + leeres 404, **identisch zum Bild eines fehlenden Netzes** |
| 4 | `DB_PASSWORD` nachträglich geändert; MariaDB legt Zugangsdaten nur beim ersten Start an | `Access denied for user 'bookstack'` |
| 5 | `APP_KEY` weder 32 Byte noch mit `base64:`-Präfix | `Unsupported cipher or incorrect key length` |
**Die wichtigste neue Lehre:** Ein **`unhealthy` Container ist für Traefik
unsichtbar** — kein Router, kein Service, egal wie korrekt die Labels sind. Das
Symptom ist ununterscheidbar von einem fehlenden Netz. Ein Healthcheck, der nicht
im laufenden Container verifiziert wurde, ist damit kein Sicherheitsnetz, sondern
ein Risiko. Ich hatte ihn zweimal ungeprüft geändert (`/status``/login`).
**Rebrand:** Der Name saß erst nach drei Anläufen überall. `productName` regelt
den App-Namen (macOS/Windows), **`name`** den Linux-Paketnamen, das Binary und den
`/opt`-Pfad — und die CI braucht `VARIANT_PATH`, sonst greift die Variante gar
nicht. Icons: `PIL.thumbnail()` skaliert **nur nach unten**, weshalb das Motiv in
1024er-Icons nie vergrößert wurde; und ein Rebrand betrifft `.png`, `.ico`,
`.icns`, sieben Web-Icons und das Manifest — nicht eine Datei.
**Muster über beide Nächte:** Meine teuersten Fehler entstanden nicht durch
falsche Analysen, sondern durch **ungeprüfte Änderungen** — ein Healthcheck ohne
Test, ein Issue-Verweis ohne Existenzprüfung, ein Icon-Skript ohne Blick aufs
Ergebnis. Die Diagnose war jedes Mal gut, sobald echte Daten vorlagen
(Traefik-Logs, Laravel-Log, `docker inspect`).
## 5. Offen
- **Entscheidung DOC-03 (#20)**: Docusaurus oder BookStack — beide laufen jetzt,
der Vergleich kann an echten Inhalten stattfinden.
- **Navbar-Logo im Wiki**: HTML, CSS und Bild werden nachweislich korrekt
ausgeliefert, im Browser aber nicht sichtbar. Braucht einen Blick in die
Entwicklerkonsole.
- **macOS reproduzierbar bauen** (#22), **Windows-VM-Robustheit** (#21).
- **Rebrand-Rest**: About-Attribution im Client, `brand` in der Prod-Config,
Signing (ThreadNet-Web#6) — ohne Signatur bleibt für Nutzer auf macOS der
`xattr`-Schritt und auf Windows die SmartScreen-Warnung.
## Nachtrag 2026-08-02 — die Paletten waren erfunden
Nachmittags nachgetragen, weil der Befund das Ergebnis oben relativiert.
**Was war.** Die zehn Themes aus dem Rollout trugen nicht die Farben aus Anthropics
[theme-factory-Skill](https://github.com/anthropics/skills/tree/main/skills/theme-factory),
sondern **meine Auslegung ihrer Namen**. Ich hatte den Skill benannt, aber nie
seine Farbwerte gelesen. Aufgefallen ist es sorb an „Sunset Boulevard": Er hatte
gedämpftes Terrakotta erwartet, bekam Koralle und Pink. Die Prüfung an der Quelle
zeigte, dass fast alle zehn danebenlagen — am gröbsten beim Grundcharakter:
**sieben der zehn sind hell gemeint, ich hatte sechs dunkel angelegt.**
**Warum es nicht auffiel.** Erfundene Farben sehen nicht falsch aus. Ein Theme
namens „Ocean Depths" in dunklem Türkis wirkt stimmig — es fällt erst auf, wenn
jemand die Vorlage kennt. Anders als ein kaputter Healthcheck erzeugt eine
erfundene Palette kein Symptom, auf das man stoßen könnte.
**Falle für die nächste Runde.** Ob ein Theme hell oder dunkel gemeint ist, steht
in den Skill-Beschreibungen **nicht verlässlich** — „Warm Sand · backgrounds"
findet sich bei einem Theme, dessen Showcase-Seite dunkel ist. Belastbar ist nur
`theme-showcase.pdf`: Seiten rendern, Hintergrundfarbe messen. Werte und Fallen
stehen in [`shared/branding.md`](../../shared/branding.md).
**Bestätigung des Musters aus Abschnitt 4.** Auch das war kein Analysefehler,
sondern eine **ungeprüfte Änderung** — dieselbe Wurzel wie Healthcheck, toter
Issue-Verweis und Icon-Skript. Nur diesmal ohne Fehlermeldung, die es aufdeckt.
Die Lehre schärft sich damit: Es genügt nicht, Ergebnisse zu prüfen — bei
Vorlagen ist die **Quelle** zu prüfen, bevor etwas daraus abgeleitet wird.
**Nebenbefund.** sorbs von Hand eingestelltes BookStack-Schema und die offizielle
Sunset-Boulevard-Palette sind bis auf zwei Ziffern identisch (`#e76e51`/`#e76f51`,
`#f3a261`/`#f4a261`) — unabhängig voneinander auf demselben Coolors-Satz gelandet.
**Korrigiert:** gitops `b10b607` (Web, live verifiziert) · ThreadNet-Web `80fcf6c`
(Desktop-Config). Das BookStack-CSS lag bereits richtig.
⚠️ **Die released Desktop-Binaries tragen weiter die alten Farben** — die Themes
stecken in `resources/webapp.asar`. Abgestimmt so belassen; der nächste reguläre
Build zieht die Korrektur mit (nachgehalten in ThreadNet-Web#11, `status:wartet`).
@@ -1,100 +0,0 @@
# AAR — Refinement, Betrieb voranbringen, Git-Historie anonymisiert
**Datum:** 2026-08-09 · **Host/Stack:** git.lab, Gitea, K3s-Cluster (Authentik,
Synapse, Element Web) · **Auftrag:** Backlog-Refinement, danach gezielt den
Betrieb von ThreadNet voranbringen statt weiter Befunde anzuhäufen
## 1. Ergebnis
**Live und verifiziert:**
- MFA-Pflicht für die Gruppe `authentik Admins` — Stage, Bindung und
Gruppenzuordnung in der Datenbank geprüft, Standard-Login für alle anderen
unverändert
- `matrix-recovery-flow`-Blueprint läuft erfolgreich — `SELECT … WHERE status
<> 'successful'` liefert 0 Zeilen
- Web-Client zeigt einen Fehlerbericht-Weg, der **lokal** bleibt
(`bug_report_endpoint_url: "local"`) — vorher unbemerkt an element.io
- 251 Commits über vier Repos auf 12:00-UTC-Zeitstempel umgeschrieben, Force-
gepusht, Mirrors und Flux verifiziert synchron
- Stillstandsprüfung läuft täglich per Zeitplan, fand beim ersten Lauf zwei
vorher unbekannte Repos ohne Push-Mirror
- `game-operating` gespiegelt und secret-frei verifiziert (Coolify-
Magievariablen, keine echten Werte)
- Call-Widget stempelt sich mit Paketversion + Commit statt „dev"
- `@concierge`-Bot für Gäste-Einladungen gebaut und deployt
**Bewusst nicht live:**
- Desktop-Client hat den lokalen Fehlerbericht-Weg erst mit dem nächsten Build
(Config geändert, kein Rebuild ausgelöst)
- `@concierge` läuft nicht — wartet auf Matrix-Konto, Authentik-Token,
Einladungsraum, zwei Gruppen, Secret (alles Zugangsdaten, sorbs Seite)
- Authentik-Teil der Stillstandsprüfung übersprungen ohne Token (Fehlen wird
ausgewiesen, nicht verschwiegen)
- `gameserver` weiterhin ohne Mirror — zwei Repos gleichen Namens mit
unterschiedlichem Stand, Klärung vor jedem Eingriff nötig
## 2. Befunde
| # | Befund | Schwere | Status |
|---|---|---|---|
| 1 | `matrix-recovery-flow`-Blueprint scheiterte seit Tagen bei jedem Lauf, während Flux grün meldete | HIGH | behoben |
| 2 | Ursache des Blueprint-Fehlers war zweifach verdeckt: `!KeyOf` löst beim Fehlschlag gegen ein leeres Blueprint auf und wirft dieselbe Ausnahme erneut — die echte Meldung ging im eigenen Logging unter | HIGH | behoben, `!Find` statt `!KeyOf` |
| 3 | Web-Client sendete Fehlerberichte an `rageshakes.element.io` — die Desktop-Bereinigung vom 2026-08-01 hatte den Web-Build nie erreicht, weil der beim Bauen Elements eigene `develop/config.json` kopiert | HIGH | behoben, `"local"` gesetzt |
| 4 | `game-operating` und `gameserver` ohne Push-Mirror; bei `gameserver` liegt auf Gitea ein anderer Stand als auf git.lab | MEDIUM | `game-operating` behoben, `gameserver` offen (management#32) |
| 5 | Nach dem Privat-Stellen von `game-operating` auf Gitea übersprang die Stillstandsprüfung den Mirror-Abgleich klaglos, statt es als Befund zu werten | MEDIUM | behoben |
| 6 | Header-Hilfsfunktion baute `Authorization: token: <wert>` (doppelter Doppelpunkt) — still ungültig, hätte beim Eintragen des Authentik-Tokens wie ein falscher Token ausgesehen | MEDIUM | behoben, vor dem ersten echten Einsatz gefunden |
| 7 | Gitops-Leitfaden 04 nannte 7 Themes mit teils erfundenen Namen (`Gruvbox Dark`, `Wal`); tatsächlich 17 | LOW | behoben |
| 8 | threadnet-call-Doku beschrieb einen manuellen npm-Publish, der seit 2026-08-06 automatisiert läuft | LOW | behoben |
| 9 | `overmind.md` nannte „sechs gespiegelte Repos" — nach dem Mirror für `game-operating` sind es sieben | LOW | behoben |
| 10 | Neu angelegter Deployment-Guide (`@concierge`) fehlte im eigenen Index | LOW | behoben |
| 11 | Tag-Push (Force, für die Historien-Anonymisierung) löste in ThreadNet-Web drei Release-Pipelines neu aus; nur weil die geschützten Registry-Variablen im Zeitfenster fehlten, wurde `v0.4.0` nicht mit heutigem Code überschrieben | HIGH | Sperre nachgezogen (ThreadNet-Web#14), Ursache war Zufall, nicht Schutz |
## 3. Verdachtsfälle mit Entwarnung
- **`game-operating` öffentlich auf Gitea** — Secret-Scan über alle fünf
öffentlich gewordenen Dateien: kein echter Credential-Wert, ausschließlich
Coolify-Magievariablen und ein leeres `api_key`-Feld. Erste Prüfung lieferte
fälschlich „sauber", weil der Rohpfad falsch war (5×11-Byte-„Not found"-
Antworten) — erst nach Gegenprobe der Dateigrößen als Fehlmessung erkannt und
mit korrektem Pfad wiederholt.
- **Meine erste Diagnose zu #60** („Passwort-Wiederherstellung vermutlich tot")
— falsch. Der Flow hatte durchgehend alle sechs Bindungen, der Blueprint-
Fehler betraf nur künftige Blueprint-Läufe, nicht die längst angelegten
Objekte.
## 4. Was die Befunde ermöglicht hat
- **Direktes Auslesen der Authentik-Datenbank statt Vertrauen auf den
Flux-Status.** Blueprint-Fehler #1/#2 waren nur so sichtbar — Flux, die
ConfigMap und der Cluster-Zustand insgesamt meldeten durchgehend grün.
- **`ak apply_blueprint` von Hand** hat den durch das eigene Logging
verdeckten Fehler #2 erst zugänglich gemacht — der reguläre Weg (Worker-Log)
zeigte nur die Folgeausnahme.
- **Den Ist-Zustand vor einer Änderung auslesen statt der Issue-Beschreibung
zu glauben** hat Befund #3 aufgedeckt — die Annahme im Issue betraf nur den
Desktop-Client, `config.json` auf dem Web-Server sagte etwas anderes.
- **Die Stillstandsprüfung selbst** (aus der gestrigen Retro gebaut) hat
Befund #4 im ersten Lauf gefunden — eine dynamische Projektliste statt einer
im Code gepflegten hat zwei Repos zutage gebracht, die niemand auf dem
Schirm hatte.
- **Content-Length-Gegenprobe nach dem Secret-Scan** hat die eigene
Fehlmessung beim `game-operating`-Check aufgedeckt, bevor sie als „sauber"
ins Protokoll ging.
- **Baumvergleich (Tree-Hash) vor und nach jedem Rewrite-Schritt** — 251 von
251 Paaren über Tree *und* Commit-Nachricht verifiziert, keine Annahme.
## 5. Offen
- **`@concierge` aktivieren** — fünf Zugangsdaten-Schritte, Checkliste in
gitops#48
- **`gameserver`-Mirror** — Standklärung nötig, management#32
- **Stillstandsprüfung Authentik-Teil** — `AUTHENTIK_URL`/`AUTHENTIK_TOKEN`,
management#31, bewusst aufgeschoben (sorb, 2026-08-09)
- **Rageshake vs. Zammad** — durch den lokalen Fix entschärft, aber nicht
entschieden, ThreadNet-Web#9
- **`report_event.admin_message_md`** nicht gesetzt — wer Inhalte meldet,
sieht keinen Kontaktweg; braucht nur eine Angabe (welcher Raum/Kontakt) von
sorb, dann eine Zeile Config
- **Desktop-Build** für den lokalen Fehlerbericht-Weg noch ausständig
- **ADR-0009** (Commit-Konventionen/Anonymisierung) nachträglich verfasst —
Lehre aus der Retro, in `decisions/` dokumentiert
@@ -1,105 +0,0 @@
# AAR — `@apo` konnte nicht telefonieren: fehlende Synapse-`profiles`-Zeile
**Datum:** 2026-08-11 · **Beteiligt:** sorb + Mac-Session · **Stack:** Synapse,
MAS, Authentik, Element Web / Element Call, MatrixRTC (K3s-Cluster)
**Auftrag:** `@apo` kann sich anmelden und schreiben, aber **kein Call kommt
zustande** — Grundursache finden und beheben, ohne weiter zu raten.
## 1. Ergebnis
**Behoben und verifiziert:**
- `@apo` telefoniert wieder. Grundursache belegt: dem Konto fehlte die Zeile in
Synapses `profiles`-Tabelle. Fix war ein einzelnes `INSERT` der
Registrierungs-Default-Zeile, an zwei gesunden Konten (`clark`,
`calltest01`) gegengeprüft.
- Gegenprobe nach dem Fix: `displayname` gesetzt (vorher keine Zeile),
`open_id_tokens` **0 → 6**, aktives `org.matrix.msc3401.call.member` im Raum.
- Dokumentiert: Runbook `docs/troubleshooting/CALLS-FEHLEN-PROFILE-ZEILE.md` im
gitops-Repo (inkl. Index-Eintrag), Merksatz im Session-Gedächtnis.
**Nebenbefund, separat behoben:**
- **Kontoübernahme-Lücke:** Der MAS-Upstream-Provider stand auf
`claims_imports.localpart.on_conflict: add` — bei Localpart-Kollision verknüpfte
MAS die neue Upstream-Identität mit einem **bestehenden** Konto (inkl.
Dienstkonten ohne Upstream-Link). Auf `on_conflict: fail` umgestellt
(gitops `ef04d86`, nach git.lab gepusht), dokumentiert als
[gitops#61](https://git.lab/axion1337.chat/axion1337.chat-gitops/-/issues/61),
`priority:high`. Ausgelöst durch die live reproduzierte case-sensitive Dublette
`boje`/`Boje`; das Zweitkonto `boje` (Authentik-ID 11) wurde gelöscht.
**Deployment verifiziert:** Das SOPS-Values-Secret aktualisierte Flux, aber MAS
lief noch mit der alten Config im Speicher (Pod älter als die Änderung) — erst
ein `rollout restart` machte `fail` aktiv. „Committet" ≠ „deployed" ≠ „aktiv".
## 2. Die Kausalkette (belegt, nicht vermutet)
| Glied | Beleg |
|---|---|
| `@apo` hat **keine `profiles`-Zeile** | `SELECT count(*) … = 0`, während `clark`/`sorb`/`calltest01` je eine haben |
| Displayname-Setzen crasht | `PUT …/displayname → 500`, `TypeError: 'NoneType' object is not subscriptable` in `_check_profile_size` (`storage/databases/main/profile.py:354`) — `txn.fetchone()` liefert `None`, `row[0]` fliegt |
| kein Displayname → Widget-Init bricht ab | Call-Klick erzeugte **null** Server-Aktivität: kein `openid/request_token`, kein `call.member`; Browser-Log damals „Messaging present but not yet started" (iframe meldet nie `ContentLoaded`) |
| kein Widget → kein Token → keine SFU | `@apo` als einziger aktiver Nutzer mit **0** Einträgen in `open_id_tokens` (die nicht geprunt werden) |
Herkunft der fehlenden Zeile: `@apo` ist ein **Vor-Authentik-Konto**, das durch
sechs Identitäts-Resets ging. Deaktivieren löscht in Synapse das Profil,
Reaktivieren legt es nicht neu an. `frank` (noch älter, nie zurückgesetzt) behielt
seine Zeile. Ob einer der früheren manuellen Eingriffe der auslösende Reset war,
ist nicht mehr zweifelsfrei zu klären — die Zeile ist jetzt wieder da.
## 3. Was ausgeschlossen wurde (gemessen)
| Verdacht | Warum entkräftet |
|---|---|
| Krypto / Cross-Signing (18 Pseudo-Geräte aus 6 Resets) | Testraum ist **unverschlüsselt** → Call braucht keine Krypto; `clark` telefoniert mit ebenfalls zurückgesetzten Schlüsseln |
| Server-Call-Pfad (SFU, RTC-Auth, OpenID-Endpoint) | `calltest01`/`sorb` bekommen sauber 200 auf `openid/request_token` und die Federation-Auflösung |
| `@apo`s Token / Session | `/sync` läuft durchgehend mit 200, Messaging intakt |
| Login-Verknüpfung MAS↔Authentik | `subject` = Authentik-`uid` `2fafe38b…`, korrekt |
## 4. Was zur Lösung geführt hat
- **Der Sprung von „welcher Nutzer telefoniert nicht" zu „welche *Tabelle* ist
anders".** Der Durchbruch war die `open_id_tokens`-Abfrage über *alle* aktiven
Nutzer: `@apo` = 0, alle anderen zweistellig+. Ein Vergleich statt einer
Einzelbetrachtung.
- **Ein unverschlüsselter Testraum** hat das größte Ablenkungsfeld
(Cross-Signing) in einem Schritt geschlossen.
- **Der Live-Mitschnitt beim echten Call-Klick** zeigte die Abwesenheit jeder
Aktivität — nicht ein Fehler, sondern *nichts* war der Befund.
- **Der Nutzer-Hinweis „Anzeigename konnte nicht gesetzt werden"** lieferte den
500er mit vollständigem Stacktrace — die letzte Meile von Korrelation zu
Ursache.
- **Der entscheidende Kontext kam von sorb:** „`apo` ist ein Alt-Konto von vor
der Authentik-Integration." Das lenkte die Suche von „angesammelter Müll" auf
„Migrations-/Provisionierungs-Lücke".
## 5. Lehren für die Zukunft
1. **Bei Call-Problemen zuerst `open_id_tokens` je Nutzer vergleichen.** 0 bei
einem sonst aktiven Konto ist das schnellste, eindeutigste Alarmsignal und
trennt Client- von Server-Ursache in einer Abfrage.
2. **Immer im unverschlüsselten Raum reproduzieren, bevor man Krypto verdächtigt.**
Das schließt einen ganzen Ursachenblock kostenlos aus.
3. **„Nichts passiert" ist ein Messergebnis, kein Sackgassen-Signal.** Die
Abwesenheit eines `openid`-Aufrufs hat den Fehler lokalisiert, nicht ein
Fehlercode.
4. **Alt-/mehrfach-zurückgesetzte Konten gegen frisch provisionierte diffen,
nicht nur gegen die Erwartung.** Der Unterschied war eine *fehlende* Zeile —
sichtbar nur im direkten Vergleich mit `clark`/`calltest01`.
5. **Jeder DB-Schreib strukturiert: betroffene Zeile vorher anzeigen, an einem
gesunden Konto gegenprüfen, per `INSERT … ON CONFLICT DO NOTHING` statt
Überschreiben.** Das ist die direkte Konsequenz aus den früheren
unstrukturierten MAS-Eingriffen dieses Vorgangs — und diesmal eingehalten.
6. **Beiläufige Symptome ernst nehmen:** die Dublette `boje`/`Boje` beim
Testkonto-Anlegen war der Faden, der die Kontoübernahme-Lücke (gitops#61)
aufdeckte — ein Sicherheitsfund, der ohne den `@apo`-Vorgang unentdeckt
geblieben wäre.
## 6. Offen / Folgetodos
- **gitops#61** (`on_conflict`-Härtung) ist gepusht und rollt über Flux; der
case-insensitive Eindeutigkeits-Check im `matrix-invitation`-Prompt-Stage
(damit der Nutzer schon bei der Registrierung statt erst beim Login scheitert)
ist dort als bewusst offener Rest vermerkt.
- Verwaiste Altlasten bei `@apo` (10 `local_notification_settings` für längst
gelöschte Geräte, 18 Cross-Signing-Pseudoeinträge) sind **kosmetisch** und
wurden bewusst **nicht** angefasst — sie haben mit dem Call-Problem nichts zu
tun, und ein weiterer Eingriff widerspräche der Lehre oben.
-93
View File
@@ -1,93 +0,0 @@
# Verfahren: Deploy-Übergabe
Für die Konstellation „einer baut, ein anderer rollt aus". Zweck ist nicht mehr
Prozess, sondern **weniger Rückfragen und weniger stille Fehlschläge**.
Eingeführt am 2026-08-01 nach dem Deploy der CVE-Pipeline (`gitops#47`), siehe
[aar/2026-08-01-cve-pipeline-gitops47.md](aar/2026-08-01-cve-pipeline-gitops47.md).
## Ablauf
1. Wer baut, öffnet **auf git.lab** ein Issue aus der Vorlage **Deploy-Übergabe**
(`.gitlab/issue_templates/Deploy-Übergabe.md`, im Feld *Description template*).
2. Wer ausrollt, arbeitet die Prüfliste unten ab und deployt.
3. Wer ausrollt, hängt den **AAR** als Kommentar an dasselbe Issue
(Vorlage: [aar-vorlage.md](aar-vorlage.md)). Bei Befunden ab MEDIUM
zusätzlich als Datei unter [aar/](aar/).
## Die vier Punkte, die den Unterschied machen
### 1. Mengengerüst vor dem Deploy
Bei allem, was etwas erzeugt — Nachrichten, Alarme, Zeitreihen, Requests —
gehört die erwartete Anzahl beim **ersten** Lauf in die Übergabe. Geschätzt ist
in Ordnung, gemessen ist besser; welches von beidem, muss dabeistehen.
Der Grund: „ein Alarm pro Fund" ist eine völlig unauffällige Zeile im Code und
harmlos bei 5 Funden. Bei 126 ist es ein Ausfall. Der Unterschied steht nirgends
im Diff — er ergibt sich erst aus den Daten, gegen die das Ding läuft.
Eine Stichprobe reicht: drei repräsentative Elemente von 29 messen und
hochrechnen kostet Minuten und liefert die Größenordnung.
### 2. Verifikation dort, wo der Dienst liest
Ein grüner Linter belegt, dass eine Datei **gültig** ist — nicht, dass sie
**geladen** wurde. Diese beiden Aussagen sind bei Bind-Mounts, Caches und
Reload-Semantiken regelmäßig verschieden.
Also im Container prüfen, in der laufenden API, im tatsächlich geladenen
Regelwerk. Ein Blick auf die Platte beweist nichts über den Prozess.
### 3. Deploy-Kommando vollständig übergeben
Inklusive Reload-, Restart- und Recreate-Schritten. Ein `docker compose up -d`,
das `Running` meldet und dabei nichts aktiviert, ist der häufigste stille
Fehlschlag: kein Fehler, kein Log, falscher Zustand.
Konkret auf dem Monitoring-Stack (CFGMON): Einzeldatei-Mounts hängen am Inode,
`git pull` benennt beim Schreiben um und erzeugt damit einen neuen — der
Container zeigt danach weiter auf die alte Datei. Es braucht
`--force-recreate`. Details: `gitops#52`.
### 4. Zustellwege stumm schalten statt Deploy verschieben
Datensammlung und Außenwirkung lassen sich fast immer getrennt scharf schalten.
Wenn der Zustellweg das Risiko ist, wird **er** abgeklemmt — nicht der ganze
Deploy verschoben.
So läuft die Datensammlung ab sofort, das Dashboard steht, echte Zahlen
ersetzen die Schätzung, und die Entscheidung über die Zustellung fällt auf
Basis von Messwerten statt Vermutungen. Wichtig: die Stummschaltung gehört
committet und dokumentiert, sonst ist sie in zwei Wochen ein Rätsel.
## Prüfliste für den Ausrollenden
- [ ] Diff gelesen, nicht nur die Beschreibung
- [ ] Mengengerüst plausibel? Bei Zweifel an einer Stichprobe selbst messen
- [ ] Configs mit den jeweiligen Werkzeugen validiert (`promtool`, `amtool`,
`compose config`, `py_compile` …)
- [ ] Außenwirkung identifiziert — was verlässt beim ersten Lauf das System?
- [ ] Nach dem Deploy **im Container** verifiziert, dass die neue Config aktiv ist
- [ ] Geprüft, ob vorher gesunde Dinge noch gesund sind (keine stille Regression)
- [ ] AAR geschrieben, Folge-Issues angelegt, Stummschaltungen dokumentiert
## Abgrenzung
Das Verfahren gilt für Übergaben zwischen Personen. Wer baut **und** ausrollt,
braucht kein Issue — der AAR lohnt trotzdem, sobald es Befunde ab MEDIUM gab.
## Kanonisierung nach CFGMON-Deploys (Topologie-Pflichtschritt)
CFGMON erreicht git.lab nicht — Commits aus Deploy-Sessions landen zwangsläufig
direkt auf dem Gitea-Mirror und werden vom nächsten Mirror-Lauf **kommentarlos
überschrieben** (zweimal passiert: dfe04c4→6ffab68 am 01.08. nachts,
2b715ca→0bd77e2 am 01.08. nachmittags). Deshalb gehört zu jeder Übergabe:
1. **CFGMON-Seite** vermerkt den Commit-Hash im Übergabe-Issue (Feld „Stand").
2. **Mac-Seite** kanonisiert zeitnah: Patch per
`https://rohana.axion1337.de/sorb/<repo>/commit/<sha>.patch` ziehen
(verwaiste Objekte bleiben eine Weile abrufbar), `git am`, Push nach git.lab.
Der Hash ändert sich dabei — **Autorschaft und Inhalt bleiben erhalten**.
3. **CFGMON** vor dem nächsten Pull: `git fetch && git reset --hard origin/main`
(inhaltsgleich, nur neuer Hash).
-47
View File
@@ -1,47 +0,0 @@
# Issue-Migration Gitea → GitLab (gitops#48)
`migrate.py` überführt Issues (offen **und** geschlossen, inkl. Kommentare)
eines Gitea-Repos in ein bestehendes GitLab-Projekt. Einmal-Werkzeug für den
#48-Cutover; hier versioniert wegen Reproduzierbarkeit.
## Eigenschaften
- **Dry-Run per Default**, `--execute` schreibt wirklich
- **Idempotent** über Marker `<!-- gitea-migration: OWNER/REPO#N -->` in der
Ziel-Beschreibung — Wiederholungsläufe überspringen Migriertes
- **Zeitstempel bleiben erhalten** (Admin-Token darf `created_at` setzen);
Autorschaft läuft auf den Token-User, Original-Autor+Datum stehen im
Migrations-Fußtext bzw. Kommentar-Präfix (bekannter, akzeptierter Verlust)
- Labels namensgleich (Gruppen-Labels müssen vorher existieren — Stand
2026-08-01 sind die 9 Gitea-Labels + 5 `host:*` als Gruppe-13-Labels angelegt)
- PRs werden ausgefiltert, geschlossene Issues nach Anlage geschlossen
## Aufruf
```
python3 migrate.py sorb/<repo> <gitlab-projekt-id> # Dry-Run
python3 migrate.py sorb/<repo> <gitlab-projekt-id> --execute
```
Tokens: `~/.config/gitea-rohana/token` (read:issue) und
`~/.config/gitlab-lab/token` (Admin) auf dem Mac.
## Stand
| Repo | Ziel | Status |
|---|---|---|
| sorb/thread-net-git | Projekt 18 | ✅ 2026-08-01 (1 Issue, nummerngleich) |
| sorb/threadnet-call | Projekt 19 | ✅ 2026-08-01 (2 Issues, nummerngleich) |
| sorb/ThreadNet-Web | Projekt 16 | ✅ 2026-08-01 (9 Issues, nummerngleich) |
| sorb/axion1337.chat-gitops | Projekt 17 | ✅ 2026-08-01 (50 Issues, **Nummern verschoben**) |
⚠️ **gitops-Nummern sind NICHT deckungsgleich**: Gitea hatte Lücken (PRs zählen
mit), GitLab vergibt lückenlos — z. B. Gitea#48 → GitLab#46, Gitea#51 → GitLab#49,
Gitea#52 → GitLab#50. Die verbindliche Zuordnung steht im Migrations-Fußtext
jedes GitLab-Issues (`Migriert aus Gitea …#N`); alte Commit-/Doku-Verweise auf
„gitops#N" meinen die **Gitea**-Nummer.
**Cutover-Nachschritte** (siehe gitops#48): Gitea-Issues schließen/als migriert
markieren ✅ 2026-08-01, Doku-Verweise umgebogen (CLAUDE.md-Topologieregel) ✅,
Milestones wurden nicht migriert (akzeptierter Verlust, gitops hatte keine
aktiven), Bot-/Token-Workflows (claude-issues → GitLab-Äquivalent) offen.
-94
View File
@@ -1,94 +0,0 @@
# Refinement und Retro — die Termine des Frameworks
Kanban braucht wenige, aber verlässliche Termine, sonst verkommt das Board zur
Ablage. Festgelegt in [ADR-0005](../docs/adr/0005-pm-framework-kanban.md); hier
steht, wie sie ablaufen.
## Termine (festgelegt im Struktur-Workshop, 2026-08-06)
| Termin | Wann |
|---|---|
| **Refinement** | **sonntagabends**, wöchentlich, 3045 min |
| **Retro light** | im **ersten Refinement des Monats**, +2030 min |
Sonntag, weil die GitLab-Backups dort ohnehin laufen (00:00) — die Woche hat an
dieser Stelle eine Kante, und die Abendblöcke, in denen real gearbeitet wird,
liegen meist am Wochenende.
Die Retro bekommt **keinen eigenen Termin**: Ein monatlicher Extra-Termin im
Solo-Betrieb ist ein Termin, der ausfällt. Sie hängt sich an das erste Refinement
des Monats an — dann ist die Vorbereitung (die AARs des Monats) ohnehin offen.
## Refinement (≈ wöchentlich, 3045 min)
Der eine Termin, der das System am Leben hält. Immer dieselbe Reihenfolge:
1. **Board von rechts nach links lesen** — zuerst `status:doing`: Läuft es noch,
oder ist es in Wahrheit blockiert? Dann `status:wartet`: Wartet es noch auf das,
was im Issue steht? Erst zuletzt `status:next`.
2. **WIP-Limit prüfen** — höchstens zwei Issues in `doing`. Ist es voll, wird nichts
Neues gezogen; stattdessen wird gefragt, was das Laufende blockiert.
3. **Nachziehen** — freie Plätze aus `next` füllen, `next` aus dem Backlog auffüllen.
Auswahlkriterium ist nicht Priorität allein, sondern **was still kaputtgeht**
(Fristen, abgeschaltete Schutzmechanismen) vor **was nervt** vor **was Spaß macht**.
4. **Entscheidungsvorlagen** — offene Fragen, die eine Entscheidung von sorb brauchen,
werden als Optionen mit Empfehlung vorgelegt, nicht als offene Fragen geparkt.
Dauerhafte Ausnahmen von Regeln werden hier zu ADRs.
5. **Datumspflicht prüfen** — jedes zeitkritische Issue trägt ein Datum, kein „bald".
## Retro light (≈ monatlich, 2030 min)
Drei Fragen, mehr nicht:
- Welche **Verfahren** haben diesen Monat getragen, welche haben gestört?
- Welche **ADRs** sind durch die Realität überholt (→ neues ADR, altes auf
„abgelöst durch")?
- **Fasert etwas aus?** Gibt es wieder Arbeit, die nur in Chatverläufen lebt?
Grundlage sind die AARs des Monats — sie sind die Retro-Vorbereitung, nicht ihr
Ersatz.
Ergebnisse werden unter [`retro/`](retro/) abgelegt, eine Datei je Termin. Die
erste: [2026-08-09](retro/2026-08-09.md).
## AAR (anlassbezogen)
Nach jedem Deploy mit Übergabe und nach jedem Incident, Vorlage in
[aar-vorlage.md](aar-vorlage.md). Ein AAR ist keine Chronik, sondern ein
Wissensspeicher: Was war das Ergebnis, welche Befunde, was hat die Eingrenzung
ermöglicht, welche Lehren, was bleibt offen. **Offene Punkte aus einem AAR werden
im selben Zug zu Issues** — sonst versacken sie in der Prosa (real passiert am
2026-08-01, nachgezogen als #14#16).
## Board-Pflege, wenn sorb länger nicht dazukommt
Festgelegt 2026-08-06. Eine Session darf das Board **abbilden**, aber nichts
**zusagen**:
| erlaubt | nicht erlaubt |
|---|---|
| `status:wartet` setzen (mit benanntem Grund) | nach `status:doing` ziehen |
| Erledigtes schließen, mit Begründung im Issue | `status:next` vergeben |
| Fristen ins `due_date`-Feld nachtragen | Prioritäten umsortieren |
| Befunde als neues Issue anlegen | Milestones neu zuordnen |
Die Trennlinie ist nicht Vorsicht, sondern Bedeutung: `doing` und `next` sind die
**Zusage-Spalten** — sie sagen, was als Nächstes wirklich passiert. Das entscheidet
sorb. Alles links davon bildet nur ab, was ohnehin schon der Fall ist.
**Jede Änderung wird im Issue begründet**, nicht still vorgenommen. Ein Board, dem
man nicht ansieht, wer warum etwas verschoben hat, ist beim nächsten Refinement
wertlos.
## Zusammenspiel mit den Sessions
Mehrere Claude-Sessions arbeiten parallel (Mac-Session, Host-Sessions auf CFGMON
und Overmind). Für sie gilt:
- Die **kanonischen Arbeitskonventionen** stehen in [`CLAUDE.md`](../CLAUDE.md) und
sind über den Gitea-Mirror von überall lesbar.
- Arbeit zwischen Sessions läuft über das
[Deploy-Übergabe-Verfahren](deploy-uebergabe.md) — Auftrag, Meldung, Protokoll
im Issue, nicht im Chat.
- Was eine Session lernt, gehört ins Repo (AAR/ADR/Doku), nicht nur in ihr
Gedächtnis — Sessions gehen verloren, Repos nicht.
-126
View File
@@ -1,126 +0,0 @@
# Retro light — 2026-08-09
Erste Retro des Frameworks, angehängt an das Refinement vom selben Tag
([Verfahren](../refinement.md)). Grundlage sind die vier AARs des Monats und 38
im August geschlossene Issues.
---
## 1. Welche Verfahren haben getragen, welche haben gestört?
### Getragen
**„Alles Offene wird ein Issue."** Das ist das Verfahren, das diesen Monat am
meisten eingebracht hat. Sämtliche stillen Fehler unten wurden nur deshalb nicht
vergessen, weil sie im Moment des Findens ein Issue bekamen — auch die, für die
gerade keine Zeit war.
**Die AAR-Pflicht.** Die vier AARs waren die einzige belastbare Vorbereitung für
diese Retro. Ohne sie wäre sie eine Erinnerungsübung geworden.
**Die Board-Pflege-Tabelle** (2026-08-06). Sie hat gehalten: `status:next` und
Meilenstein-Zuordnung blieben sorbs Entscheidung, auch als es unbequem war.
management#15 und #20 lagen drei Tage ohne Spalte — das ist der beabsichtigte
Preis, nicht ein Fehler.
### Gestört
**Der Status-Label-Satz ist in der Oberfläche nicht vollständig ablesbar.**
`status:next`, `status:doing`, `status:wartet` — und „ohne Label = Backlog". Die
vierte Spalte ist damit die einzige, die man nicht *sieht*, sondern erschließen
muss. Genau deshalb hat eine Session am 2026-08-06 ein `status:offen` erfunden und
in vier Projekten angelegt; aufgefallen ist es erst zwei Tage später.
Die Regel bleibt richtig — ein Label für „nichts Besonderes" wäre Rauschen. Aber
der Reiz, es zu erfinden, ist real und wird wiederkommen. **Festgehalten statt
geändert.**
---
## 2. Welche ADRs sind durch die Realität überholt?
**Keine überholt — aber eine Lücke.**
⚠️ **Die Commit-Konventionen und die Anonymisierung der Historie hätten eine ADR
gebraucht.** Am 2026-08-07 wurde eine dauerhafte Prozessregel eingeführt (englische
Conventional Commits, Zeitstempel auf 12:00 UTC) und am 2026-08-09 rückwirkend auf
251 Commits angewandt — eine **irreversible** Änderung an vier Repos, mit
Force-Push durch einen Mirror, von dem Flux liest.
Nach unserer eigenen Regel („ADR-Pflicht bei Architektur-/Prozessentscheidungen")
ist das ein Lehrbuchfall. Stattdessen steht die Regel nur in der `CLAUDE.md` und
die Durchführung in einer Zuordnungstabelle. Nachzuholen als **ADR-0009**.
**Beobachtung zu ADR-0005:** Das Kanban-Framework wurde diese Woche zweimal
erweitert (Titel ohne Priorität, Meilenstein-Pflicht) — beides in der `CLAUDE.md`,
nicht in der ADR. Das ist vertretbar, solange die ADR die *Entscheidung* hält und
die `CLAUDE.md` die *Regel*. Es ist aber genau die Zwei-Orte-Konstruktion, die wir
bei den Titel-Präfixen gerade aufgelöst haben. **Im Auge behalten.**
---
## 3. Fasert etwas aus?
**Nein — aber es gibt ein Muster, und das ist der eigentliche Befund des Monats.**
### Sechs stille Fehler in neun Tagen
| Was | Wie es aussah | Wie es wirklich stand |
|---|---|---|
| `build_embedded` (threadnet-call) | grün, seit jeher | lud **nie** ein Artefakt hoch, falscher Pfad |
| npm-Paket `0.19.2-threadnet.6` | veröffentlicht | 12,5 KB statt 12,8 MB, **ohne `dist/`** |
| Blueprint `matrix-recovery-flow` | Flux grün, ConfigMap aktuell | seit Tagen bei **jedem** Lauf verworfen |
| gitops-Arbeitskopie | „normal" | `main` trackte **Gitea** — ein `git push` wäre in die verbotene Richtung gegangen |
| Leere Pipelines | rot | **nichts kaputt** — der umgekehrte Fall, Rauschen, das rot abtrainiert |
| Release-Pipeline auf `v0.4.0` | lief nach Tag-Push an | hätte ein veröffentlichtes Image überschrieben |
Gefunden wurde **keiner** davon durch eine Überwachung. Vier durch Zufall beim
Suchen nach etwas anderem, zwei durch gezieltes Nachprüfen einer Behauptung.
### Das gemeinsame Merkmal
Alle sechs betreffen Vorgänge, die **erfolgreich aussehen, ohne es zu sein** — oder
die genau umgekehrt Alarm auslösen, wo nichts ist. Der Verbund hat für keinen
dieser Fälle eine Antwort auf die Frage: *Wer merkt es, wenn etwas leise aufhört zu
funktionieren?*
Es gibt Issues für Einzelfälle — gitops#50 (Configs greifen nicht ohne Neustart),
management#28 (Mirror-Ausfall unbemerkt), ThreadNet-Web#14 (Release überschreibbar,
behoben). Was fehlt, ist die Klammer.
⚠️ **Der letzte Fall ist der unangenehmste.** Dass `v0.4.0` nicht überschrieben
wurde, lag daran, dass die geschützten Registry-Variablen in genau diesem Fenster
nicht verfügbar waren — **Glück, nicht Absicht.** Eine Schutzmaßnahme, die
zufällig griff, ist kein Schutz.
### Vorschlag
Eine **Stillstandsprüfung**: ein geplanter Job, der die Invarianten prüft, die wir
diesen Monat einzeln und mühsam gelernt haben — Blueprint-Status ≠ error, Mirror
synchron, Pipeline ohne Jobs, Artefakt vorhanden, Paketgröße plausibel. Kein
weiterer Agent, der Meldungen erzeugt, sondern **eine** Prüfung mit einem Ergebnis.
Das ist die Verallgemeinerung von management#28, das am 2026-08-06 bewusst nach
hinten gestellt wurde. Die Rückstufung war zu dem Zeitpunkt vertretbar; sechs
Fälle später sieht der Einzelfall aus wie ein Symptom. **Zur Entscheidung
vorgelegt, nicht eigenmächtig umgestuft.**
---
## Beschlüsse dieses Refinements
- `status:next`: management#15 und #20 (fällig 31.08.) — Zusage von sorb
- `status:wartet` entfernt bei threadnet-call#4 und ThreadNet-Web#11: der im Issue
benannte Grund war weggefallen
- **M5 — Härtung** angelegt, 14 Issues aus M1 verschoben. Trennlinie: *Ist etwas
Vorhandenes kaputt (M1) oder fehlt etwas, das wir noch nie hatten (M5)?*
Verteilung danach: M1 18 · M2 21 · M3 4 · M4 13 · M5 14
⚠️ Die beiden letzten Punkte sind einer Session **allein** untersagt
([Board-Pflege](../refinement.md)). Sie fanden im Refinement mit sorb statt. Die
Regel ist damit nicht aufgeweicht.
## Offen aus dieser Retro
1. **ADR-0009** zu Commit-Konventionen und Historien-Anonymisierung nachziehen
2. **Stillstandsprüfung** — Entscheidung von sorb
-71
View File
@@ -1,71 +0,0 @@
# Stillstandsprüfung
Sucht Dinge, die **leise aufgehört haben zu funktionieren**. Beschlossen in der
[Retro 2026-08-09](retro/2026-08-09.md).
## Warum es sie gibt
In neun Augusttagen sind sechs Fehler aufgefallen, die alle dasselbe Merkmal
hatten: Sie sahen erfolgreich aus, ohne es zu sein — eine grüne Pipeline, die nie
ein Artefakt hochlud; ein veröffentlichtes npm-Paket ohne Inhalt; ein Blueprint,
der bei jedem Lauf verworfen wurde, während Flux grün meldete.
**Keiner davon wurde durch eine Überwachung gefunden.** Vier durch Zufall beim
Suchen nach etwas anderem. Genau diese Lücke schließt das Skript.
## Was geprüft wird
Jede Prüfung bildet einen **real passierten** Fall ab. Nichts steht hier auf
Vorrat.
| Prüfung | Der Fall dahinter |
|---|---|
| Repo ohne aktiven Push-Mirror | `game-operating` wurde angelegt und nie gespiegelt — auf Gitea existierte es nicht |
| Mirror-Drift | MIRROR-01 (management#28): fällt der Mirror aus, liefert Flux still den letzten Stand weiter |
| Pipeline mit null Jobs | ThreadNet-Web 203/204, threadnet-call 187 — rot, ohne dass etwas kaputt war |
| Erfolgreicher Job ohne Artefakt | `build_embedded` lief seit jeher grün und lud **nichts** hoch |
| npm-Paket zu klein | `0.19.2-threadnet.6`: 12,5 KB statt 12,8 MB, ohne `dist/` |
| Authentik-Blueprint ≠ successful | `matrix-recovery-flow` wurde tagelang bei jedem Lauf verworfen |
Die Projektliste wird **zur Laufzeit aus der Gruppe gelesen**, nicht im Code
gepflegt — eine Liste im Quelltext wäre genau die Stelle, an der ein neues Repo
jahrelang durchrutscht. (Beim ersten Lauf kamen so zwei Projekte zum Vorschein,
die niemand auf dem Schirm hatte.)
## Wie sie läuft
Geplanter CI-Job im management-Repo, zusätzlich von Hand über *Run pipeline*
auslösbar. Befunde färben die Pipeline **rot** — das ist bei uns die Alarmanlage,
nicht ein zusätzlicher Meldeweg (siehe `gitops/CLAUDE.md` zur TURN-Rotation).
Lokal:
```bash
export GITLAB_TOKEN=$(cat ~/.config/gitlab-lab/token)
export GITEA_TOKEN=$(cat ~/.config/gitea-rohana/push-token) # fuer private Spiegel
export LAB_CA=.../ci/lab-ca-chain.crt
python3 scripts/stillstandspruefung.py
```
## Zwei Regeln für diese Prüfung
**Ein „kann nicht geprüft werden" ist ein Befund, kein Übersprungen.** Real
aufgefallen am 2026-08-09: `game-operating` wurde auf Gitea privat gestellt, und
die Prüfung übersprang den Mirror-Abgleich klaglos. Ein Repo, das gespiegelt wird,
dessen Gegenseite aber unlesbar ist, ist **ungeprüft** — und das darf nicht wie
„in Ordnung" aussehen.
**Ein Befund wird zum Issue, nicht weggeklickt.** Sonst wird die Prüfung zu dem,
was sie sucht: etwas, das läuft, ohne dass jemand hinsieht.
⚠️ **Fehlt ein Zugang, bricht sie ab — sie überspringt sich nicht still.** Eine
Prüfung, die sich bei fehlendem Token selbst deaktiviert, ist wertlos: Sie meldet
dann jahrelang nichts, und niemand merkt den Unterschied zu „alles in Ordnung".
Ausnahme sind die klar benannten optionalen Teile (Authentik), die ihr Fehlen im
Ergebnis ausweisen.
## Erweitern
Neue Prüfungen kommen dazu, **wenn wieder etwas still ausgefallen ist** — mit einem
Docstring, der den konkreten Fall nennt. Prüfungen auf Verdacht erzeugen Rauschen
und kosten die Glaubwürdigkeit, die diese hier braucht.
-116
View File
@@ -1,116 +0,0 @@
# Textbausteine für Sessions
Kurze, kopierbare Blöcke, die man einer Claude-/Agenten-Session voranstellt.
Die Konventionen stehen kanonisch in [`CLAUDE.md`](../CLAUDE.md) — aber eine
Session liest sie nur, wenn sie dazu aufgefordert wird. Diese Bausteine sind die
Aufforderung.
## Zwei Regeln für diese Datei
**Die Bausteine verweisen auf die Regeln, sie wiederholen sie nicht.** Stünden die
Regeln hier ausgeschrieben, gäbe es eine zweite Fassung, die driftet — real
passiert am 2026-08-02, als `gitops/CLAUDE.md` „keine Gitea-Ausnahme mehr" behauptete,
während die `management/CLAUDE.md` zwei nannte.
**Höchstens acht Zeilen je Baustein.** Der Test ist banal: Wer zum Kopieren scrollen
muss, benutzt es nicht. Was länger wäre, gehört in die CLAUDE.md — nicht hierher.
---
## 1 · Session-Start (Mac, mit Lab-Zugang)
```
Lies zuerst CLAUDE.md im management-Repo auf git.lab und halte dich daran.
Kanonisch ist git.lab; nie direkt nach Gitea pushen.
Alles Offene wird zum Issue, nicht zur Chat-Notiz — auch Nebenbefunde.
Bevor du ein Issue schließt oder darüber urteilst: vollständig lesen, inklusive
Kommentare.
Bevor du aus einer Vorlage/Spezifikation ableitest: die Quelle öffnen, nicht raten.
Verifiziert und vermutet klar trennen; fremde Messungen als fremde kennzeichnen.
```
> Die letzten drei Zeilen stehen hier, weil genau das dreimal an einem Tag
> schiefging: erfundene Theme-Paletten statt gelesener Skill-Quelle; ein Sweep nach
> dem Pfad `sorb/Backlogs` statt nach dem Namen `Backlogs`; und ein Issue, von dem
> 750 von 1237 Zeichen gelesen wurden — samt übersehenem Korrekturkommentar, der
> seit 16 Stunden darunterstand.
## 2 · Host-Session (CFGMON, MATRIX — ohne Lab-Zugang)
```
Du arbeitest auf einem Hetzner-Host ohne direkte Lab-Route.
Konventionen: CLAUDE.md im management-Repo — von hier lesbar über den Gitea-Mirror
rohana.axion1337.de/sorb/management. Dort NUR lesen, niemals hinpushen.
Für git.lab (Issues, Pushes) muss sorb erst den Site-to-Site-Tunnel einschalten.
git.lab-API: PRIVATE-TOKEN-Header — .netrc gilt nur für clone/push (sonst 401,
bei privaten Projekten irreführend 404, sieht aus wie "Projekt gibt es nicht").
Ping auf 10.58.73.17 schlägt IMMER fehl (nur 443 + DNS offen), das ist kein
Tunnelproblem — prüfen mit: curl https://git.lab/users/sign_in
```
> Soll in dieser Session etwas ausgerollt werden, kommt **Baustein 3** dazu — der
> Deploy-Weg samt AAR-Pflicht steht dort, nicht hier, damit dieser Block kurz bleibt.
## 3 · Deploy-Übergabe
```
Öffne auf git.lab ein Issue aus der Vorlage "Deploy-Übergabe"
(Feld "Description template") und fülle ALLE Felder — Verfahren und Begründung
je Feld: verfahren/deploy-uebergabe.md.
Pflicht: Stand (Repo/Branch/Commit) · Testtiefe (ehrlich, "ungetestet" ist gültig)
· Mengengerüst (geschätzt oder gemessen, dazuschreiben welches) · vollständiges
Deploy-Kommando inkl. Reload/Recreate · Verifikation DORT WO DER DIENST LIEST
· Außenwirkung und Not-Aus · Rollback · bewusst offen Gelassenes.
Wo nichts zutrifft: "-" eintragen, nicht das Feld löschen.
```
## 4 · Abschluss einer Session
> **Dieser Baustein ist zugleich unsere Definition of Done für Änderungen ohne
> Deploy** (festgelegt 2026-08-06). Für Deployments gilt weiterhin das
> [Übergabe-Verfahren](deploy-uebergabe.md) — das ist die längere DoD.
>
> Bewusst kein eigenes DoD-Dokument: Es wäre die dritte Fassung derselben Regeln
> und damit die dritte, die driften kann.
```
Vor dem Ende prüfen und benennen:
- Alle Commits über git.lab gepusht, kein Rest im Arbeitsverzeichnis, Mirror grün.
- Jeder offene Punkt und Nebenbefund ist ein Issue — nichts bleibt nur im Chat.
- Zeitkritisches trägt ein Datum im due_date-Feld, nicht nur im Fließtext.
- Genau ein status:*-Label je angefasstem Issue; status:wartet nur mit Grund.
- Gedächtnis aktualisiert: nur was kein Repo festhält.
- Wiederaufsetzpunkt in einem Satz: Was ist als Nächstes dran, und wer ist dran?
```
## 5 · Entscheidungsvorlage
```
Leg mir das als Entscheidung vor, nicht als offene Frage:
24 Optionen, je eine Zeile Konsequenz, und deine Empfehlung zuerst mit Begründung.
Sag dazu, was du gemessen und was du angenommen hast.
Wenn die Entscheidung eine dauerhafte Ausnahme von einer Regel schafft, ist sie
ADR-pflichtig (decisions/, siehe CLAUDE.md) — dann leg die ADR gleich mit vor.
```
---
## Wann welcher
| Situation | Baustein |
|---|---|
| Neue Session auf dem Mac | 1 |
| Session auf CFGMON/MATRIX/game | 2 |
| Etwas gebautes soll ausgerollt werden | 3 |
| Session neigt sich dem Ende | 4 |
| Eine Frage braucht sorbs Entscheidung | 5 |
Bausteine 1 und 2 schließen sich aus; 35 kommen anlassbezogen dazu.
## Pflege
Ein Baustein wird ergänzt, wenn **derselbe Fehler zweimal** passiert ist — nicht
vorsorglich. Sonst wachsen sie, bis sie niemand mehr kopiert, und dann wirken sie
gar nicht mehr. Wächst einer über acht Zeilen, gehört der Inhalt in die CLAUDE.md
und hier bleibt der Verweis.