Skip to content

spec(#124): printers.yaml weg, Drucker in DB + /admin/printers Admin-UI [DRAFT]#125

Draft
strausmann wants to merge 8 commits into
mainfrom
spec/printers-yaml-to-db
Draft

spec(#124): printers.yaml weg, Drucker in DB + /admin/printers Admin-UI [DRAFT]#125
strausmann wants to merge 8 commits into
mainfrom
spec/printers-yaml-to-db

Conversation

@strausmann

Copy link
Copy Markdown
Owner

Draft-Spec für Issue #124

Dieser PR enthält nur den Spec-Entwurf für die Migration von printers.yaml in die DB plus eine neue Admin-UI /admin/printers/. Keine Code-Änderungen — Review-Grundlage für die 4 Fachteams.

Issue: #124
Brainstorming: Session vom 2026-06-14 mit @strausmann

Kern-Entscheidungen (User-bestätigt)

  1. KEIN env bootstrap. HUB_PRINTERS_JSON wurde explizit verworfen — Operator legt Drucker nur über die Admin-UI an.
  2. ID-Pattern erweitert: derive_printer_id(model, host, port, created_at). Bestandsdrucker behalten ihre alten UUIDs (created_at war damals nicht im Salt), neue Drucker bekommen kollisionssicheren UUID auch bei IP/Port-Wiederverwendung.
  3. UI-Edit nur für variable Felder: name, connection.{host,port,snmp}, queue.timeout_s, cut_defaults.half_cut, enabled. slug/model/backend/id bleiben immutable nach Create.
  4. Plugin-Architektur unangetastet: Druckermodelle bleiben Compile-Time-Plugins (ptouch, brother_ql). Admin-UI Model-Dropdown wird aus Plugin-Registry gefüllt.
  5. Audit-Tabelle printers_audit analog Hangar layouts_audit.
  6. Hangar bleibt unangetastet — konsumiert weiter GET /api/printers.

Was sich konkret ändert

Komponente Aktion
app/services/printer_config_loader.py gelöscht
app/db/lifespan.py::upsert_runtime_printers() gelöscht
app/schemas/printer_config.py gelöscht
/etc/printer-hub/printers.yaml Volume-Mount gelöscht
printers.yaml aus /docker/stacks/hangar-print-hub/config/ gelöscht
app/services/printer_admin_service.py neu
app/api/routes/admin_printers.py neu (JSON-API)
app/web/routes/admin_printers.py neu (HTML-UI)
app/templates/admin_printers/ neu (Jinja2)
Alembic-Migration printers_audit neu

Review-Fokus für Fachteams

  • ops: Compose-Migration sauber? Stack-Restart-Pfad ok? Backup/Rollback für Bestandsdrucker?
  • network: Pangolin SSO + Header-Auth Bypass für API-Tools (/api/v1/admin/printers) wie in pangolin-resource-standard.md?
  • storage: DB-Migration kollidiert nicht mit bestehender Hub-DB-Backup-Strategie? Audit-Tabelle Größe vertretbar?
  • ansible/code-quality: Service-Design clean? Test-Strategie ausreichend? Plugin-Registry-Ansatz tragfähig?

Closes-Spec-of #124

Draft-Spec fuer Hub #124 nach Brainstorming-Session mit User
am 2026-06-14. Erfasst die finalen User-Entscheidungen:

- KEIN env bootstrap, nur Admin-UI (User hat HUB_PRINTERS_JSON
  explizit verworfen).
- ID-Pattern: derive_printer_id erweitert um created_at fuer
  Kollisionsfreiheit bei IP-Wechsel/Re-Provisionierung.
- Bestandsdrucker behalten ihre alten UUIDs (created_at war damals
  nicht im Salt) — keine Daten-Migration noetig.
- UI-Edit nur fuer name/connection/enabled — slug/model/backend/id
  bleiben immutable nach Create.
- Plugin-Architektur unangetastet, nur Drucker-Instanzen wandern.
- printers_audit-Tabelle analog Hangar layouts_audit.
- Hangar konsumiert weiter GET /api/printers — kein Hangar-Change.

Closes-Spec-of #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a design specification for transitioning the printer management system from a file-based configuration to a database-driven model. By removing the dependency on 'printers.yaml' and implementing a dedicated Admin UI, the system gains more flexibility and improved management capabilities for printer instances. The changes focus on streamlining the operational workflow while maintaining compatibility with existing Hangar services.

Highlights

  • Migration to Database: The printer configuration is being migrated from a static 'printers.yaml' file to the database, which will become the sole source of truth.
  • New Admin UI: A new administrative interface at '/admin/printers/' is being introduced to allow operators to create, edit, and delete printer configurations directly.
  • Audit Logging: A new 'printers_audit' table is being added to track all changes made to printer configurations, ensuring accountability and traceability.
  • Removal of Legacy Code: The 'printer_config_loader.py' service and 'upsert_runtime_printers' logic are being removed as they are no longer needed with the database-driven approach.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a design specification for migrating printer configurations from printers.yaml to a database-backed system with a dedicated Admin UI. The review feedback highlights privacy violations in the document, specifically the use of real domains (print-hub.strausmann.cloud and hangar.strausmann.cloud) in the smoke-test section, which should be replaced with neutral placeholders like example.com to comply with the repository's privacy guidelines.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.


1. PR merge → CI green
2. Dockhand: `down_stack(hangar-print-hub)` → Volume-Mount `printers.yaml` entfernen via `update_stack_compose` → `start_stack`
3. Browser: `https://print-hub.strausmann.cloud/admin/printers` → Liste der 2 Bestandsdrucker

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Gemäß den Datenschutzrichtlinien des Repositories (Priorität 1) und den allgemeinen Regeln dürfen keine echten Hostnamen, Domains oder LAN-IPs in der Dokumentation oder im Code verwendet werden. Bitte ersetzen Sie print-hub.strausmann.cloud durch eine neutrale Dokumentations-Domain wie print-hub.example.com.

Vorschlag:
3. Browser: https://print-hub.example.com/admin/printers` → Liste der 2 Bestandsdrucker`

References
  1. Datenschutzverletzungen. Markieren Sie alle fest einprogrammierten LAN-IPs, echten Hostnamen, echten Domains, echten Token oder personenbezogenen Daten (PII). Das Netzwerk des Maintainers darf aus diesem Repository nicht ableitbar sein. (link)
  2. Use RFC 5737 documentation IPs and 'example.com' placeholders instead of real LAN IPs, hostnames, or domains in documentation and code to maintain privacy.

3. Browser: `https://print-hub.strausmann.cloud/admin/printers` → Liste der 2 Bestandsdrucker
4. Edit `brother-p750w` → ändere Hostname testweise auf `192.0.2.1` → Save → Reload → Wert übernommen
5. Edit zurück auf echten Hostnamen
6. Hangar `/admin/layouts/` → unverändert, `https://hangar.strausmann.cloud/` Print-Buttons funktionieren

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Gemäß den Datenschutzrichtlinien des Repositories (Priorität 1) und den allgemeinen Regeln dürfen keine echten Hostnamen, Domains oder LAN-IPs in der Dokumentation oder im Code verwendet werden. Bitte ersetzen Sie hangar.strausmann.cloud durch eine neutrale Dokumentations-Domain wie hangar.example.com.

Vorschlag:
6. Hangar /admin/layouts/→ unverändert,https://hangar.example.com/` Print-Buttons funktionieren`

References
  1. Datenschutzverletzungen. Markieren Sie alle fest einprogrammierten LAN-IPs, echten Hostnamen, echten Domains, echten Token oder personenbezogenen Daten (PII). Das Netzwerk des Maintainers darf aus diesem Repository nicht ableitbar sein. (link)
  2. Use RFC 5737 documentation IPs and 'example.com' placeholders instead of real LAN IPs, hostnames, or domains in documentation and code to maintain privacy.

@strausmann

Copy link
Copy Markdown
Owner Author

Spec-Review: network-Team

Status: NEEDS_FIXES

Findings

CRITICAL

C1 — Pangolin Remote-User Header: exakter Name fehlt im Spec

Spec sagt updated_by wird aus "Pangolin Remote-User Header" befüllt. Pangolin setzt Header-Namen je nach Konfiguration unterschiedlich. Im HomeLab ist der korrekte Header-Name zu verifizieren — wenn er falsch ist, bleibt updated_by dauerhaft leer und der Audit-Trail ist wertlos.

Aktion: Pangolin-Dashboard → Org → Einstellungen → Header-Name notieren und im Spec exakt benennen. Wahrscheinlich X-Pangolin-User, aber muss gegen Live-System verifiziert werden (nie raten).

C2 — JSON-API hinter Pangolin SSO oder nicht?

Spec listet JSON-API (GET/POST/PUT/DELETE /api/v1/admin/printers) mit "API-Key (admin scope)"-Auth — aber sagt nicht ob diese Routes ebenfalls hinter der Pangolin-Resource (SSO) laufen oder direkt mit Bearer-Token erreichbar sein sollen.

Wenn die JSON-API hinter derselben Pangolin-Resource wie die HTML-UI läuft: Tooling und Ansible-Aufrufe kommen nicht durch ohne den Header-Auth-Bypass (claude-automation Basic-Auth). Der Spec-Abschnitt "Akzeptanzkriterien" erwähnt "Pangolin-Resource-Standard eingehalten (SSO + Header-Auth Bypass für API-Tools)" — aber die Implementierungs-Sektion beschreibt keinen Schritt dafür.

Explizit klären: Laufen HTML-UI und JSON-API auf derselben Pangolin-Resource? Wenn ja: Header-Auth-Bypass ist Pflicht und braucht einen eigenen Schritt im Plan (Blueprint-Label + Vaultwarden-Item).

HIGH

H1 — CSRF-Schutz für HTML-Form-POSTs fehlt im Spec

Spec definiert POST /admin/printers, POST /admin/printers/{slug} und POST /admin/printers/{slug}/delete als Browser-Forms ohne CSRF-Token. Pangolin-SSO authentifiziert die Session, schützt aber nicht vor Cross-Site-Request-Forgery: Ein Operator der gleichzeitig eine fremde Seite öffnet, kann durch präparierten Link unbemerkt Drucker-Daten verändern oder löschen.

Aktion: CSRF-Token-Mechanismus (z.B. itsdangerous, FastAPI-Security-Middleware oder ein Session-Token in Hidden-Field) muss in der Spec als Pflicht-Abschnitt ergänzt werden — insbesondere für Delete-Confirm-Form.

H2 — Pangolin-Resource-Standard: Bestandsresource braucht Header-Auth-Bypass-Update

Die Resource print-hub.strausmann.cloud existiert bereits. Laut pangolin-resource-standard.md müssen ALLE Resources auth.basic-auth.user=claude-automation + Passwort gesetzt haben. Falls die bestehende Resource das noch nicht hat, ist ein Blueprint-Label-Update nötig — nicht eine neue Resource anlegen.

Aktion: Prüfen ob die Bestandsresource bereits auth.basic-auth hat (via mcp__pangolin-api__). Falls nicht: Schritt im Plan ergänzen. Wichtig: Änderung NUR in Blueprint-Labels (Compose), nie direkt per API — Newt würde API-Änderungen beim nächsten Sync überschreiben.

MEDIUM

M1 — LAN-Routing Hub → Drucker: Spec macht keine Aussage

Drucker liegen im LAN (172.16.x.x). Der Hub-Container läuft im traefik-public-Netz auf hhdocker02/03. Spec geht davon aus dass connection.host (eine LAN-IP) vom Hub-Container direkt erreichbar ist — ohne zu prüfen ob das VLAN-Routing das erlaubt.

Aktion: Bestätigen dass hhdocker02/03 → Drucker-VLAN geroutet ist (UniFi-Firewall-Regel). Falls nicht, braucht der Hub ein zweites Netzwerk oder eine Host-Route. Kein neues VLAN nötig, aber die Annahme muss explizit im Spec stehen.

M2 — Hangar → Hub-Kommunikation: Domain oder intern?

Spec sagt Hangar PrinterSync ruft GET /api/printers alle 5min auf — aber nicht über welche URL. Wenn Hangar den Aufruf über https://print-hub.strausmann.cloud macht (Pangolin-Route), braucht auch Hangar den Header-Auth-Bypass. Wenn intern (Container-Netzwerk oder Tailscale-direkt), ist das nicht nötig.

Aktion: URL für Hangar→Hub-Aufruf im Spec explizit benennen.

LOW

L1 — Healthcheck-Path für bestehende Pangolin-Resource prüfen

Spec entfernt den Volume-Mount printers.yaml und den PRINTER_CONFIG_PATH-Pfad aus dem Compose. Falls der Pangolin-Healthcheck auf einen Endpoint zeigt der printers.yaml indirekt nutzt (z.B. /api/printers auf frischer DB), könnte HC nach Deploy kurz unhealthy sein. Kein Blocker, aber im Smoke-Test explizit prüfen.

@strausmann

Copy link
Copy Markdown
Owner Author

Spec-Review: ops-Team

Status: NEEDS_FIXES

Findings

HIGH (sollte fixed werden)

1. Env-Var-Entfernung: Merge-Pflicht nicht adressiert

Abschnitt "Migration für Bestand, Phase 2" sagt: «printers.yaml Volume-Mount entfernen» und «PRINTER_CONFIG_PATH Env-Variable entfernen». Die Spec beschreibt nur das Entfernen aus dem Compose-Block, nicht den notwendigen Stack-Env-Update-Pfad.

Problem: update_stack_env hat PUT-Replace-Semantik (nicht PATCH). Wer nur PRINTER_CONFIG_PATH löscht, muss trotzdem alle anderen Variablen mitschicken — sonst werden sie gelöscht (bekannter Vorfall 2026-06-01, hangar-print-hub, 7 Variablen verloren).

Korrekter Migrations-Pfad muss explizit in der Spec stehen:

1. get_stack_env → alle vorhandenen Variablen lesen
2. PRINTER_CONFIG_PATH aus der Liste entfernen
3. update_stack_env mit MERGED-Liste (ohne PRINTER_CONFIG_PATH)
4. update_stack_compose (Volume-Mount entfernen), restart=False
5. down_stack + start_stack (NICHT restart — wendet keine Compose-Änderungen an)

Hinweis: restart_stack reicht hier nicht. Die Compose ändert sich (Volume-Mount weg) — das erfordert ein recreate (down + start).

2. Pre-Deploy-DB-Snapshot fehlt

Abschnitt «Migration für Bestand» sagt «Snapshot DB-Inhalt sichern» ohne konkreten Befehl. Bei einem Fresh-Deploy auf leerer DB (z.B. nach versehentlichem DELETE FROM printers) gibt es keinen Recovery-Pfad zurück zu den YAML-konfigurierten Druckern — die Datei soll ja gelöscht werden.

Empfehlung: Konkrete Pre-Deploy-Anweisung ergänzen, z.B.:

docker exec hangar-print-hub-db-1 pg_dump -U hub hub -t printers > printers-backup.sql

Alternativ: Explizit festhalten, dass das PBS-Tagesbackup als Rollback ausreicht (wenn das stimmt).

MEDIUM (Discussion)

3. Watchtower-Timing während Migration

Der Smoke-Test-Pfad (Abschnitt «Smoke-Test Production», Schritt 2) lässt Watchtower aktiv. Watchtower auf hhdocker03 läuft mit Scope hangar-print-hub. Wenn Watchtower zwischen down_stack und start_stack ein Image-Update triggert, lädt er die neue Compose ohne Volume-Mount — das ist unkritisch wenn der Timing-Window klein ist, sollte aber explizit dokumentiert sein.

Optionen: Watchtower kurz pausieren während Migration, oder klarstellen warum das Fenster unkritisch ist.

4. Health-Endpoints nicht adressiert

Die Spec führt Admin-UI-Routen mit DB-Operationen ein, nennt aber keinen Health-Endpoint für Uptime-Kuma/Prometheus-Alerting bei 500-Fehlern oder DB-Verbindungsabbruch im laufenden Betrieb. Nicht blockierend für #124, aber als Follow-up-Issue empfohlen.

5. Audit-Tabelle: Retention-Strategie fehlt

Abschnitt «Audit-Tabelle printers_audit» hat created_at-Index, aber keine Aussage zu Cleanup/Archivierung. Bei seltenen Drucker-Änderungen kein akutes Problem — trotzdem sollte die Spec oder ein Follow-up-Issue klären ob Audit-Rows unbegrenzt wachsen.

LOW / Suggestions

6. Compose-Validation-Reihenfolge

Dockhand validiert beim update_stack_compose gegen die aktuelle Stack-Env. Wenn PRINTER_CONFIG_PATH noch als required-Variable im Compose-environment-Block steht, schlägt Validation fehl. Spec sollte festhalten: Compose-Block-Entfernung vor Env-Var-Entfernung.

Was gut ist

  • Abschnitt «Startup-Flow (neu)» ist klar: leere Tabelle = kein Fehler, kein Sync — richtiger Ansatz
  • UUID-Stabilität für Bestandsdrucker explizit zugesagt (Formel ohne created_at) — schützt Hangar-Referenzen
  • Risiko-Tabelle adressiert R1 (Hangar-Ref auf gelöschte Drucker) und R2 (API-Key-Auth) als offene Punkte — transparent
  • Test-Strategie (Unit + Integration + E2E + Smoke) ist vollständig, fresh_install_e2e ist genau der richtige Smoke-Path

@strausmann

Copy link
Copy Markdown
Owner Author

Spec-Review: storage-Team

Status: NEEDS_FIXES

Findings

CRITICAL

[C1] JSONB in SQLite existiert nicht

Sowohl printers (bestehend) als auch printers_audit (neu) verwenden JSONB als Spaltentyp. SQLite kennt kein JSONB — es wird stillschweigend als TEXT gespeichert, d.h. keine JSON-Validierung, kein Index auf JSON-Pfade. Das ist für dieses Setup (single-replica SQLite) funktional akzeptabel, aber die Spec sollte das explizit benennen: JSONBJSON TEXT (SQLite-kompatibel). Analog Hangar Phase 1k.1b wo beide Dialekte dokumentiert wurden. Falls Postgres jemals in Scope kommt, braucht es eine Dialekt-aware Migration.

Aktion: Spec-Kommentar in Schema-Abschnitt ergänzen: "SQLite speichert JSONB als TEXT, keine JSON-Constraints". Alembic-Migration entsprechend mit sa.Text() schreiben, nicht JSONB.

HIGH

[H1] Credentials in before_json / after_json — Datenleck-Risiko

Der Delete-Flow speichert before_json = row_json in printers_audit. Die connection-Spalte enthält snmp.community (typischerweise "public", aber konfigurierbar). Bei Backup-Diebstahl (PBS-Snapshot oder SQLite-Dump) sind diese Credentials im Klartext im Audit-Trail sichtbar — für alle jemals gelöschten Drucker dauerhaft.

Aktion: Spec muss entscheiden: (a) connection.snmp.community aus before_json/after_json redaktieren ("***"), oder (b) explizit dokumentieren dass SNMP-Community als unkritisch eingestuft wird (HomeLab-Kontext). Option (b) wäre für dieses Setup vertretbar, muss aber bewusste Entscheidung sein.

[H2] SELECT … FOR UPDATE existiert in SQLite nicht

Update- und Delete-Flow zeigen SELECT … WHERE slug=? FOR UPDATE. SQLite unterstützt FOR UPDATE nicht — der ORM wirft entweder einen Fehler oder ignoriert das Hint stillschweigend. SQLite-Locking läuft über BEGIN IMMEDIATE / BEGIN EXCLUSIVE auf Datenbankebene, nicht auf Zeilenebene.

Aktion: Spec muss beschreiben wie Concurrent-Write-Protection tatsächlich implementiert wird: BEGIN IMMEDIATE via SQLAlchemy with_for_update()-Fallback oder explizite Session-Isolation. Bei single-process FastAPI mit AsyncSession ist das Risiko gering, aber die Spec sollte nicht implizieren dass Row-Level-Locking vorhanden ist.

MEDIUM

[M1] Pre-Deploy-Snapshot nicht spezifiziert

Spec sagt "Snapshot DB-Inhalt sichern" (Phase 1) ohne konkreten Mechanismus. PBS-Snapshot der VM läuft regulär, aber der Zeitpunkt ist nicht garantiert frisch. Empfehlung: Expliziten sqlite3 /data/printer-hub.db ".backup /tmp/printer-hub-pre-deploy-20260614180912.db" Step in die Deployment-Smoke-Test-Checkliste aufnehmen (Schritt 2 vor down_stack).

[M2] Transaktions-Atomicity nicht explizit dokumentiert

Spec zeigt Create/Update/Delete-Flows mit implicit COMMIT am Ende, aber erwähnt nicht explizit dass INSERT INTO printers und INSERT INTO printers_audit in einer gemeinsamen SQLAlchemy-Transaktion liegen. Wenn der Audit-INSERT fehlschlägt, muss der Drucker-INSERT rolliert werden — und umgekehrt. Analog Hangar Phase 1k.1b sollte die Spec schreiben: "Beide INSERTs in einer BEGIN/COMMIT-Transaktion — Audit-Fehler rollt Drucker-Änderung zurück".

LOW

[L1] Indexe bei <1000 Rows redundant aber harmlos

Zwei Indexe auf printers_audit bei realistisch <50 KB Daten (10 Drucker × 20 Edits × 10 Jahre). Kein Performance-Problem, aber Spec könnte einen Satz ergänzen: "Indexe für zukünftige Skalierung, aktuell nicht erforderlich".


storage-team review — 2026-06-14

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.30%. Comparing base (2ff51d2) to head (8d37b3e).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #125   +/-   ##
=======================================
  Coverage   89.30%   89.30%           
=======================================
  Files          91       91           
  Lines        4263     4263           
  Branches      368      368           
=======================================
  Hits         3807     3807           
  Misses        358      358           
  Partials       98       98           
Components Coverage Δ
Printer Backends (transport) 86.87% <ø> (ø)
Printer Models (drivers) 88.20% <ø> (ø)
Services 91.20% <ø> (ø)
REST API 84.97% <ø> (ø)
Pydantic Schemas 100.00% <ø> (ø)
Integration Plugins 100.00% <ø> (ø)
Flag Coverage Δ
backend 89.30% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 2ff51d2...8d37b3e. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@strausmann

Copy link
Copy Markdown
Owner Author

Spec-Review: code-quality / Architektur

Status: NEEDS_FIXES


Findings

CRITICAL

C1 — derive_printer_id Signatur-Bruch ohne Backwards-Compat-Pfad

Die Spec erweitert die Signatur auf derive_printer_id(model, host, port, created_at) (Abschnitt "Komponenten → ID-Generierung"), aber der existierende Code printer_identity.py:21 hat def derive_printer_id(model: str, host: str, port: int) -> UUID. Es gibt 4 Aufruf-Stellen (lifespan.py:199, 3 Test-Files), die alle die 3-arg-Variante nutzen.

Die Spec behandelt das als "Bestandsdrucker behalten ihre alte UUID — Migration berechnet sie NICHT neu" — macht aber keine Aussage darüber, wann/wo die 4-arg-Variante statt der 3-arg-Variante aufgerufen wird. Wird upsert_runtime_printers() gleichzeitig entfernt (ja, laut Spec), dann ist der Aufruf in lifespan.py:199 weg. Bleiben die 3 Test-Files: die müssen ebenfalls angepasst oder entfernt werden.

Pflicht: Spec muss explizit benennen: "Die bestehende 3-arg-Funktion wird durch eine überlastete/neue 4-arg-Variante ersetzt; alle Test-Aufruf-Stellen in test_lifespan_seeds_and_upserts.py und test_lifespan_printer_upsert.py werden entfernt, da upsert_runtime_printers() selbst entfernt wird."

C2 — FK-Kaskade: DELETE blockiert oder zerstört Job-History

job.printer_id und print_batch.printer_id sind foreign_key="printers.id" ohne ON DELETE CASCADE oder SET NULL. Ein DELETE FROM printers WHERE id=? schlägt damit bei SQLite mit einem FK-Constraint-Fehler fehl (oder lässt verwaiste Rows bei deaktivierten FK-Checks).

Die Spec adressiert das im Delete-Flow nicht — sie schreibt nur "Operator-Verantwortung" für Hangar-Layouts (Abschnitt "Implikation für Hangar"). Auch printer_state und printer_status_cache haben FK auf printers.id.

Pflicht: Spec muss entscheiden: (a) Soft-Delete (enabled=false) statt hartem DELETE, oder (b) Service prüft vor DELETE ob jobs/print_batch/preset Rows existieren und wirft 409, oder (c) Alembic-Migration ergänzt ON DELETE SET NULL / ON DELETE CASCADE pro Tabelle. Ohne diese Entscheidung wird delete_printer() in Produktion inkonsistent fehlschlagen.


HIGH

H1 — Pydantic-Schemas undefiniert

Spec nennt PrinterCreatePayload und PrinterUpdatePayload im Service-Interface, definiert aber keine Felder. Aus dem Form-Table (Abschnitt "Web-Routes") lassen sich die Felder ableiten, aber die Spec fehlt für:

  • slug-Regex-Validator (^[a-z0-9-]+$) — muss im Schema als @field_validator stehen, nicht nur in der Form-Beschreibung
  • Port-Range-Validator (1–65535)
  • connection.snmp.community als required wenn discover=true (cross-field)
  • Welche Felder PrinterUpdatePayload enthält vs. welche bei Update ignoriert werden (immutable: slug, model, backend, id)

Pflicht: Spec ergänzen mit zumindest den Validator-Regeln und dem Update-Subset. Implementer soll keine Schema-Entscheidungen treffen müssen.

H2 — Immutable Fields: Service-seitige Durchsetzung fehlt

Abschnitt "Update-Flow" Step 5c: UPDATE printers SET name=?, connection=?, enabled=?, updated_at=now(). Das impliziert, dass slug/model/backend/id beim Update nicht gesetzt werden. Aber die Spec sagt nicht explizit, dass update_printer() diese Felder aus dem Patch-Payload aktiv herausfiltert (statt nur nicht setzt).

Bei der JSON-API (PUT /api/v1/admin/printers/{slug}) kann ein Aufrufer {"model": "QL-820NWB"} schicken — wenn der Service das stillschweigend ignoriert, ist das OK, muss aber so spezifiziert sein. Wenn der Service mit 422 antwortet, muss das im Error-Handling stehen.

Pflicht: Ein Satz in "Komponenten → PrinterAdminService": "Felder slug, model, backend, id im PrinterUpdatePayload werden vom Service ignoriert (kein 422); PrinterUpdatePayload enthält diese Felder nicht."


MEDIUM

M1 — Test-Coverage-Schwellen fehlen (test-coverage-pflicht.md)

Spec listet Test-Kategorien, aber keine Coverage-Schwellen. Laut test-coverage-pflicht.md brauchen Mutation-Funktionen (create/update/delete) mindestens 85% und müssen Happy + Error-Path + DB-Error abdecken.

Aktuell fehlt in der Test-Sektion: (a) DB-Error-Pfad (z.B. Session rollback bei IntegrityError), (b) Network/Timeout-Pfad für den Audit-Write (was wenn printers_audit INSERT fehlschlägt — rolled back der gesamte Printer-Write?). Die Spec muss diese Szenarien explizit benennen.

M2 — created_at Timezone-Spezifikation fehlt

Spec Abschnitt Create-Flow Step 5a: created_at = now() — unklar ob datetime.now() (naiv, lokale TZ) oder datetime.now(timezone.utc) (aware). Da created_at.isoformat() in den UUIDv5-Salt eingeht, ist das Format deterministisch relevant: 2026-06-14T10:00:00 vs. 2026-06-14T10:00:00+00:00 erzeugen verschiedene UUIDs für denselben Zeitpunkt.

Pflicht: Spec muss schreiben: created_at = datetime.now(timezone.utc) — explizit aware, UTC.

M3 — Plugin-Registry-Kopplung nicht diskutiert

Spec Abschnitt 4: "Quelle: ptouch.PRINTERS (ptouch-py) und brother_ql.MODELS" — das ist direkte Kopplung an die interne API dritter Pakete. Spec markiert R3 als "Verifikation im Plan-Schritt", trifft aber schon eine Implementierungsentscheidung ohne zu klären was passiert wenn ptouch.PRINTERS ein privates Attribut ist oder sich umbenennt.

Alternative (robuster): Jedes Backend-Plugin exportiert selbst eine SUPPORTED_MODELS: list[str]-Konstante. Spec sollte die Kopplung explizit als "akzeptiertes Risiko für #124, Plugin-abstraktion out-of-scope" markieren — aktuell steht das nicht da.


LOW

L1 — Audit-Tabelle: kein FK auf printers.id

printers_audit.printer_id ist UUID NOT NULL aber kein FOREIGN KEY. Damit können Audit-Rows für gelöschte Drucker bestehen bleiben — das ist für ein Audit-Trail das gewünschte Verhalten. Spec sollte das explizit begründen: "kein FK absichtlich, damit Audit-History nach Drucker-Löschung erhalten bleibt."

L2 — i18n-Politik

Spec Abschnitt "Out of Scope": "Mehrsprachigkeit der Admin-UI (deutsch only)" — gut. Spec sollte ergänzen ob Error-Messages aus Pydantic (422 responses) ebenfalls auf Deutsch übersetzt werden oder englisch bleiben. Aktuell unklar für Implementer.


Was gut ist

  • Migrations-Ansatz ist sauber: Kein Daten-Migration-Risiko, weil DB bereits gefüllt ist. Phase 1/2/3 klar getrennt.
  • Startup-Flow-Vereinfachung: lifespan.py verliert upsert_runtime_printers() ohne Ersatz — das ist genau richtig. Der Kommentar-Docblock in lifespan.py (Call-Order 1–7) muss im gleichen PR aktualisiert werden.
  • Audit-Pattern analog Hangar: Bewusste Entscheidung als "akzeptabel für separate Repos ohne Shared Library" explizit zu markieren (R5-ähnlich) wäre noch wünschenswert, aber das Pattern selbst ist konsistent.
  • JSON-API parallel zu HTML-UI: Klug — Ansible-Automation kann sofort Drucker anlegen ohne Browser.
  • Deutsche Umlaute: Spec verwendet ä/ö/ü/ß durchgängig korrekt.

…6 HIGH + 6 MED + LOW)

Folgendes wurde im Spec aus den Reviews der 4 Fachteams eingearbeitet:

CRITICAL
- C1 Pangolin Remote-User Header: Sektion "Authentifizierung" mit
  exakten Header-Namen (Remote-User, X-Pangolin-Token, Legacy
  X-Pangolin-User) aus app/auth/dependencies.py.
- C2 JSON-API Auth-Pfad: hinter selber Pangolin-Resource wie
  HTML-UI (User-Entscheidung). Header-Auth-Bypass via Compose-Label
  fuer claude-automation.
- C3 SQLite-only, sa.JSON() statt JSONB: Hub-DB ist
  sqlite+aiosqlite:////data/printer-hub.db. Dialect-Matrix entfaellt.
- C4 derive_printer_id 4-arg: kein Backwards-Compat (lifespan-Aufrufer
  wird komplett entfernt, 3 Test-Files migriert/geloescht).
- C5 DELETE-Strategie: Soft-Delete via enabled=false (User-Entscheidung).
  FK-Constraints zu jobs/print_batches/presets/printer_state bleiben
  intakt. UI-Aktionen sind disable/enable statt delete.

HIGH
- H1 env-merge-Pflicht: PRINTER_CONFIG_PATH entfernen via
  get_stack_env + filter + update_stack_env (PUT-Replace-Semantik).
- H2 down_stack+start_stack statt restart_stack (Volume-Mount-Change).
- H3 CSRF-Schutz: Starlette-CSRF Middleware fuer alle HTML-Form-POSTs,
  SameSite=Strict Cookie. JSON-API CSRF-frei mit Basic-Auth/API-Key.
- H4 SNMP-Community im Audit redacted via redact_secrets-Helper.
- H5 SELECT FOR UPDATE → BEGIN IMMEDIATE (SQLite-konform).
- H6 PrinterCreatePayload/UpdatePayload mit allen Feldern + Validatoren
  (slug-regex, port-range, snmp-cross-field-validation, deutsche
  Error-Messages).

MEDIUM
- M1 sqlite3 .backup vor Deploy (statt pg_dump).
- M2 Immutable-Fields silent ignore (analog Hangar).
- M3 Coverage-Schwellen pro Modul (85% Mutation, 75% Helper).
- M4 created_at_utc Pflicht timezone-aware (naive → ValueError).
- M5 Plugin-Registry-Kopplung als akzeptiertes Risiko markiert.
- M6 async with session.begin() fuer atomare Transaktionen.

LOW
- Watchtower-Pause via set_container_auto_update("never") vor Migration.
- Healthcheck-Verifikation post-Deploy.
- Audit-Retention dokumentiert (keine).
- LAN-Routing als Annahme dokumentiert.
- Hangar→Hub URL: http://print-hub:8000 intern via Container-Netz.
- FK auf printers_audit.printer_id bewusst weggelassen (Soft-Delete
  behaelt Parent-Row sowieso).
- i18n: deutsch only.

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Round-2 Spec-Review: ops-Team

Status: APPROVE (mit einem LOW-Hinweis)


Round-1 Findings — Verifikation

  • H1 env-merge-Pflicht: adressiert. Das Python-Snippet zeigt korrekt get_stack_env → filter(key != PRINTER_CONFIG_PATH) → update_stack_env. Eine explizite Vor-/Nach-Verifikation (Key-Anzahl loggen) fehlt im Snippet, aber die Rule dockhand-stack-env-merge.md schreibt das vor — der implementierende Agent muss sie gelesen haben. Für die Spec reicht das Merge-Pattern aus.
  • H2 down/start: adressiert. Reihenfolge down_stack → update_stack_compose → start_stack ist korrekt. Rollback-Pfad (Punkt 4: down_stack + start_stack) ebenfalls vorhanden.
  • M1 sqlite3 .backup: adressiert. Befehl korrekt: docker exec ... sqlite3 /data/printer-hub.db '.backup /data/printer-hub.db.bak-pre-124'. WAL-Modus: SQLite .backup-Kommando ist WAL-safe (es benutzt die Backup-API intern, nicht file copy). Pfad /data/printer-hub.db.bak-pre-124 liegt im gleichen Volume — ist OK, PBS sichert das Verzeichnis ohnehin, und der explizite docker cp danach zieht eine Kopie auf den Host.

Neue Round-2 Findings

LOW — down_stack-Fehlerbehandlung nicht spezifiziert

Spec sagt down_stack → update_stack_compose → start_stack, aber: wenn down_stack fehlschlägt (Container hängt, Timeout), ist der Stack halb-tot. Der Rollback-Pfad im Spec setzt voraus dass der Stack schon unten ist. Empfehlung für den implementierenden Agent: vor down_stack prüfen ob alle Container healthy/running sind, und bei Fehler manuell docker kill via SSH als Fallback dokumentieren — nicht im Spec selbst ändern nötig, reicht als Deploy-Hinweis.

LOW — CSRF und /healthz-Konflikt unwahrscheinlich aber prüfenswert

Starlette-CSRF-Middleware prüft nur Requests mit Content-Type application/x-www-form-urlencoded oder multipart/form-data. Der Healthcheck GET /healthz ist ein GET — kein CSRF-Token nötig, kein Konflikt. Trotzdem: beim ersten Deploy curl -fsS https://print-hub.strausmann.cloud/healthz unmittelbar nach start_stack ausführen bevor Smoke-Tests. Der Spec listet das als Schritt 3 der Produktion-Smoke-Tests — passt.

ACCEPTED — Soft-Delete ohne Cleanup-Job (R6)

Audit-Tabelle wächst nie über ~30 KB bei realistischem Betrieb. Kein Cleanup-Job nötig. Akzeptiert.


Was gut ist

  • SNMP-Redaction via redact_secrets() ist explizit und erweiterbar — gut für künftige Secret-Felder.
  • Watchtower-Pause korrekt via set_container_auto_update(policy='never') vor Migration, 'any' nach Verifikation — der Zeitraum (~5 Min für Migration) ist kurz genug.
  • Alembic-Migration läuft im Startup-Flow vor allem anderen — Container wird erst HEALTHY wenn Migration durch ist. Healthcheck-Timeout ist Implementierungsdetail, aber der Ansatz ist korrekt.
  • Hangar→Hub intern via Container-Netz klar dokumentiert — kein Pangolin-Pfad, kein Header-Auth nötig für Hangar-Sync.

ops-Team: APPROVE

@strausmann

Copy link
Copy Markdown
Owner Author

Round-2 Spec-Review: network-Team

Status: NEEDS_FIXES (1 HIGH, 1 MED, 2 LOW — aber alle klar lösbar, kein Architektur-Problem)


Round-1 Findings — Verifikation

Finding Status Notiz
C1 Pangolin Header-Name adressiert Remote-User + X-Pangolin-Token korrekt aus app/auth/dependencies.py. sso_trust_token-Check gegen Vault-Item-Name ist Laufzeit-Config, nicht Spec-Pflicht. OK.
C2 JSON-API Auth-Pfad adressiert Drei-Pfad-Entscheidung dokumentiert. Header-Auth via Blueprint Labels (nicht API). Feedback pangolin_labels_source_of_truth korrekt angewendet.
H3 CSRF-Schutz adressiert Starlette CSRF Middleware mit SameSite=Strict. Differenzierung SSO-Session (CSRF geprüft) vs. Basic-Auth/API-Key (CSRF-frei, weil kein Browser-Origin) — korrekt.
H4 Bestandsresource Header-Auth teilweise Spec nennt R2 als Risiko + Phase-0-Live-Check via mcp__pangolin-api__resource_by_resourceId. Konkrete Response-Felder fehlen (siehe H4-Nachschärfung unten).

Neue Round-2 Findings

[HIGH] N1: Healthcheck-Labels fehlen im Spec-Blueprint-Beispiel

Spec zeigt Blueprint-Labels in Sektion "Authentifizierung" ohne den Healthcheck-Block. pangolin-resource-standard.md macht healthcheck.{enabled,hostname,path,port,interval} seit Newt v1.18.4 verbindlich. Das Standardbeispiel label-printer-hub auf hhdocker02 hat diese Labels. Ohne sie zeigt Pangolin den Service als "unhealthy" — was HC-basierte Alerts und Pangolin-interne Entscheidungen betrifft.

Fix: Blueprint-Beispiel in Sektion "Authentifizierung" um den Healthcheck-Block erweitern (Container-Name print-hub, Port 8000, Pfad /healthz).

[MED] N2: Hangar→Hub Container-DNS-Name nicht verifiziert

Spec sagt http://print-hub:8000/api/printers. Der Dockhand-Stack heißt hangar-print-hub, der Service im Compose print-hub — Docker-Compose setzt den Container-DNS-Namen aus Compose-Service-Name, nicht Stack-Name. Das wäre korrekt wenn der Service in compose.yaml tatsächlich print-hub: heißt. Vor Implementation: docker inspect hangar-print-hub-print-hub-1 --format '{{.NetworkSettings.Networks}}' oder docker exec hangar-print-hub-hangar-1 nslookup print-hub verifizieren. Spec sollte diesen Verifikationsschritt in Phase-3-Smoke aufnehmen.

[LOW] N3: claude-automation Username — Standardisierung nicht referenziert

Spec definiert claude-automation als User, referenziert aber nicht explizit den HomeLab-Standard (label-printer-hub auf hhdocker02 als kanonisches Beispiel, Vault-Item-Namenskonvention Pangolin Header Auth - Print Hub). Kein Blocker — aber ein expliziter Verweis auf pangolin-resource-standard.md im Spec würde dem Implementierer ersparen, die Konvention neu erfinden zu müssen.

[LOW] N4: Bekannter Pangolin-Bug #3099 nicht erwähnt

Mit auth.sso-enabled=true + auth.basic-auth.* zeigt Pangolin (Issue fosrl/pangolin#3099, aktuell open) Browser-Usern einen Basic-Auth-Dialog statt SSO-Redirect. Cancel im Dialog bringt die SSO-Page. Für Browser-UX relevant — sollte im Spec als bekannter Zustand dokumentiert sein, damit der Implementierer keinen Bug-Report öffnet.


Was gut ist

  • H1 env-merge + H2 down_stack statt restart exakt nach dockhand-stack-env-merge.md — vorbildlich.
  • feedback_pangolin_labels_source_of_truth korrekt angewendet: Labels als Source of Truth, kein API-Set.
  • SSO-Label-Syntax (auth.sso-enabled=true, nicht auth.sso=true) korrekt.
  • CSRF-Differenzierung ist wasserdicht: Basic-Auth/API-Key können nicht aus Browser-Origin missbräuchlich verwendet werden.
  • Phase-0-Live-Check für Bestandsresource explizit genannt.

Pflicht vor Merge: N1 (Healthcheck-Labels) in Blueprint-Beispiel ergänzen. N2 als Verifikationsschritt in Phase-3-Smoke aufnehmen. N3 + N4 als Kommentar oder Risiko-Tabelleneintrag — kein Blocking.

@strausmann

Copy link
Copy Markdown
Owner Author

Round-2 Spec-Review: storage-Team

Status: NEEDS_FIXES (1 HIGH neu, 1 MED neu, Rest APPROVE)


Round-1 Findings — Verifikation

  • C3 SQLite/JSONB: ✅ adressiert — sa.JSON() in Audit-Migration korrekt, konsistent mit printers.connection.
  • H4 SNMP-Redaction: ⚠️ teilweise — siehe neues Finding H4-B unten.
  • H5 SELECT FOR UPDATE: ✅ adressiert — BEGIN IMMEDIATE-Ansatz ist korrekt für aiosqlite.

Neue Round-2 Findings

HIGH — H4-B: SNMP-Schema-Inkompatibilität (flach vs. verschachtelt)

Problem: Die neue PrinterConnection in der Spec ist flach (snmp_discover, snmp_community als Top-Level-Felder). Das bestehende YAML-Schema (printer_config.py) ist verschachtelt (snmp.discover, snmp.community). Die laufende upsert_runtime_printers() schreibt aktuell nur {host: ..., port: ...} in die DB — SNMP-Felder fehlen komplett in Bestandsrows.

Das führt zu zwei Problemen:

  1. redact_secrets(payload) sucht nach connection.snmp_community (flach), aber Bestandsdrucker aus YAML haben entweder kein SNMP-Feld oder eine andere Struktur in connection. → Redaction greift bei Bestand-Updates ggf. ins Leere.
  2. Spec zeigt connection.snmp_community im Audit-Kommentar aber snmp_discover/snmp_community im Pydantic-Schema. Kein explizites Statement was in connection JSON gespeichert wird vs. was im Pydantic-Payload ist.

Fix: Spec muss explizit definieren: (a) was genau in printers.connection gespeichert wird (alle Felder der PrinterConnection-Klasse, serialisiert via model_dump()?), und (b) ob redact_secrets auf das gespeicherte dict oder den Pydantic-dump operiert. Empfehlung: connection speichert exakt PrinterConnection.model_dump() — dann ist das Schema konsistent und Redaction robust.

MED — M7: BEGIN IMMEDIATE + SQLAlchemy session.begin() Zusammenspiel

Spec sagt async with session.begin() plus BEGIN IMMEDIATE für Update-Flow, aber beschreibt nicht wo BEGIN IMMEDIATE exakt ausgeführt wird. Bei aiosqlite/SQLAlchemy async öffnet session.begin() eine reguläre Transaktion. Ein explizites await session.execute(text('BEGIN IMMEDIATE')) innerhalb von session.begin() schlägt fehl (sqlite3.OperationalError: cannot start a transaction within a transaction).

Korrekte Umsetzung: Entweder Connection-Level-Event vor dem session.begin() (event.listen(engine.sync_engine, 'begin', lambda conn: conn.execute(text('BEGIN IMMEDIATE')))) oder isolation_level='IMMEDIATE' am Engine setzen. Spec sollte die gewählte Variante explizit nennen — beide sind valide, aber der Implementer muss es wissen.


Was gut ist

  • Soft-Delete / kein FK auf printers_audit.printer_id: sauber und richtig begründet.
  • Transaktion Rollback (M6): async with session.begin() rollt bei Exception automatisch zurück — Drucker-INSERT + Audit-INSERT sind atomar. Korrekt.
  • .backup-Befehl (M1): SQLite .backup synchronisiert bei aktivem WAL-Mode automatisch den Checkpoint — kein manuelles PRAGMA wal_checkpoint nötig. Ist korrekt so.
  • Slug-Recycling / Soft-Delete: Richtig erkannt dass ein disabled Drucker seinen Slug blockiert. Das Out-of-Scope-Statement in Sektion Out of Scope reicht.
  • Audit-Retention: <50KB/10 Jahre ist realistisch, kein Cleanup-Code nötig.

@strausmann

Copy link
Copy Markdown
Owner Author

Round-2 Spec-Review: code-quality

Status: NEEDS_FIXES (2 Medium, 1 High — kein neues CRITICAL)


Round-1 Findings — Verifikation

  • C4 derive_printer_id Backwards-Compat: ⚠️ teilweise adressiert — Spec benennt die 3 richtigen Test-Files. Im Live-Code existieren aber noch 2 weitere 3-arg-Aufrufer die die Spec übersieht:

    • tests/integration/test_lifespan_seeds_and_upserts.py:88derive_printer_id("PT-P750W", "192.0.2.50", 9100) (3-arg)
    • tests/integration/db/test_lifespan_printer_upsert.py:51,101,102,161 — mehrfach 3-arg
      Diese Tests testen upsert_runtime_printers (wird entfernt), sind also mit dem Löschen von upsert_runtime_printers hinfällig. Spec sollte sie explizit als "auch zu löschen" listen — sonst bricht CI beim Merge, weil die Tests gegen eine nicht mehr existente Funktion importieren.
  • C5 DELETE-Strategie (Soft-Delete): ⚠️ teilweise adressiert — Spec dokumentiert disable_printer korrekt. Aber: der bestehende PrintService prüft enabled nicht beim Submit eines Print-Jobs via UUID. app/services/print_service.py erhält eine printer_id UUID direkt und prüft nie ob enabled=true. Der Spec-Text sagt "PrintRequest mit UUID eines disabled Druckers → 409 printer disabled" — aber kein Implementierungsschritt nennt wo diese Prüfung eingebaut wird (Service? Repository? Route?). Das ist ein implementierender Subagent wird das übersehen.

  • H6 Pydantic-Payload-Felder:adressiertPrinterConnection ist flat (snmp_discover, snmp_community). Aber: bestehende Bestandsdrucker-Rows haben connection = {"host": ..., "port": ...} ohne SNMP-Keys (gesetzt von upsert_runtime_printers). Neuer PrinterConnection-Parser erwartet host+port als Pflichtfelder — das passt. SNMP-Felder sind optional (default False/None). Kein Breaking Change für Bestandsdaten.

  • H2 Immutable-Fields silent ignore:adressiert — Sektion "Komponenten" benennt das Verhalten. Fehlender Test-Case (Update mit anderem slug → 200 + DB unverändert) ist jetzt explizit in Testing-Sektion gelistet: "Versuch slug/model/backend zu ändern → wird ignoriert (kein 422)". ✅


Neue Round-2 Findings

HIGH

H-NEW-1: test_lifespan_seeds_and_upserts.py + test_lifespan_printer_upsert.py fehlen in der Lösch-Liste (C4-Nachzügler)

Beide Dateien importieren upsert_runtime_printers und derive_printer_id mit 3-arg-Signatur. Spec nennt nur 3 Files zum Löschen/Migrieren. Diese 2 Integration-Test-Files werden beim Merge gegen eine entfernte Funktion importieren → CI bricht. Spec muss tests/integration/test_lifespan_seeds_and_upserts.py und tests/integration/db/test_lifespan_printer_upsert.py in die Liste aufnehmen.

MEDIUM

M-NEW-1: C5 — PrintService.submit_print_job prüft enabled nicht

Der Spec-Satz "PrintRequest mit UUID eines disabled Druckers schlägt mit 409 printer disabled fehl" ist korrekt als Ziel. Aber im bestehenden Code (app/services/print_service.py) gibt es keine enabled-Prüfung — nur UUID-Lookup. Ohne einen expliziten Implementierungsschritt ("In PrintService.submit_print_job oder im Printer-Repository: vor Job-Eintrag prüfen ob printer.enabled=true, sonst raise 409") wird der Implementer das übersehen. Akzeptanzkriterien nennen es nicht. Ein Test-Case fehlt ebenfalls.

M-NEW-2: redact_secrets — kein Modul-Pfad, keine Test-Strategie

Spec sagt "Helper redact_secrets(payload: dict) -> dict im Service". Der Testing-Abschnitt listet redact_secrets-Unit-Tests explizit — gut. Aber der Modul-Pfad fehlt: lebt er in printer_admin_service.py (private Funktion) oder als eigenes app/services/audit_redaction.py? Ohne Festlegung wird der Implementer entweder eine private Methode (schlecht testbar ohne Import) oder ein eigenes Modul bauen. Coverage-Threshold ist nur für printer_admin_service.py definiert — wenn redact_secrets in ein separates Modul wandert, fehlt der Threshold-Eintrag. Empfehlung: app/services/audit_redaction.py mit eigenem 85%-Threshold (Mutation-Logic — ersetzt Secret-Werte).

LOW

L-NEW-1: CSRF-Test-Strategie zu vage

middleware/csrf.py hat 80% Coverage-Threshold aber kein konkretes Test-Pattern. Starlette-CSRF-Middleware ist ein Third-Party-Package — testen heißt: Token fehlt → 403, Token falsch → 403, Token korrekt → 200, JSON-API-Pfad bypass (Basic-Auth) → kein CSRF-Check. Spec sollte mindestens diese 3 Fälle in der Integration-Test-Liste nennen (3 davon fehlen, nur der POST /admin/printers ohne CSRF → 403 ist gelistet).

L-NEW-2: Trailing-Slash-Konvention für JSON-API nicht festgelegt

6 neue /api/v1/admin/printers/...-Endpunkte. FastAPI-Standard ist kein Trailing Slash, aber Spec zeigt inkonsistente Notation (mal …/disable, mal /api/v1/admin/printers). Einmal festlegen, damit OpenAPI-Schema sauber wird.


Was gut ist

  • SNMP-Schema-Konsistenz: Flaches PrinterConnection-Schema ist konsistent mit dem was upsert_runtime_printers in connection-JSON schreibt ({"host": ..., "port": ...}) — kein Migration-Risk für Bestandsdaten.
  • BEGIN IMMEDIATE für SQLite statt SELECT FOR UPDATE — korrekter Dialect-Workaround, gut begründet.
  • redact_secrets-Test explizit in Unit-Tests — das Richtige, nur der Modul-Pfad fehlt.
  • Migration-Schritte (Snapshot, down_stack, env-merge) sind vollständig und reproduzierbar.

Blockierend vor Umsetzung: H-NEW-1 (CI bricht sonst beim Merge) und M-NEW-1 (fehlendes Akzeptanzkriterium für disabled-Printer-Check).

…D + 4 LOW)

Round-2 Reviews: ops APPROVE, network/storage/code-quality NEEDS_FIXES.
Folgendes wurde eingearbeitet:

HIGH
- H7 (network) Healthcheck-Labels im Blueprint-Snippet vollstaendig
  gemaess pangolin-resource-standard.md (alle Pflichtfelder seit
  Newt v1.18.4: name, full-domain, protocol, ssl, target+healthcheck,
  auth.sso-enabled, auth.basic-auth) plus Vault-Item-Konvention.
- H8a (storage) SNMP-Schema verschachtelt (snmp.discover, snmp.community)
  statt flach — konsistent mit altem YAML (User-Entscheidung).
- H8b (storage) Alembic-Backfill-Migration fuer Bestand-Drucker:
  queue_timeout_s + cut_defaults_half_cut als neue Spalten, plus
  connection.snmp Backfill mit Defaults fuer Rows ohne SNMP-Block.
  Phase 1b in Migration-Sektion.
- H9 (code-q) 2 weitere Test-Files in C4-Liste:
  test_lifespan_seeds_and_upserts.py + test_lifespan_printer_upsert.py
  beide komplett geloescht. Verifikations-Greps im Akzeptanzkriterium.

MEDIUM
- M7 (storage) BEGIN IMMEDIATE Konflikt geloest: aiosqlite-Engine mit
  isolation_level="SERIALIZABLE" + Connect-Listener fuer journal_mode=WAL
  und foreign_keys=ON. session.begin() startet dann automatisch im
  IMMEDIATE-Modus. Kein manuelles BEGIN IMMEDIATE noetig.
- M8 (code-q) PrintService.submit_print_job enabled-Check: neue
  PrinterDisabledError-Exception (HTTP 409), Code-Snippet + 2 Test-Cases
  im Spec.
- M9 (code-q) redact_secrets in eigenem Modul
  app/services/audit_redaction.py mit SECRET_PATHS-Set, 80% Coverage,
  4 Edge-Case-Tests (None, Bestandsdrucker ohne SNMP, etc.).
- M10 (network) Container-DNS-Name `print-hub` Live-Verifikation
  als erster Schritt in Phase-3-Smoke.

LOW
- Pangolin-Resource-Standard + Vault-Item-Naming verlinkt.
- Pangolin Bug #3099 (Basic-Auth-Dialog statt SSO-Redirect) als
  bekanntes Phaenomen in Risiken-Tabelle (R8) — nicht als Bug reporten.
- CSRF-Test-Strategie konkret in 4 Faellen (Cookie+Token Match/Miss/
  Wrong, Authorization-Header skipped).
- Trailing-Slash-Konvention: ohne (FastAPI-Standard), konsistent mit
  existing Hub-Endpoints.

Akzeptanzkriterien-Liste auf 18 Punkte erweitert (5 neue:
Backfill-Verifikation, PrintService-enabled-Check, redact_secrets-Modul,
SQLite-Engine-Setup, Container-DNS-Verifikation).

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Round-3 Spec-Review: ops-Team

Status: APPROVE

Round-2 (APPROVE) bestätigt?

Ja. Alle bisherigen APPROVE-Grundlagen (H1 env-merge-Pflicht, H2 down/start, Blueprint-Labels, Snapshot-Befehl, Watchtower-Pause) sind in Round-3 unverändert korrekt. Keine der neuen Bausteine zieht das Round-2-OK in Zweifel.

Neue Round-3 Findings

LOW: WAL-Pragma + Backup-Artefakte (.wal/.shm)

Der Connect-Listener in engine.py setzt journal_mode=WAL. Falls die Production-DB bisher im Rollback-Journal-Modus läuft, switcht das erste Open den Modus und erzeugt .wal + .shm Dateien neben der .db. Der Spec-Backup-Befehl (sqlite3 .backup) ist korrekt: SQLite's .backup-Befehl handhabt WAL-Frames automatisch und erzeugt eine konsistente Kopie. Kein Anpassungsbedarf.

Einzige Lücke: Phase 1a sichert explizit printer-hub.db.bak-pre-124 — nach dem ersten Start existieren auch printer-hub.db-wal und printer-hub.db-shm. Der PBS-Job sichert /data/ sowieso vollständig. Kein Blocking-Issue, aber der Rollback-Pfad erwähnt nur die .db-Datei. Empfehlung (non-blocking): Rollback-Sektion um einen Hinweis erweitern: rm -f /data/printer-hub.db-wal /data/printer-hub.db-shm vor dem Restore, um WAL-Artefakte zu beseitigen.

LOW: Alembic-down für Phase-1b fehlt im Rollback-Pfad

Der Rollback-Pfad beschreibt SQLite-Restore + Compose-Revert. Das reicht in der Praxis (DB wird zurückgespielt, Alembic-Version-Tabelle kommt mit). Formal fehlt aber ein alembic downgrade -1 für den Fall dass jemand einen Rollback ohne vollständigen DB-Restore versucht. Da der Spec explizit sagt "SQLite-Restore aus Backup" als primären Rollback, ist das kein Blocking-Issue. Empfehlung (non-blocking): Im Rollback-Pfad explizit dokumentieren: "Alembic-down wird NICHT genutzt — ausschließlich DB-Restore-Pfad.

LOW: Container-DNS-Smoke-Schritt ist fragil bei abweichendem Compose-Service-Name

getent hosts print-hub funktioniert nur wenn der Compose-Service-Name tatsächlich print-hub heißt. Der Spec-Hinweis "falls Service-Name abweicht: Compose-Service-Definition prüfen" ist ausreichend. In modernen Compose-Versionen (Docker Compose v2) wird der Bindestrich-Container-Name verwendet (hangar-print-hub-print-hub-1), der DNS-Alias im Container-Netz ist aber der Service-Name (print-hub). Das ist korrekt. Non-blocking.

LOW: PrinterDisabledError → 409 (API-Contract-Änderung)

POST /api/v1/print gibt neu 409 statt nur 404 für disabled Drucker. Der Hangar-Client ruft diesen Endpoint aktuell aus einem enabled=true-gefilterten Cache auf — ein disabled Drucker sollte Hangar daher gar nicht erreichen. 409 ist trotzdem eine Contract-Änderung für Drittaufrufer. Spec stuft das korrekt als Out-of-Scope #124 ein und R1 dokumentiert das Risiko. Non-blocking; Hangar-Issue zum Hinweis auf 409-Handling wäre sauber.

Was gut ist

  • M7 (IMMEDIATE via Engine-Default): Die Strategie, isolation_level="SERIALIZABLE" auf Engine-Ebene zu setzen statt manuell BEGIN IMMEDIATE in Sessions zu injizieren, ist die korrekte SQLAlchemy+aiosqlite Lösung. Kein doppeltes BEGIN-Problem.
  • H8b Backfill-Idempotenz: if "snmp" not in conn_json verhindert Doppel-Writes bei Migrations-Wiederholung sauber.
  • Rollback-Vollständigkeit: Snapshot vor Deploy, lokale Kopie, Stack-Env-Merge-Revert — alle drei Schritte dokumentiert.
  • Phase-3-Verifikations-Checkliste: Die SQL-Verifikation der Backfill-Ergebnisse ist konkret und copy-paste-ready.

ops-Team APPROVE — alle Findings sind LOW und non-blocking.

@strausmann

Copy link
Copy Markdown
Owner Author

Round-3 Spec-Review: network-Team

Status: APPROVE

Round-2 Findings — Verifikation

  • H7 Healthcheck-Labels: vollständig adressiert. Der Blueprint-Snippet enthält alle Pflichtfelder per pangolin-resource-standard.md: name, full-domain, protocol=http, ssl=true, targets[0].method=http, targets[0].port=8000, targets[0].path-match=prefix, alle fünf healthcheck.*-Labels (enabled, hostname=print-hub, path=/healthz, port=8000, interval=30), auth.sso-enabled=true, auth.basic-auth.user + password. Port 8000 und Compose-Service-Name print-hub stimmen mit dem Architektur-Diagramm überein.

  • L3 Pangolin Bug #3099: adressiert. Risiko R8 benennt das Phänomen (Basic-Auth-Dialog statt SSO-Redirect), verlinkt den Upstream-Issue und erklärt den Cancel-Fallback. Kein weiterer Handlungsbedarf.

  • L4 CSRF-Strategie: adressiert. Vier explizite Test-Fälle in tests/middleware/test_csrf.py sind dokumentiert (gültiger Match → 303, Cookie ohne Hidden-Field → 403, falsches Hidden-Field → 403, Authorization-Header → CSRF skipped → 200). JSON-API-Pfad korrekt als CSRF-frei ausgewiesen wenn Basic-Auth oder API-Key genutzt wird.

Neue Round-3 Findings

Keine. Die Backfill- und PrintService-Änderungen (H8b, M8) haben keine neuen Pangolin- oder Netzwerk-Implikationen. Hangar→Hub-Routing bleibt intern via Container-Netz — Pangolin-Resource nicht betroffen.

Was gut ist

  • Vault-Item-Konvention (Pangolin Header Auth - Print Hub, Username claude-automation, Collection Automation/Claude-Team) ist exakt nach Standard.
  • Phase-0-Live-Check mit mcp__pangolin-api__resource_by_resourceId + Erwartung headerAuth != None und healthCheck.enabled=true ist präzise genug für die Implementierung.
  • Der Hinweis, Header-Auth ausschließlich via Blueprint Labels zu setzen (nie per API), ist explizit dokumentiert und referenziert das richtige Memory (feedback_pangolin_labels_source_of_truth).
  • auth.sso-enabled=true (korrekte Syntax, nicht auth.sso=true) ist im Snippet richtig.

@strausmann

Copy link
Copy Markdown
Owner Author

Round-3 Spec-Review: storage-Team

Status: APPROVE (mit einem dokumentierten LOW-Finding)


Round-2 Findings — Verifikation

  • H8a SNMP-Schema verschachtelt: vollständig adressiert. SNMPConfig als Sub-Class mit korrekter DB-JSON-Form, Pydantic und DB konsistent.
  • H8b Backfill: vollständig adressiert. Alembic-Migration ergänzt snmp-Block idempotent (if "snmp" not in conn_json), neue Spalten queue_timeout_s + cut_defaults_half_cut mit NOT NULL + server_default. SQLite 3.35+ unterstützt das. Verifikations-SQL ist explizit in Phase 3 gelistet — gut.
  • M7 Transaktions-Strategie: vollständig adressiert. isolation_level="SERIALIZABLE" auf Engine-Level (aiosqlite mappt auf IMMEDIATE), Connect-Listener für journal_mode=WAL + foreign_keys=ON. Service-Layer nutzt nur async with session.begin(): — kein doppelter BEGIN-Konflikt möglich.

Neue Round-3 Findings

LOW — Backfill Edge-Case: connection IS NULL

Der Backfill-Code schreibt bei connection IS NULL ein {"snmp": ...} ohne host/port:

conn_json = row.connection or {}
if "snmp" not in conn_json:
    conn_json["snmp"] = {"discover": False, "community": "public"}
    conn.execute(... .values(connection=conn_json))

NULL-connection ist im Schema erlaubt (nullable=True). Bei Bestand tritt das nicht auf (upsert_runtime_printers schrieb immer host+port). Für zukünftige Migrations-Sicherheit empfehle ich eine defensive Schutzklausel:

if not conn_json or "host" not in conn_json:
    # skip + log — connection ohne host macht keinen Sinn
    continue

Alternativ reicht ein Kommentar: "Invariante: connection enthält immer host+port (upsert_runtime_printers-Garantie) — skip wenn NULL".

LOW — Kein Downgrade-Pfad dokumentiert

Spec enthält keinen def downgrade() für die kombinierte Migration. Bei fehlgeschlagenem Upgrade (z.B. Backfill-Fehler mitten in der Schleife) bleibt die Alembic-Versions-Row unkonsistent. Da der Rollback-Pfad über SQLite-Restore läuft (Phase Rollback), ist das akzeptabel — aber die Spec sollte explizit dokumentieren: "Downgrade-Migration ist leer / nicht vorgesehen — Rollback erfolgt via SQLite-Restore (Phase Rollback-Pfad)".


Was gut ist

  • isolation_level="SERIALIZABLE" auf Engine-Ebene ist die richtige Lösung für den BEGIN IMMEDIATE/session.begin()-Konflikt — sauber und zukunftssicher.
  • Atomicity-Garantie INSERT printers + INSERT printers_audit in einer Transaktion ist explizit und korrekt.
  • Idempotenz-Schutz im Backfill (if "snmp" not in conn_json) verhindert Doppel-Schreibungen bei versehentlichem Migrations-Replay.
  • Verifikations-SQL in Phase 3 ist copy-paste-ready — genau richtig für Produktiv-Deploy.
  • Kein FK auf printers_audit.printer_id ist die korrekte Entscheidung bei Soft-Delete.

Die beiden LOW-Findings blockieren nicht — APPROVE.

storage-agent, Round-3

@strausmann

Copy link
Copy Markdown
Owner Author

Round-3 Spec-Review: code-quality

Status: NEEDS_FIXES (2 neue Medium-Findings)


Round-2 Findings — Verifikation

  • H9 Test-Files (5 statt 3): ✅ adressiert — Sektion "ID-Generierung" listet alle 5 Files explizit, inkl. test_lifespan_seeds_and_upserts.py und test_lifespan_printer_upsert.py. grep-Verifikationsschritt ist im Akzeptanzkriterium verankert. Reicht.
  • M8 enabled-Check: ✅ adressiert — Code-Snippet, 2 Test-Cases, Sektion "Implikationen für Hangar+PrintService" klar.
  • M9 redact_secrets Modul: ✅ adressiert — app/services/audit_redaction.py, Skeleton, 4 Edge-Cases, 80%-Schwelle.
  • L CSRF 4 Fälle / Trailing-Slash: ✅ beide adressiert.

Neue Round-3 Findings

[MEDIUM] M11 — LabelHubException existiert nicht

Spec definiert:

class PrinterDisabledError(LabelHubException):
    http_status = 409

Live-Check: backend/app/printer_backends/exceptions.py kennt nur PrinterError als Basisklasse. Eine Datei app/exceptions.py existiert noch nicht (laut Spec wird sie neu angelegt). LabelHubException ist nirgendwo definiert.

Der Implementer steht vor der Wahl: LabelHubException neu einführen (was die Basis für alle künftigen HTTP-Exceptions wäre) oder PrinterDisabledError von Exception oder PrinterError ableiten. Die Spec muss das entscheiden — sonst entstehen zwei divergierende Implementierungen wenn Backend-Team und Hub-Team unabhängig arbeiten.

Fix: Spec ergänzt app/exceptions.py mit class LabelHubException(Exception): http_status: int = 500 als Basis ODER tauscht die Basis auf PrinterError aus (dann ist http_status-Convention aber nicht standardisiert).


[MEDIUM] M12 — model_dump() → flache Spalten: Mapping unspezifiziert

Create-Flow Step 5c:

row_dict = payload.model_dump() + {"id": printer_id, "created_at": ..., "updated_at": ...}
# INSERT INTO printers (...)

payload.model_dump() gibt {"queue": {"timeout_s": 30}, "cut_defaults": {"half_cut": false}, "connection": {...}, ...}. Die Spalten queue_timeout_s und cut_defaults_half_cut sind aber flach (Alembic-Migration Phase 1b). row_dict kann so nicht direkt als INSERT-Payload genutzt werden — printers.queue existiert als Spalte nicht.

Der Implementer muss manuell flatten:

row_dict["queue_timeout_s"] = payload.queue.timeout_s
row_dict["cut_defaults_half_cut"] = payload.cut_defaults.half_cut
row_dict.pop("queue")
row_dict.pop("cut_defaults")

Das ist nicht trivial und fehleranfällig (besonders beim update_printer-Pfad mit optionalem Patch). Spec sollte ein explizites _to_db_row(payload) Helper-Snippet zeigen oder zumindest die Flattening-Logik im Data-Flow Step benennen.


[LOW] Engine-Snippet Reihenfolge (Lesbarkeit)

M7-Snippet zeigt @event.listens_for(engine.sync_engine, "connect") als Dekorator vor create_async_engine — das wäre zur Laufzeit ein NameError. Live engine.py macht es korrekt (create first, listen second). Das Snippet ist misleading für einen neuen Implementer. Empfehlung: Snippet in chronologische Reihenfolge bringen oder als Pseudo-Code kennzeichnen.

Außerdem: Live engine.py hat bereits _apply_pragmas mit WAL + foreign_keys + busy_timeout. Die Spec soll nur isolation_level="SERIALIZABLE" ergänzen — das steht nicht explizit als Diff. Bitte klarstellen was neu hinzukommt vs. was bereits vorhanden ist.


Was gut ist

  • Alle 5 H9-Test-Files sauber kategorisiert (löschen vs. migrieren).
  • redact_secrets mit frozenset[tuple]-Pfad-Konvention ist erweiterbar und klar.
  • SNMPConfig-Pfad ("connection", "snmp", "community") ist konsistent mit Pydantic-Schema.
  • M7-Entscheidung (Engine-Level IMMEDIATE statt manuelles BEGIN) ist architektonisch sauber — kein session.begin()-Konflikt.
  • CSRF-Strategie (Authorization-Header skipped) deckt den JSON-API-Pfad korrekt ab.
  • Akzeptanzkriterien vollständig und messbar.

… 1 LOW)

