TGV · Betrieb · Stand 19.07.2026 · UMG-1 (Register #33)
Alles, woraus die TGV-Welt technisch besteht — Dienste, Arbeitsplatz, Server, Daten, Automatisierung, Kanäle — gemessen mit Fundstellen, Lücken ehrlich markiert, Secrets nur als ORTE (nie Werte).
| Dienst | Was | Fundstelle |
|---|---|---|
| app.startact.ai | CORE-App (Magic-Link-Login), nginx auf Hetzner | deploy.yml:33-56 |
| gabriel.megabook.fr | gabriel-Cockpit + /api/skills (FastAPI) | 08_OPS/hetzner/README.md |
| skills.megabook.fr | kanonischer Skill-Host (LE-Zertifikat bis 30.08.) | nginx-skills-megabook.conf |
| Hetzner-Server | 178.105.181.47 · CPX22 Nürnberg · Ubuntu 24.04 · 8,49 €/Monat | 08_OPS/hetzner |
| Supabase | Projekt egcdlozsvyhhvipylxky (DB + Auth + RLS) | → §5 |
| n8n | jcbinetti.app.n8n.cloud, Projekt N0GxOHd4eVS1RsFY | → §6 |
| GitHub | jcbinetti/TGV (privat) · tgv-dashboard (public = ALTE v11 auf Pages) | README.md |
| Container | Docling (OCR) :5001 · Gotenberg (PDF) :3000 — auf dem Hetzner | → §4 |
| IONOS | 10 Domains, FR-Kundennummer 457482269 | reference_domains_registry |
| Resend · Drive | Mail-Versand · Shared Drive „TGV-CORE" 0AB1RMdD8smYoUk9PVA | → §7 |
Werkzeuge: git · gh · PowerShell + Git-Bash · Python 3.13 (C:\Python313, verdrahtet in .mcp.json). Worktree-Modell: eine Session = ein Worktree unter .claude/worktrees/.
~/.ssh/id_ed25519 → root. ② VPN AUS vor jedem Server-Schritt. ③ Absolute C:\Dev\TGV\repo-Pfade treffen den MAIN-Worktree, nicht deinen — Stashes sind repo-global. ④ Bash-Profil kann hängen → PowerShell nutzen; git nie in Start-Job; checkout master hängt im Worktree.Verzeichnis-Kanon README.md:5-30: 00_LEITDOKUMENT = Wahrheit … 99_ARCHIV; Credentials NUR 08_OPS/credentials (gitignored). Branch-Modell: master mit PR-Pflicht (Branch-Protection); Lanes auf eigenen Branches. ⚠ Staging deployt aus design02/native-screens-v3, nicht master.
| CI-Workflow (8) — ✅ CI-PAUSE BEENDET (15.08.2026) | Wacht über |
|---|---|
| deploy.yml | Cockpits/CSS/i18n/Assets → Hetzner (Matrix, 443-Inhalts-Verify) |
| deploy-service.yml | FastAPI-Service (Bundle + Wrapper-Weg) |
| familie-guard.yml | DIESE Familie: C1–C9 + D1–D9, täglich per Cron + je Merge (Stempel im Index). ⭐ Lokal-Lauf bleibt Pflicht vor jedem Familien-PR — python 06_DASHBOARD/konzepte/familie_guard.py: der Cron findet den Bruch erst am NÄCHSTEN Morgen, der PR-Autor findet ihn sofort. |
| kanon-merge-freeze.yml | Freeze-Gate (misst Trigger, gilt auch gegen Tower) |
| kanon-quellen-ssot.yml | Regel-/Migrations-/Katalog-Divergenz |
| lint-wrappers.yml | SIGPIPE-Klasse (die Prüfer-lügen-Narbe) |
| skill-registry-ssot.yml | Skills-SSOT _registry.yaml |
| testdata-prod-guard.yml | Testdaten-in-Prod-Sperre (BR#04) |
steps:[], 2–3 s) = Infra-Signatur, kein Guard-Befund. JC hat das Actions-Budget auf 30 USD/Stop-usage gesetzt; die übrigen Budgets bleiben bewusst 0. Beleg statt Behauptung: die familie-guard-Cron-Stempel liefen wieder durch — Commits aa64f0c0 (12.08.) · 61060a48 (13.08.) · 40412aef (14.08.) · 4ed6e286 (15.08.). Die Lehre bleibt betriebsrelevant: rote Checks vor jedem Merge EINMAL per Log-Blick klassifizieren — steps:[] = Infra (Merge legitim), echter Step-Fail = Guard-Befund (STOPP).Die Narbe: in der Nacht 10.→11.08. starb die Shell live. Ein Hand-Deploy war Stückwerk (engine/vendor/icons vergessen → 404), und eine Repo-nginx-conf überschrieb handgepflegte Live-Locations. Beides war nicht durch Sorgfalt zu verhindern, sondern nur durch eine Liste, die es vorher nicht gab.
Der SSOT: 08_OPS/deploy_manifest.json = die EINE Liste aller Serve-Artefakte (id · src · target · url · channel · ctype, teils marker für den Inhalts-Verify). Die Regel steht im Manifest-Kopf selbst (deploy_manifest.json:4): „Neues Serve-Artefakt = neue Zeile HIER + eigener CI-Job + eigene nginx-Location." Drei Prüfungen halten sie (08_OPS/deploy_guard.py): check-drift (Manifest ⊇ CI, rein lokal) · smoke (serven ALLE URLs 200 — read-only GET, EIN Report) · conf-diff (würde ein conf-Deploy handgepflegte Live-Zeilen clobbern? STOPP bei live-only, offline testbar via --live-file).
abgleich2.html in einer Dreifach-Drift — Tür im Live-Menü, aber Datei in KEINEM der drei Register (Manifest · deploy.yml · nginx-Snippet), also nie deployt (Register #447). ⭐ Genau das ist die Klasse, die ein Mensch nicht findet: jedes Register für sich sah plausibel aus. Stand 15.08.: Smoke 38/38 (Register #453). ⛔ VPN-Falle: aktives GlobalProtect blockt 22/443 zu Hetzner → smoke/conf-diff scheitern mit Timeout, NICHT weil etwas kaputt ist.⛔ Die Regel oben nennt DREI Orte — es sind VIER REGALE, und ein Merge erreicht nur eines.
PLAT#10 · #548 · 22.08.2026, gemessen gegen master 7af15d5f
/tgv_e4_labels.js war live 404, obwohl PR #1852 den Fix vollständig gemergt hatte.
Der Grund stand nirgends im Code: eine Auslieferung braucht vier Regale, und drei davon berührt kein Merge.
| Regal | Wo | Wird vom Merge bedient? |
|---|---|---|
| ① Manifest | 08_OPS/deploy_manifest.json |
✅ ja — und es stoppt den lokalen Voll-Deploy an der ersten Lücke (deploy_full.ps1:156, Design: kein stiller Teil-Deploy) |
| ② CI-Trigger | .github/workflows/deploy.yml paths: + changes-Job |
⛔ nur scheinbar — der Job feuert ausschliesslich, wenn sich die QUELLDATEI ändert. Die e4-Datei lag seit 01.08. unberührt ⇒ der Job meldete in jedem Lauf skipped |
| ③ Wrapper-Allowlist | live /opt/tgv-deploy/tgv-deploy-html.sh (root:root) |
⛔ nein — nur 08_OPS/server/reinstall_wrappers.sh spiegelt ihn (root-Hand) |
| ④ nginx-Location | live /etc/nginx/snippets/tgv-skills.conf |
⛔ nein — nur ein serve_*.sh setzt sie |
Gegenmessung (warum es nicht „die Actions-Route macht es anders" war): die drei anderen
.js-Artefakte sind live 200, weil sie alle vier Regale haben. ⭐ Die Wurzel ist nicht ein
fehlender Fix, sondern ein Fix, der ein Regal von vier bediente — und dieses eine nur im Repo. Der Handgriff
für ③+④ (08_OPS/server/serve_e4_labels_asset.sh) lag seit dem Merge da und war nie gelaufen:
die Arbeit war getan, nur nicht zugestellt.
08_OPS/check_deploy_route_traegt.py
(„die Route existiert, trägt aber nie") misst jedes Manifest-Artefakt gegen alle vier Regale, dazu die
Gegenrichtung (Allowlist-Zeile ohne Manifest-Zeile = tote Tür) und die Drift der zwei Repo-Kopien der EINEN
nginx-conf. Eingehängt in lint-wrappers.yml; die Auslöser umfassen bewusst Manifest, deploy*.yml
und den nginx-Spiegel — sonst schläft er bei genau der Änderung, die ihn braucht.
⛔ Seine Reichweite steht in seinem Kopf — nach der Regel, dass eine Aussage über Nicht-Existenz nur so gross ist wie die Liste, die sie geprüft hat, und diese Liste deshalb mitführen muss: er misst ③+④ am REPO-Stand. „Repo grün" heisst
das Regal ist gebaut, nicht es steht live — die Live-Frage beantwortet deploy_guard.py smoke
(200 + Content-Type + Marker). Genau diese Lücke war die Wurzel.REFUSED, sondern
Connection closed … port 22 (exit 255). Der Beweis ist die Uhr: zu jedem der 13 roten Läufe steht
im journalctl -u ssh ein sekundengenaues drop connection #10 … Maxstartups.
Wurzel: sshd -T = maxstartups 10:30:100, der Workflow öffnete aber bis zu
40 SSH-Verbindungen gleichzeitig (concurrency ist PRO DATEI und serialisiert nichts).
⇒ max-parallel je Matrix, Summe ≤ 10 (cockpit 3 · i18n 2 · css 1 · design01 1 · dataviz 1 · ux 1 · e4 1).
Der Server wurde bewusst NICHT geschwächt — fail2ban zählt dort 8.499 Fehlversuche und 1.164 Banns.
⭐ Ein Workflow, der bei jedem zweiten Lauf aus Zufall rot ist, verdeckt echte Rots und trainiert das Wegsehen.location ^~ /DESIGN_01/ allein deckt drei).
② Vergleiche Mengen, nicht Zeilen. Ein diff der beiden nginx-Kopien meldete 33 Zeilen als
„fehlend", die live nur an anderer Stelle stehen — ein Zeilen-diff hätte sie zerstört; der Mengen-Vergleich
der location-Muster zeigte die wahre Lücke.
③ Miss einen Exit-Code nie hinter einer Pipe. python … | tail; echo $? liefert den Exit von
tail — daraus entstand hier ein Falsch-Befund gegen zwei intakte Prüfer, der zurückgezogen werden musste..old und index_sha256,
und „was live verschwände“ nur für nginx (deploy_guard.py:197). deploy_guard.py parity läuft täglich über das Manifest.
Es fehlt: eine Sicherung vor dem HTML-Überschreiben (tgv-deploy-html.sh:264). Die HTML-Prüfung vergleicht nur die Versionsmarke (deploy.yml:166: „NICHT VERIFIZIERT“).
Der Drift-Abgleich normalisiert keine Zeilenenden und prüft gegen einen Ref statt gegen die Commit-Historie. ⇒ #4108, Vorschlag DEPLOY#04.
Die Browser-Probe bei 375 und 1280 px über alle Flächen ist #4112; heute misst uxaudit_runner.py eine Breite je Lauf.
Einzeltabelle mit Fundstellen: 00_LEITDOKUMENT/KONZEPT_WEBSITE_FACTORY.md §4.2b.GitHub-Secrets (nur NAMEN — Werte leben in GitHub → Settings → Secrets): HETZNER_DEPLOY_KEY · HETZNER_HOST · HETZNER_DEPLOY_USER · HETZNER_KNOWN_HOSTS (optional) · HETZNER_SERVICE_DEPLOY_KEY deploy.yml:81-84 · INSTALL_DEPLOY.md:72-78.
nginx: snippets/tgv-skills.conf + sites gotenberg/skills-megabook; Serving-Root /opt/tgv-skills/www. Deploy-Mechanik: /opt/tgv-deploy/*.sh = root-owned forced-command-Wrapper; ZWEI getrennte ed25519-Keys (html ohne sudo · service mit sudo NUR für systemctl restart|is-active tgv-skills); Deploy-User-Shell MUSS /bin/sh sein (nie nologin). Nach Wrapper-Änderung: bash 08_OPS/server/reinstall_wrappers.sh INSTALL_DEPLOY.md:34-107. User: tgvdeploy (Deploy) vs. root (JC-Admin). Container: docling-serve-cpu :5001 (mem 3g, Health /docling/health) + Gotenberg 8 :3000.
www/<subdir> root-owned → chown · requirements.txt-Diff blockt Deploy (exit 7 → --apply --bootstrap) · Skill-Route ist /api/skills/ PLURAL.Migrations-Ritual: autoritativ ist NUR die Tabelle tgv_migrations — Datei-Header lügen (Kanon-Falle). DDL-Weg: Supabase Management-API POST /v1/projects/…/database/query mit Bearer-Mgmt-Token und User-Agent-Pflicht (sonst Cloudflare-403); Pooler/Direktverbindung unzuverlässig. Auth: Redirect-Allowlist MUSS app.startact.ai/** enthalten. SQL-Editor-Falle: leeres ARRAY = '{}'.
public.tgv_migrations alle vier als NICHT APPLIED — 250/251/252 trugen gar keinen INSERT, 240 einen OHNE applied_at (die Spalte hat keinen Default → NULL-Zeile → jedes Werkzeug liest „nicht appliziert").
applied_at, Selbsttest beidseitig (14 Fälle; gegen den alten master-Stand beisst er 13×, gegen den neuen 0×). Kopiervorlage: 05_SCHEMA/_LEDGER_BAUSTEIN.sql mit den drei Pflichten — ① existiert (auch STAGED) ② setzt applied_at ③ überschreibt ein Erst-Datum nicht (COALESCE). ⛔ ON CONFLICT DO NOTHING ist verboten: es lässt eine alte NULL-Zeile für immer NULL. Messwerkzeug (read-only, JC-Hand): 08_OPS/check_migration_ledger.py.
KI-Dienste: Mistral mistral-embed 1024-dim, EU-Residenz executors_rag.py:13,90-101 · Anthropic: chat_engine DEFAULT_MODEL claude-sonnet-4-6, native Messages-API via urllib llm_client.py:29-31,93-154.
| Workflow (ID) | Auslöser | 30 d · 7 d · Fehler · zuletzt | Zweck in einem Satz · wer ruft | Klasse | Empfehlung |
|---|---|---|---|---|---|
| LM#07 Health Cron haD67MbvJcXUxGDI | alle 6 h | 120 · 28 · 9 (24.–29.09.) · 11.10. 10:00Z | Fragt den Dienst nach seiner Gesundheit und meldet über Resend. Zwilling GitHub ① cron_health_check.py: Lauf 11.10. 05:34Z up v1.12.369, ALARMWEG-ANNAHME anbieter=brevo zustand=angenommen http=200 | ② abgelöst | Erledigt — JC hat ihn am 11.10. abgeschaltet. Gegenprobe s. u. |
| Skill Periodic Engine tnuT3QN4AwnbYdSz | Cron 30 3 */2 + Webhook | 15 · 4 · 0 · 11.10. 01:30Z | Nabe: ruft per Webhook Cost Collector, Safety Net, Read-Back (Kopf n8n). Zwilling GitHub ④ cron_periodic_engine.py, Trockenlauf 11.10.: 4 von 4 Jobs via: local | ② abgelöst | Welle 2, als erster |
| Skill Cost Collector DV1qyZgjSk7cTbim | Cron 0 3 + Webhook | 58 · 15 · 3 (13./14./22.09.) · 11.10. 01:30Z | Sammelt Kosten (n8n, Supabase, GitHub) in cost_metrics; liest dafür die n8n-Variable TGV_N8N_API_KEY. Zwilling GitHub ③ cron_cost_collector.py | ② abgelöst | Welle 2 |
| Contract Stub Safety Net eATEzPsbFjHUWcpD | Webhook | 14 · 4 · 0 · 11.10. 01:30Z | Zieht Vertrags-Stubs nach. Einziger Rufer: Periodic Engine. Zwilling GitHub ⑤ + POST /api/skills/contract-stub-safety-net | ② abgelöst | Welle 2 |
| Entity Read-Back (R3) MkcSBtQersDjbtZq | Webhook | 14 · 4 · 0 · 11.10. 01:30Z | Liest Entitäten zurück. Einziger Rufer: Periodic Engine. Gegenstück /api/skill/classify-entity-readback über GitHub ④ | ② abgelöst | Welle 2 |
| VAT Quality-Gate Poller 5MdBamTHJZE8yqBV | alle 6 h | 120 · 28 · 0 · 11.10. 10:00Z | USt-Qualitätstor auf documents.vat_quality_warnings. Zwilling GitHub ②a/②b (skill.py --apply + cron_vat_determine.py) — beide schreiben dieselbe Zelle | ② abgelöst | Welle 2 |
| Cron Mail Failure-Notify G9a0SNY1bMhPfIUS | täglich | 30 · 7 · 0 · 11.10. 06:00Z | Soll Mail-Ausfälle melden — meldet in die Route tgv-skill-send-mail, deren Workflow seit 05.09. inaktiv ist (#3794): kann nicht melden. Zwilling GitHub ⑧ | ② abgelöst | Welle 2 |
| Iter 54 A6 Shadow Diff Cron O6WPZZWXpdG6MuDb | täglich | 30 · 7 · 0 · 11.10. 00:00Z | Vergleicht den A2-Schattenmodus; A2 ist inaktiv, SICHERHEIT#07 (#1918): „der Beobachter-Schalter schaltet keinen Weg“. Leser von shadow_diff: 0 | ③ tot | Welle 1 |
| Webhook Resend Bounce 8n3VhGkb4OqU7g8c | Webhook, externer Rufer (Svix/Resend) | 9 · 0 · 0 · 21.09. | Nimmt Rückläufer von Resend an. Dienst-Gegenstück POST /api/skills/mail/bounce (app.py:2742, seit 1.12.160, Signatur Pflicht). Brevo ist seit 10.10. Standard, und der letzte Resend-Sender (Health Cron, MAIL#06 #3936) ist seit 11.10. aus — ob wirklich nichts mehr über Resend geht, misst die Gegenprobe von Welle 3 | ② abgelöst | Welle 3 — nach dem Löschen des Webhooks in der Resend-Konsole |
| Skill Store File To Drive dT1VRLPnDdYxVIKH | Webhook | 11 · 11 · 0 · 11.10. 01:10Z | Legte Belege in Google Drive ab. Alle 11 Läufe = Fehlauslösung (Kasten oben). Gegenstück /api/skill/store-file-to-drive; der MCP ruft seit #1668 dorthin (tgv_mcp_server.py:2004) | ③ tot | Welle 1, als erster — heute |
| Cascade Trigger RKh1lq9ruckrpR24 → Skill Router mvOybw4cBU7y8X5Y → Orchestrator CbNkDkMss3YiHAmN → Pattern Engine RtTqaFYxiFtc9Pdm | Webhooks (Cascade-Zeitplan disabled) | je 0 · 0 · 0 · — | Die alte Verteiler-Kette. n8n-intern rufen nur inaktive Workflows (A2, WF B); Dienst /api/skills/dispatch und /api/skills/orch/plan | ③ tot | Welle 1, in dieser Reihenfolge |
| Image Vision Branch j5lervqxZggrEQUs → Fetch Drive Binary tBwu2D2vK8jIsU0B | Webhooks | je 0 · 0 · 0 · — | Bildanalyse und Drive-Bytes. Rufer n8n-intern: nur WF B (inaktiv) bzw. Image Vision. Bytes kommen heute aus dem Bucket | ③ tot | Welle 1, in dieser Reihenfolge |
| Die 14 Skill-Blätter: PDF Invoice Extract · PDF Text Snippet · Parse Docling · RAG Ingest · Detect Duplicate · Resolve Supplier · Classify Party Role · Classify CSV · Import Banking CSV · Import Timetrack CSV · Tax Extract · contract-extract Wrapper · Merge Duplicates · PDF OCR Fallback | Webhooks | je 0 · 0 · 0 · — | Die heimgeholten Skills; der Dienst bedient 20 von 20 lokal. Rufer ausserhalb: nur Alt-Skripte in 07_TESTS/ und 08_OPS/ und Rückweg-Zeilen in Docstrings. ⚠️ Parse Docling trägt ein Klartext-Credential (S2) | ③ tot | Welle 1, Reihenfolge frei |
| Skill Tax Send Package mZyMkagJPmMCNQqX | Webhook | 0 · 0 · 0 · — | Hat nie gesendet — Phase 1 erlaubt nur dry_run (#3152). Kein Rufer | ③ tot | Welle 1 |
| Claude Proxy ceurPCxGciw1CN0v | Webhook | 0 · 0 · 0 · — | Alter Durchgang zu Anthropic; KAI läuft im Dienst. Rufer: Liste 1 KEINE, Liste 2 0 lebende. Steht unter JCs Schutz vom 16.08. | ③ tot | Welle 1 — nur mit JCs Ja zur Aufhebung des Schutzes |
Die 14 inaktiven (KB Learn · B2 Body-Router · Docling Wake-Cron · Post Intake · Extract Contract PDF · Mail-Body-To-PDF · RAG Search · Reconcile Payment · Send Mail · Upsert Contract · WF A2 · WF B v2 · WF E · seit 11.10. Health Cron): 0 Läufe im Horizont, nichts zu tun. ⛔ Abschalten heisst Unpublish, nicht Löschen — der Rückweg bleibt ein Klick.
Die exakten Namen, aus der n8n-API gelesen — damit geklickt wird, was auf dem Schirm steht: Ⓐ Workflow TGV WF A2 Mail Intake + Universal Triage v01.34 (fx9POuhD3ybynhat), oben rechts der Active-Schalter; der Rückweg ist derselbe Schalter. Ⓑ der kleinere Schritt: im selben Workflow den Knoten Cascade Trigger Fire (HTTP Request → /webhook/tgv-cascade-trigger) → Deactivate; ⛔ das stoppt nur die Kaskade, nicht den Mail-Eingang und ersetzt Ⓐ nicht. ⛔ Und nach Ⓐ muss binnen Minuten geprüft werden, ob ein mail_intake.poll.done mit mails > 0 erscheint — kommt keiner, A2 sofort wieder an. Ohne diese Prüfung ist das Abschalten fahrlässig.
⛔ Und „Drive-Adapter-Ziel umhängen" ist obsolet, nicht offen: JC am 22.08. 16:40 wörtlich „Inbox Drive nutze ich nicht mehr. Mail factures oder bald KAI." ⇒ der Kopf fällt ganz (#566). Wären es Umhängungen, wären es vier Knoten, nicht einer: POST Skill Router · POST Skill Router Docling · POST Orchestrator · POST Orchestrator Docling.
⭐ WAS FERTIG IST — der Dienst zeigt nirgendwo mehr nach n8n: SKILL_TARGETS in dispatch.py führt 0 Ziele der Art n8n; /api/skills/health meldet unabhängig "targets":{"local":20}. Die zehn ("n8n", …)-Zeilen, die eine Suche in app.py noch findet, stehen in Docstrings als benannter Rückweg — je eine Zeile, absichtlich wörtlich hinterlegt. ⛔ Zwei echte Bindungen bleiben, beide env-gebunden: N8N_BUCHHALTER_WEBHOOK und N8N_SEND_MAIL_WEBHOOK in app.py.
⭐ UND DER REPO-REST IST WEG: die Landkarte vom 22.08. nannte dispatch.py:496 als letzten Repo-Rufer des n8n-Orchestrators. Er steht heute als auskommentierter Rückweg da (dispatch.py:41), nicht als Aufruf. Das ist die einzige Position der alten Karte, die sich von selbst erledigt hat.
| Workflow | 30d | seit 22.08. | zuletzt | Stand |
|---|---|---|---|---|
| WF A2 Mail Intake fx9POuhD3ybynhat | 51 | 10 | 27.08. 10:20 | ⛔ DOPPELT — lokal längst scharf, n8n nie abgeschaltet |
| Cascade Trigger RKh1lq9ruckrpR24 | 44 | 10 | 27.08. 10:20 | ⛔ feuert bei jeder Mail mit A2 — fällt mit A2 |
| WF B Drive Adapter KRNRcSLSUiVC8nTW | 30 | 6 | 27.08. 10:00 | ⛔ JC hat am 22.08. „stilllegen" entschieden — es ist nicht geschehen |
| Health-Cron · VAT-Poller | je 120 | je 22 | 27.08. 10:00 | ⛔ Ports existieren (betriebs-crons), n8n läuft weiter |
| Cost Collector · Periodic Engine · Entity Read-Back · Contract Stub Safety Net | 62 · 16 · 16 · 17 | 12 · 3 · 3 · 3 | 27.08. 01:30 | ⛔ dieselbe Lage: portiert, nicht abgelöst |
| Skill Router mvOybw4cBU7y8X5Y | 356 | 2 | 23.08. 10:00 | lebt nur noch über den Drive-Adapter |
| Skill Orchestrator CbNkDkMss3YiHAmN | 116 | 2 | 23.08. 10:00 | ebenso — kein Repo-Rufer mehr |
| B2 Body-Router · Mail-Body-To-PDF | 56 · 26 | 0 | 11.08. | still seit 16 Tagen, fällt mit A2 |
| Triage Agent bzbnkMvWF0FcWYI8 | 0 | 0 | — | ⭐ seit dem 22.08. INAKTIV — der einzige vollzogene Schritt |
| Post Intake · KB Learn · WF E v1.7.2 · Docling Wake-Cron | 0 | 0 | — | inaktiv, gefallen |
| RAG Search · Claude Proxy | 0 | 0 | — | aktiv ohne Lauf; Claude Proxy ist JC-geschützt |
Die vier Handgriffe, in der Reihenfolge, die das Werkzeug erzwingt (WO: Git-Bash, Repo-Wurzel; ohne --apply schreibt nichts): ① python 08_OPS/heimholung09/stilllegen_null_laeufer.py --gruppe drive — der Drive-Kopf, JCs Entscheid vom 22.08. ② --gruppe null — die Null-Läufer. ③ A2 + Cascade abschalten (Gruppe a2) — beendet den Wettlauf um die Mailbox. ④ --gruppe drive-glieder — erst danach, der Riegel misst selbst, ob der Kopf wirklich still ist. ⛔ Der Riegel meldet vor Schritt ③ zu Recht „Cascade Trigger ruft den Router" — das ist kein Fehler, sondern die Reihenfolge.
| Kann sterben — 30 | Beleg | Läufe 30d/7d |
|---|---|---|
| Router · PDF-Invoice-Extract · Detect-Duplicate · RAG-Ingest · Classify-Party-Role · Store-File · Resolve-Supplier · PDF-Text-Snippet · Image-Vision · Parse-Docling · contract-extract · Classify-CSV · Import-Banking-CSV · Import-Timetrack · Tax-Extract · Merge-Duplicates · RAG-Search · Tax-Send-Package | 18 · Ziel in SKILL_TARGETS | 356 … 0 / alle 0 |
| Orchestrator | /api/skills/orch/plan | 116 / 0 |
| Health · Cost Collector · Periodic Engine · Contract Stub Safety Net · Entity Read-Back | 5 · Port in betriebs-crons.yml | ⛔ laufen noch 28·13·3·3·3 |
| Cascade Trigger · Fetch Drive Binary · PDF OCR Fallback | 3 · ⛔ fallen mit ihrem AUSLÖSER, nicht weil ein Ersatz da wäre | 8 / 0 / 0 |
| Welle | Was | Läufe 30d / 7d | Beleg der Tragfähigkeit | Umschaltmoment · Rückweg |
|---|---|---|---|---|
| 1 · Kopf | WF B v2 Drive Adapter | 31 / 7 | ⛔ kein Ersatz nötig: JC 22.08. „Inbox Drive nutze ich nicht mehr“, und ab 01.09. gibt es die Drive-INBOX nicht mehr | ersatzlos stilllegen · Active-Schalter |
| 2 · Leser | Router · Orchestrator · Invoice-Extract · Snippet · Docling · Detect-Duplicate · RAG · Party-Role · Resolve-Supplier · Pattern-Engine · Image-Vision · Fetch-Drive-Binary · CSV-Familie · Tax-Extract | je 1–356 / je 0 | ⭐ seit 23.08. still — sie hängen alle am Kopf aus Welle 1 | nach Welle 1, mit Riegel je Glied · Active-Schalter |
| 3 · Schreiber | Store File To Drive | 113 / 0 | ⛔ braucht ABLAGE#02: erst wenn der Bucket-Ausgang misst, dass er trägt | ⛔ nicht vorher — ein Ausgang ohne gemessenen Ersatz ist ein Abriss, kein Umhängen |
| 4 · Versand | WF-E Buchhalter-Anhänge | 0 / 0 | ⛔⛔ die 0 heisst hier MONATLICH, nicht tot — das August-Paket ist zum 01.09. fällig | Umbau auf den Zeiger-Kanon, nicht Stilllegung |
Workflows (Stand-Liste 16.05. in TGV_CORE_V1_ARCHITECTURE_CURRENT.md:23-59 — 2 Monate alt, live nachprüfen): A2 Mail-Intake · WF-B Drive (KRNRcSLSUiVC8nTW) · Cascade · Skill-Router · Orchestrator · Post-Intake · WF-D Reclass · Send-Mail · WF-E Buchhalter · Failure-Notify · Resend-Bounce; JSON-Backups in 03_WORKFLOWS/. Drive-Zugang: Service-Account tgv-n8n-service@… (Credential RLAzXGpz6RT5OW3q) statt OAuth. Trigger: WF-B-Webhook + MCP run_inbox_cascade; API per X-N8N-API-KEY. Schedules: WF-B täglich 12:00 UTC; die 3:00-Cascade ist DISABLED.
$vars (nur ORTE — Werte leben in n8n → Variables): TGV_SUPABASE_URL · TGV_SUPABASE_SERVICE_ROLE_KEY · TGV_SUPABASE_MGMT_PAT · TGV_N8N_API_KEY · TGV_ANTHROPIC_API_KEY · TGV_OPENAI_KEY · TGV_RESEND_API_KEY · TGV_RESEND_WEBHOOK_SECRET · TGV_DOCLING_URL.
Die Bauart (Rang 1, #1934) — nichts neu erfunden: ein Baustein 08_OPS/betrieb/zeitgeber.py (Kalender in UTC, zeichengleich zu betriebs-crons.yml · Takt · Meldeweg) und ein Thread in app.py nach dem Muster _mail_intake_loop: erst warten, dann arbeiten; ein Fehler wird Zustand, nie der Tod des Takts; Zustand im /api/skills/health unter zeitgeber. ⭐ EIN Weg, zwei Rufer: jedes Cron-Skript trägt zeitgeber_lauf(apply, herkunft) — dieselbe Funktion ruft main() auf dem GitHub-Runner und der Scheduler auf Hetzner. Sonst wäre die Zeilengleichheit, die vor dem Umschalten gemessen wird, durch Bauart verschieden. Read-Back und Safety-Net laufen im Dienst in-Prozess (kein HTTP-Selbstaufruf über nginx).
| Zeitgeber (Stempel) | n8n-Original | GitHub-Zwilling | Kalender UTC | Hetzner-Schalter system_config.* | Stand |
|---|---|---|---|---|---|
| cron-cost-collector | Cost Collector DV1qyZgjSk7cTbim | ③ | 0 3 * * * | zeitgeber_hetzner_cost_collector | gebaut; Quelle n8n enthält sich benannt, wenn N8N_API_KEY nicht in der Dienst-.env steht |
| cron-periodic-engine | Periodic Engine tnuT3QN4AwnbYdSz (ruft Cost, Safety-Net, Read-Back) | ④ | 30 3 */2 * * | zeitgeber_hetzner_periodic_engine | gebaut; Read-Back MkcSBtQersDjbtZq läuft mit (in-Prozess) |
| cron-contract-stub-safety-net | Safety Net eATEzPsbFjHUWcpD | ⑤ | 30 1 * * * | zeitgeber_hetzner_safety_net | gebaut; Rahmen lag schon im Dienst (/api/skills/contract-stub-safety-net), es fehlte nur der Takt |
| cron-vat-determine → cron-vat-poller | VAT Quality-Gate Poller 5MdBamTHJZE8yqBV | ②a → ②b | 5 */6 * * * | zeitgeber_hetzner_vat_poller (beide Hälften) | gebaut; Reihenfolge erst bestimmen, dann prüfen (FES-AP4) |
| cron-mail-failure-notify | Mail Failure-Notify G9a0SNY1bMhPfIUS (tot seit 05.09.) | ⑧ | 5 */6 * * * | zeitgeber_hetzner_mail_failure_notify | Rang 2 (#1935) zuerst repariert: from/to in der Nutzlast, Transport = Buchführung, Stempel gegen Ausnahme, mailfail/ausgeliefert/vatdet im Rot-Block |
| cron-health-check | LM#07 Health haD67MbvJcXUxGDI | ① | 5 */6 * * * | — | ⛔ bleibt auf GitHub: „der Zeuge muss ein anderer sein als der Beobachtete" |
Die Messung vor dem Umschalten (08_OPS/mess_zeitgeber_zwillinge.py --tage 7): jeder Lauf schreibt seit 14.09. eine Zeile agent_events.event_type = zeitgeber.lauf mit tool_output.herkunft (github-actions · hetzner-scheduler · cli) — ein Feld, kein Textmuster (#1658, Messfehler ①). Das Werkzeug paart je Zeitgeber und Slot beide Seiten und zählt GLEICH · UNGLEICH · nur GitHub · nur Hetzner. Beim Cost Collector zählt nur die Art (Metrik-Zahl, Fehler, Enthaltung), weil die Werte zeitabhängig sind. Die Gleichheit der Tabellen selbst hat #1658 gemessen; beide Zahlen zusammen sind die Vorlage. Reihenfolge beim Umschalten (#1908 §3): Periodic Engine zuerst — sie ruft die drei anderen um 01:30, umgekehrt fielen sie in 404.
Mail: EINE Quelle source='email' (imap/gmail = Legacy) · Zustell-Kanon: SPF/DKIM/DMARC zusammen + Multipart + BCC + Rechnungsnummernkreis · Beleg-Eingang factures@convis.fr; Reply tgv@; Versand c3@/c4@/kivisai@ via Resend BETRIEBSHANDBUCH:78-81 · DNS-Prüfbefehle je Domain: reference_ionos_domain_inventory:70-75.
Drive-Ordner-IDs: 00_TGV-CORE 1wBII7… · 01_TGV-PROJECTS 1ZJ_kv… · 02_ARCHIV 1rtOVJ…; Service-Account ist fileOrganizer (bewusst KEIN Manager). Domains: Tabelle kb_domains (Mig 185) + IONOS-Register.
MCP „tgv": .mcp.json → C:\Python313 + TGV -App files/tgv_mcp_server.py; env braucht APPDATA+USERPROFILE (pywin32). Memory-Pfad: C:\Users\jcbin\.claude\projects\C--Dev-TGV-repo\memory — projekt-, nicht worktree-gebunden. Auto-Mode-Klassifikator: blockt Subagenten bei Live-PII/Service-Role (nur Hauptsession mit JC-Consent, read-only) + Massen-n8n ohne allow-Regel.
| Komponente | Hängt an | Prüfen mit | Status |
|---|---|---|---|
| Video-Player | Ebene 1: nativer <video>+Range (MP4/H.264); Ebene 2 (ffmpeg) NICHT gebaut | — | Konzept |
| i18n | deploy.yml-Matrix listet 6 Locales (de/fr/en/it/pt/ru) — es.json fehlt = Diskrepanz zur 7-Sprachen-Ansage (deckt sich mit dem PLAT#02-Wrapper-Fund) · Übersetzungs-Skill geplant, nicht live | 200 + JSON je /i18n/<loc>.json | 🔨 |
| TTS/STT | OpenAI tts + Whisper via Skills (kein Web-Speech — iOS) | Skill-Call | gebaut, Bau-Gate |
| PDF-Vorschau | Drive-iframe-Mechanik — in der OPS-Doku NICHT dokumentiert (lebt nur im Shell-Code design02 openDocPanel) | Staging, Quelle-Tab | Doku-Lücke |
| OCR | Docling (de+fr; schwach bei Handschrift/FR) + Claude-Vision-Pfad; media_type=scan → Vision | /docling/health | live |
tgvdeploy anlegen — Shell MUSS /bin/sh sein./opt/tgv-deploy/ (root-owned).chown tgvdeploy.authorized_keys mit forced-command, Rechte 600./etc/tgv-skills · systemd-Unit · ZWEITER Wrapper+Key · gescoptes sudoers.tgv_migrations als Migration 1 · Mgmt-API-Token · Auth-Redirect-Allowlist.08_OPS/credentials/.env.JC, 30.08.2026: „Es gab auch Bedarf an der ‚Backend-Verwaltung' mit User, System-Prompt, Agenten-Script, Rechten usw. … wie wir an alles, was wir schon haben, rankommen in der Administration. Wir erfinden nicht, wir verdrahten."
§1–§10 beantworten „was läuft wo". Dieser Paragraph beantwortet die Frage eine Ebene höher: wo liegt die Wahrheit, wer darf sie sehen oder ändern, und über welche Fläche komme ich ran. Gemessen am 30.08.2026 (ADMIN#03, Register #962).
| Gegenstand | Wo die Wahrheit liegt (Fundstelle) | Wer darf | Fläche heute | Was zum Rankommen fehlt |
|---|---|---|---|---|
| System-Prompts | prompt_config — versioniert, Single-Active-Trigger 05_SCHEMA/151_lm13_prompt_config.sql · Loader 08_OPS/credentials/_prompts.py | RLS: mandant_admin des Mandanten, Betreiber | ✅ LIVE — secPrompts core_shell.html:9650 über prompt-katalog app.py:5878, Detail :9613. Kam mit PR #2170 (27.08.) | Nur der Schreibweg: gated POST /api/skill/prompt-config mit Dry-Run-Pflicht (prompt-preview app.py:2798 existiert bereits) + _audit_event. ⛔ hartes JC-Gate |
| Skills / Fähigkeiten | v_skill_definitions_public 05_SCHEMA/063 (gehärtet 081/160) · SSOT 02_SKILLS/_registry.yaml (985 Z., generiert 6 Listen) | View ist _public → per User-JWT lesbar, RLS-ehrlich (kein service_role) | ❌ keine | Ein Read-Endpunkt auf die View per User-JWT. Fläche danach trivial: fxRender("data-table") wie secPrompts |
| Agenten | ⛔ existiert als Tabelle nicht. Geprüfte Regale: 05_SCHEMA/*.sql (agents/agent_definitions/agent_registry) und alle *.py — kein Treffer. Verhalten lebt in Prompts + Code, Spuren in agent_events | — | ❌ keine | Das echte DB-Register — JC-Entscheid vom 13.07.2026, bis heute unumgesetzt. Einziger echter Neubau dieser Landkarte, deshalb zuletzt |
| Rechte / User | user_mandant_roles (Rechte-SSOT) · entities · Audit in agent_events | Scope aus der Mitgliedschaft des Aufrufers, Default-Deny (serverseitig) | ⚠️ Hülle — ADMIN_IA core_shell.html:3141 führt admin.members; jeder Unterpunkt rendert nur card.native_note („native Sektion folgt", :16797) | ⭐ Nichts im Backend. Elf fertige Endpunkte: invite-user:5326 · change-role:5397 · remove-user:5427 · list-members:5453 · assign-mandant-role:5544 · revoke-mandant-role:5589 · create-mandant:5632 · account-status:5662 · list-mandanten:5701 · mandant-detail:5737 · audit-log:5774. Die Shell ruft davon nur account-status |
| Audit / Verlauf | agent_events · Endpunkt audit-log app.py:5774, scopet serverseitig nach user_mandant_roles | Verwalter des Mandanten | ⚠️ verdrahtet, aber am falschen Ort — nur im Dokument-Weg gerufen core_shell.html:16460, nicht als Verwaltungs-Screen | Fast geschenkt: dieselbe Quelle als /verwaltung/audit rendern |
| Regeln & Tabellen | 08_OPS/entity_regeln.json (gelernte Zuordnung) · 08_OPS/ablage_kanon.yaml · ⛔ ZWEI SCHICHTEN, nicht eine Tabelle (Korrektur 31.08., #992): PLAN = policies.taxonomy in prompt_config → orch_apply schreibt documents.skills_needed — das ist der Agenten-Weg, und es gibt ihn · NETZ = LEGACY_MAP dispatch.py, greift nur, wenn kein Plan vorliegt · entities (bucket, intake_mail) | Repo-Dateien: wer schreiben darf. prompt_config/system: is_system_admin. entities: RLS nach Mandant | ✅ TEILWEISE — /verwaltung/regeln (#964) zeigt Taxonomie + Entitäten und benennt die drei unerreichbaren Quellen mitsamt Grund | Die zwei Repo-Dateien sind vom Browser nicht erreichbar — deploy.yml liefert 08_OPS/* nicht aus (gemessen 30.08.) ⇒ Deploy-Job + nginx-Location oder Lese-Endpunkt. LEGACY_MAP steht im Programmtext, hat keinen Endpunkt — sie zu lösen ist eine Routing-Änderung (JC/Tower), kein Lese-Screen. ⛔ Die Zwei-Tabellen-Wunde wird gezeigt, nicht gemessen: 08_OPS/check_routing_zwei_tabellen.py |
| Modelle je Skill | bereits kartiert in Register #958 (Reise-Karte Fassung 3): Modellname + Pflegeort je Etappe; Wahl teils aus prompt_config (ohne Auslieferung umstellbar), teils fest im Programmtext (pdf-invoice-extract/skill.py:116, vision-extract/skill.py:78) | Betreiber | ❌ keine | Anzeige „welches Modell an welcher Etappe", Quelle = #958. Umschaltbarkeit nur dort, wo sie schon in prompt_config liegt |
Dreimal dieselbe Lage — und keine davon heißt „uns fehlt eine Fähigkeit":
| # | Schritt | Aufwand | Warum hier |
|---|---|---|---|
| 1 | Mitglieder & Rechte /verwaltung/members — read-only aus list-members + list-mandanten + mandant-detail | klein (nur Fläche) | Höchster Nutzen, null Backend-Arbeit. Beendet die elffache Nicht-Nutzung; JC sieht endlich, wer was darf |
| 2 | Audit /verwaltung/audit — audit-log als eigener Screen | sehr klein | Endpunkt läuft bereits, wird nur am falschen Ort gerufen |
| 3 | Fähigkeiten /verwaltung/skills — Read-Endpunkt auf v_skill_definitions_public + Tabelle | mittel | Erste echte Backend-Ergänzung, aber klein und RLS-ehrlich |
| 4 | Prompts schreiben — gated prompt-config + Dry-Run + Audit | mittel | Fläche steht read-only bereits. ⛔ hartes JC-Gate vor jedem Live-Write |
| 5 | Regeln & Tabellen — Anzeige; vorher LEGACY_MAP aus dem Programmtext lösen | mittel | Der Prüfer existiert; die Zwei-Tabellen-Wunde zuerst |
| 6 | Agenten-Register — echte DB-Tabelle | groß | Einziger echter Neubau, deshalb zuletzt |
LEGACY_MAP als die Stream-Zuordnung, die „im Programmtext steht und ohne Auslieferung nicht änderbar“ ist. Jeder Halbsatz stimmt — das Ganze führt in die Irre. Gemessen: plan_skills nimmt documents.skills_needed (vom Orchestrator aus der Taxonomie) und fällt nur dann auf LEGACY_MAP zurück, wenn dort nichts steht. ⭐ JCs Frage „sollte das nicht ein Agent übernehmen?“ war also mit Ja zu beantworten — der Agenten-Weg existiert, mein Text hat ihn nur unsichtbar gemacht. ⛔ Beschreiben ist nicht lösen — aber falsch beschreiben ist schlimmer als schweigen: es erzeugt Arbeit an einer Frage, die keine war.entities.ablage_struktur stand hier als Gegenwart — die Spalte existiert nicht. Die Live-Tabelle führt id, mandant_id, slug, name, kind, bucket, intake_mail, created_at, test_run, ist_geteilt; die Migration liegt als 05_SCHEMA/PROPOSED_entities_ablage_struktur.sql — PROPOSED, nicht angewandt. prompt_config.worker_set las sich wie eine Spalte; es ist keine. Der Wert liegt unter policies.taxonomy.doc_types[…].worker_set bzw. structural_routes in den Zeilen domain_taxonomy_*.08_OPS/db_query.py gegen information_schema/pg_policies), nicht wieder nur gelesen. Eine Landkarte, die ihre eigenen Felder nicht misst, ist eine Behauptungssammlung.secSkillsAdmin-Bau lief bei 2141 Ref-Tips in den Timeout. Geprüft und nicht gefunden in: origin/master · Branch claude/strange-hypatia-ed9846 · lokale Branch-Liste · git ls-remote origin · gh pr list --state all (Suchen „skills admin", „prompt admin verwaltung"). Nicht geprüft: die vollständige Ref-Menge. Es ist also „dort nicht gefunden", nicht „existiert nicht".