Ctrl K
ComunidadGuías…

OPEN.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# Offen aus den Audits vom 06./07.10.2026

Sieben Audit-Durchläufe an zwei Tagen — zwei vor `acdd26e`, fünf danach.
Was behoben ist, steht in den Commit-Botschaften; **hier steht nur, was
offen ist**, damit es nicht im Sitzungsverlauf verschwindet.

Drei Zustände, und sie sind nicht dasselbe:

* **gemeldet** — ein Agent hat es behauptet, ich habe es *nicht*
  nachgestellt. Ein Agentenbericht ist ein Hinweis, kein Beweis.
* **nachgestellt** — ich habe es selbst gemessen, mit der Messung dabei.
* **entschieden** — angesehen und bewusst so gelassen, mit Grund.

Erledigtes wird hier gelöscht, nicht abgehakt: eine Liste voller
durchgestrichener Zeilen liest irgendwann niemand mehr.

---

## 1 — ~~Zusagen, die nichts absicherte~~ — alle sieben erledigt

**07.10.2026.** Die suiteweite Mutationsprobe hatte sieben Kernzusagen
gefunden, deren Durchsetzung man löschen kann, ohne dass eine von 181
Prozeduren rot wird. Alle sieben haben jetzt eigene Prüfungen, und
`test_guard_mutations.py` bricht für jede die Zeile, die sie durchsetzt
— 69 Einträge, Schuldenliste **leer**.

* **Rollback** — `test_rollback.py`. Der wichtigste Fall ist der, in
  dem es *nichts* tun darf: ohne Backup wird `name` nicht angefasst,
  weil das die einzige Kopie sein kann, die der Nutzer noch hat.
* **CSRF** — `test_web_csrf.py`, durch `do_POST` mit echten Headern.
  Die zwei Prozeduren, die den Schutz abschalteten, tun es nicht mehr.
* **Auth-Tor der Web-UI** — `test_web_auth_gate.py`. `webauth.verify`
  war abgesichert, das Tor davor nicht; acht Prozeduren schalten es ab,
  damit ihre Seite rendert. Dazu die Regel, dass das Passwort je
  Anfrage frisch gelesen wird — sonst wirkt »Passwort setzen« erst nach
  einem Neustart.
* **Update-Fenster** — `test_update_window_enforcement.py`, beide
  Hälften samt der Frage, wessen Wochentag die Stunden nach Mitternacht
  gehören.
* **Wochenbericht-Zeitplan** — `test_weekly_schedule.py`. Beide
  Richtungen sind schlimm: immer-ja schickt den Bericht bei jedem Tick,
  immer-nein hält eine Funktion an, die erst eine Woche später auffällt.
* **`healthcheck.py`** — `test_healthcheck_exit.py`, als Unterprozess,
  weil der Exit-Code die Schnittstelle ist. Keine Prozedur hatte die
  Datei überhaupt geladen; ein Syntaxfehler darin wäre ausgeliefert
  worden.
* **`--rm`-Abhängige** — `test_netns_dependent_recreate.py`. Ein
  `AutoRemove`-Container hört im Moment des Stops auf zu existieren,
  also muss `inspect` **vor** dem Stop laufen. Die alte Prüfung war
  `"Capture config up front" in checker`, und dieser Text steht nur im
  Kommentar: den Kommentar umformulieren hätte sie rot gemacht, den
  Code verschieben nicht.
* **Ruhezeiten und Wartungsmodus** — am 06.10. erledigt.

## 1b — Der Durchlauf vom 10.10.: 39 Zusagen, die nichts hält

Audit E, isoliert in einem eigenen Worktree, auf die Zusagen angesetzt,
die beim ersten Durchlauf **nicht dran waren**. Methode: Zeile
entfernen, alle 194 Prozeduren fahren, zählen was rot wird —
`PYTHONDONTWRITEBYTECODE=1`, Absturz zählt nicht als Treffer, beidseitige
Kontrolle.

**Erledigt:** die Verdrahtung Gesundheit → Rollback. Alle **drei**
Stellen ließen sich abschalten, ohne dass eine Prozedur rot wurde —
selbst nachgestellt. Ich hatte letzte Woche `_rollback_to_old` gründlich
abgesichert und geglaubt, die Zusage sei gedeckt; gedeckt war der
Aufruf, nicht die Entscheidung ihn zu tun. Jetzt
`test_health_rollback_wiring.py`, beide Richtungen.