Round-3 Reviews: ops/network/storage APPROVE, nur code-quality
NEEDS_FIXES mit 2 MED + 1 LOW. Eingearbeitet:

- M11 LabelHubException existiert nicht: PrinterDisabledError leitet von
  der existierenden PrinterError-Basisklasse in
  app/printer_backends/exceptions.py ab. Konstruktor mit printer_id+slug.
  Error-Handler analog TapeMismatchError-Pattern in api/routes/print.py.
  Mapping 409 statt 404 da Drucker semantisch existiert.

- M12 model_dump-Flattening: 3 explizite Helper-Funktionen mit
  Code-Snippets im Spec:
  - _payload_to_row(payload, id, ts): Pydantic → flache DB-row
  - _apply_update_patch(row, patch): nur changed Spalten zurueck
  - _row_to_audit_view(row): flach → verschachtelt fuer Audit-JSON
  Tests fuer alle 3 Helper benannt. Create-Flow + Update-Flow
  konsistent auf die Helper umgestellt.

- LOW Engine-Snippet Reihenfolge: Snippet als Pseudo-Code markiert,
  korrekte Reihenfolge (create_async_engine VOR @event.listens_for).
  Delta-Hinweis was an engine.py NEU vs EXISTING ist.

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Round-4 Spec-Review: code-quality

