This commit is contained in:
2026-09-07 17:20:38 +02:00
3 changed files with 70 additions and 553 deletions

View File

@@ -6,7 +6,7 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
## Pflichtlektüre pro Sitzung (Reihenfolge)
1. **`docs/teilprojekte.md`** (Quick-Index-Block) — kanonischer Stand, Scope, Abhängigkeiten, **offene** TPs. **1 Read reicht.**
1. **Status/Index = Gitea-Board** (seit TP36-Cutover 2026-09-07): [`admin/bf-infra` · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues) — TP = Issue, Status = Label `status:*`, Gruppierung = `thema:*`. Für den **Detail/Kontext** eines TP dann den `### TPnn`-Prosa-Abschnitt in **`docs/teilprojekte.md`** (aktiv) bzw. **`docs/teilprojekte-archiv.md`** lesen. *(Die frühere „Quick-Index-Block, 1 Read reicht"-Zeile ist überholt — Index/Status stehen nicht mehr in der Datei.)*
2. **`docs/handoff-*.md`** (neueste) — wo die letzte Session aufgehört hat.
3. Bei Live-Writes oder Inventur: **`docs/cf-tunnel-mapping.md`**, **`docs/teilprojekte-archiv.md`**.
4. ✅ **TP16.3 §5 durch (2026-09-01):** [`docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md`](docs/handoff-20260901-tp16.3-paragraph5-flip-durch.md)
@@ -34,6 +34,21 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
6. ap3-Bring-up 2026-08-30: [`docs/handoff-20260830-ap3.md`](docs/handoff-20260830-ap3.md) — sechs Funde zu Werks-MikroTiks, CAPsMAN und Golden-Config-Lücken.
7. Inventur-Befund 2026-08-27: [`docs/handoff-20260827-inventur-und-architektur.md`](docs/handoff-20260827-inventur-und-architektur.md) — Master-Struktur, Asymmetrie, Drift.
## SSoT & Branch-Konvention (TP36-Cutover 2026-09-07)
- **SSoT-Dreiteilung:** **Index/Status = Gitea-Issues** (`admin/bf-infra`, Labels `status:*`/`thema:*`) ·
**Prosa/Detail = Repo** (`docs/teilprojekte.md` aktiv + `teilprojekte-archiv.md`, `### TPnn`-Abschnitte) ·
**Live-Gerätedaten = Nautobot** (Compliance-Zahl, RouterOS-Version …). Ein Status ändert sich **in Gitea**,
nicht mehr in einer Repo-Datei. Sync-/Abgleich-Werkzeug fürs Archiv: `bin/gitea-tp-sync.py` (Dry-Run default,
Token aus `~/.hermes/.env`, **nie ausgeben**).
- **Branch-Konvention:** Branch `tpNN-kurz` **jederzeit** mitten in der Session möglich (`git switch -c`,
uncommittete Arbeit wandert mit); für Multi-Session-TPs der Normalfall. **Merge nach `main`, sobald der
Code/Config des TP steht.** Weil Steuerungs-Updates nach **Gitea** gehen (nicht in `teilprojekte.md`),
kollidieren TP-Branches nicht mehr auf der Steuerungs-Doku — nur noch der eigene `### TPnn`-Prosa-Abschnitt
wird angefasst.
- **TP anlegen/schließen:** neues aktives TP = **direkt als Gitea-Issue** (Labels setzen), Prosa-Abschnitt
im Repo dazu; abschließen = Issue `closed`, Prosa wandert bei Bedarf ins Archiv. **Keine Handtabelle mehr.**
## Was du sofort wissen musst (Inventur 2026-08-27)
- **`bf-infra`** ist aktiver Master **mit Gitea-Remote**: `origin` = `https://gitea.emsenhuber.net/admin/bf-infra.git` (live verifiziert 2026-08-30). ⚠️ **Die frühere Zeile „lokal, kein Gitea-Remote → verloren bei Festplattencrash" ist überholt** — sie stammt aus der Inventur vom 2026-08-27 und war damals richtig; das Remote existiert seit spätestens 2026-08-28 (`origin/main` = `b917691`). **Die Backup-Lücke ist damit keine Struktur-, sondern eine Disziplinfrage: `git push`.** Am 2026-08-30 lagen vier Commits nur lokal. 🔴 **Das Remote ist nur so viel wert wie der letzte Push** — der Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5) bricht Commits ab, solange kein Backup markiert ist; **das ist die Erinnerung ans Pushen, kein Hindernis.** Nicht mit `--no-verify` umgehen. 🔴 **Merkregel:** diese Zeile hat eine Session dazu gebracht, „kein Remote" zu behaupten, statt `git remote -v` zu tippen — vor jeder Aussage über Remotes/Backups **live prüfen**.
@@ -50,12 +65,15 @@ DNS/DHCP-Ablösung, WLAN via **CAPsMAN auf dem Core** (TP28, 2026-08-17 — der
Wenn während einer Session ein neues Teilprojekt, eine Abhängigkeit oder ein Querschnittsthema sichtbar wird, das bisher in keinem TP abgebildet ist:
- Vorschlagen und mit dir abstimmen (nicht eigenmächtig anlegen).
- Auf dein Okay hin anlegen: Quick-Index-Zeile + `### TP<n>`-Abschnitt mit Status `🔜`, Scope,
Abhängigkeiten, Abgrenzung. Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus
verworfenen TPs bleiben frei).
- Ist-Stand-Block mitziehen (Datum, betroffene Zeilen).
- Auf dein Okay hin anlegen: **Gitea-Issue** `TPnn — <Kurztitel>` mit Labels `status:🔜`(todo) +
passendem `thema:*` + Body (Anriss + Verweis auf den Prosa-Abschnitt) **und** einen
`### TP<n>`-Prosa-Abschnitt in `docs/teilprojekte.md` (Scope, Abhängigkeiten, Abgrenzung).
Nächste freie Nummer vergeben (höchste vergebene + 1; Lücken aus verworfenen TPs bleiben frei).
- **Status/Reihenfolge/„was lief zuletzt" gehen ins Gitea-Issue** (Label ändern, Kommentar), **nicht**
in eine Quick-Index-Tabelle oder einen Ist-Stand-Block — die gibt es seit dem TP36-Cutover (2026-09-07)
nicht mehr in der Datei.
**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt.** Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — und der **Ist-Stand-Block wird bei jeder Änderung mitgezogen**, nicht nur der betroffene TP-Abschnitt.
**Abgeschlossene TPs** stehen seit 2026-08-23 in `docs/teilprojekte-archiv.md` (Volltext, unverändert, mit Inhaltsverzeichnis) und ihr Gitea-Issue ist `closed` (Label `status:✅`/`❌`). **Ein TP wandert erst ins Archiv, wenn nichts mehr daran zu tun ist — und wieder zurück, sobald doch etwas dazukommt** (Issue dann `reopen`). Ein archiviertes TP ist ein *geschlossenes Ticket, kein versiegeltes*: bei neuer Arbeit an derselben Baustelle wandert der **Volltext zurück** nach `teilprojekte.md` (Zeiger bleibt im Archiv), **kein** Zwillings-TP mit neuer Nummer (Christoph-Direktive 2026-08-30, erster Fall TP28/`ap3`) — nur noch der betroffene `### TPnn`-Prosa-Abschnitt wird angefasst.
---

View File

@@ -3,11 +3,15 @@
TP36 (reframed) — teilprojekte.md Index/Status → Gitea Issues (admin/bf-infra).
Idempotenter Sync (get-or-create, Match ueber "TPnn" im Titel):
- Quick-Index-Tabelle in docs/teilprojekte.md -> aktive Issues (open/closed je Marker)
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die NICHT aktiv sind)
- Archiv-Header in docs/teilprojekte-archiv.md -> closed Issues (nur TPs, die in Gitea
NICHT offen sind; ein reopentes TP mit Archiv-Zeiger bleibt offen).
- Labels status:* (Marker) + thema:* (Gruppierung) werden vorab get-or-create.
SSoT-Modell: Gitea fuehrt Index+Status; die per-TP-Prosa bleibt in docs/teilprojekte.md.
SSoT-Modell (seit TP36-Cutover 2026-09-07): Gitea fuehrt Index+Status — aktive TPs werden
DIREKT als Gitea-Issue angelegt/gepflegt, NICHT mehr aus einer Datei-Tabelle abgeleitet
(die Quick-Index-Tabelle wurde aus teilprojekte.md entfernt). Dieses Werkzeug haelt nur noch
die ARCHIVIERTEN (closed) TPs mit teilprojekte-archiv.md idempotent abgeglichen und fasst
offene Issues nie an. Die per-TP-Prosa bleibt in docs/teilprojekte.md / teilprojekte-archiv.md.
Token: ~/.hermes/.env (GITEA_ACCESS_TOKEN, GITEA_HOST) — wird NIE ausgegeben.
Aufruf:
@@ -100,25 +104,9 @@ def api(method, path, body=None):
# --------------------------------------------------------------------------- parse
def parse_active():
"""Quick-Index-Zeilen: > | TP44 | ⏳ | Kurztitel … | Datum |"""
out = []
row = re.compile(r"^>\s*\|\s*(TP[\d.]+)\s*\|\s*([^\s|]+)\s*\|\s*(.+?)\s*\|\s*(.*?)\s*\|\s*$")
for line in ACTIVE.read_text().splitlines():
m = row.match(line)
if not m:
continue
tp, marker, title, date = m.groups()
num = tp[2:]
# Titel auf den Kern kuerzen (vor dem ersten " — " / " (" wenn lang)
short = re.split(r"\s+—\s+|\s*\(", title)[0].strip()
short = re.sub(r"\*\*|`", "", short)[:80]
out.append({"tp": tp, "num": num, "marker": marker.strip(),
"title": short, "date": date.strip(), "src": "active"})
return out
def parse_archiv(active_nums):
def parse_archiv(exclude_nums):
"""Archiv-Header -> archivierte TPs. exclude_nums = TP-Nummern, die in Gitea OFFEN sind
(reopente TPs mit Archiv-Zeiger) -> die bleiben offen, nicht als closed abgleichen."""
out = []
hdr = re.compile(r"^#{2,4}\s*(TP[\d.\-A-Za-z]+)\s*—\s*(.+?)\s*▸", )
seen = set()
@@ -128,7 +116,7 @@ def parse_archiv(active_nums):
continue
tp, title = m.groups()
num = tp[2:]
if num in active_nums or num in seen:
if num in exclude_nums or num in seen:
continue
seen.add(num)
short = re.sub(r"\*\*|`", "", title).strip()[:80]
@@ -164,22 +152,26 @@ def all_issues():
def main():
print(f"Gitea {HOST} repo {OWNER_REPO} mode={'COMMIT' if COMMIT else 'DRY-RUN'}")
active = parse_active()
active_nums = {a["num"] for a in active}
archiv = parse_archiv(active_nums)
items = active + archiv
print(f"Geparst: {len(active)} aktive + {len(archiv)} archivierte = {len(items)} TP\n")
st, existing_labels = api("GET", f"/repos/{OWNER_REPO}/labels?limit=100")
label_id = ensure_labels(existing_labels if isinstance(existing_labels, list) else [])
issues = all_issues()
by_tp = {}
open_nums = set()
for iss in issues:
# Titel ist "TPxx — …"; volle ID inkl. Buchstaben/Bindestrich (TP14.2a, TP-R) bis zum " — "
m = re.match(r"^(TP[\w.\-]*?)\s+—", iss["title"])
if m:
by_tp[m.group(1)] = iss
if iss.get("state") == "open":
open_nums.add(m.group(1)[2:])
# Aktive TPs sind seit dem TP36-Cutover SSoT direkt in Gitea -> nur das Archiv abgleichen,
# und offene (reopente) TPs davon ausschliessen, damit sie nicht faelschlich geschlossen werden.
items = parse_archiv(open_nums)
print(f"Geparst: {len(items)} archivierte TP (aktive TPs = SSoT in Gitea: "
f"{len(open_nums)} offen, nicht angetastet)\n")
created = updated = 0
for it in items:

View File

@@ -25,479 +25,24 @@ IP-Blöcke: `.1–10` Router/Gateways · `.11–20` Switches/APs. **GW je VLAN =
Mgmt (VLAN99, Ist 2026-08-16): core `.99.1` · branch `.99.2` · ap1 `.99.11` · ap2 `.99.12`.
*(Historisch, überholt: „ap1=.1, ap2=.2 · ALLNET=.11, Omada=.12" — aus der Zeit vor dem Core-Cutover.)*
## Ist-Stand (Kurz)
## Status & Index → Gitea-Board (SSoT seit 2026-09-07, TP36-Cutover)
**Stand 2026-09-04 — live gegen die Geräte geprüft (MikroMCP/REST/SSH), nicht aus der Doku abgeschrieben.**
**Compliance 140/140, 0 Drifts** (2026-09-04 nachmittags). Der TP25-Kanalplan ist **committed**:
Template auf DFS-Frequenz-Listen umgestellt (`ch-5g-ap1=5260,5280`, `ch-5g-ap2=5300,5320`) + die
ap1-Provisioning-Zeilen wieder aufgenommen → Transaktion `TP25-kanalplan-ap1-live` geschlossen und der
`wifi-provisioning`-Drift weg. `wifi-security` gruen (`ft=no` auf `sec-main`).
🔴 **Zusätzlich live behoben (nicht compliance-sichtbar):** ap3 trug auf `wifi1/wifi2` einen Werks-Inline
`security.ft=yes` — gemischtes FT-ESS, das die 3/3-Strich-Abbrüche vom 09-04 erklärt. Unset → uniform
`ft=no` on-air; Bring-up-Quelle `tp28-ap3-phase2-cap.rsc` um `!security.ft` ergänzt (s. Fund-E-Block).
*(Die Fassung vom 2026-08-16 stand bis zum 18. hier und war an vier Stellen überholt. Wer diesen Block
liest, liest ihn als Erstes: er wird bei jeder Änderung mitgezogen, nicht nur die TP-Abschnitte.)*
- 🗺️ **TP37/34/36-Strang geplant + TP44-Drift bereinigt (2026-09-06, „erst planen, nichts bauen"):**
Live gegen Nautobot geprüft — **TP37 Punkt 3 (CF-Connectoren) ist faktisch durch TP44 erledigt**
(native `nautobot.cloud`-Modelle, Etappe 1 live: Account 1/ResType 6/Network 4/Service 14). Echt offen
bleibt **TP37 Punkt 1+2** (Substrat: pve/NAS/macmini-Hosts + LXC/Docker als `virtualization.VM`, Dienste
als `ipam.Service` — live **VM 0/Cluster 0/Service 0/CustomField 0**), und darauf sitzt **TP34** (Caddyfile
via GC). **TP36** (Live-Daten-CustomFields + Datei-Split) ist davon **unabhängig** → parallel. Datenmodell-
Entscheid: **native Modelle** (konsistent zu TP44), keine eigene App. **TP44 nachgezogen:** eigene
Quick-Index-Zeile + Abschnitt, Seeder `nautobot/tp36-…` → `tp44-cf-estate-seed.py` umbenannt. Runbook:
[`docs/handoff-20260906-tp37-34-36-plan.md`](handoff-20260906-tp37-34-36-plan.md). **Kein Nautobot-Write.**
- ✅ **TP25 Multi-Pin-Konsole (2026-09-04):** die Klick-Konsole (`docs/tp6-marker/server.py`) ist auf
**N Clients** generalisiert und hat eine **Pin-UI** bekommen. Sie liest die Client-Liste aus
`geraete.txt` (schreibt `tp25-walk-messung.sh`), zeigt je Client AP/Signal/Ping-Verlust aus
`sampler-<name>.csv`/`walk-ping-<name>.csv` (kein eigenes SSH mehr) und pinnt N Clients band-genau
über `tp25-pin-multi.sh` direkt in der Konsole. Die alte 2-Client-Gegen-AP-Automatik ist entfernt.
Handoff: [`docs/handoff-20260904-tp25-konsole-n-clients.md`](handoff-20260904-tp25-konsole-n-clients.md).
- ✅ **TP25 Fund E GESCHLOSSEN (2026-09-02):** die vorgemerkte Abschlussmessung ist gelaufen —
`reauthenticating` am 09-02 nur noch **4 in 8,6 h** (alle iPhone `F2:62`, isolierte Einzel, 0 Ausfälle)
gegen Baseline 93 bzw. Hebel-1 **19/h**. Verlauf **218 → 40 → 4**; Vorbedingung geprüft (Core-Uptime
1d18h, `caps joined=0` → keine Reboot-Artefakte). ⇒ **7.24.1 (TP27) war die Ursache/der Fix, nicht
`ft-over-ds`.** Hebel 1 widerlegt+zurückgebaut, Hebel 2–4 entfallen, **kein Geräte-Write**. **Am
2026-09-04 hat Christoph `ft=no` auf `sec-main` als Dauerzustand entschieden** (Hebel 2 doch gezogen,
Template + bf-golden-config + Nautobot-Sync, `wifi-security` gruen). 🔴 **Nachtrag 2026-09-04:** „ohne FT"
galt bis dahin **nur für Manager/ap1/ap2** — **ap3** trug einen Werks-Inline `security.ft=yes` auf
`wifi1/wifi2` (Bring-up-Skript unset `!ft-over-ds`, aber nicht `!ft`; die Abnahme zählt nur benannte
security-Objekte und sah den Inline-Wert nicht). Das ist ein **gemischtes FT-ESS** und erklärt den
Abbruch bei 3/3 Strichen: Log zeigte wiederkehrende Strong-Signal-Disconnects (−43/−45 dBm) **nur auf
ap3** (`cap-wifi2/4`, `dirtyvader-main`). Fix: `!security.ft` auf ap3 → uniform `ft=no`; Quelle
korrigiert. **Merkregel: nach jeder `sec-main`-FT-Änderung jeden CAP einzeln auf `security.ft` prüfen —
CAP-lokale Inline-Overrides sind compliance-unsichtbar.** Offen in
TP25 bleibt nur der separate Punkt **ap2 unterperformt** (2 m/−80 dBm). Handoff:
[`docs/handoff-20260902-tp25-fund-e-messung.md`](handoff-20260902-tp25-fund-e-messung.md).
- ⏸️ **TP41 CF-Access-Reparatur — ZURÜCKGESTELLT 2026-09-01 (User-Entscheid; Rest wartet auf Info):** Session 720h,
login_method-Loch zu (`bf-users` email-only), `zt-`-Gruppen, vw voll Bypass + `vw/admin` per WAF
geblockt (Mobilfunk-verifiziert), API-Token auf vollständiges `…4148` konsolidiert. **T4 ✅**
(ein Access-Service-Token `zt-svc`, Terpene-Worker per wrangler auf zt-svc umgestellt, Alt-Tokens
gelöscht). **T3 ✅** (WARP Default-Profil fallback_domains `emsenhuber.net → .36/.37`). **T2 ✅**
(WAF-Regel gelöscht, Caddy-Origin-404 für `/admin` via CF-Header; Edge=404, lokal=200).
**T5 ✅** (GitHub- **und** Google-Login laufen, User-bestätigt 2026-09-01; Google via Consent-Fix §A).
**Facebook/Meta für TP41 abgesagt** (optional, kein CF-IdP angelegt) — Geli über OTP
`angelikahy@outlook.de` + Google `pillepalle670@gmail.com` voll gedeckt.
**Token-Altlast geklärt:** `b00722`=live Caddy-DNS-Token, `d47435`=stale Kopie → **✅ d47435 revoked**,
**✅ T3-WARP-Client-Verify** (User). **Geli CF-seitig fertig** (Enrollment via `zt-humans` + `geli`-Policy,
2026-09-01 live geprüft) → verbleibt nur ein **Device-Handgriff bei Geli** (WARP-Client, Team `emsenhuber`).
**Claude-/CF-Seite damit vollständig.** Handoff: [`docs/handoff-20260901-tp41.md`](handoff-20260901-tp41.md) · [Follow-up](handoff-20260901-tp41-followup.md).
- ✅ **TP14.4 als in TP41 T4 aufgegangen markiert (2026-09-02):** kein eigenständiger `claude-mcp`-Token
mehr — er wurde in TP41 T4 zu `zt-svc` konsolidiert, `mcp/cloudflare/.env` trägt jetzt
`CF_ACCESS_CLIENT_ID`/`_SECRET`. Die TP14.4-Ist-Stand-Notiz („.env leer") war seit 09-01 stale;
Quick-Index + TP-Abschnitt nachgezogen.
- ✅ **TP31 abgeschlossen markiert (2026-09-02):** Schritt 5+6 sind über den Piloten TP16.3 (09-01)
miterledigt — `bf-public` 12 conns, vier öffentliche Namen über Caddy, Alttunnel weg. Abnahme-Tool
`caddy/cf-sync.py` von einem Fehlalarm befreit (prüfte nur `http://`, gebaut ist `https://…:443`) und
**grün** (nur `ds-bf` als bewusst intern). Schritt-6-Teil „MCP auf FQDN" durch TP35 Schicht 1
umgekehrt (LAN-IP), damit gegenstandslos.
- ✅ **TP42 durch (2026-09-02):** interner Namensraum auf **einen** Suffix `bf.emsenhuber.net` vereinheitlicht.
(A) DHCP-domain über die GC-Pipeline geflippt (Template + `push-gc-template.py` `7a4068c4` + neuer Job
`tp42-dhcp-domain` + Deploy an core) → **7/7 live, Compliance 140/140**. Nebenbefund: Remote-GC-Template
hing hinterher (`.bf.lan`), mit angeglichen. (B) `.16` (`adguard_bf1`) aus dem **mit FM geteilten** Stack
195 getrimmt-redeployt — `.16` weg, `.17` (FM) läuft (kurzer ~40 s Blip). Nautobot-Doku-Teil (Scope 4)
an TP37 gekoppelt.
- 🔜 **TP42 Namens-Vereinheitlichung (2026-09-01, angelegt):** internes `.bf.lan` (Core-DNS, DHCP-Search-
Domain) und die `.bf.emsenhuber.net`-Aliase (Caddyfile) auf **einen** Namensraum `emsenhuber.net`
ziehen; stale `.16/.17`-Rewrites abbauen; Sollzustand in Nautobot. Handoff:
[`docs/handoff-20260901-tp42-naming.md`](handoff-20260901-tp42-naming.md).
- 🔜 **TP43 dw-Standort-DNS-Uplink (2026-09-01, angelegt):** dem dw-Standort einen eigenen Resolver/
DNS-Uplink mit **site-spezifischen Overrides** geben (erster Fall `420.teilzeitladen.xyz` → BF-Caddy
über WG), **inkl. Verhalten bei WG-Disconnect** (Override auf BF-interne IP fällt sonst aus). FritzBox
standard kann keinen Per-Host-Override. Geli-Sofortlösung = **WARP** (nicht Teil von TP43). Cross-Projekt.
- ✅ **TP16.3 ABGESCHLOSSEN (2026-09-01):** Flip **und** Abbau durch. Öffentlich laufen nur noch
`bf-public` (12 Connectoren) und `fm` — die drei Alttunnel `bf-mm`/`bf-nas`/`bf-pve` **und** der
projektfremde `terpene-db` sind gelöscht, **7 cloudflared-Container** entfernt (inkl.
`terpene-cloudflared`, weil `420.teilzeitladen.xyz` nun über Caddy/CF läuft — vor der Löschung mit
echtem Origin-Inhalt belegt). §6 wurde auf User-Entscheid vorgezogen. **TP wandert ins Archiv.**
- 🆕 **TP16.3 §5 DURCH (2026-09-01): `n8n`/`gitea`/`hass`/`vw` laufen über `bf-public`** — der
öffentliche Verkehr geht damit erstmals über **Caddy** statt an ihm vorbei, und die
Host→Origin-Zuordnung lebt nur noch im `Caddyfile`. Je Name einzeln geflippt, dazwischen gemessen;
Antwortzeiten vorher/nachher **unverändert** (Caddy im Pfad kostet nichts Messbares).
Nachweise: `gitea` echte Gitea-Antwort · `hass` **WebSocket `101 Switching Protocols`** (= der
Companion-App-Pfad, den das Runbook als „nur vom User prüfbar" führte) · `vw` `/alive` + `/api/config`
200, WS-Hub `401` von Vaultwarden selbst · `n8n` von Christoph im Browser bestätigt.
🚫 **§6 (Abbau) bewusst NICHT** — die drei Alttunnel bleiben als Rückfallebene (healthy/8).
🔴 **Christoph hat den Flip ausdrücklich vorgezogen freigegeben** („aus Gründen"); die 7-Tage-Regel
betrifft ohnehin §6, nicht den Flip.
- 🆕 **TP39 ABGESCHLOSSEN + archiviert (2026-09-01):** letzter offener Punkt — der Resolver-Sweep —
ist durch: **pve alle 13 LXCs** auf `10.40.20.1`, und die **DS923-Container via Portainer** (ep14)
gegengeprüft — kein `.41.254`-Override, alle laufenden lösen in **66–110 ms** auf (inkl. Grenzfall
`gitea`, erstellt 2026-07-10), kein eingefrorener Dead-Resolver, kein Recreate nötig. Die
Abnahme-Regel („vor jedem LXC-Reboot `systemctl --failed` + Port-Vergleich") steht als
CLAUDE.md-Gotcha, der Sweep ist als wiederkehrende Kontrolle dort verankert. **Volltext → Archiv.**
🔴 Rest offen nur in **anderen** TPs: root-DB-PW-Rotation (→ TP30), HA-Recorder-Retention (→ TP40).
- 🆕 **TP39 fast durch (2026-09-01):** `skip-name-resolve` auf der MariaDB **aktiviert** (mysql.user
vorher geprüft — kein Hostname-Grant, gefahrlos; Wirkung: gitea/vw/hass 0 von 36 frischen Anfragen
langsam). LXC 302 war **100 % voll** → `+30G` online (95 GB/69 %). 🔴 **root-DB-Passwort dabei ins
Transkript geraten** (`.my.cnf` ohne `=`), zu rotieren (→ TP30).
- 🆕 **TP40 angelegt (2026-09-01, User-Okay):** HA-Recorder-Retention — die `homeassistant`-DB frisst
59 GB (`states.ibd` **41 GB**), unbegrenzt gewachsen. Das `+30G` aus TP39 ist ein Puffer; die
dauerhafte Lösung (`purge_keep_days`, `exclude`, `purge repack`) ist HA-seitig und **Christophs
Policy-Entscheidung** — kein eigenmächtiges Löschen.
- 🆕 **TP39: `networking.service` in vier Containern repariert (2026-09-01).** Ursache war dieselbe
Altlast wie beim Resolver: tote `auto`-Stanzas in `/etc/network/interfaces` aus dem VLAN1-Schema
(`eth0` → `10.40.41.x`) und alten Versuchen — `ifup -a` scheiterte an Interfaces, die es nicht gibt,
während `veth20` lief. **Abnahme war ein echter Reboot:** influxdb kam danach **von selbst** hoch
(vorher gar kein Startversuch). Alle 13 LXCs stehen jetzt auf `dns=10.40.20.1`, `networking` überall
sauber. **Offen in TP39: nur noch `skip-name-resolve` und der wiederkehrende Sweep.**
- 🆕 **TP39 angelegt (2026-09-01, User-Okay):** LXC-Hygiene auf pve — die zwei Restpunkte aus dem
Wartungsfenster unten (`skip-name-resolve` auf der MariaDB, kaputtes `networking.service` in vier
Containern) plus Resolver-Sweep als wiederkehrende Kontrolle. **Ein** TP, weil beides dieselbe Ecke
betrifft und in einem Fenster erledigt wird.
- ✅ **GELÖST 2026-09-01: der 10-Sekunden-Zoll auf `gitea`/`vw`/`hass` — es war Reverse-DNS auf einen
toten Resolver.** MariaDB (LXC **302**, `10.40.20.23`) hatte `skip-name-resolve` **auskommentiert**
und machte damit bei **jeder neuen Verbindung** einen Reverse-Lookup; ihr `/etc/resolv.conf` zeigte
auf **`10.40.41.254`** — den toten VLAN1-Resolver aus dem alten Schema (`nc` bestätigt tot).
**Gemessen im Container: 10.010 ms je Lookup.** Bestehende Pool-Verbindungen waren schnell, neue
kosteten 10 s — daher wirkte es sporadisch und unerklärlich.
🔑 **Die Blast-Radius war größer als gedacht:** an derselben DB hängen **Vaultwarden (`.127`),
Home Assistant (`.105`) und gitea (`.157`)** — belegt über die Verbindungstabelle, nicht vermutet.
*(Die erste Zuschreibung „vaultwarden nutzt SQLite, also nicht betroffen" war eine **Annahme** und
falsch — Christoph hat sie korrigiert, die Verbindungsliste hat es bewiesen.)*
**Fix (Wartungsfenster, User-Freigabe):** `pct set <id> --nameserver 10.40.20.1` + Reboot für
**fünf** LXCs — `302 mariadb`, `102 mqtt5`, `106 influxdb2`, `107 redis`, `150 iob-exposed`.
**Nachher: Lookup 21–24 ms statt 10.010 ms; gitea 0 von 25 Proben langsam** (vorher ~10 % bei
10,1 s), langsamste Probe 75 ms. Alle drei Dienste wieder verbunden.
⚠️ **Strukturell offen:** `skip-name-resolve` ist weiterhin aus — der Lookup ist jetzt nur schnell,
nicht abgeschafft. Ihn zu setzen bricht alle Grants, die auf **Hostnamen** statt IPs lauten; das
gehört vorher gegen `mysql.user` geprüft (DB-Zugang fehlt: `root@localhost` ohne Passwort verweigert).
- 🔴 **Latenter Fund aus demselben Wartungsfenster: `networking.service` und
`ifupdown-wait-online.service` sind in `101`, `102`, `106`, `107` fehlgeschlagen.** Folge:
Dienste mit `After=network-online.target` starten nach einem Reboot **nicht von selbst** —
**influxdb2 kam nach dem Neustart nicht hoch**, Unit `enabled`, aber **kein einziger Startversuch
im Journal**. Von Hand gestartet lief es sofort.
🔑 **Nicht durch diese Session verursacht:** `101 iob-master` wurde nie angefasst und zeigt dieselben
fehlgeschlagenen Units — die Container liefen nur 13+ Tage ohne Neustart, deshalb fiel es niemandem
auf. **Merkregel: diese vier Container sind einen Reboot davon entfernt, still Dienste zu verlieren.**
- 🔴 **Befund am Rand des Flips, NICHT von ihm verursacht: `gitea` hängt sporadisch exakt ~10,2 s.**
~10 % der Anfragen, quer über Web-UI, **`/api/healthz`** und den **Git-Pfad** (`info/refs`) — also
kein Rendering-Problem. `vw` und `hass` über **dieselbe** Strecke: **0 von 30**. Damit sind CF-Edge,
AP-Connector, Firewall und Caddy entlastet; übrig bleibt **Caddy → `10.40.20.157:3000`** oder gitea
selbst. **Immer ~10,15–10,43 s** = Timeout-Signatur (klassisch: ein toter Resolver, s. den
VLAN20-Docker-DNS-Gotcha in der CLAUDE.md), kein Lastproblem. Weiter nur mit LAN-Zugriff.
- 📎 **Drei Messfallen, die diese Session gekostet haben** (alle drei sind Standort-, keine Netzfehler):
1. **Split-Horizon macht Tests von zuhause wertlos:** der Haus-Resolver liefert für alle vier Namen
direkt `10.40.20.40`, die Anfrage sieht Cloudflare **nie**. Prüfen geht nur mit festgenagelter
CF-Edge-IP (`curl --resolve <name>:443:188.114.96.10`) oder von außerhalb.
2. **Access antwortet je nach Standpunkt verschieden:** `n8n` gibt aus dem Haus **200** (die
`bf4`-Policy trägt die WAN-IP), von außen **302** zum Login. Beides gesund. Für die drei
`bypass`-Namen (`hass`/`vw`/`gitea`) gibt es gar keinen Login — die Access-App heisst zu Recht
`hass, vw, gitea`, sie führt alle drei in `self_hosted_domains` (das Feld `domain` zeigt nur den
ersten). **Ein Rename wäre eine Verschlechterung gewesen.**
3. 🔴 **„Der Host ist ausgefallen" war der eigene Standortwechsel.** Ping/SSH/Caddy waren tot,
weil die Session-Maschine auf einen **Hotspot** gewandert war (Default-GW `172.20.10.1`) —
dieselbe Klasse wie die WARP-Falle von 2026-08-13, nur ohne WARP. Es wurde daraufhin ein bereits
erfolgreicher `n8n`-Flip **grundlos zurückgerollt**. **Merkregel: bevor ein Ausfall gemeldet wird,
`route -n get default` lesen** — ein Standortwechsel sieht exakt aus wie ein Totalausfall.
- ⚠️ **Nebenbefund: Caddy führt gar kein Access-Log.** Im `Caddyfile` steht keine `log`-Direktive, im
Journal landen nur `info`/`warn`. Der Abnahmepunkt „Caddy sieht den Request" (Runbook §5.3) **kann
nicht funktionieren** — ein erfolgreicher Request hinterlässt dort nichts, und leere Ausgabe liest
sich wie „kam nie an". Dass der öffentliche Reverse Proxy keine Requests protokolliert, ist für sich
eine Lücke (gehört zu TP31/TP34).
- 🆕 **TP16.3: die sechs AP-Drifts sind ins Soll gehoben (2026-08-31) — Compliance 139/140.**
`veth` / `ip address` / `ip firewall nat` der drei cloudflared-Connectoren stehen jetzt im
**AP-Template** (Block `[7]`) und im ConfigContext `ap-common` (`ap.container`), **nicht** in einer
`gc-transaktion`: ein Dauerzustand gehört ins Soll, eine Transaktion ist ein befristeter Versuch.
**Kein Re-Baseline, kein Rückbau, keine Zeile aufs Gerät** — nur die SoT nachgezogen.
🆕 **28. ComplianceRule `interface-veth`** (vorher gab es für `/interface veth` keine ⇒ ein
gelöschtes veth hätte **nichts** gemeldet). 🔴 **Zwei Funde dabei:** (a) der veth-Export trägt
**zwei gerätevergebene MACs** — eine nackte Regel hätte Dauer-Drift auf allen drei APs erzeugt;
gelöst mit zwei eng auf die veth-Zeile verankerten `ConfigReplace`-Regeln (`60-`/`61-veth-*`,
`nautobot/ingest-veth-rule.py`), bewusst mit der vorhandenen Maske `<<REDACTED>>`, weil die im
Repo zugleich die **Push-Sperre** ist. (b) **Der erste Compliance-Lauf meldete `SUCCESS` und war
trotzdem unvollständig** (139 statt 140 Einträge, `crs326-core`/`interface-veth` fehlte;
Job-Log: `RENDER COMPLIANCE TASK GROUP failed: Broken pipe`). Zweiter Lauf sauber.
⚠️ **Merkregel: nach einem Refresh nicht nur die Konformitätsquote lesen, sondern die ANZAHL der
Einträge** — sie muss `Regeln × Geräte` sein. Eine fehlende Kombination sieht aus wie „die neue
Regel greift nicht" und ist in Wahrheit ein halb gelaufener Job.
*(Die damals übrige `crs326-core`/`wifi-security`-Drift = Transaktion `TP25-fund-e-ft-over-ds` ist mit
dem Rückbau 2026-09-01 gegenstandslos; Fund E am 2026-09-02 geschlossen, Compliance 140/140.)*
- 🆕 **MCP-Bestand 2026-08-28 korrigiert:** **9 MCPs identisch** in `~/.claude.json` und `~/.config/opencode/opencode.jsonc` (vorher Asymmetrie 7 vs. 11 — `portainer-mcp`/`shelly-mcp` raus, `gitea-mcp`/`n8n-mcp` rein). **Mac mini = n8n-Host, NICHT zentraler MCP-Host** (Performance-Restriktion); zentraler MCP-Host = **DS923** (`nas-bf`, Portainer Endpoint 14). Inventur-Stand `mcp/INVENTORY.md` + Policy `docs/mcp-host-policy.md`.
- 🆕 **MCP-Migration FQDN→IP (2026-08-29, TP35 Schicht 1):** `gitea-mcp` und `n8n-mcp` in `~/.config/opencode/opencode.jsonc` von CF-FQDN auf LAN-IP umgestellt — `gitea-mcp` → `http://10.40.20.167:3000/mcp` (macmini, **ohne** Bearer, intern), `n8n-mcp` → `http://10.40.20.167:3001/mcp` (mit `Bearer {env:N8N_MCP_BEARER}`). **Befund dabei:** der Handoff 1.2 nannte für gitea-mcp die falsche IP `.157:3000` (das ist die Gitea-Web-UI, `400 Invalid CSRF`); der echte gitea-mcp läuft auf `.167:3000` (`serverInfo: gitea-mcp 0.0.10`). `cloudflare`-MCP bleibt auf dem offiziellen FQDN (User-Direktive).
- 🆕 **TP16.3 §2/§3 durchgezogen (2026-08-31, Vor-Ort):** je ein `cloudflared`-Container auf
**ap1/ap2/ap3**, `bf-public` **healthy mit 12 Connectoren** — der Tunnel hat damit zum ersten Mal
überhaupt Connectoren. 🔴 **Zwei Runbook-Fehler dabei aufgedeckt:** die Flash-Rechnung war um Faktor
2,5 zu optimistisch (entpacktes cloudflared = **69,5 MiB**, nicht 28 MB ⇒ das offizielle Image passt
auf keinen der drei APs; Ersatz ist ein selbst gebautes `FROM scratch`-Image mit 35,3 MiB, Binary-SHA
`e93824f6…` gepinnt), und `/container/envs` heisst `list=`, nicht `name=`.
🔴 **Offen: Compliance 128/135.** §3.4 verlangt Direkt-Writes für `ip address`/`nat`, die unter
GC-Kontrolle stehen ⇒ **sechs neue `extra`-Drifts** (zwei je AP), inhaltlich richtig, aber nicht im
Soll. Braucht eine Entscheidung (ins Template heben), **kein Re-Baseline**. Details im TP16.3-Abschnitt.
- 🆕 **TP37 angelegt (2026-08-30, User-Okay):** Nautobot soll durchgehende Netzwerkdoku werden, erster
Anwendungsfall die CF-Connectoren. 🔴 **Befund dabei, live gegen die API:** Nautobot kennt heute
**nur die 8 Netzgeräte** (4 aktiv, 4 decommissioned) — `ipam.Service`, `CustomField`, `Relationship`,
`VirtualMachine` und `Tag` sind **alle 0**, installiert ist nur `golden-config`. Kein Host, kein
Container, kein Dienst ist erfasst. **TP37 ist damit zuerst eine Datenmodell-Frage** und liefert das
Fundament, das **TP34** (Caddyfile via GC) und **TP36** (SSoT für diese Datei) ebenfalls brauchen —
Reihenfolge: **TP37 vor TP34**.
- 🆕 **CF-Public-Strang eingedampft (2026-08-30, TP16.3 grosser Schnitt):** **12 Hostnames** aus Ingress
**und** DNS entfernt — `pve`/`portainer`/`pve-mcp`/`ha-mcp`/`ds-bf`/`ccu`/`iobroker`/`z2m25`/`pbs`
plus die drei CF-Access-SSH-Ziele. Ingress: `bf-mm` 6→4, `bf-nas` 6→3, `bf-pve` 9→2. **Öffentlich
bleiben nur `vw`/`hass`/`n8n`/`gitea`** (weiterhin HTTP 200). Intern unverändert über AdGuard→Caddy
(9× HTTP 200). ✅ **Nachtrag 2026-08-30 (F1 durch): `gitea-mcp`/`n8n-mcp` sind jetzt AUCH
geschnitten** — damit **14 Hostnames** gesamt und `bf-mm` **4→2** (nur noch `n8n` + Catch-all).
Möglich wurde das, weil zuvor **12 MCP-Client-Configs** auf LAN-IP migriert wurden (nicht 10 wie
geschätzt: zwei Worktree-Kopien in `bf-network-r6{,.opencode}` kamen dazu). 🔴 **`bf-public` hat weiterhin 0 Connectoren** — er gehört
laut Entscheid auf **ap1/ap2**, dort fehlen aber `container`-Paket **und** `device-mode container=yes`
(physische Bestätigung nötig). Bis dahin stehen `bf-mm`/`bf-nas`/`bf-pve` mit **6 Containern**.
Backups: `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json`.
- 🆕 **TP16.3/TP31 Schritt 5 vorbereitet (2026-08-30):** Runbook für die Vor-Ort-Session geschrieben
([`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)), CF-Rollback-Snapshots
unter `docs/cf-backup/*-20260830-vor-tp16.3-schnitt.json` **neu angelegt** (es gab sie vorher nicht).
`docs/tp31-schritt5-runbook.md` ist damit **abgelöst** (read-only-Header): es kannte ap3 nicht,
unterstellte einen Registry-Pull (`fetch: no` auf ap1/ap2) und enthielt eine **falsche**
Firewall-Freigabe-Behauptung. **CF-Stand:** `bf-public` `inactive` / 0 Connectoren; die vier
öffentlichen Namen laufen noch über bf-mm/bf-nas/bf-pve **an Caddy vorbei** direkt aufs Backend.
- 🆕 **TP38 angelegt (2026-08-31):** OOB-Management — je ein untagged-VLAN99-Port auf beiden Switches.
Auslöser: beim Core-Neustart war `crs326-branch` nicht erreichbar, weil sein Mgmt **ausschließlich**
über den Core läuft (`vlan99` dort `tagged=ether1,ether24,bridge`, kein untagged-Member).
🔴 **Nebenbefund mit eigenem Gewicht: `crs326-branch` hat gar keine `input`-Firewallregeln** — sein
Schutz ist heute allein die Erreichbarkeit über den gehärteten Core. Das ist die Hauptauflage des TPs.
- 🆕 **ROS-Upgrade 7.24.1 auf allen fünf (2026-08-31, User).** In Nautobot nachgezogen: **ap1** stand
als einziges Gerät noch auf `7.24` (die anderen vier waren schon richtig) ⇒ auf `7.24.1` gesetzt.
⚠️ **Korrektur (2026-08-31, nachträglich):** die dabei gesehene `wifi-security`-Drift auf
`crs326-core` (`sec-main` `ft-over-ds=no` statt `yes`) hatte **nichts mit dem Upgrade zu tun** — sie
ist **TP25 Fund E, Hebel 1**, um 08:09 bewusst gesetzt (Core-Upgrade war erst 14:36) und als
**offene Transaktion `TP25-fund-e-ft-over-ds`** geführt (Scope `wifi-security`, Ablauf 2026-09-03).
**Die 134/135 sind Absicht**, nicht Schaden: grün machen würde den laufenden Versuch unsichtbar
machen, und `gc-transaktion.py` verweigert den Commit ohnehin, solange der Scope driftet.
🔑 **Lehre:** vor jeder Drift-Bewertung `nautobot/gc-transaktion.py status` lesen — Drift aus einem
laufenden Versuch sieht identisch aus wie ein Versehen, und genau dafür gibt es das Werkzeug.
- 🆕 **TP16.3 §1-Blocker remote erledigt (2026-08-31):** Ingress von `bf-public` auf
`https://10.40.20.40:443` + `originServerName`; Firewall-Freigabe `vlan99 → vlan20:443` **über
Nautobot GC** (Plan `Completed`, Re-Baseline `ingest-core-firewall.py --apply`, danach
**135/135 compliant / 0 Drifts**); `ROUTER_AP3_PASS` in `~/.secrets`. **Offen bleibt nur die
Vor-Ort-Session** (§2/§3: `container.npk`, `device-mode`, Container) und danach Flip + Abbau.
- 🆕 **Globale Verhaltensregeln konsolidiert (2026-08-29, TP35 Schicht 3):** `~/.config/opencode/AGENTS.md` ist jetzt die **SSoT** für globale Verhaltensregeln (Secrets, VLAN-Schema, 5 Lessons, SoC, Übergabe-Mechanik, Mid-Action-Checkpoint, Commit-Hinweis, Backup-Guard). `~/.claude/CLAUDE.md` hält denselben Block als Claude-Code-Spiegelung mit Verweis auf die SSoT. opencode liest AGENTS.md mit Vorrang vor `~/.claude/CLAUDE.md`.
- **Alle fünf MikroTik laufen auf RouterOS `7.24.1`** (🆕 **Angleichung am 2026-08-31 von Christoph
selbst durchgezogen, TP27 damit erledigt** — ap2 08:56, core 14:36, ap1 14:50, branch 14:52;
ap3 kam bereits so. RouterBOOT jeweils = `upgrade-firmware` = `7.24.1`, live gegengelesen).
Vorher: vier auf 7.24 (08-17), nur ap3 auf 7.24.1. Neu in der Flotte **`ap3`**
(cAP ax `cAPGi-5HaxD2HaxD`, **`10.40.99.13`**, Uplink core `ether21`).
✅ **Folge für TP16.3:** die versionsgebundene `container.npk` ist damit **eine** Datei für alle drei
APs (alle `arm64`) — die Zwei-Dateien-Regel vom 08-30 war rein versionsbedingt und ist erledigt.
*(Hier stand bis 08-29 „Alle vier MikroTik … 7.24" — durch ap3 und das Upgrade zweifach überholt.)*
- **L3 + WAN-Edge = `crs326-core`** (CRS326-24G-2S+, Technik UG): Routing, DHCP (7 Server,
54 Reservierungen), DNS, NAT, **NTP-Server für den Standort** und der WG-Hub (TP10, Peers `fm`/`dw`)
laufen hier. Mgmt-SVI **`10.40.99.1`** (vlan99), Gateway `.1` je VLAN. **WAN = `vlan100`** — DHCP-Client
hält die öffentliche Liwest-IP **`90.146.33.177/24`** direkt vom Arris (Bridge-Mode) → kein Double-NAT.
**DNS-Upstream = DoH zum CF-Gateway mit `verify-doh-cert=yes`** (TP23 Stufe 1 ✅ 08-18);
`servers=1.1.1.1` steht nur noch als Bootstrap für den DoH-Hostnamen.
- 🔥 **Die Firewall ist deny-by-default — in BEIDEN Ketten** (TP5 E3/E4 ✅ 08-18). `TP10-seal-lan` ist
`disabled`; Mgmt-Input kommt nur noch aus VLAN10, von HA (`8728`), von Nautobot (`22`) und über
`cf-mesh`. Ein neuer Dienst über eine VLAN-Grenze braucht ab jetzt eine explizite Regel.
- **L2-Branch = `crs326-branch`** (Technik OG): reiner L2-Switch, Mgmt **`10.40.99.2`**,
Uplink `ether1` → core `ether8`.
- **WLAN = CAPsMAN auf dem Core** (TP28 ✅ 08-17, GC-Modell nachgezogen 08-18): ap1 **`10.40.99.11`**
(an core `ether15`), ap2 **`10.40.99.12`** (an branch `ether24`), beide **reine CAPs** —
`cap enabled=yes`, `discovery-interfaces=vlan99`. Die APs halten **keine eigene WLAN-Config** mehr;
Kanäle, SSIDs, Security und Steering kommen vom Manager. 802.11k/v aktiv; **802.11r (FT) aus** —
`ft=no` auf allen SSIDs (sec-main seit 2026-09-04, User-Entscheid).
🆕 **Dritter AP `ap3` (cAP ax) ✅ LIVE seit 2026-08-30** (TP28 dafür wieder geöffnet):
`10.40.99.13`, Uplink `ether1` → **core `ether21`** (Trunk), reiner CAP, **RouterOS 7.24.1**
(🆕 **seit 2026-08-31 die ganze Flotte** — die frühere Versionsdifferenz ist aufgelöst). Kanäle
**`5180/ax/Ce/I`** + **`2437/ax`** (ch6) nach dem TP25-Plan vom 2026-08-24, am Gerät nachgemessen.
🔴 **ap1 ist funkstill** — es steht bis zu seinem Umzug ans Sideboard direkt neben ap3 und würde
auf 5180 kollidieren; es hat deshalb bewusst **keine Provisioning-Regel** am Manager, bleibt aber
erreichbar. Beim Umzug: ap1 → `5260/Ce` (Band B, DFS, **Frequenz-Liste statt Einzelwert**),
ap2 → `5300/Ce`, Uplink ap1 → `core ether3`.
**Compliance jetzt 130/130 / 0 Drifts über 5 Geräte** (vorher 104/104 über 4).
- **SSDP-Relay** (TP7.1, **nachgebessert 08-25**): LXC **108 `mcast-relay`** auf pve, zweibeinig
`10.40.10.21` (VLAN10) + `10.40.40.21` (VLAN40) — dafür trägt `core ether13` beide VLANs; mDNS macht
weiterhin der Core. **Läuft seit 08-25 ohne `--ssdpUnicastAddr`** — die Flagge verwarf die M-SEARCH
der App auf dem Weg nach VLAN40 (Messung: **0 Pakete `via eth1` gegen 308 `via eth0`** in 20 min,
**0×** `Received SSDP Unicast` im ganzen Journal seit 08-18). Der Rückweg läuft jetzt **nativ geroutet**
über die längst offene Regel `TP5: MEDIA->MAIN`; nachgemessen kommen `HTTP/1.1 200 OK` von
`10.40.40.193/.195` an beiden MacBook-Beinen an. Unit liegt im Repo: **`mcast-relay/multicast-relay.service`**,
Details als Gotcha in `CLAUDE.md`. ⚠️ **Der Fall hatte zwei unabhängige Ursachen** — nach der
Relay-Reparatur suchte die **App** immer noch nicht (macOS-Local-Network-Gate stand auf „an" und
griff nicht; Fix war Schalter aus/ein + Neustart, User-bestätigt 08-25). **Beide sind erledigt.**
- **Reverse Proxy = `caddy-bf1`** (LXC 130 auf pve, **`10.40.20.40`**, TP31 ✅ Schritt 1–3 seit
2026-08-25, **`hass` 400 behoben 2026-08-26, Split-Horizon seit 2026-08-26**): Caddy 2.11.4,
**16 Sites** (4 public, 12 intern mit `.bf.`-Suffix), Zertifikate ausschliesslich
per **DNS-01** mit einem eng geschnittenen `Zone:DNS:Edit`-Token (`CF_DNS_API_TOKEN`, **nicht** das
account-weite). Abnahme über den Host-Header von einem zweiten Host: 11× 200, `nautobot` 302.
Offen: **Schritt 5–6** (Connector für `bf-public`, CNAME-Flip, Abbau der sechs Container) —
**Runbook steht: [`tp16.3-bf-public-ap-connector-runbook.md`](tp16.3-bf-public-ap-connector-runbook.md)**,
Vor-Ort-Session an ap1/ap2/ap3. ✅ **Die drei §1-Blocker sind am 2026-08-31 remote erledigt** —
**(a)** Ingress auf `https://10.40.20.40:443` + `originServerName` je Regel umgestellt (die alte
`:80`-Zeile hätte über CF eine Redirect-Schleife gebaut, weil Caddy dort 308 auf https antwortet);
**(b)** Freigabe `vlan99 → vlan20:443` **über Nautobot GC** deployt (`ip-firewall-filter` steht unter
Drift-Kontrolle, TP26 ⇒ kein Direkt-Write), Position vor `TP5: log-only` verifiziert, **135/135
compliant, 0 Drifts**; **(c)** `ROUTER_AP3_PASS` in `~/.secrets` nachgetragen (REST-Auth 200).
⇒ **Vor Ort startet die Arbeit bei §2/§3** (Pakete, `device-mode`, Container).
- **DNS/Resolver = AdGuard-Paar `agh-bf1`/`agh-bf2`** (LXC 110/111 auf pve, **`10.40.20.36`/`.37`**,
TP23 Stufe 2 + TP31 Schritt 4 ✅ seit 2026-08-26): Upstream DoH zum CF-Gateway, conditional
`[/bf.emsenhuber.net/] → 10.40.20.1`, 47 Rewrites (16 Service → Caddy `.20.40` + 10 Host-Namen
mit `.bf.`-Suffix + alte suffixlose parallel). DHCP liefert `.36`/`.37` als DNS in allen 7 VLANs
(`domain=emsenhuber.net`), Firewall forward UDP+TCP/53 auf beide. Der Core-Resolver (`10.40.X.1`)
bleibt als Bootstrap unverändert (DoH, `allow-remote-requests=yes`, `mdns-repeat-ifaces`).
🆕 **TP23 Stufe 4+5 ✅ 2026-09-04:** `clients.persistent` = 7 VLAN-CIDR-Gruppen + `dns-bypass-exempt`
(Policy-Matrix, INFRA/MEDIA/MGMT `filtering=aus`), API-User `automation`, Sync-Tool + redigierte
Repo-SSoT unter `adguard/`. HA-Self-Service (Button → JIT-Domain-Freigabe, nativer Timer-Revert) live.
Offen nur **Stufe 6** (Bypass-Sperre, TP5). Werkzeuge/Doku: `adguard/{agh-sync,agh-exception}.py`,
`adguard/{README,ha-self-service}.md`.
- **Ausgebaut / gelöscht:** ALLNET-`sw1` und Omada-`sw2` sind **physisch weg** (2026-08-08 bzw. 2026-07-23,
in Nautobot *Decommissioned*); Omada-Controller + `omada-mcp` gelöscht (2026-07-16); FritzBox 6660 raus
seit 2026-07-01 (Liwest-Tausch auf Arris). **VLAN1 ist leer** (`/ip arp` auf `bridge` = 0 Einträge,
08-18) — TP19 ist geschlossen; `cudy1` hängt stromlos an core `ether11`, physischer Ausbau ist hands-on.
- 🆕 **WLAN-Logging persistent seit 2026-08-30 (TP25 Fund E):** `wireless` + `caps` gehen vom Core
in den TP5-Kollektor (`10.40.20.18:514`); `/system logging` steht unter der **27. ComplianceRule**
`system-logging`. Vorher nur Ringpuffer → ein seit Wochen gemeldeter iPhone-Abbruch war rückwirkend
nicht diagnostizierbar.
- ⚖️ **TP25 Fund E — Hebel 1 ZURÜCKGEBAUT, Entscheidung faellt am 2026-09-02 (Stand 09-01 09:2x):**
Zwei Sessions haben denselben Fall bewertet und kamen zu entgegengesetzten Schluessen. **Aufgeloest
zugunsten der Kollektor-Messung:**
- Die Nacht-Session las `get_log … sinceMinutes=1440` → `total 0` und schloss auf „geheilt". Das ist
der **fluechtige Ringpuffer des Routers**: live nachgemessen **1000 Zeilen, aeltester Eintrag
01.09. 00:38:58** — keine 9 h Tiefe. Der Core-Reboot 14:36 hatte ihn geleert, der 31.08. Vormittag
war darin **nicht sichtbar**. 🔴 Genau die Falle, wegen der am 30.08. der persistente Kollektor
eingerichtet wurde.
- Im **Kollektor** stehen fuer 08:09–14:36 (Hebel 1 **allein**, vor dem Upgrade) **124×
`reauthenticating`**, Stundenverteilung 09h=1 · 11h=2 · 12h=20 · 13h=31 · 14h=70, **alle vom
iPhone allein** (= Client-Befund nach der eigenen Merkregel „viele MACs = Infrastruktur"), dazu
Christophs **zwei erlebte Ausfaelle** um 13:16 und 14:23. Zwischen 09:00 und 14:36 gab es
**keinen Reboot**, der sie erklaeren koennte.
- ⇒ Hebel 1 hat im upgrade-freien Fenster **nichts** bewirkt; der Einbruch faellt exakt auf 7.24.1.
**Deshalb am 2026-09-01 zurueckgebaut** (`ft-over-ds=yes`, Provisionierung nachgezogen, frische
DHCP-Lease abgenommen, Transaktion **`zurueckgebaut`**, **Compliance 140/140**). Der Rueckbau **ist
das Experiment**: bleibt es ruhig, war 7.24.1 die Ursache; kehrt das Haemmern zurueck, hatte die
Nacht-Session recht und Hebel 1 wird als neue Transaktion erneut gezogen. **Bis zur Messung am
2026-09-02 frueh committet niemand.** Handoff:
[`handoff-20260901-fund-e-hebel1-zurueckgebaut.md`](handoff-20260901-fund-e-hebel1-zurueckgebaut.md).
- 🔴 **ABER: neuer Teilbefund am 23:02 (2026-08-31) — `ap2` unterperformt, NICHT FT.** Christoph am
Schreibtisch, 5G, kein Telefonat. Log: iPhone `F2:62:4C:E1:AF:09` ping-pongt im Sekundentakt
`cap-wifi5 ↔ cap-wifi6` = **beide Radios von ap2** (5G 5220 ↔ 2,4G ch11/2462, Band-Steering-Thrash
auf demselben AP), `disconnected, not responding` um 22:37:40 und 23:01:00. Feldstärke **−75…−88 dBm
— bei laut Christoph 2 m Abstand** (ap2 auf 2,3 m, gleicher Raum). Bei 2 m wären −35…−45 normal.
Aktuell **0 Clients auf ap2**, alle 29 auf ap3. ⇒ kein Coverage-/FT-Problem, sondern **ap2s Funk
selbst** (RF/Antenne/HW) und/oder die **DTU-Störung auf ap2/ch11** (Transaktion `TP17.1-dtu-pin-ap2`,
−59 dBm auf ap2s 2,4-Radio). Als eigener 🔬-Unterpunkt geführt, In-situ zu messen. Handoff:
[`handoff-20260901-tp25-ap2-verdacht.md`](handoff-20260901-tp25-ap2-verdacht.md).
- 🔴 **TP25 Fund E — Hebel 1 ist GESCHEITERT (Stand 2026-08-31 nachmittags):** `sec-main` steht am
Core auf **`ft=yes ft-over-ds=no`** (seit 08:09), aber das Symptom blieb: **124× `reauthenticating`
nach dem Eingriff** und **zwei von Christoph erlebte Ausfälle** — 13:16 (Videocall bricht ab,
7 min ohne WLAN) und 14:23 (5,5 min, nur durch WLAN-Toggle beendet). Stundenvergleich auf
gleicher Firmware: **07:00–08:00 = 79 · 14:00–15:00 = 70**. Erfolgskriterium war „~0" ⇒ nicht
eingetreten. Gewertet wird bewusst nur **08:09–14:36** (danach kommt das TP27-Upgrade als zweite
Variable dazu; Christoph-Entscheid: „Vormittag als Ergebnis werten").
⇒ Transaktion **`TP25-fund-e-ft-over-ds` bleibt OFFEN** (Ablauf **2026-09-03**), **Compliance
weiter bewusst 134/135**; Rollback oder Commit ist eine Entscheidung, kein Aufräumen.
Geräte-Backup `pre-tp25-funde-ftoverds.backup` liegt auf dem Core.
**Nächste Entscheidung:** Hebel 2 (`ft=no`) **oder** `authentication-types=wpa2-psk,wpa3-psk` —
`sec-main` ist auch die einzige SSID ohne `wpa2-psk`, und `association rejected, no cached PMK`
(157× am 31.08., 0× am 30.08.) ist eine SAE/PMKSA-Meldung. **Vorher prüfen, ob 7.24.1 auf dem
Manager das Verhalten schon verändert hat** — zwischen 14:37 und 15:12 stand nur **1**
`reauthenticating` im Log, keines vom iPhone (35 min = noch keine Aussage).
Details: [`handoff-20260831-tp27-upgrade-und-hebel1-befund.md`](handoff-20260831-tp27-upgrade-und-hebel1-befund.md).
- ⚠️ **Messfalle, die diese Session fast erwischt hat:** die 13 CAP-`joined`/`activity timeout` vom
31.08. sahen nach CAPWAP-Instabilität aus — es waren die **Reboots des TP27-Upgrades**. Und die
**`cap-wifiN`-Karte hat sich an einem Tag zweimal verschoben** (08:09, 14:36), womit die im
Vorgänger-Handoff festgehaltene Tabelle nach sechs Stunden falsch war. Interface-Namen in
Logzeilen sind ohne zeitnahen Karten-Snapshot **nicht zuschreibbar**.
- **Kontrolle / Governance:** MikroMCP für Reads/Debug (alle 5 Geräte in der Registry),
**Config-Änderungen ausschließlich über Nautobot Golden Config** (TP21/TP22 — Deploy-Pipeline scharf,
**135/135 Compliance-Checks grün, 0 Drifts** bei **27 Rules × 5 Geräten** (Stand 2026-08-30;
**seit 2026-08-31 vorübergehend 134/135 — der eine Drift ist die offene Transaktion
`TP25-fund-e-ft-over-ds`, s. oben, kein Fehler**) — live gegen die API
geprüft am **2026-08-23**; zuletzt dazugekommen: **`wifi-access-list`** (08-22), **`queue`**
(08-23, TP6) und **`ip-firewall-mangle`** (08-23, TP6.1), s. TP22-Nachtrag im Archiv).
*(Hier stand bis zum 08-23 „100/100 bei 25 Rules" — eine Zählung, die TP6.1 am selben Tag überholt
hat. Die Merkregel gilt: der Ist-Stand-Block wird bei jeder Änderung mitgezogen, nicht nur der TP.)*
<details><summary>📜 Historische Fassung (Stand bis 2026-08-08, Topologie <b>vor</b> dem Core-Cutover — bewusst erhalten, nicht als Ist lesen)</summary>
> ALLNET-Core `.11` (FW2.2.1, VLANs 10/20/30/40/50/90/99/100; GE1/GE2/GE8 Trunk; **TP1 ✅ 2026-06-15**). Omada-Switch `.12` (adopted, **TP2 ✅ 2026-06-15**; Trunk-ALL auf P1+P2, Access-Profile auf P3-P8; Profile noch PVID=1 bis Omada-PVID-Fix).
> Omada-Controller **`.175`** (DS923/macvlan, mbentley 6.2, 🟢 live). omada-mcp **`.176`** (pve-LXC, StreamableHTTP, 🟢 live, 84 read-Tools; Controller-API v2 intern via christoph@emsenhuber.com).
> 2× hAP ax² (7.23.1): ap1 `.1` (ALLNET P1) — **TP4 ✅ 2026-07-01: Router + WAN-Edge** (VLAN-Subinterfaces, DHCP, DNS, NAT, Public-Edge). ap2 `.2` (Omada P2 — Trunk).
> **Edge-Cutover ✅ 2026-07-01:** FB6660 physisch entfernt (Liwest-Kabelmodemtausch, kein Zurück), Arris-Kabelmodem im Bridge-Mode; WAN = `sw1 gi16 Access VLAN100 → gi15 Trunk → ap1 ether2 → vlan100 auf bridge` (**NICHT** ether1); ap1 hält Public-IP direkt (Liwest, kein CGNAT), Double-NAT weg. Telefonie war auf der FB und wird nicht weitergeführt. Vorarbeit: TP10-Kern durch (2026-06-27, WG-Hub live) → Bridge-Gate vorher gelöst. Runbook: `routeros/wan-cutover-runbook.md`.
> Kontrolle: MikroMCP (MikroTik) live · ALLNET = Klickanleitungen · Omada → Controller + omada-mcp 🟢.
</details>
**Nächster Schritt (Stand 2026-08-30): Vor-Ort-Session ap1/ap2** — sie ist der einzige Weg, den
Public-Strang auf `bf-public` zu heben und die drei Alt-Tunnel samt sechs Containern abzubauen
(TP16.3 Follow-up F2). ✅ **F1 ist am 2026-08-30 durch** (12 Client-Configs auf LAN-IP, danach
`gitea-mcp`/`n8n-mcp` aus Ingress+DNS geschnitten) — davor unabhängig machbar bleibt nur noch die
**Announcement-Doku (TP35 Schicht 4)**, die den anderen Projekten die neuen MCP-URLs mitteilt.
**Vorherige Fassung (Stand 2026-08-28): drei Folge-Sessions parallel** (siehe `docs/handoff-20260828-mcp-bestand-und-performance.md`):
> - ✅ **Session A** — `/projektstatus`-Skill global für opencode (TP35 Schicht 6). Funktionsbeweis erbracht 2026-08-28.
> - 🔜 **Session B** — shelly-mcp-Container auf DS923 (`10.40.20.10`) deployen (live, ssh). Repo-Materialien committed (`mcp/shelly/{Dockerfile,BUILD.md,requirements.txt,.gitignore}`); `.env` und Tunnel-Secret lokal — kein MCP-Client (User-Direktive).
> - 🔜 **Session C** — `caddy/cf-sync.py`-Drift-Check + Backup-Guard (`~/dev/.bin/backup-guard.sh`, TP35 Schicht 5).
> 🧭 **Ist-Stand, kritischer Pfad und Quick-Index leben ab jetzt als Gitea-Issues**, nicht mehr in
> dieser Datei: **[admin/bf-infra · Issues](https://gitea.emsenhuber.net/admin/bf-infra/issues)**.
> **TP = Issue · Status = Label `status:*`** (todo/wip/paused/idea/concept/done/dropped) ·
> **Gruppierung = Label `thema:*`**. Filtern über die Label-Chips der Issue-Liste, z.B.
> [nur offene](https://gitea.emsenhuber.net/admin/bf-infra/issues?type=issue&state=open).
>
> 🆕 **Dazu seit 2026-08-30: TP16.3/TP31 Schritt 5 = Vor-Ort-Session** an ap1/ap2/**ap3**
> ([Runbook](tp16.3-bf-public-ap-connector-runbook.md)). **Reihenfolge beachten:** die drei Blocker in
> §1 sind **remote** zu erledigen, *bevor* jemand in den Keller geht — sonst steht man vor einem
> Connector, der `healthy` meldet, während die Seite eine Redirect-Schleife dreht.
> **Diese Datei führt nur noch die per-TP-Prosa** — die `### TPnn`-Abschnitte unten; Abgeschlossenes im
> **[Archiv](teilprojekte-archiv.md)**. **Live-Gerätedaten** (Compliance-Zahl, RouterOS-Version …) kommen
> aus **Nautobot**, nicht von hier. Sync-Werkzeug Datei/Archiv → Gitea (idempotent, Dry-Run default):
> `bin/gitea-tp-sync.py`.
>
> 🆕 **Strukturelle Folge = TP36:** SSoT für `teilprojekte.md`-Live-Daten via Nautobot (eigene Session, nicht heute). Befund 2026-08-28: manuell gepflegte Datei verliert gegen die Realität, sobald sie lang wird. Quickref-Index, TP-Abschnitt und Ist-Stand-Block müssen bei jedem Edit mitgezogen werden — keine schlägt die andere. **Merkregel im Lessons-Block.**
> *(TP36-Cutover 2026-09-07: die frühere Quick-Index-Tabelle und der chronologische „Ist-Stand (Kurz)"-Block
> sind aus dieser Datei entfernt, damit TP-Branches nicht mehr auf der Steuerungs-Doku kollidieren — Status-
> Updates gehen nach Gitea statt in diese Datei. Die alte Ist-Stand-Historie steht in der Git-Historie.)*
> 🆕 **Neu am 2026-08-24/25:** [TP31](#tp31) löst **TP16** und **TP16.2** ab und dampft cloudflared
> von **sechs Containern auf einen** ein; [TP32](#tp32) gibt die Nummern- und Adress-Systematik vor
> (und hat nebenbei die **falsche Startreihenfolge auf pve repariert** — HA startete vor seiner
> Datenbank). **TP16 ✅, TP25 ✅ und TP16.3 (Pilot) ✅** sind am 24./25. durchgezogen worden.
**Vorherige Fassung (Stand 2026-08-23): Christoph entscheidet — der kritische Pfad ist LEER.**
> 🎯 **Das Projekt ist in seinem Kern fertig.** deny-by-default steht (TP5), Golden Config steht und
> meldet **104/104 compliant bei 0 Drifts** (live geprüft 2026-08-23), WLAN steht über CAPsMAN auf dem
> Core (TP28), VLAN1 ist leer (TP19) — und **TP6 ist beantwortet statt offengelassen**: CAKE gebaut,
> gemessen, wieder ausgebaut. **Nichts blockiert mehr etwas.**
> 🟢 **Was zuletzt wirklich etwas gebracht hat, war TP6.1** (2026-08-23): nicht Shapen, sondern
> **Klassentrennung** — NAS-Bulk `priority=8` gegen interaktiv `priority=1` drückt dieselbe Strecke
> unter derselben Sättigung von **40,8 auf 11,0 ms (−73 %)**. Die zuerst notierte DOCSIS-Erklärung aus
> TP6 ist damit **widerlegt**; der Fehler lag in der Messmethode (Last und Messung vom selben Host).
> 📋 **Offen und ohne Blockadewirkung:** **TP25** (Fund F = 5:05-Takt bei IoT, plus die Entscheidung
> „5 GHz auf 20 MHz") · **TP16** (CF-Tunnel-HA degradiert, vier tote Alt-Tunnel) · **TP16.2/16.3** ·
> **TP23 Stufe 2** · **TP24** (DC-USV) · **TP13/TP14.4** (beide mit fertigem Runbook) ·
> ❌ **TP17.1 VERWORFEN 2026-08-23** (Zigbee-Weg bewiesen, Ziel damit nicht erreichbar — das Gerät
> bleibt WLAN-Client; `s9` zurückgebaut) · ✅ **TP17.3 ERLEDIGT** (Ursache **`eco_mode`** bewiesen;
> `s23` + `s26` repariert; **`eco_mode` flottenweit aus**, 22/22 außer `s24`) ·
> ➡️ **TP17.2 ist am 2026-08-24 in TP30 aufgegangen** (Shelly-Gerätepasswort = Erstvergabe eines
> Credentials; TP30 ist das Sammelbecken dafür) ·
> 🔑 **Alle Passwort-/Token-Rotationen sind seit 2026-08-24 TP30** (Sammel-TP) — **bewusst
> gebündelt gegen Projektende**, User-Entscheid. Damit ist die ältere Einreihung „hinter TP17.1"
> gegenstandslos (TP17.1 ist verworfen). **Etappe 1 — die Service-Accounts — ist durch;**
> das `admin`-Passwort selbst ist **nicht rotiert** und bleibt es bis dahin bewusst.
> 📌 **Einstieg für die nächste Session:
> [`handoff-20260825-agh-dns-cutover.md`](handoff-20260825-agh-dns-cutover.md)** — AdGuard als
> Resolver + Split-Horizon (TP23 Stufe 2 ∥ TP31 Schritt 4). **Bewusst eigene Session:** der Schritt
> verschiebt die DNS-Auflösung des ganzen Standorts in einen Container.
> 📜 Älter: [`handoff-20260824-session.md`](handoff-20260824-session.md)
> — ⚠️ dessen Empfehlung „Start bei der Passwort-Rotation Etappe 2" ist durch den TP30-Entscheid
> **überholt**; der Rest der Übergabe gilt. Ältere Sitzungsvorschläge:
> [`handoff-20260822-naechste-schritte.md`](handoff-20260822-naechste-schritte.md).
> ✅ **`crs326-core` ist transaktionsfrei** (seit 2026-08-22, geprüft): ein gemeldeter Drift ist ab
> jetzt **echt**, nicht beabsichtigt — `python3 nautobot/gc-transaktion.py status`.
<details><summary>📜 Vorherige Fassungen dieses Absatzes (2026-07-22 → 2026-08-21, bewusst erhalten)</summary>
> *Fassung 2026-08-21 abends (erledigt — TP25s Dead Spot ist am 08-22 repariert, TP6 am 08-23 geschlossen):*
> **Nächster Schritt: TP25 — Steering-Test in den Schlafräumen.**
> 📋 **Einstieg: [`handoff-tp25-session-20260821.md`](handoff-tp25-session-20260821.md)** — dort steht
> der verifizierte Ist-Stand, die Messfallen und der nächste Test.
> ✅ **`crs326-core` ist transaktionsfrei (2026-08-22).** `TEST-ether3-trunk-ap1-sideboard` und
> `TEST-infra-ssid-matter-pairing` sind beide zurückgerollt und geschlossen; ap1 hängt wieder auf
> `ether15`, `ether3` ist wieder untagged VLAN40. Ein gemeldeter Drift ist ab jetzt **echt**, nicht
> beabsichtigt — `python3 nautobot/gc-transaktion.py status`.
> 🆕 **TP29** (Anwesenheitserkennung fürs Energiesparen) ist als Idee angelegt, nicht auf dem Pfad.
> 📌 **Die TP6-Prämisse ist am 2026-08-20/21 gemessen** (Runbook §5): formal Fall B (+28…+47 ms unter
> Dauer-Upload), aber im Videocall **nicht wahrnehmbar** — kein einziger Problem-Klick über sechs
> Lastphasen. Laut war stattdessen der Funk: **Sticky Client + Band-Ping-Pong, zwölf Wechsel in neun
> Minuten**, je Wechsel bis zu 4 s ohne Durchsatz →
> [`tp25-wlan-roaming-messung-20260820.md`](tp25-wlan-roaming-messung-20260820.md). Deshalb steht jetzt
> **TP25 vor TP6**; TP6 bleibt offen und begründet, aber nachrangig.
>
> *Vorherige Fassung (Stand 2026-08-18, erledigt):*
TP6 wird hier seit Monaten als „DAS Ziel" geführt, aber `docs/call-quality-diagnosis.md` hat den
Bufferbloat 2026-06-16 bereits gemessen (**A/A+**, „CAKE wäre marginal, nicht deployt"). Die Messung
stammt aus der FritzBox-Ära, der Edge ist seither getauscht — also **einmal gegen den heutigen Edge
nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Nacht auf den 18. abgeschlossen worden (Etappe 3 + 4: deny-by-default in `forward` **und** `input`, MGMT isoliert, `seal-lan` abgeschaltet), TP23 Stufe 1 (DoH auf dem Core) ebenfalls. Nebenher offen, blockiert nichts: **TP25** (Signal-Boden + Re-Walk), **TP28-Rest** (die fünf `wifi-*`-Features vom AP-Scope auf den Core umhängen — die einzigen verbliebenen Compliance-Drifts), **TP7.1** (SSDP-Relay für die macOS-Sonos-App), **TP16.3**, **TP24**.
> *Vorherige Fassung (Stand 2026-08-14, überholt): „TP5 — Deny-by-default am Ende von `forward`." Alles Davorliegende ist durch:* CRS326-Bring-up + L3-Umzug ap1→`crs326-core` (TP15/TP21.1 ✅ 2026-08-08), WLAN-Neuprovisionierung ohne CAPsMAN (TP3 ✅ 2026-08-08), Nautobot-Plattform + Fleet-Adoption + Deploy-Pipeline (TP21/TP22 ✅ 2026-08-13). Danach **TP6 (CAKE) = das Projektziel**. Kleine Follow-ups nebenher: DoH-Cloudflare auf core, DC-USV (**TP24**, seit 2026-08-16 eigener TP). ⚪ **Omada-PVID-Fix ist obsolet** (User-Entscheid 2026-07-22 — Omada spielt seit der TP15-Korrektur keine Rolle mehr im Netz, nie mehr aufgreifen).
> *Vorherige Fassung (Stand 2026-07-22, erledigt): „TP15 CRS326-Bring-up als neuer L3-Core, danach WLAN-Neuprovisionierung ohne CAPsMAN; TP5 folgt nach TP21/CRS326-Adoption."*
</details>
## Governance & genehmigte Ausnahmen (stehende Regeln — kein Status)
> 🎯 **Ziel: sauberes System „from scratch", keine Leichen/Altlasten.** Migrations-Session-Endstand = **VLAN1 + altes cudy-WLAN komplett weg** (VLAN1 nur noch Mgmt, `defconf`-Client-DHCP zurückgezogen, cudy-Uplinks aufs Ziel-Client-VLAN getaggt oder cudy abgebaut). **Unterpunkte/Aufräum-Liste bei Migration:** (a) die Interim-Härtung ap2 `wifi1/wifi2 frame-types=admit-only-vlan-tagged` (TP3.1, 2026-07-06) bleibt gültig — verschwindet automatisch mit VLAN1; (b) ~~`st-main rrm/wnm` = reines Roaming-Provisorium~~ → **kein Provisorium mehr (2026-08-13, TP3.3): Steering ist fester Bestandteil des Golden-Config-Templates + ComplianceFeature `wifi-steering`** — es war die Neuprovisionierung, die es stillschweigend verloren hatte; (c) ~~**cudy1 = verbleibender aktiver VLAN1-Leak**~~ → **ERLEDIGT (User-bestätigt + verifiziert 2026-08-14): cudy1 ist offline.** `core ether11` trägt **kein Link** (kein `R`-Flag), Nautobot-Status *Offline* (TP22 Schritt 5). Damit existiert kein aktiver VLAN1-Leak mehr; der Dual-DHCP-Race aus `project_vlan1_dhcp_incident` ist Historie. Der formale Teardown (Gerät ausbauen, Doku/Inventar) bleibt TP19.
> ⚠️ **HW-Constraint hAP ax²:** 1 GB RAM, aber nur **128 MB Flash + KEIN USB** → **Container auf dem Router NICHT praktikabel**.
@@ -522,44 +67,7 @@ nachmessen und dann entscheiden**. Blocker gibt es keine mehr. TP5 ist in der Na
> ~~**TP17.2** ↔ TP17~~ *(TP17.2 ist am 2026-08-24 in **TP30** aufgegangen und kein eigenes TP mehr)*
> und **TP20.1** ↔ TP20).
>
> ### 📊 Quick-Index (nur dieser Block ist für den Statusbaum nˆtig — 1 Read)
>
> | # | Status | Kurztitel | Abschluss |
> |---|---|---|---|
> | TP5.1 | ⏸️ | DNS-Visibility Airfryer | — |
> | TP8 | ⏸️ | Identity / 802.1X | — |
> | TP11 | ⏸️ | Monitoring | — |
> | TP12 | ⏸️ | IPv6 (3 Standorte) | — |
> | TP12.1 | 🔜 | IPv6 ULA + v6-Firewall | — |
> | TP13 | 🔜 | Botschafts-WLANs | — |
> | TP14.4 | ✅ | CF-Access-Token — **in TP41 T4 aufgegangen** (`claude-mcp`→`zt-svc`, `.env` geschrieben) | 2026-09-01 |
> | TP16 | ✅ | CF-Tunnel HA wieder voll | 2026-08-24 |
> | TP16.2 | 🔜 | Härtung bypass-Dienste | — |
> | TP16.3 | ✅ | cloudflared-Reduktion — Flip + Abbau durch; nur noch `bf-public`+`fm`, 7 Container weg | 2026-09-01 |
> | TP17.1 | ❌ | Shelly Gen4 → Zigbee | 2026-08-23 |
> | TP17.3 | ✅ | Shelly-Regression behoben | 2026-08-23 |
> | TP18 | 🔜 | Garage-Objektspeicher | — |
> | TP20.1 | 🔜 | Vaultwarden → Platform-PG | — |
> | TP23 | ⏳ | DNS-Policy (Stufe 1–5 ✅ 09-04) — offen nur Stufe 6 (Bypass-Sperre, in TP5) | 2026-09-04 |
> | TP24 | 🔜 | DC-USV | — |
> | TP25 | ⏳ | WLAN-Stabilisierung (Kern ✅) — **Fund E ✅ geschlossen 09-02: 7.24.1 war die Ursache, nicht `ft-over-ds` (4/8,6 h, 0 Ausfälle)**; **`ft=no` auf `sec-main` als Dauerzustand ✅ 09-04**; **Konsole auf N Clients + Pin-UI ✅ 09-04**; offen nur ap2 unterperformt (2 m/−80, In-situ) | 2026-09-04 |
> | TP28 | ✅ | WLAN/CAPsMAN — dritter AP `ap3` (cAP ax) **live** | 2026-08-30 |
> | TP29 | 💡 | Anwesenheitserkennung (Idee) | — |
> | TP30 | 🔜 | Credentials (Etappe 1 ✅, Rest gebündelt) | — |
> | TP31 | ✅ | Reverse Proxy Caddy — 1–4 ✅ (08-25/26), **5+6 via TP16.3 (09-01)**; Abnahme-Tool `cf-sync.py` grün | 2026-09-01 |
> | TP32 | 🟢 | Nummern-Systematik (Konzept) | — |
> | TP33 | 💡 | Sonos fern (Idee, zurückgestellt) | — |
> | TP34 | 🔜 | Caddyfile via Nautobot/GC (Stub, Inventur-Handoff 2026-08-27) | — |
> | TP35 | ⏳ | MCP-Schichtung + 3-Schicht-CLAUDE.md + Backup-Guard (Schicht 1+3+6 ✅) | — |
> | TP36 | 🔜 | SSoT via Nautobot für teilprojekte.md-Live-Daten + Datei-Aufteilung | — |
> | TP37 | 🔜 | Nautobot als Netzwerkdoku: Hosts/Container/Dienste + CF-Connectoren (Okay 08-30) | — |
> | TP38 | 🔜 | OOB-Mgmt: untagged-VLAN99-Port je Switch | — |
> | TP40 | 🔜 | HA-Recorder-Retention: 41 GB `states` beschneiden (Okay 09-01) | — |
> | TP41 | ⏸️ | CF-Access-Reparatur **zurückgestellt 2026-09-01** (User): **T2/T3/T4/T5 ✅** (GitHub+Google-Login live); **Facebook abgesagt**; **Geli CF-seitig fertig** (zt-humans+geli-Policy) → offen nur noch WARP-Client auf Gelis Gerät, wartet auf User-Info | — |
> | TP42 | ✅ | Namens-Vereinheitlichung: ein interner Suffix **`bf.emsenhuber.net`** — DHCP-domain via GC geflippt (140/140), `.16` abgebaut (`.17`=FM bleibt); Nautobot-Doku-Teil an TP37 | 2026-09-02 |
> | TP43 | 🔜 | dw-Standort-DNS-Uplink: eigener Resolver mit site-Overrides (`420.teilzeitladen.xyz`→BF-Caddy/WG), WG-Disconnect bedacht; Geli sofort per WARP | — |
> | TP44 | ⏳ | CF-Estate als Doku-SSoT in Nautobot — **Etappe 1 ✅ live** (`nautobot.cloud`: Account 1/ResType 6/Network 4/Service 14/Assign 3); offen Etappe 2 (CF-API→Nautobot-Sync) + IPAM-Prefixe | — |
> | — | — | **35 archivierte TPs** in `teilprojekte-archiv.md` (inkl. TP39 ✅ 2026-09-01) | — |
> 📊 **Index/Statustabelle → [Gitea-Board](https://gitea.emsenhuber.net/admin/bf-infra/issues)** (siehe Kopf dieser Datei). Hier unten folgt nur noch die per-TP-Prosa.
### TP5.1 — DNS-Visibility & Flow-Observability für Low-Trust-Geräte (Airfryer-Fall) ▸ vorgemerkt
- **Kontext (2026-07-08/09):** Airfryer „airschreier" (Philips/Versuni, ESP, `10.40.30.180`) bekam per DHCP-Option 6 `dns 1.1.1.1` als Override, weil das OTA-Update über den ap1-DoH-Gateway-Pfad nicht durchging (DoH-Latenz beim primitiven ESP-Stack — **kein** Content-Block, via Gateway-Logs bewiesen, s. `memory/ref_esp_ota_doh_not_block`). Nebenwirkung: public **1.1.1.1 = anonymer Consumer-Resolver ≠ accountgebundener Gateway-DoH** (`px35ynlol3.cloudflare-gateway.com`) → **DNS-Sicht auf das Gerät weg**, Gerät umgeht zudem die Filterung.
@@ -2874,18 +2382,17 @@ kann in Ruhe nachziehen.
---
### TP36 — SSoT via Nautobot für `teilprojekte.md`-Live-Daten + Datei-Aufteilung ▸ 🔜 ANGELEGT 2026-08-28, Scope-Ausarbeitung eigene Session
- **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte „Nächster Schritt (2026-08-26): TP31 Schritt 5" als Output, obwohl seit dem 26. drei Folge-Sessions parallel offen waren. **Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.**
- **Befund:** Drei Stellen pro Edit berücksichtigen müssen — Quick-Index (Tabelle), TP-Abschnitt, Ist-Stand-Block. Keine schlägt die andere. Wer eine ändert, **muss** die anderen mitlesen. Die Datei hat aktuell 1940 Zeilen, der Ist-Stand-Block steht bei Z. 28, das Archiv-Cover bei Z. 1771. Strukturelles Risiko, kein Einzelfall.
- **Scope (Skizze, eigene Session):**
1. **Nautobot Custom Fields für Live-Daten** — RouterOS-Version, Compliance-Zahl (104/104, 26 Rules × 4 Geräte), DHCP-Server-Anzahl, MGMT-IPs, DNS-Upstream-Modus. Script `nautobot/gc-snapshot.py` rendert `docs/teilprojekte.md` (oder einen schmaleren Ersatz) als **read-only Snapshot**.
2. **Datei-Aufteilung** — `docs/teilprojekte-index.md` (1 Seite: nur aktive TPs, Titel + Status + Datum), `docs/teilprojekte-aktuell.md` (Live-Snapshot aus Nautobot), `docs/teilprojekte-archiv.md` (bleibt Volltext). `/projektstatus`-Skill liest Index + Archiv, nicht die Snapshot-Datei.
3. **Archiv-Anzeige im Quick-Index** — die heute 34 Namen stehen heute in *einer* Zeile am Ende. Sichtbarkeit = „Geschichte zählt". Quick-Index-Block bekommt eine zweite Tabelle mit „abgeschlossen seit …" und Link aufs Archiv.
4. **Migrationspfad** — kompatibel mit bestehendem Workflow (`/projektstatus`-Skill konsumieren, niemand außer mir editiert Live-Markdown). Testphase = beide Dateien parallel führen.
- **Abgrenzung:** TP36 ist Querschnitt, nicht Kern-BF. Eigene Session, eigener Handoff. Nicht heute.
- 🔴 **Klarstellung 2026-09-06:** **unabhängig vom TP37-Substrat** — die Live-Daten-CustomFields (Punkt 1) hängen an den **4 bestehenden `dcim.Device`**, nicht an neuen VM/Service-Objekten → TP36 kann **parallel/vorgezogen** laufen. Datenmodell: native `extras.CustomField` (Compliance-Zahl kommt aus GC selbst). ⚠️ **Namensfalle bereinigt:** das Skript `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 — es ist die TP44-CF-Estate-Befüllung und heißt seit 2026-09-06 `tp44-cf-estate-seed.py`. TP36 hat **noch kein** Skript (`gc-snapshot.py` ist zu bauen).
- **Verzahnt mit:** TP22 (Nautobot-GC als Vorbild), TP35 (geht Hand in Hand — beide sind „SSoT + Konsolidierung"), `bf-infra/CLAUDE.md`-Lessons.
- **Referenzen:** Lessons-Block-Eintrag „Manuell gepflegte Doku mit Live-Daten verliert gegen die Realität" · Handoff Session A 2026-08-28.
### TP36 — Index/Status als SSoT nach Gitea (Steuerung aus der branchbaren Datei herauslösen) ▸ ⏳ **CUTOVER 2026-09-07** — Index/Status = Gitea-Issues, Prosa bleibt Datei
- **Auslöser:** Skill-Run 2026-08-28 (`docs/handoff-20260828-session-a-projektstatus.md`) lieferte einen überholten „Nächster Schritt"-Output; **manuell gepflegte Doku mit Live-Daten verliert gegen die Realität, sobald sie lang wird.** Verschärft 2026-09-06: `docs/teilprojekte.md` wurde in **40/40** letzten Commits angefasst — jeder TP editierte Quick-Index **und** Ist-Stand-Block → **Merge-Konflikt-Magnet**, der Branch-pro-TP verhinderte (ein Parallel-Branch hatte so die TP37-Doppelvergabe verursacht).
- 🔄 **Reframe 2026-09-06 (Christoph):** von „SSoT via **Nautobot**" auf „SSoT via **Gitea**". Grund: Index+Status sind **kuratierte Steuerung** (Reihenfolge, Marker, Abhängigkeiten), kein Geräte-Live-Zustand — die gehören auf ein **Issue-Board**, nicht in Custom Fields. **Nautobot bleibt nur für Live-Gerätedaten** relevant (Compliance-Zahl, RouterOS-Version …) → als optionaler Rest unten, entkoppelt von diesem TP.
- **Modell (umgesetzt):** TP = **Gitea-Issue** in [`admin/bf-infra`](https://gitea.emsenhuber.net/admin/bf-infra/issues) · Status = Label `status:*` (todo/wip/paused/idea/concept/done/dropped) · Gruppierung = Label `thema:*`. Seeder `bin/gitea-tp-sync.py` (idempotent, Dry-Run default, `--commit` schreibt; Token aus `~/.hermes/.env`, **nie ausgeben**). Erstlauf: **68 Issues (25 open/43 closed) + 15 Labels**.
- ✅ **Erledigt (A1–A3):** Labels/Modell · 68 TPs → Issues · Board = Index (Label-Filter statt Handtabelle).
- ⏳ **Cutover 2026-09-07 (diese Session):**
- **A4** — Quick-Index-Tabelle + „Ist-Stand (Kurz)"-Block aus dieser Datei entfernt; Header zeigt aufs Gitea-Board; die vier **Genehmigten Ausnahmen** unter eigenem Governance-Header behalten. Alle `### TPnn`-Prosa bleibt.
- **A5** — `/projektstatus` liest Baum+Status aus der **Gitea-API** (Issues + `status:*`/`thema:*`), Transaktionen weiter aus `nautobot/gc-transaktion.py status`, Prosa-Detail bei Bedarf aus dem Repo. Beide Skill-Kopien (`~/.claude/skills/…` **und** `~/.config/opencode/skills/…`, `cp`, kein Symlink).
- **A6** — Projekt-`CLAUDE.md`: Pflichtlektüre = Gitea-Board (Status) + Prosa-Datei (Detail); Branch-Konvention (`tpNN-kurz` jederzeit, Merge nach `main` wenn Code/Config steht, Steuerungs-Updates nach Gitea); SSoT-Notiz.
- 🔜 **Optionaler Rest (entkoppelt, „TP36-Live"):** Live-Daten-CustomFields auf den **4 `dcim.Device`** (`ros_version`, `compliance_pct`, `dhcp_server_count`, `mgmt_ip`, `dns_upstream_mode`) + Snapshot-Skript `nautobot/gc-snapshot.py` (existiert noch nicht). Unabhängig von diesem Cutover; nur relevant, wenn die Doku Live-Zahlen tragen soll. ⚠️ **Namensfalle bereinigt:** `nautobot/tp36-cf-estate-seed.py` gehörte **nie** zu TP36 → seit 2026-09-06 `tp44-cf-estate-seed.py`.
- **Verzahnt mit:** TP35 (SSoT + Konsolidierung), TP44 (native Nautobot-Modelle), `bf-infra/CLAUDE.md`-Lessons. Plan: `~/.claude/plans/geht-das-los-auch-reflective-lark.md`.
<a id="tp37"></a>
### TP38 — Out-of-Band-Management: ein untagged-VLAN99-Port je Switch ▸ 🔜 **ANGELEGT 2026-08-31**