**Ebenfalls erledigt:** `/metrics` und `/api/status`, der größte
Einzelblock. Acht Rückbauten, alle ohne rote Prozedur — keine Prozedur
rief `_serve_metrics` oder `_serve_status_json` je auf. Jetzt werden
beide Endpunkte wirklich bedient und die Bytes zurückgelesen, gegen
einen Aufbau mit zwei Hosts, von denen einer nicht antwortet. Alle acht
Rückbauten gegengeprüft, alle rot.

**Und erledigt:** die Selbst-Update-Kette. Sechs Rückbauten, keine rot
— der Pfad, auf dem sich das Programm selbst ersetzt, und der einzige,
auf dem ein Fehler niemanden mehr hat, der ihn meldet. #43 und #62 sind
beides Narben davon. Jetzt `test_selfupdate_chain.py`: die Reihenfolge,
beide Fehlerarme getrennt, die Stop-Frist, und die Boot-Meldung, die den
Satz sucht, den das Skript wirklich schreibt.

**Und erledigt:** Container-Gruppen und Platzwarnung. Die Gruppen-Regeln
sind das, was eine Gruppe zur Gruppe macht — eine Datenbank und ihre
Anwendung in derselben Sekunde neu zu bauen heißt, dass die Anwendung
gegen eine Datenbank startet, die noch nicht hört. Die Platzwarnung
scheitert in beide Richtungen schlimm: feuert sie nicht, läuft die
Platte still voll; feuert sie bei jedem Tick, schaltet der Nutzer den
Kanal stumm und verliert den nächsten echten Alarm mit.

Zwei Dinge dabei, die ich offen hinschreibe. Die beiden Wartezeiten im
Stapel werden als **Baum** gelesen, nicht gefahren: `_process_update_batch`
braucht ein Dutzend Mitspieler, bevor es dort ankommt, und ich habe beim
sechsten Stub abgebrochen — ein so weit gestützter Prüfstand misst sein
eigenes Gerüst. Und die erste Fassung war schlechter als beides, sie hat
die Schleifenbedingung des Motors abgeschrieben und die Kopie geprüft.

**Die Schuldenliste ist leer** (16.10.2026, `DEBT = 0`). Die sechs
Einträge — Major-Bestätigung, `MIN_IMAGE_AGE_DAYS`, Schwärzen im
Audit-Trail, OOM-Erkennung, das Altersfenster der Warteschlange,
`insecure_registries` — sind alle gefahren und gegengeprüft.

Was „leer" heißt, und was nicht: jede Zusage, *die diese Datei kennt*,
ist widerlegbar. Nicht jede Zusage. Der Durchlauf vom 10.10. fand sechs,
an die niemand gedacht hatte, der vom 11.10. neun weitere. Eine leere
Liste ist eine Einladung, wieder zu kehren, kein Ergebnis.

Zwei der sechs hat die Gegenprobe zuerst durchgewunken, beide aus
meinem Fehler: das Altersfenster las `MAX_AGE_SECONDS` aus dem Modul,
das es prüfen sollte — die Erwartung wanderte mit der Mutation mit —,
und beim Major-Riegel habe ich das Richtlinien-Tor gefahren und das
Bestätigungs-Tor behauptet. Es sind zwei verschiedene Stellen.

**Gemeldet, Anker noch nicht festgenagelt** — gehören hier hinein,
sobald sie eindeutig sind, denn ein veralteter Anker ist eine Prüfung,
die bestanden hat ohne zu laufen:


**Erledigt am 11.10.2026:** der Audit-Trail (die vier offenen Punkte),
die Restore-Parität und die `Retry-After`-Kappe.