Status: APPROVE

Round-3 Findings — Verifikation

  • M11 PrinterDisabledError-Basisklasse: ✅ vollständig adressiert. exceptions.py bestätigt: PrinterError ist die echte Root-Basisklasse (nicht LabelHubException). PrinterDisabledError(PrinterError) mit __init__(self, printer_id: UUID, slug: str) ist korrekt platziert in app/printer_backends/exceptions.py. Die Ablage ist pragmatisch akzeptabel — alle Hardware-/Status-Exceptions leben dort, und HTTP-Mapping erfolgt sauber getrennt in api/routes/print.py analog TapeMismatchError. Keine semantische Spannung: die Klasse beschreibt einen Drucker-Zustand, nicht einen HTTP-Fehler.

  • M12 Flattening-Helper: ✅ vollständig adressiert. Drei explizite Funktionen mit klarer Verantwortung:

    • _payload_to_row mappt verschachtelt → flach, connection bleibt als JSON-Blob
    • _apply_update_patch returnt nur geänderte Spalten (effizientes partial UPDATE)
    • _row_to_audit_view rekonstruiert verschachtelte Form für redact_secrets

    Test-Cases für alle drei sind explizit im Testing-Abschnitt benannt. ✅

  • LOW Engine-Snippet: ✅ adressiert. Korrekte Reihenfolge (create_async_engine vor @event.listens_for), als Pseudo-Code markiert, Delta-Hinweis ("was sich konkret ändert") ergänzt.

Neues Finding — R4a: _apply_update_patch partial-connection-Update nicht möglich

Die Spec sollte explizit dokumentieren (empfohlen, kein Blocker):

_apply_update_patch behandelt patch.connection atomar — wenn connection nicht None ist, wird die gesamte connection-JSON-Spalte ersetzt. Ein Operator, der nur connection.host ändern will, muss die gesamte PrinterConnection mitschicken (inkl. snmp, port). Das ist für den API-Pfad nicht offensichtlich.

Empfehlung: einen Satz in der _apply_update_patch-Beschreibung ergänzen: "Partial-Update von Sub-Feldern in connection ist nicht unterstützt — der Client muss das vollständige connection-Objekt senden." Alternativ als Akzeptanzkriterium oder API-Doku. Kein Code-Änderungsbedarf — das Verhalten ist bewusst und konsistent mit dem Update-Schema.

Was gut ist

  • _row_to_audit_view mit "id": str(row["id"]) ist korrekt: UUID → String-Konvertierung nur hier nötig (andere Felder sind primitive Typen, die SQLite/JSON direkt serialisiert). Kein Konsistenzproblem.
  • Atomicity-Garantie (printers + printers_audit in einer Transaktion) ist klar dokumentiert mit Rollback-Semantik.
  • silent ignore von slug/model/backend/id in _apply_update_patch ist jetzt explizit im Docstring und im Kommentar im Update-Flow — keine Überraschungen mehr für Implementierer.
  • Exception-Datei verifiziert: TapeMismatchError als Pattern-Referenz für den print.py-Handler ist eine solide Vorlage.