Der **Telegram-Seam** wird jetzt gefahren statt gezählt. Was dort stand,
war `tg.count('audit.record("telegram"') == 1` — eine Zahl im Quelltext,
die ein auskommentierter Aufruf genauso erfüllt, und genau so war
`/hosts check` auf beiden Verbindungen tot bei drei grünen Prüfungen.
Jetzt geht der Befehl durch `_handle_message` und der Eintrag wird
zurückgelesen, mit der Gegenrichtung: ein Befehl von einem Fremden
hinterlässt keine Spur (der Seam sitzt *nach* der Auth-Prüfung), eine
normale Chat-Nachricht auch nicht, und ein aufgezeichneter Befehl ist
weiter ein beantworteter. Dafür `/help`, nicht `/stop`: der
`/stop`-Prüfstand brauchte Motor, Hosts und Lifecycle-Modul, bevor er
irgendetwas Beobachtbares erreichte — so weit gestützt misst er sein
eigenes Gerüst.

Dazu der **Akteur bei Token-Zugriff** (ein Prometheus-Scrape ist
`token:prom` über die Quelle `api`, kein Mensch), die
**History-Seite** (gerendert und die Bytes zurückgelesen: Zeit, Akteur,
Befehl, Ziel, `id="audit"` zum Finden, geschärft, und der
Schwärzungs-Marker statt eines Werts) und die **Werte-Kürzung** — und
zwar am Eintrag auf der Platte, nicht nur am Rückgabewert von `redact`.
Vier Einträge in der Ratsche, alle vier gegengeprüft.

`test_restore_parity.py` fragt beide Türen dasselbe — die Kappe, das
`schema_version`-Siegel, Nicht-JSON, eine Datei ohne `.json` — und
dazu die Gegenrichtung, dass eine echte Sicherung weiter durchkommt.
Die erste Fassung war grün und wertlos: sie las die Antwort nach den
Wörtern »backup« und »json«, und `restore_offer` beginnt mit »A backup
arrived«, während die Antwort den Dateinamen `backup.json` zitiert.
Beide Ablehnungsprüfungen waren vom Annahmepfad erfüllt — die
Gegenprobe hat es gesagt, nicht das Lesen. Jetzt wird gemessen, ob
*angeboten* wurde: bei Discord der Bestätigungs-Token, bei Telegram die
eigene `_pending_restores`-Tabelle. Die Zeile, die ich zuerst
überwacht glaubte, ruft Telegram auf diesem Pfad nie auf.

Die `Retry-After`-Kappe fährt jetzt zwei Fälle durch
`post_json_with_retry`: zwei Sekunden werden abgewartet und der Versuch
wiederholt, eine Stunde nicht — die Nachricht geht in die
Warteschlange, statt den Scheduler-Thread eine Stunde schlafen zu
legen.

**Die drei Dinge, denen die Durchsetzung ganz fehlte — erledigt:**

* Der **Scheduler schreibt jetzt in den Audit-Trail**. Die
  Connections-Seite verspricht »… and the scheduler« seit jeher, und
  `record` dokumentiert die Quelle `"schedule"` mit dem Akteur
  `"system"` — aber jede Aufrufstelle im Programm war eine Oberfläche.
  Ausgerechnet das unbeaufsichtigte Update, die einzige Art, bei der
  niemand zugesehen hat, hinterließ keine Spur. Mit Namen, nicht mit
  Anzahl: »2 Container« beantwortet keine Frage, die man ein Woche
  später an ein Protokoll stellt.
* **`/api/backup_import` hat dieselbe Obergrenze wie die beiden Chats.**
  Vorher wurde `Content-Length` geglaubt und am Stück gelesen — ein
  angekündigtes Gigabyte war ein Gigabyte im Speicher, bevor irgendetwas
  hinsah. Und es wird höchstens die Kappe gelesen, denn die angegebene
  Länge ist das Wort des Aufrufers.
* `test_dependents_recreate.py` sucht nicht mehr nur `"failed"`, wo die
  Meldung `"FAILED"` schreibt.

**Erledigt am 11.10.2026: Prozeduren, die ohne Defekt rot werden
konnten.** Vollständig aufgezählt statt geschätzt — es waren neun
Dateien, 23 feste Bezeichner auf dem Daemon (15 Containernamen, ein Pod,
zwei gebaute Image-Tags, zwei Podman-Verbindungen, zwei feste Ports) und
18 Startaufrufe, von denen 15 bei nicht beschaffbarem Image die Zusage
als gebrochen gemeldet hätten; zwölf lasen den Rückgabewert gar nicht.
Null davon waren pro Lauf eindeutig.

Gemessen, nicht behauptet: mit einem fremden `ds_ctf-a-even` auf dem
Daemon hat die alte `test_glob.py` ihn entfernt — danach ist er noch da.
Das ist das ganze Argument für `livefixture.TAG`.

Jetzt ein gemeinsames `scripts/livefixture.py`: `name()` hängt die PID
an, `free_port()` ersetzt 12375 und 8791, `start()` liest den
Rückgabecode und macht aus einer Absage einen SKIP, der die bereits
erreichten Prüfungen **mitnimmt** statt sie wegzuwerfen. Dazu
`test_multihost_live.py` bringt seinen eigenen Container mit (vorher
`sorted(remote_names)[0]` → IndexError gegen einen leeren Daemon, also
ein Absturz, der weder Prüfung noch SKIP ist), `test_image_inherit.py`
sagt einen fehlgeschlagenen Build laut (vorher `if ok:` ohne `else`:
fünf Prüfungen verschwanden und PASS stand trotzdem da),
`test_podman_live.py` prüft, ob Podman überhaupt antwortet, und
`test_remote_transports.py` wirft beim dind-SKIP nicht mehr die zwei
Dockerfile-Prüfungen weg.

Als Wächter festgehalten: `test_live_fixtures.py` liest die ganze Suite
als Baum und wird rot, sobald eine Prozedur wieder einen festen Namen
anlegt oder einen Container startet, ohne das Ergebnis anzusehen —
beides mit Gegenprobe, dass der Scanner wirklich beides findet.

**Sauber gemessen:** ohne Daemon laufen alle 194 durch, in drei
Zeitzonen (+14, −11, +5:45) je ein kompletter Durchgang ohne Rot, kein
`datetime.now()` in den zeitabhängigen Prozeduren, jedes `urlopen`
gemockt.

## 2 — ~~Dieselbe 4xx-Sache in vier weiteren Kanälen~~ — gemessen, behoben

**Erledigt am 07.10.2026.** Gemessen war es nicht, was der Agent
vermutete, sondern das Gegenteil — und derselbe Fehler wie bei SMTP, nur
andersherum: keiner der Kanäle wiederholte eine 4xx, aber **alle warfen
`429 Too Many Requests` und `503 Service Unavailable` ersatzlos weg**.

ntfy.sh drosselt kostenlose Konten. Ein Schwall fehlgeschlagener Updates
ist genau der Moment, in dem eine Drosselung greift *und* in dem die
Nachricht zählt — die Meldung, die am ehesten verschwand, war die
wichtigste. Jetzt in `base.TEMPORARY_STATUS`, für alle sechs Kanäle
gleich, mit `test_channel_backoff.py` als eine Prozedur statt sechs
Kopien, und zwei Einträgen in der Ratsche.

`500` bleibt bewusst auf der »nein«-Seite: es kann ebenso ein dauerhafter
Serverfehler sein, und dann ist die Warteschlange eine Schleife.

## 3 — ~~Behauptungen im Code, die ungeprüft dastehen~~ — erledigt

**07.10.2026.** Alle sieben fahren jetzt, jede mit einem Eintrag in
`test_guard_mutations.py`: `STOP` ist falsy und sperrt seinen Kanal
(aber nicht die anderen), `usable([])` ist False, die Engine-Reihenfolge
wird mit einer Ausgabe geprüft, die **beide** Namen enthält — das ist
der Fall, für den sie existiert, und keine Fixture hatte ihn —,
`EMITTED` wird gegen dreizehn gefahrene Szenarien gehalten, und die
englischen Texte werden gelesen: `hostcheck_ready` darf nicht
»writable« sagen.

Die vier überdehnten Sätze sind korrigiert. `SSLZeroReturnError` steht
jetzt ausdrücklich als Lesart da, nicht als Messung. Die
`parseaddr`-Begründung sagt, was wirklich kaputt war: nicht der Header
auf der Leitung, sondern die ID, die an den Retry geht. Und die Zeile
»another attempt would be a second copy« sagt das nicht mehr, weil
`STOP` zwei Ursachen hat und bei einer Ablehnung nichts zugestellt
wurde.