Freigabe zur Implementierung.

Basiert auf Spec Round-4 final (Commit 613bb97) — alle 4 Teams APPROVE.

Phasen:
- 0: Live-Check (Pangolin, DB-Schema, Test-Inventar)
- 1: Foundation (Engine SERIALIZABLE+WAL, PrinterDisabledError,
     Alembic-Migration mit Backfill, derive_printer_id 4-arg)
- 2: Service-Layer (Pydantic-Schemas, audit_redaction, Plugin-Registry,
     PrinterAdminService mit Flattening-Helper + CRUD)
- 3: API + Web-Routes + CSRF-Middleware
- 4: PrintService enabled-Check + 409-Mapping
- 5: Removal (PrinterConfigLoader, 5 Test-Files, lifespan-Aufrufer)
- 6: Pangolin-Resource-Standard (Vault-Item + Blueprint-Labels)
- 7: Fresh-Install E2E-Test
- 8: Production-Deploy (DB-Snapshot, Watchtower-Pause, Stack-Env-Merge,
     down/start, Smoke)

Self-Review-Tabelle deckt alle Spec-Sektionen ab. Coverage-Schwellen pro
Modul explizit (85% Mutation-Logic, 75% Helper).

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-1: storage-Team

Status: NEEDS_FIXES (2 Medium, 2 Low)


Findings

MEDIUM — Task 1.1: isolation_level="SERIALIZABLE" mappt nicht zuverlässig auf aiosqlite

Das Plan-Snippet setzt create_async_engine(..., isolation_level="SERIALIZABLE"). In SQLAlchemy 2 + aiosqlite wird SERIALIZABLE auf BEGIN IMMEDIATE gemappt — aber das ist ein undokumentiertes Implementierungsdetail des aiosqlite-Dialekts. Der Test prüft engine.dialect.isolation_level == "SERIALIZABLE" über eine OR-Bedingung, die so breit ist dass sie fast immer True ergibt unabhängig vom tatsächlichen Verhalten. Besser: in test_connect_listener_sets_wal_and_foreign_keys das BEGIN-Verhalten direkt verifizieren via PRAGMA journal_mode (bereits vorhanden) statt die Dialect-Introspection zu testen. Der Test für journal_mode=wal und foreign_keys=1 ist ausreichend und korrekt — die Isolation-Level-Assertion ergibt keinen Mehrwert und kann entfernt werden um False-Confidence zu vermeiden. Handlungsempfehlung: test_engine_isolation_level_is_serializable ersatzlos streichen, nur den Pragma-Test behalten.

MEDIUM — Task 1.3: downgrade() raises NotImplementedError statt pass/no-op

alembic downgrade -1 bricht hart mit NotImplementedError. In CI-Umgebungen die Migration-Roundtrip testen (down + up) würde das die Pipeline lahmlegen. Für einen SQLite-only-Service ist das Rollback-Modell (Restore aus Backup) korrekt — aber die Ablehnung gehört als Kommentar/Logging, nicht als Exception. Handlungsempfehlung: downgrade() durch reinen No-op ersetzen (pass mit erklärendem Docstring), damit alembic downgrade -1 in CI durchläuft. Der Rollback-Hinweis im Docstring bleibt. Kein Code-Pfad sollte NotImplementedError auf alembic-Aufrufe werfen.

LOW — Task 1.3 Test-Wording: "Defaults durch die Migration" ist irreführend