Offen geblieben: dass `ps -aq` auch unter Podman den neuesten zuerst
listet, ist weiter **nicht gemessen** — dafür braucht es einen Podman-Host
mit mehreren Containern. Der Kommentar nennt jetzt Docker.

## 4 — Struktur und Nutzererfahrung

Sechs von sieben erledigt am 07.10.2026: `/hosts check` läuft auf einem
eigenen Thread (gemessen: `_handle_message` kehrt nach 0,00 s zurück,
die Antwort kommt trotzdem), der Prüf-Timeout ist auf die 30 s des
Betriebs angehoben, `{endpoint}` wird bei 200 Zeichen gekappt — damit
ist der letzte bekannte Gleichstands-Unterschied weg —, `MAX_QUEUED`
gilt auch beim Zurückschreiben, `/hosts CHECK` wird erkannt und
überzählige Argumente werden nicht mehr stumm verworfen, und der
Datenschutzsatz im README sagt jetzt, dass `TELEGRAM_ALLOWED_USERS`
regelt, *wer fragen darf*, nicht wer die Antwort liest.

Bei `smtp_tls` kam ein Fund dazu, den kein Audit gemeldet hatte: der
Vergleich war **case-sensitiv**. `SMTP_TLS=SSL` traf keinen der beiden
Zweige und fiel auf Klartext-SMTP durch — mit gesetztem `smtp_user`
also das Passwort unverschlüsselt auf die Leitung. Jetzt wird
kleingeschrieben verglichen, und ein unbekannter Wert wie `tls`
verbindet gar nicht erst.

**Offen:**