Der Test test_backfill_sets_snmp_defaults_for_existing_printers kommentiert "Backfill darf in Test-Setup nicht erneut laufen (Migration ist schon angewandt) — also prüfen wir nur dass Defaults greifbar sind" und testet dann queue_timeout_s == 30. Dieser Wert kommt aus dem server_default der DDL, nicht aus dem Backfill-Pfad der Migration. Das ist im Kommentar bereits halb erklärt, aber der Test-Name suggeriert er testet den Backfill. Der eigentliche Backfill-Pfad (SNMP-Block ergänzen für Rows ohne ihn) wird im Test-Setup nicht getriggert weil der Row NACH der Migration inserted wird. Handlungsempfehlung: Test umbenennen zu test_new_rows_get_server_defaults_after_migration. Einen separaten Test ergänzen der die Backfill-Logik direkt in einer In-Memory-DB testet ohne alembic (Unit-Test der upgrade()-Funktion mit eigenem Connection-Mock).

LOW — Task 2.5 / Phase 8.4: before_json/after_json als String via _json_or_none

Das _record_audit-Insert übergibt before/after als json.dumps()-String an den aiosqlite-Bind-Param. Die Spalte printers_audit.before_json ist sa.JSON() — SQLAlchemy würde bei ORM-Nutzung automatisch serialisieren. Mit raw text()-Insert muss der String explizit sein, was korrekt ist. Aber: bei Phase 8.4 Backfill-Verifikation gibt json_extract(connection, '$.snmp.discover') für SQLite einen Integer (0/1) zurück, kein Boolean. Das Smoke-Output-Matching in der Verifikations-Anleitung sollte snmp_discover=0 (nicht False) als expected Wert dokumentieren. Handlungsempfehlung: Kommentar in Task 8.4 Step 3 ergänzen: json_extract gibt 0/1 (Integer) zurück, nicht true/false.


Was gut ist

  • Backfill-Schutzklauseln (NULL-connection skip, missing-host skip, Idempotenz via "snmp" in conn_json: continue) sind vollständig und korrekt.
  • WAL-Backup-Sicherheit: sqlite3 .backup ist WAL-aware und konsistent — richtige Wahl.
  • Env-Merge (Phase 8.3) folgt sauber dem Merge-Pattern aus dockhand-stack-env-merge.md.
  • server_default für queue_timeout_s und cut_defaults_half_cut mit sa.false() ist idiomatisch korrekt für SQLAlchemy+SQLite.
  • No-FK auf printers_audit.printer_id ist die richtige Entscheidung — Soft-Delete behält den Parent-Row sowieso.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-1: ops-Team

Status: NEEDS_FIXES
Reviewer: ops-agent | Datum: 2026-06-14
Plan: 2026-06-14-printers-yaml-to-db-plan.md (Commit 44d543a)


Findings

HIGH

H1 — Parameter-Name-Bug Task 8.2: auto_update= existiert nicht
set_container_auto_update(auto_update="never") — der MCP-Tool-Parameter heißt policy=, nicht auto_update=. Der Call würde mit einem TypeError scheitern und Watchtower läuft weiter während des Deploys.

Fix: set_container_auto_update(containerName="...", environmentId=10, policy="never")

H2 — Rollback-Pfad fehlt komplett
Phase 8 hat keinen expliziten Sub-Task für den Fall dass der Health-Check nach start_stack nicht grün wird. Was ist dann zu tun?

Minimaler Rollback-Step nach 8.3:

8.3.R Rollback (falls Health-Check rot):

  1. down_stack("hangar-print-hub")
  2. docker exec hangar-print-hub-print-hub-1 sqlite3 /data/printer-hub.db ".restore /data/printer-hub.db.bak-pre-124"
  3. update_stack_compose(restart=False) mit dem alten Compose (ohne neue Labels)
  4. start_stack("hangar-print-hub")
  5. set_container_auto_update(policy="any") (Watchtower re-aktivieren)

Ohne diesen Pfad ist kein sicheres Abbrechen möglich.


MEDIUM

M1 — Phase 0: Volume-Mount-Check fehlt
Step 0 prüft DB-Schema und Pangolin-Resource, aber nicht was tatsächlich bei /data im laufenden Container gemountet ist. Empfehlung: Step 0.X ergänzen:

mcp__dockhand__exec_container(command=["sh", "-c", "mount | grep /data"])

Schützt vor dem Fall dass das Backup auf den falschen Pfad läuft.

M2 — Pangolin Newt-Sync-Delay nicht berücksichtigt (Smoke 8.4 Step 4)
Nach start_stack kann es bis zu 5 Minuten dauern bis neue Blueprint-Labels via Newt in Pangolin aktiv sind. Ein Browser-Test als Step 4 in 8.4 kann spurious failen.

Fix: Vor dem Browser-Test explizit warten — sleep 60 oder Retry-Loop mit Pangolin-Health-API-Check (mcp__pangolin-api__get_resource bis Status "healthy").


LOW

L1 — Task 3.3 Granularität
4 HTML-Templates in einem einzigen TDD-Step ist für Subagent-Driven-Development großzügig. Bei Auftreten von Subagent-Context-Erschöpfung ggf. aufteilen: 3.3a (list + detail) / 3.3b (create + edit).


Was gut ist

  • Phase 1.3 Alembic-Backfill korrekt: NULL-Guard + fehlender-Host-Guard + idempotentes Check auf "snmp"-Key — für Prod-DB ausreichend defensiv.
  • Task 8.3 Env-Merge sauber: get_stack_env → Filter PRINTER_CONFIG_PATH → merged list → update_stack_env — kein PUT-Replace-Fehler.
  • 8.3 Compose-Update-Pipeline korrekt: update_stack_compose(restart=False) + down_stack + start_stack (nicht restart_stack).
  • Phase 5 vor Phase 7 — Dependency-Reihenfolge ist korrekt.
  • SQLAlchemy WAL + SERIALIZABLE Connect-Listener, printers_audit SNMP-Redaction, Soft-Delete mit FK-Safety — alles solide.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-1: code-quality

Status: NEEDS_FIXES (2 HIGH, 3 MEDIUM, 2 LOW — kein Blocker für Implementierungsstart wenn HIGH-Punkte vor Task 3.2 bzw. 4.1 adressiert werden)


Findings

HIGH

H1 — GET /api/printers filtert enabled NICHT (bestätigtes Gap)

backend/app/repositories/printers.py list_all() macht select(Printer).order_by(...) ohne WHERE enabled = true. Die bestehende Route GET /api/printers in printers.py ruft printers_repo.list_all(session) auf — deaktivierte Drucker werden also zurückgegeben. Der Plan-Self-Review markiert diesen Spec-Punkt als ✅ (Task 7.1 Step 3 test_fresh_install_disable_filters_from_public_list), aber kein Task in Phase 1–5 repariert list_all oder die bestehende Route.

Task 7.1 testet das Verhalten erst als E2E — wenn list_all nie angepasst wurde, schlägt der Test fehl und der Implementer muss zurückgehen. Klarer wäre: in Phase 4 oder 5 einen eigenen Task „printers_repo.list_all filtert enabled=true by default" einfügen, mit isoliertem Failing-Test auf Repository-Ebene. Alternativ: expliziter Kommentar im Spec-Coverage-Check dass list_all in Task 5.2 angepasst wird.

H2 — Task 3.2 ...-Placeholder verletzt test-coverage-pflicht.md-Pflicht explizit

test_create_printer_duplicate_slug_409, test_get_printer_by_slug, test_update_printer_silently_ignores_slug, test_disable_printer_204, test_disable_already_disabled_409, test_enable_after_disable_200 haben {...} bzw. ... als Body. Das sind 6 von 9 Tests in der Datei. Die Rule sagt: keine "Similar to Task N" Verweise, Mutation-Pfade brauchen Happy-Path + Error-Path + Network-Error. „Implementer ergänzt nach Pattern" delegiert Entscheidungen die in den Plan gehören. Mindestens die Payloads und die assert-Statements müssen ausgeschrieben sein.


MEDIUM

M1 — Task 3.4 „Pattern wie Task 3.3" für 5 Routes reicht nicht

Task 3.3 schreibt 5 Tests (list, new-form, create-post, edit-prefilled-stub, confirm-disable-stub). Task 3.4 hat kein einziges Test-Snippet für die 5 fehlenden Routes (edit-GET, edit-POST, disable-GET, disable-POST, enable-POST). Für Soft-Routes wie enable/disable-POST sind Redirect-Ziel und 409-Konflikt-Fall die Spec-relevanten Assertions. Ein Implementer der nur Task 3.4 liest, schreibt unter Zeitdruck Minimal-Tests ohne die Fehler-Pfade.

M2 — Phase 6 ohne Verifikations-Task für Pangolin Header-Auth

Phase 6 endet mit Labels im Compose. Der erste echte Smoke-Test ist Task 8.4 Step 5 (PrintService 409). Es fehlt ein direkter curl-Test claude-automation → 200, der vor dem Stack-Neustart in Phase 8 ausgeführt wird und der Pangolin-Resource-Standard-Checklist (curl -u claude-automation:<pw>) entspricht. Ohne diesen Schritt ist der Header-Auth-Bypass erst nach dem Deploy verifiziert — Rollback-Fenster ist dann enger.

M3 — hangar_meta-Tabelle im Hub nachweislich nicht vorhanden, „weglassen"-Kommentar reicht nicht

Plan sagt: „Falls hangar_meta-Tabelle im Hub nicht existiert: weglassen." Das erzeugt einen OperationalError zur Laufzeit wenn Task 5.2 blindlings deployed wird. Der Implementer braucht entweder: (a) den Marker komplett streichen (kein Wert für HomeLab-Betrieb), oder (b) einen expliziten CREATE TABLE IF NOT EXISTS-Step davor. Die aktuelle Formulierung lässt den Implementer raten.


LOW

L1 — Task 4.1 Fixture-Setup nicht ausgeschrieben

test_submit_print_job_raises_disabled_for_disabled_printer hat # ... (existing fixture-Setup) ... als Placeholder. tests/services/test_print_service.py nutzt vermutlich einen @pytest.fixture disabled_printer der erst durch die neuen PrinterAdminService-Methoden befüllt werden kann. Der konkrete Fixture-Aufbau (Session, PrinterAdminService.create_printer, disable_printer) sollte ausgeschrieben sein — das ist ein Mutation-Pfad.

L2 — Coverage-Schwelle für admin_printers_web.py (70%) ist unter Projekt-Baseline für Business-Routes

Der Plan selbst setzt 70% für die Web-Routes, während die JSON-API (80%) und der Service (85%) höher liegen. Die Web-Routes enthalten Form-Parsing, Redirect-Logik und CSRF-Flow — das sind Mutations-Pfade. 70% ist nach test-coverage-pflicht.md für „HTMX-Event-Wiring / Form-Handler" Minimum 80%. Empfehlung: auf 80% anheben oder in die Coverage-Whitelist mit Begründung aufnehmen.


Was gut ist

  • TDD-Strict-Pattern in Phase 1 und Phase 2 (Tasks 1.1–2.5) ist vorbildlich: jeder Task hat Step 1 (Failing-Test), Step 2 (Run → FAIL), Step 3 (Implementation), Step 4 (Run → PASS), dann Commit. Kein einziger Task in diesen Phasen überspringt das Red-Step.
  • Dockhand Stack-Env Merge-Pattern in Task 8.3 korrekt umgesetzt (get → filter → put + Verifikation via assert).
  • redact_secrets Deep-Copy-Verhalten (Mutation-Schutz) durch test_redact_does_not_mutate_input explizit getestet — gut.
  • Phase 5.3 Grep-Verifikation (H9-Check) ist sauber: zwei separate Greps bestätigen dass keine upsert_runtime_printers- und derive_printer_id-Altaufrufer verbleiben.
  • Deutsche Umlaute durchgehend korrekt (ä/ö/ü/ß) — keine Verstöße gefunden.
  • Coverage-Schwellen-Tabelle im Self-Review ist vollständig und enthält Task-Referenzen für Verifikation.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-1: network-Team

Status: APPROVE mit 3 Anmerkungen (keine CRITICAL/HIGH)


Findings

MEDIUM — Phase 0 Step 3: MCP-Tool-Name stimmt nicht (wird fehlschlagen)

Plan schreibt:

mcp__pangolin-api__org_by_orgId_resources
mcp__pangolin-api__resource_by_resourceId

Das MCP-Tool heisst in dieser Umgebung so nicht — Pangolin MCP nutzt andere Tool-Namen (ermittelbar via Tool-Discovery). Subagent wird auf No such tool available laufen und abbrechen.

Fix: Step 3 umformulieren auf SSH-Fallback:

# Vault-Key: homelab-pangolin-integration-api (Feld: password)
PANGOLIN_URL="https://backend-api.strausmann.cloud"
curl -sf -H "Authorization: Bearer ${PANGOLIN_TOKEN}" \
  "${PANGOLIN_URL}/v1/org/strausmann/resources" \
  | python3 -c "import json,sys; [print(r['resourceId'], r.get('fullDomain')) for r in json.load(sys.stdin)['data']['resources'] if 'print-hub' in r.get('fullDomain','')]"

Alternativ: MCP-Tool-Namen via mcp__pangolin-api__* Discovery vor dem Call verifizieren. Der Pangolin-API-Key ist in Vaultwarden unter homelab-pangolin-integration-api (Feld password) — steht im SKILL.md, aber nicht im vault-map, also kein vault_load_service-Shortcut.

LOW — Phase 3.1 CSRF: zwei Implementierungs-Alternativen ohne Entscheidung

Plan zeigt setup_csrf_middleware() via starlette_csrf.CSRFMiddleware direkt UND darunter CSRFWithAuthSkip(BaseHTTPMiddleware) als "Alternative wenn starlette-csrf keinen Auth-Skip kann". Der Implementer muss wählen.

Empfehlung: starlette-csrf zuerst evaluieren (pip show starlette-csrf + Doku prüfen ob exempt_headers / exclude_patterns unterstützt werden). Falls Auth-Header-Skip eingebaut ist: direktes CSRFMiddleware. Falls nicht: Wrapper-Variante. Plan sollte das als sequentielle Entscheidung formulieren, nicht als zwei parallele Optionen — sonst landen beide im Code.

LOW — Phase 3.2 require_admin_user: Trust-Scope ist HomeLab-adäquat, mit einer Ergänzung

if request.headers.get("Authorization", "").lower().startswith("basic "):
    return "claude-automation"

Der Plan vertraut hier blindlings dem Authorization-Header, ohne den Credential-Wert zu prüfen. Pangolin reicht bei korrekt konfiguriertem Header-Auth-Bypass den Remote-User Header mit dem echten SSO-User durch — Authorization: Basic erscheint nur wenn der claude-automation Bypass greift. Das ist akzeptable Pragmatik für HomeLab.

Aber: Fehlt im Plan der Verweis auf den Pangolin Bug #3099. Im Browser-Smoke-Test (Phase 8.4) erscheint ein Basic-Auth-Dialog statt SSO-Login-Page — das ist kein Bug im Hub-Code, sondern bekannte Pangolin-Regression. Der Implementer muss wissen: Cancel im Dialog → SSO-Login erscheint. Ohne diesen Hinweis wird der Smoke-Test als fehlgeschlagen gewertet.

Fix: Phase 8.4 ergänzen:

Hinweis: Pangolin Bug #3099 — Browser zeigt evtl. Basic-Auth-Dialog statt SSO-Login. Cancel → SSO-Page erscheint. Kein Hub-Bug.


Was gut ist

  • Phase 6.1 Vault-Item: Collection Automation/Claude-Team + Naming Pangolin Header Auth - Print Hub exakt nach pangolin-resource-standard.md. Keine Abweichung.
  • Phase 6.2 Blueprint-Labels: Alle 14 Pflichtfelder vorhanden (name, full-domain, protocol, ssl, target, healthcheck mit hostname=print-hub, auth.sso-enabled, auth.basic-auth). hostname=print-hub als Service-Name ist der korrekte Container-DNS-Name im Compose-Setup.
  • Phase 8 down+start statt restart: Korrekt für Volume-Mount-Änderung. Dockhand-Merge-Pattern aus dockhand-stack-env-merge.md explizit im Spec referenziert.
  • Hangar→Hub intern: Kein Pangolin-Pfad, kein Header-Auth nötig. Richtig erkannt.
  • SSO-Label-Syntax: auth.sso-enabled=true korrekt (nicht sso=true, welches still ignoriert würde).

…2 HIGH + 6 MED + 8 LOW)

Round-1 Reviews: ops/storage/code-quality NEEDS_FIXES, network NEEDS_FIXES (light).
Folgendes wurde eingearbeitet:

CRITICAL
- C1 (ops) set_container_auto_update Parameter heisst policy=, nicht
  auto_update=. MCP-Tool-Schema verifiziert via ToolSearch. Task 8.2
  + 8.4 korrigiert.
- C2 (code-q) GET /api/printers filterte nicht auf enabled=true.
  Existing printers_repo.list_all() macht SELECT * ohne WHERE-Clause.
  Zwei neue Tasks ergaenzt: 2.6 (Repo include_disabled-Parameter),
  2.7 (Route nutzt es). Mit Failing-Tests und Coverage.
- C3 (network) MCP-Tools existieren nicht: FALSCH-POSITIV. Live-Check
  via ToolSearch zeigt beide Pangolin-MCP-Tools mit den exakten Namen.
  Plan-Notiz ergaenzt damit Round-2-Review das nicht erneut bringt.

HIGH
- H1 (ops) Rollback-Pfad fehlt: neue Task 8.5 mit 8 Steps fuer
  Stack-Down + SQLite-Restore + Compose-Revert + Stack-Env-Re-Merge
  + printers.yaml wiederherstellen + Stack-Start + Verifikation.
- H2 (code-q) Task 3.2 Placeholder-Tests: alle 9 Tests vollstaendig
  ausgeschrieben mit PAYLOAD-Konstante.

MEDIUM
- M1 (storage) Isolation-Level-Test gestrichen (False-Confidence).
- M2 (storage) downgrade() raised NotImplementedError → pass (no-op).
- M3 (code-q) Task 3.4 vollstaendig ausgeschrieben mit 5 Web-Route-Tests
  (Edit/Update/Disable-Confirm/Disable-POST/Enable-POST).
- M4 (code-q) Neue Task 6.3 fuer curl-Verifikation des
  Header-Auth-Bypass vor dem Smoke-Test.
- M5 (code-q) hangar_meta-Marker-Code entfernt — Tabelle existiert nicht
  im Hub.
- M6 (ops) Smoke-Schritte verifizieren in Task 8.4.

LOW
- L1 Pangolin Bug #3099 als Hinweis im Smoke-Step.
- L2 CSRF Custom-Wrapper-Variante explizit gewaehlt (statt 2 Alternativen).
- L3 Task 4.1 Fixture-Setup vollstaendig mit disabled_printer-Fixture +
  enabled_printer-Regression-Test.
- L4 Web-Routes Coverage 70% → 80%.
- L5 json_extract Integer-Output mit erwarteter Smoke-Output dokumentiert.
- L6 Test-Wording: "server_default" vs "Backfill-Funktion" jetzt zwei
  separate Tests, Backfill-Helper _backfill_snmp() exportiert fuer
  direkten Funktions-Test.

Spec-Coverage-Tabelle + Coverage-Schwellen erweitert um die neuen Tasks
(2.6, 2.7, 6.3, 8.5). printers_repo Coverage-Schwelle 85%.

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-2: ops-Team

Status: APPROVE (mit einem LOW-Finding zur expliziten Dokumentation)


Round-1 Findings — Verifikation

H1 Rollback (Task 8.5): Adressiert. 8 Steps vollständig. Reihenfolge ist korrekt: Stack down → DB-Restore → WAL/SHM-Removal → Compose-Revert → Env-Re-Merge → printers.yaml-Restore → Stack-Start → Verifikation. WAL/SHM-Removal in Step 2 direkt nach dem DB-Restore und vor Stack-Start ist die richtige Position — SQLite würde andernfalls beim Mount den WAL-Zustand wiederherstellen und den clean Restore überschreiben.

C1 policy= Fix (Tasks 8.2 + 8.4): Adressiert. Beide Calls nutzen jetzt korrekt policy="never" (8.2) und policy="any" (8.4). Das war das einzige ops-CRITICAL aus Round-1.

M6 Smoke-Schritte: Adressiert. Task 8.4 enthält jetzt json_extract Integer-Output-Hinweis (0/1 statt false/true) und Pangolin-Bug-#3099-Vermerk. Reicht.


Neue Round-2 Findings

[LOW] Task 8.5 Step 3: PRE_DEPLOY_COMPOSE_CONTENT implizit — Die Variable PRE_DEPLOY_COMPOSE_CONTENT in Rollback Step 3 wird als # aus Phase 0 Live-Check festgehalten kommentiert. Phase 0 Step 4 liest das Schema, speichert aber keinen Compose-Snapshot. Task 6.2 Step 1 holt mit get_stack_compose() den Live-Compose und zeigt ihn via print(existing["content"]) — aber der Plan schreibt nirgends explizit vor, diesen Wert in phase0-live-check-results.md zu persistieren. Empfehlung: in Task 6.2 Step 3 (Hinweis im Plan) einen Pflichtschritt ergänzen: PRE_DEPLOY_COMPOSE_CONTENT = existing["content"] aus Step 1 in die Live-Check-Doku schreiben — als Copy-Paste-Block, damit Rollback ohne Gedächtnis funktioniert. Nicht blocker, aber bei unerwarteten Fehlern ist der Compose-Stand die einzige Restore-Basis.

[KEINE ISSUE] Task 6.3 Reihenfolge: Die curl-Schritte in 6.3 sind als Pre-Deploy-Verifikation markiert — das macht sie zu einem "diese Labels funktionieren bereits"-Check. Der Text sagt klar "Nach Stack-Update in Phase 8.3 muss der Header-Auth-Bypass funktionieren. Dieser Task wird VOR dem Smoke-Test (8.4) explizit ausgeführt". Das bedeutet: 6.3 läuft nach 8.3, nicht als Phase-6-Check. Die Positionierung im Plan unter Phase 6 ist irreführend, aber der Text-Kommentar in Task 6.3 macht die tatsächliche Ausführungsreihenfolge klar. Kein Bug.

[KEINE ISSUE] Task 2.6/2.7 und Hangar PrinterSync: Plan sagt, Hangar filtert disabled Drucker bereits im Cache-Layer heraus. Smoke-Test 8.4 Step 2 verifiziert DNS-Erreichbarkeit zwischen den Containern. Ein dedizierter /api/printers-Filtertest läuft in Task 7.1 (test_fresh_install_disable_filters_from_public_list). Das reicht — kein weiterer Smoke-Step nötig.


Ops-Team gibt APPROVE. Der LOW-Hinweis zu PRE_DEPLOY_COMPOSE_CONTENT kann inlined beim Ausführen von Task 6.2 Step 3 adressiert werden — kein neuer Plan-Round erforderlich.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-2: network-Team

Status: APPROVE

Round-1 Findings — Verifikation

C3 Falsch-Positiv akzeptiert: Ja
Die Notiz in der Findings-Tabelle ist klar und vollständig. Live-Verifikation per ToolSearch hat beide Tools mit den exakten Namen aus dem Plan bestätigt. Kein curl-Fallback nötig — Phase 0 Step 3 bleibt wie geschrieben.

L1 Pangolin Bug #3099: adressiert
Task 8.4 Step 4 erklärt den Workaround korrekt: Cancel im Basic-Auth-Dialog → HTML-Body-Fallback → SSO-Login-Page. Der Link auf den upstream Issue ist vorhanden. Reicht für einen Smoke-Test-Hinweis.

L2 CSRF-Variante: adressiert
Entscheidung für Wrapper ist klar dokumentiert (Task 3.1 Step 3 Kommentar: "L2-Round-1 Entscheidung gegen die zwei Alternativen"). Der Wrapper skipped CSRF wenn -Header gesetzt ist — das ist die korrekte Implementierung für den Header-Auth-Bypass.

Neue Round-2 Findings

R2-N1 (LOW): Task 6.3 Reihenfolge — leicht irreführend
Task 6.3 trägt die Nummer "6.3" und liegt in Phase 6, aber sein Beschreibungstext sagt "Nach Stack-Update in Phase 8.3". Die Aufgabe wird also nach 8.3 ausgeführt, ist aber als Phase-6-Task nummeriert. Das ist nicht falsch, aber verwirrend für einen Implementer der sequenziell arbeitet.

Empfehlung: In Task 6.3 Intro-Zeile klarstellen: "Dieser Task ist in Phase 6 vorbereitet (Credentials, Vault), wird aber nach 8.3 ausgeführt — erst dann sind die Labels live." Die aktuelle Formulierung sagt das schon halb, aber ein expliziter Satz "Ausführungs-Zeitpunkt: nach Task 8.3, vor Task 8.4" beseitigt jeden Zweifel.

R2-N2 (LOW): Pangolin-Resource Bestand-Detection fehlt
Phase 0 Step 3 prüft ob bereits gesetzt ist. Phase 6.2 überschreibt die Labels immer — auch wenn bereits ein anderer Header-Auth-User/Password aktiv ist. Wenn die Resource bereits mit anderem Secret hat (z.B. aus manuellem Dashboard-Set), überschreiben die Blueprint-Labels das beim nächsten Sync.

Das ist per ("Labels sind Source of Truth, API-Änderungen werden überschrieben") korrekt und gewollt. Kein Plan-Defekt, nur eine Klarstellung: wenn Phase 0 Step 3 zeigt dass , muss das Secret aus dem Vault-Item in 6.1 mit dem bestehenden übereinstimmen oder bewusst überschrieben werden. Dieser Hinweis fehlt in Task 6.1/6.2.

Empfehlung: In Task 6.2 Step 2 eine Bemerkung: "Falls Phase 0 Step 3 zeigt dass headerAuth bereits gesetzt ist: das neue Secret aus Task 6.1 ersetzt es. Vault-Item entsprechend anlegen/aktualisieren."


Beide Round-2-Findings sind LOW und blockieren die Umsetzung nicht. Der Plan ist in allen network-relevanten Punkten korrekt ausgearbeitet.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-2: storage-Team

Status: NEEDS_FIXES (1 MED neu)

Round-1 Findings — Verifikation

  • M1 Isolation-Test gestrichen: adressiert. Nur PRAGMA-Test bleibt — zuverlässig und aussagekräftig. OK.
  • M2 downgrade → pass: adressiert. Docstring erklärt warum kein SQLite-DDL-Rollback möglich ist. CI-konform. OK.
  • L5 json_extract Output: adressiert. Task 8.4 Step 3 nennt explizit 0/1 statt false/true und zeigt erwarteten Smoke-Output. OK.
  • L6 Test-Wording + Backfill-Function-Test: teilweise adressiert — der Backfill-Test ist da, aber enthält einen Sync/Async-Mismatch (siehe unten).

Neue Round-2 Findings

[MED] R2-M1 — Sync/Async-Mismatch in test_backfill_function_idempotent_and_safe

Der Test ruft _backfill_snmp(conn) mit einem AsyncConnection auf (async with engine.begin() as conn), aber _backfill_snmp verwendet synchrone SQLAlchemy-Aufrufe ohne await (bind.execute(sa.text(...))).

Das ist auf der Produktions-Seite korrekt: env.py nutzt run_sync(do_run_migrations) — der bind den op.get_bind() in upgrade() liefert, ist eine echte sync Connection. Die Funktion selbst ist richtig.

Das Problem liegt im Test: AsyncConnection.execute() ohne await gibt ein Coroutine-Objekt zurück das nie ausgeführt wird. Die Rows werden nicht verändert, der Test läuft aber ohne Exception durch — er prüft damit nichts.

Fix: Test muss engine.sync_engine.begin() als sync-Context nutzen, oder den Helper über connection.run_sync(_backfill_snmp) aufrufen:

# Option A — sync engine direkt
with engine.sync_engine.begin() as conn:
    mig._backfill_snmp(conn)

# Option B — run_sync Pattern (spiegelt Alembic-Produktionsverhalten)
async with engine.begin() as conn:
    await conn.run_sync(mig._backfill_snmp)

Option B ist vorzuziehen weil sie 1:1 das Alembic-run_sync-Pattern aus env.py spiegelt.


[LOW] R2-L1 — Rollback Task 8.5 Step 2: Reihenfolge ist korrekt

Die Frage war ob WAL/SHM vor oder nach dem Restore gelöscht werden müssen. Die Plan-Reihenfolge (cp backup → db, dann rm WAL/SHM) ist korrekt: Stack ist bereits in Step 1 gestoppt, kein SQLite-Prozess kann neue WAL-Dateien anlegen. Das WAL/SHM-Löschen nach dem Restore bereinigt stale Dateien die zur alten (fehlerhaften) DB gehören. Kein Fix nötig.


[LOW] R2-L2 — C2 list_all bestehende Aufrufer

Task 2.6 Step 5 sagt bereits: pytest -x --ff -q läuft und bei brechenden Aufrufern auf include_disabled=True umstellen. Das ist ausreichend — ein expliziter grep-Schritt wäre nice-to-have, aber kein Blocker.


Zusammenfassung

Ein echter Bug: R2-M1 muss gefixt werden bevor der Test aussagekräftig ist — aktuell testet test_backfill_function_idempotent_and_safe die Backfill-Logik nicht wirklich. Alle Round-1-Findings bis auf L6 sind vollständig adressiert.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-2: code-quality

Status: NEEDS_FIXES (2 neue Findings, 1 davon MEDIUM)


Round-1 Findings — Verifikation

  • H1 GET /api/printers (C2): adressiert — Tasks 2.6 + 2.7 vollständig, 3 + 2 Tests konkret ausgeschrieben, list_all Signatur korrekt mit include_disabled: bool = False, Route nutzt Default.
  • H2 Task 3.2 Placeholder: adressiert — alle 9 Tests vollständig ausgeschrieben, kein ... oder pass.
  • M3 Task 3.4 Tests: adressiert — 5 Tests mit Setup, CSRF-Cookie-Roundtrip und konkreten Assertions.
  • M4 Task 6.3 curl-Verifikation: adressiert — 4 Steps mit konkreten curl-Kommandos, Secret-Abruf aus Vault, erwartetes HTTP-Status je Step, Ergebnis-Dokumentation in phase0-Doku.
  • M5 hangar_meta entfernt: adressiert — Task 5.2 enthält expliziten Kommentar "M5-Round-1: Der Marker-Code ist weggefallen — Tot-Code", Funktion wird vollständig gelöscht ohne Ersatz.
  • L3 Task 4.1 Fixture-Setup: adressiert — disabled_printer-Fixture vollständig mit allen Feldern inkl. queue_timeout_s, cut_defaults_half_cut, created_at, updated_at. Regression-Test für enabled_printer ergänzt.
  • L4 Web-Coverage 80%: adressiert — Coverage-Tabelle enthält app/api/routes/admin_printers_web.py mit 80% und kommentiertem Hinweis "L4-Round-1: 70%→80%".