* **`tcp://…:2376`: die Meldung ist erledigt (11.10.2026), die
  Client-Zertifikate nicht.** Die Vorab-Prüfung sagt jetzt
  `needs_tls` statt „Nothing answered" — in 16 Sprachen, und sie sagt
  ausdrücklich, dass die Lücke bei mir liegt und nicht beim Leser. Das
  ist auch nötig: ich habe am 18.08. in #2 selbst geschrieben,
  Docksentry „then just talks to `tcp://host:2376`", und das stimmt
  nicht. Wer dem folgte, bekam seine Maschine als unerreichbar gemeldet.

  Entschieden wird nicht am Port, sondern gemessen — `speaks_tls()`
  bietet einen Handshake an. Nachgestellt gegen zwei echte dinds: mit
  `DOCKER_TLS_CERTDIR=/certs` kommt der Handshake zustande, dasselbe
  Image ohne TLS lehnt mit `WRONG_VERSION_NUMBER` ab. Beides als
  Prüfung festgehalten, die Gegenrichtung mit: ein totes Port und eine
  echte Ablehnung dürfen nicht als TLS gelesen werden. Nebenbei
  gelernt: Docker 29.5 sagt es inzwischen selbst („Client sent an HTTP
  request to an HTTPS server"), Podman sagt nur „ping response was
  400" — deshalb wird gefragt und nicht geraten.

  **Offen bleibt:** TLS-Client-Zertifikate selbst
  (`DOCKER_CERT_PATH`, `--tlsverify --tlscacert --tlscert --tlskey`).
  Bewusst hinter den Kern-Umbau gestellt: es sind nur zwei
  `-H`-Aufrufstellen, aber die Zertifikate gehören pro Verbindung
  konfiguriert, und diese Schicht entsteht erst dort — vorher gebaut
  heißt zweimal gebaut. Gefragt hat danach übrigens niemand; die
  einzige Erwähnung von 2376 im Tracker ist meine eigene.

## 5 — Texte über 16 Sprachen

**Erledigt am 11.10.2026:** der `hostdiag`-Hinweis ist übersetzt. Fünf
Sätze × 16 Sprachen, und alle fünf Anhängepunkte fragen jetzt in der
Sprache des Lesers — der geplante Durchgang, `/check`, `/status`, die
Vorab-Prüfung in Telegram und die in Discord. Ein deutscher Leser bekam
die Diagnose auf Deutsch und die eine Zeile, die ihm sagt was zu tun
ist, auf Englisch.

Der englische Boden bleibt in `hostdiag._EN` und wird gegen `en.json`
gehalten: ein Aufrufer ohne Übersetzer darf den Satz nicht verlieren,
und zwei Fassungen desselben Satzes dürfen nicht auseinanderlaufen.
Was dort an Prüfungen stand, war `"hostdiag.hint" in sched` und zwei
`count() >= N` — jetzt als Baum gelesen: jede `hostdiag.hint`-Aufrufstelle
in `app/` muss ein `t=` mitgeben, und es sind genau fünf.

* `{endpoint}` wird ungekappt ausgegeben, `{error}` auf 300 Zeichen.
  Mit einem 4011-Zeichen-Endpunkt gehen die beiden Bots auseinander:
  Telegram splittet, Discord klippt. `ep[:200]` schließt es. Das ist
  der **einzige** bekannte Gleichstands-Unterschied.
* `{error}. Those are…` ergibt `…connect.. Those are…`, wenn die
  CLI-Meldung auf einen Punkt endet. In 16 Sprachen gleich.
* `zh.json`: drei der neuen Schlüssel benutzen ASCII-`, ` und `: `,
  während die Datei sonst 322× `,` und 182× `:` hat.
* „inspect" ist innerhalb einer Datei mal übersetzt, mal nicht
  (ar/hi/ja/ko/tr/zh/de/ru/uk).
* „Expected one of: {schemes}" nennt die vier empfohlenen, akzeptiert
  werden sechs. Der Satz behauptet Vollständigkeit, die er nicht hat.

## 6 — Die Suite selbst

**08.10.2026 abgearbeitet**, bis auf die zwei Posten unten.

* Die **24 Quelltext-Prüfungen, die ein Kommentar erfüllt**, sind auf 21
  gesunken, und die vier mit der größten Folge sind weg:
  * **Die Cookie-Flags** (`HttpOnly`, `SameSite=Lax`, kein `Secure`)
    waren ein grep über `web_ui.py`, erfüllt vom Kommentar direkt über
    der Zeile, die sie setzt. Jetzt wird die Anmeldung gefahren und der
    `Set-Cookie`-Header gelesen. Gegenprobe: jedes Flag entfernen macht
    rot.
  * **Der Token-Vergleich der Metrics-Schnittstelle** war
    `"compare_digest" in src`, und das Wort steht drei Zeilen höher im
    Docstring. Jetzt als Baum gelesen *und* gefahren — inklusive eines
    Tokens, das nur das Präfix teilt, was ein einschleichendes
    `startswith` fängt, das der alte grep nie gesehen hätte.
  * **`test_compose_recreate_budget.py`** hatte alle sechs Prüfungen auf
    `src.index("# Recreate service via compose.")` verankert, also auf
    einem Kommentar: ihn umzuformulieren ließ die Prozedur *abstürzen*,
    den Code zu verändern nicht. Jetzt als AST. Genau umgekehrt
    nachgemessen — die drei echten Defekte werden rot, die
    Kommentar-Umformulierung bleibt grün.
  * **`test_gluetun_docs.py`**: die beiden Nadeln über `--rm` und
    »Capture config up front« deckt jetzt
    `test_netns_dependent_recreate.py` verhaltensbasiert ab.
* **`test_trust_running.py`** benutzt das `checks`-Dict statt nackter
  `assert`s. Vorher hielt der erste Fehlschlag die anderen beiden Fälle
  auf — darunter den einzigen, der wirklich zählt: dass das
  »Laufzustand vertrauen«-Flag eine Absturzschleife *nicht* verschluckt.
  Und 14 Sekunden echte Docker-Zeit erschienen in `VERIFICATION.md` als
  zwei Prüfungen.
* **`_policy_decision`** — beide zuständigen Tests überschrieben die
  Weiterleitung, um die Regel zu prüfen. Die Weiterleitung ist jetzt ihre
  eigene Zusage: der Bot fragt den Motor, und was er zurückgibt, kommt
  unverändert heraus.

**Erledigt am 16.10.2026 — die Prüfungen, die nur ihre eigene Form
lasen.** Audit 4 hat sie als Mutation belegt, jede einzelne, und hier
stehen nur die, die ich selbst nachgefahren habe: die Gruppen-Wartezeit
und die 30-Sekunden-Untergrenze (vier Mutationen, vier Mal grün),
`shlex.quote` beim Image, die Boot-Meldung nach einem zurückgerollten
Recreate, `/selfupdate previous`, der `return` nach einem
fehlgeschlagenen Pull, der Kurzschluss bei identischem Image, und das
Fenster von 700 Zeichen in `test_ssh_hint.py`, das nur die erste von
drei Aufrufstellen sah.

Zwei Dinge dabei, die ich hinschreibe, weil sie dieselbe Lehre zweimal
sind. Die Gegenprobe hat *mich* zweimal erwischt: der Pull-Fehler blieb
grün, weil der Prüfstand dem Code einen zweiten Grund zum Anhalten gab
(gleiches Image), und die drei neuen Selbst-Update-Prüfungen waren
zuerst grün, weil `run()` schon an der eigenen Container-Suche
ausstieg. Beide Male hat die Gegenrichtung es gesagt, nicht das Lesen.

Und die 23 Stunden der Platzwarnung sind jetzt bei 23 geprüft, nicht
bei 24 — die Prüfung hieß „after twenty-three" und testete 24, also
hielt die dokumentierte Zahl nichts.

**Offen:**

* **Die Quelltext-Prüfungen, die ein Kommentar erfüllt**, sind weiter
  gesunken. Am 16.10. sind die vier weg, die ein Audit einzeln belegt
  hat: `"sessions.clear()" in src` (die Verdrahtung wird jetzt in
  `test_secret_handling.py` durch ein echtes `do_POST` gefahren — ich
  habe den Aufruf unerreichbar gemacht und die alte Prüfung blieb
  grün), `"container:<name>"` und `"no pull, no version change"` und
  `"Capture config up front"`, alle drei reiner Docstring- oder
  Kommentartext, alle drei anderswo verhaltensbasiert gedeckt.

  Dazu eine vakuöse Prüfung, die kein Grep war: `not any(… for i in
  range(1, len(options)))` über ein Kommando mit genau einer Option —
  die Schleife ist leer, die Prüfung hielt nichts.

  Was bleibt, sichert Kleineres ab (Meldungstexte, Dockerfile-Zeilen,
  Hilfsnamen). `not in`-Prüfungen sind nicht darunter — die macht ein
  Kommentar rot, nicht grün, und das ist die sichere Richtung.
* **Flakiness, Stand 09.10.2026**: die serielle Aussage von oben ist
  **zurückgezogen** — sechs Läufe ergaben fünf grün, einen rot, immer
  auf derselben Prüfung. Ursache war das Fixture (ein Probe-Container,
  dessen Start nie geprüft wurde); behoben, danach fünf grün und ein
  sauberer SKIP. Offen bleibt: beide Live-Docker-Prozeduren benutzen
  **feste Containernamen** (`ds_trust_test`, `ds-tcp-transport-test`,
  `probe`) und einen festen Port 12375, zerstören sich also bei zwei
  gleichzeitigen Läufen gegenseitig.
* Die `*_resilience.py` wiederholen weiter identische
  Assertion-Ausdrücke. `test_channel_backoff.py` hat den wichtigsten
  Teil davon zusammengefasst; der Rest wäre Kosmetik mit Risiko.

## 8 — Ein stiller Kanal ist nur im Log sichtbar

**Aufgekommen am 16.10.2026 in #63.** @NotRetarded konnte nicht selbst
feststellen, warum seine Mails ausbleiben — er musste fragen. Der
Test-Knopf beweist einen Kanal im Moment des Tests; ein Kanal, der
danach verstummt, taucht nirgends in der Oberfläche auf. Der Grund
steht ausschließlich im Container-Log (`SMTP error: …`).

Gemessen, damit die Diagnose nicht daran hängt: eine tote SMTP bringt
die anderen Kanäle *nicht* zum Schweigen (abgelehnt: Discord und
Telegram in 0,0 s; angenommen-und-stumm: beide nach 15 s), und der
Wochenbericht geht per Mail raus. Die Verdrahtung ist also in Ordnung.

Was fehlt, ist die Anzeige: letzter Versand je Kanal, Zeitpunkt und
Grund, auf der Connections-Seite. Die Daten dafür entstehen bereits —
`_dispatch` fängt die Ausnahme pro Kanal und druckt sie, und der
Monitor liest seit #66 den Rückgabewert. Sie werden nur nirgends
aufgehoben.

## 11 — Einmal gesehen, nicht erklärt: Stille auf allen drei Kanälen

**17.10.2026, #63.** @NotRetarded hat per Arcane einen SIGKILL auf
Uptime Kuma gegeben und bekam auf **keinem** der drei Kanäle etwas.
Danach hat er seine Mailrise-Konfiguration gerichtet, und seitdem
läuft es — Discord und die nachgereichte Mail beide belegt.

Was das erklärt: die Mail. Was es **nicht** erklärt: warum Discord und
Telegram damals still blieben. Gemessen: ein SIGKILL auf Uptime Kuma
gibt 137, und 137 löst einen Alarm aus — der Fall hätte feuern müssen.

Bewusst offen gelassen statt zugezogen. Keine Theorie ohne Messung,
und die Messung fehlt: ohne die Logzeile von damals ist alles andere
Raten. Wenn es wiederkommt, ist sie das Entscheidende; bis dahin steht
es hier, damit es nicht als erledigt durchgeht.

## 10 — Der Mail-Betreff trägt nichts

**17.10.2026, aus #63.** @NotRetarded will in Mailrise nach Betreff
sortieren und kann es nicht. Gemessen an einer echten Nachricht gegen
einen Test-Server:

```
Subject: Docksentry
Content-Type: text/plain; charset="utf-8"
```

Alarme bekommen den festen Betreff `Docksentry` (`smtp.py`,
`_attempt("Docksentry", …)`); nur `send_updates_available` und
`send_update_result` tragen einen echten, übersetzten Titel.
`SMTP_SUBJECT_PREFIX` setzt ein `[deins] ` davor, mehr nicht.

Wer Mails nach Betreff filtert — und das ist der Normalfall bei einem
Relais wie Mailrise —, bekommt von uns einen Eimer. Containername und
Ereignis gehören hinein, in 16 Sprachen, und die Titel dafür gibt es
teilweise schon (`notify_updates_title`). Angeboten in #63, wartet auf
seine Antwort.

Nebenbei gemessen und damit erledigt: kein HTML, nie — ein typischer
Crash-Alarm ist 325 Bytes inklusive Kopfzeilen. An einem Größenlimit
des Relais liegt sein Problem nicht.

## 9 — Beobachten: cloudflared und das Versions-Label

**17.10.2026.** [cloudflare/cloudflared#1682](https://github.com/cloudflare/cloudflared/issues/1682)
will `org.opencontainers.image.version` auf die Release-Images setzen;
jemand hat sich das Thema genommen.

Das Label ist bei uns kein Nebenschauplatz: `container_info` und
`update_checker` lesen es, es füllt die Versions-Badge und beantwortet
in `/status` „welche Version läuft hier wirklich" (#36) — und es hat
uns #46 eingebracht, wo ein alter Wert beim Recreate am Container
klebte. Gemessen am aktuellen Image: cloudflared setzt heute
`org.opencontainers.image.source`, aber kein `…image.version`. Deshalb
steht bei einem cloudflared-Container keine Version, nicht weil wir
nicht suchen.

Zu tun ist nichts — wenn es oben landet, erscheint die Version bei uns
ohne eine Zeile Code. Im Blick behalten, weil cloudflared bei unseren
Leuten verbreitet ist (#65), und weil `git describe` gern
`2026.10.1-6-gabc1234` liefert: `_clean_version_label` normalisiert das
zwar, aber die Badge könnte dann ein Commit-Suffix tragen.

## 7 — Entschieden, bleibt so

* `de.json` übersetzt die Metavariable zu `<endpunkt>`, die anderen 15
  behalten `<endpoint>`. Für einen deutschen Leser ist das besser, und
  der kopierbare Teil des Befehls ist unberührt.
* `fd://` und `npipe://` bleiben **akzeptiert**, werden aber nicht mehr
  empfohlen: wer so ein Setup hat, weiß es besser als unsere Liste.
* Die Vorab-Prüfung gibt es in der Web-UI nicht, und kein Text behauptet
  etwas anderes.