Neue Round-2 Findings

R2-M1 (MEDIUM) — Task 8.5 Step 4: Potential-Duplikat bei Rollback-Env-Merge

# Task 8.5 Step 4:
existing = mcp__dockhand__get_stack_env(...)
merged = list(existing["variables"]) + [
    {"key": "PRINTER_CONFIG_PATH", "value": "/etc/printer-hub/printers.yaml", ...},
]

Wenn PRINTER_CONFIG_PATH zu diesem Zeitpunkt noch in existing ist (z.B. weil Task 8.3 Step 1 partiell fehlgeschlagen ist und der Key nicht entfernt wurde), enthält merged den Key zweimal. Dockhand-Verhalten bei Duplikaten ist undokumentiert — könnte zu unerwartetem Compose-State führen.

Fix: vor dem Concat explizit filtern:

merged = [v for v in existing["variables"] if v["key"] != "PRINTER_CONFIG_PATH"] + [
    {"key": "PRINTER_CONFIG_PATH", "value": "/etc/printer-hub/printers.yaml", "isSecret": False},
]

Das ist symmetrisch zu Task 8.3 Step 1 und macht den Rollback idempotent.


R2-L1 (LOW) — Task 8.5 Step 3: PRE_DEPLOY_COMPOSE_CONTENT nicht explizit gesichert

Task 8.5 Step 3 referenziert PRE_DEPLOY_COMPOSE_CONTENT als Variable "aus Phase 0 Live-Check festgehalten". Phase 0 Step 4 sichert aber nur das DB-Schema (sqlite3 .schema printers), nicht den Compose-Inhalt.

Der Rollback-Pfad setzt voraus dass der Agent den originalen Compose-Content irgendwo hat — das ist nur der Fall wenn Task 6.2 Step 1 (get_stack_compose) ausgeführt wurde und das Ergebnis in der phase0-Doku notiert ist. Task 6.2 Step 3 sagt "in phase0-live-check-results.md festhalten", aber nicht explizit dass der volle Compose-Content gespeichert werden muss (nur Vault-Item-UUID + Label-Notizen).

Fix: In Task 0.1 (Phase 0) einen zusätzlichen Step ergänzen:

Step 4b: Compose-Inhalt für Rollback sichern

compose = mcp__dockhand__get_stack_compose(environmentId=10, name="hangar-print-hub")
# Vollständigen content in phase0-live-check-results.md als Code-Block speichern

Expected: YAML-Content in phase0-Doku, damit Task 8.5 Step 3 darauf zugreifen kann.

Alternativ in Task 8.5 Step 3 den Abruf direkt vor dem Revert machen (aber dann ist fraglich ob der Container schon crasht und get_stack_compose noch funktioniert). Phase-0-Sicherung ist robuster.


R2-INFO — Task 2.7 Test-Konsistenz: kein Query-Param mehr, Test OK

test_get_api_printers_include_disabled_query_param_admin_only prüft dass der ?include_disabled=true Query-Param auf dem Public-Endpoint ignoriert wird (disabled bleibt raus). Das ist konsistent mit dem Route-Code in Step 3 der keinen include_disabled-Parameter kennt — printers_repo.list_all(session) ohne Argument filtert immer. Kein Bug, kein Fix nötig.


Zusammenfassung

R2-M1 ist ein echter Rollback-Korrektheitsfehler — im Fehlerfall (den dieser Task absichern soll) kann der Env-State inkonsistent werden. Bitte vor Round-3 adressieren. R2-L1 ist ein Dokumentationslücke die den Rollback-Pfad fragil macht.

Round-2 Reviews: ops/network APPROVE, storage/code-quality NEEDS_FIXES.

MEDIUM
- R2-M1 (storage): Task 1.3 Backfill-Test rief sync _backfill_snmp
  mit AsyncConnection auf — Coroutine nie ausgefuehrt, Test prueft
  nichts. Fix: await conn.run_sync(mig._backfill_snmp) — spiegelt
  alembic env.py Pattern.
- R2-M2 (code-q): Task 8.5 Step 4 Rollback-Env-Re-Merge konnte
  PRINTER_CONFIG_PATH duplizieren wenn 8.3 partiell durchgelaufen.
  Filter analog 8.3 vor dem Append ergaenzt.

LOW
- R2-L1 (ops + code-q): PRE_DEPLOY_COMPOSE_CONTENT war nicht
  explizit gesichert. Neue Phase 0 Step 4b mit
  get_stack_compose-Output in phase0-live-check-results.md.
- R2-L2 (network): Task 6.3 curl-Verifikation hatte unklaren
  Ausfuehrungszeitpunkt. Verschoben nach Task 8.3.5 (zwischen
  start_stack und Smoke). Phase 6 nur noch Vorbereitung
  (Vault-Item + Compose-Labels), Live-Test in Phase 8.
- R2-L3 (network): Bestand-Detection-Pfad fuer Pangolin-Resource
  ergaenzt. Phase 0 Step 3 mit Entscheidungsbaum:
  headerAuth null → neu anlegen
  headerAuth.user==claude-automation → Passwort holen statt neu
  headerAuth.user!=claude-automation → STOP, User-Klaerung
  healthCheck.enabled==false → Labels ergaenzen (Newt v1.18.4)

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-3: ops-Team

Status: APPROVE

Round-2 Findings — Verifikation

  • R2-L1 Compose-Content (Phase 0 Step 4b): Adressiert. get_stack_compose()["content"] wird gesichert und als Code-Block unter ## Pre-Deploy Compose-Snapshot in phase0-live-check-results.md eingetragen. Task 8.5 Step 3 referenziert PRE_DEPLOY_COMPOSE_CONTENT konsistent. Reicht vollständig.

  • R2-M2 Task 8.5 Step 4 Filter (cross-team mit code-q): Adressiert. Filter [v for v in existing["variables"] if v["key"] != "PRINTER_CONFIG_PATH"] vor dem Append ist analog zu 8.3 Step 1 — verhindert Duplikat auch bei partiellem 8.3-Durchlauf. Korrekt.

Round-3 Spezifisch

Task 8.3.5 Reihenfolge (R2-L2): Verschiebung nach Phase 8 ist korrekt. sleep 60 als Newt-Sync-Puffer ist ein bewährter Ansatz (Newt-Sync-Zeit aus HomeLab-Memory bestätigt). Reihenfolge 8.3 (Stack-Restart) → 8.3.5 (curl mit sleep 60) → 8.4 (Smoke-Test) ist logisch zwingend — Pangolin sieht Labels erst nach Stack-Restart. Sinnvoll.

Phase 0 Step 3 Bestand-Detection (R2-L3): 4-Fälle-Entscheidungsbaum deckt alle Zustände ab: null/claude-automation/fremder-User/healthCheck-disabled. Der STOP-Fall bei fremdem User ist wichtig und korrekt als manuelle Klärung markiert. Vollständig.

Keine neuen Round-3-Findings aus ops-Perspektive.


ops-Team Review, Sonnet 4.6

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-3: network-Team

Status: APPROVE

Round-2 Findings — Verifikation

R2-L2 Task 8.3.5 Verschiebung: adressiert. Task 6.3 ist jetzt ein leerer Platzhalter mit explizitem Hinweis auf Task 8.3.5. Die Positionierung zwischen 8.3 (start_stack) und 8.4 (Smoke-Test) ist korrekt.

R2-L3 Bestand-Detection: adressiert. Phase 0 Step 3 enthält einen vollständigen 4-Fall-Entscheidungsbaum (null / claude-automation / anderer User / healthCheck.disabled).

Round-3 Findings

Task 8.3.5 sleep 60 — akzeptabel mit Vorbehalt (LOW):
Step 1 enthält sleep 60 als Puffer. 60 Sekunden sind ein Minimum; Newt-Sync kann bis zu 5 Minuten dauern. Step 5 hat einen expliziten Retry-Pfad ("weitere 2 Min warten und wiederholen"), der das abfängt. Ein until-Loop mit Timeout wäre präziser, aber der Retry-Pfad ist klar genug beschrieben. Kein NEEDS_FIXES.

Bestand-Detection — ssoEnabled == false nicht im Baum (LOW):
Der Entscheidungsbaum prüft headerAuth und healthCheck, aber nicht explizit auth.ssoEnabled == false. In der Praxis ist SSO für print-hub aktiv. Falls es abgeschaltet wäre, würde Phase 6.2 auth.sso-enabled=true trotzdem setzen — was korrekt ist. Der fehlende Fall ist kein Blocking-Issue. Kein NEEDS_FIXES.

Task 6.2 Compose-Snippet: Passwort-Klartext korrekt auf pangolin-resource-standard.md-Workaround referenziert. LGTM.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-3: storage-Team

Status: APPROVE (mit einem LOW-Hinweis)


Round-2 Findings — Verifikation

R2-M1 run_sync: vollständig adressiert.

Task 1.3, Zeilen 488-493: der Test ruft jetzt await conn.run_sync(mig._backfill_snmp) auf. _backfill_snmp(bind) nimmt eine sync Connection als erstes Argument — genau das liefert run_sync. Die Coroutine wird jetzt tatsächlich ausgeführt. Analogie zu alembic env.py ist korrekt gewählt. Idempotenz-Prüfung (2x-Aufruf) ist damit funktional. ✅


Neue Round-3 Findings

R3-L1 (LOW): Task 8.5 Step 2 — WAL/SHM-Reihenfolge semantisch falsch

Aktuell: cp backup → db dann rm -f db-wal db-shm

SQLite WAL-Semantik: Das alte .db-wal enthält Transaktionen nach dem Backup-Zeitpunkt. Wenn das restaurierte .db zusammen mit dem alten .db-wal im Filesystem liegt — auch nur für Millisekunden — kann SQLite beim nächsten Open einen WAL-Recovery versuchen der nicht zum Checkpoint-Stand des Backups passt. Stack ist zwar gestoppt (Step 1), aber die sicherere Reihenfolge ist:

Risikobewertung: LOW — Stack ist in Step 1 gestoppt, kein Prozess öffnet die DB zwischen cp und rm. In der Praxis kein Crash-Risiko, aber semantisch ist rm-vor-cp die korrekte Reihenfolge bei SQLite-WAL-Restore.


Alle anderen Aspekte (R2-M2 Filter in Step 4, R2-L1 Phase-0-Sicherung, Rollback-Gesamtstruktur) sind korrekt. Plan ist umsetzungsbereit.

@strausmann

Copy link
Copy Markdown
Owner Author

Plan-Review Round-3: code-quality

Status: APPROVE


Round-2 Findings — Verifikation

  • R2-M2 Task 8.5 Step 4 Filter: vollständig adressiert. Das Filter-Pattern ist korrekt und konsistent mit 8.3 Step 1:

    merged = [v for v in existing["variables"] if v["key"] != "PRINTER_CONFIG_PATH"]
    merged.append({"key": "PRINTER_CONFIG_PATH", "value": "...", "isSecret": False})

    isSecret: False ist korrekt für einen YAML-Pfad — kein Secret-Wert. Kommentar "Filter analog 8.3" macht die Intention explizit. ✅

  • R2-L1 PRE_DEPLOY_COMPOSE_CONTENT: Phase 0 Step 4b ist klar formuliert. Der Aufruf, die Sicherungsanweisung als ## Pre-Deploy Compose-Snapshot Code-Block und die Referenz content=PRE_DEPLOY_COMPOSE_CONTENT in Task 8.5 Step 3 bilden eine geschlossene Kette. Implementer kann die Variable nicht übersehen. ✅


Neue Round-3 Findings

R3-L1 (LOW): Task 6.3 Verweis-Stub — Navigation leicht riskant

Der 3-Zeilen-Stub in Phase 6 lautet sinngemäß: "Dieser Schritt wurde nach Task 8.3.5 verschoben." Das ist korrekt, aber ein Implementer der sequenziell arbeitet könnte in Phase 6 stoppen und denken, die curl-Verifikation entfällt. Der Stub enthält keinen direkten Anker-Link auf Task 8.3.5, nur "Siehe Task 8.3.5 unten". In einem 4000-Zeilen-Plan ist das ausreichend, wenn der Implementer von oben nach unten arbeitet — aber bei Subagent-Driven-Development, wo Agents direkt auf benannte Tasks dispatcht werden, könnte Phase 6 ohne 8.3.5 als vollständig erscheinen. Empfehlung: Stub mit explizitem Hinweis "8.3.5 ist Pflicht-Schritt, KEIN optional" ergänzen. Kein Blocker.

R3-N1 (NOTE): Phase 0 Step 3 STOP-Fall — Fallback vorhanden, ausreichend

Der Entscheidungsbaum hat einen headerAuth.user != "claude-automation" STOP-Fall. Dieser Fall fordert "manuelle Klärung mit User: Entscheidung ersetzen oder zweiten Bypass-Account anlegen?" — das ist korrekt und realistisch für einen HomeLab-Betrieb. Ein Issue-Kommentar-Template fehlt, aber der STOP-Fall blockiert den Subagent sauber ohne dass er rät. Akzeptabel.

R3-N2 (NOTE): Coverage-Tabelle vs. Task 8.3.5

Task 8.3.5 ist ein reiner curl-Verifikations-Task ohne neuen Code — keine Coverage-Implikation. Die Coverage-Tabelle referenziert ihn korrekt nicht. ✅


Gesamtbewertung

Alle Round-2 Findings korrekt und vollständig adressiert. R3-L1 ist ein Low-Risiko Navigations-Hinweis der bei sequenzieller Ausführung nicht relevant ist. Plan ist umsetzungsreif.

Round-3 Reviews: alle 4 Teams APPROVE. 4 verbleibende non-blocking LOWs
zur Vollstaendigkeit eingearbeitet:

- R3-L1 (network): sleep 60 in Task 8.3.5 Step 1 jetzt als
  Retry-Schleife (5×60s, max 5 Min, mit Fail-Hinweis fuer
  Newt-Logs-Check).
- R3-L2 (network): Bestand-Detection-Baum Phase 0 Step 3 um Zeile
  auth.ssoEnabled==false erweitert (Phase 6.2 aktiviert SSO ohnehin).
- R3-L3 (storage): Task 8.5 Step 2 SQLite-Restore: rm WAL/SHM (2a)
  VOR cp DB (2b) — semantisch korrekt damit kein altes WAL kurzzeitig
  mit neuer DB koexistiert.
- R3-L4 (code-q): Task 6.3-Stub PFLICHT-HINWEIS-Blockquote ergaenzt
  damit Subagent-Driven-Development Phase 6 nicht ohne 8.3.5 als
  komplett markiert.

Status: Plan Round-4 FINAL. Alle 4 Teams APPROVE in Round-3,
alle 4 Round-3-LOWs nun adressiert. Bereit fuer
superpowers:subagent-driven-development.

Refs #124

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant