Skip to content

docs: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes (Spec + Plan)#108

Merged
strausmann merged 9 commits into
mainfrom
spec/phase-1k1-layout-engine
Jun 9, 2026
Merged

docs: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes (Spec + Plan)#108
strausmann merged 9 commits into
mainfrom
spec/phase-1k1-layout-engine

Conversation

@strausmann

@strausmann strausmann commented Jun 5, 2026

Copy link
Copy Markdown
Owner

Summary

Spec-Only PR fuer Phase 1k.1 Layout-Engine. Keine Code-Aenderungen — nur das Design-Dokument unter docs/superpowers/specs/2026-06-05-phase-1k1-layout-engine-design.md.

Brainstorming-Workflow: Vollstaendig durchlaufen mit User (5 Klaerungs-Fragen + 5 Design-Sektionen + User-Approval + 5 Review-Runden). Subsumiert die Phase 7e-Spec (docs/superpowers/specs/2026-05-17-phase-7e-template-layout-v2-design.md) die nie implementiert wurde.

Highlights

Hard-Cut Migration: System ist in Entwicklung, keine Legacy-Kompatibilitaet. Alle 21 hartcodierten YAML-Templates + LabelRenderer + TemplateLoader + Templates-Tabelle werden geloescht. jobs.template_key bleibt als nullable Audit-Spalte erhalten (Provenance-Hint fuer historische Jobs).

7 ContentTypes (semantisch, tape-unabhaengig):

  • qr_only, qr_one_line, qr_two_lines, qr_three_lines (3-Zeilen-Layouts mit secondary), text_one_line, text_two_lines, qr_with_listing (Kallax-Aggregation aus 7e)

TapeGeometry initial fuer 7 Tape-Groessen (4/6/9/12/18/24/62mm — int, kleinste PT-TZe ist 4mm). 12mm scan-verifiziert via Phase 1i V4-Winner; andere via Pixel-Ratio extrapoliert + Smoke-Test follow-up. Bestehende QL-DK-Breiten (29/38/50/54mm) sind initial out-of-scope (-> UnsupportedTapeError, Folge-Phase moeglich).

Tape-Unabhaengigkeit: Hangar sendet content_type (kein tape_mm), Hub liest preflight.loaded_tape_mm und rendert passend. TapeMismatchError wird obsolet.

YAML-frei: Hangar Categories werden nicht mehr aus YAML, sondern aus Go-Code DefaultCategories Slice initial geseedet. HUB_LAYOUTS_PATH wird in 1k.1c deprecated.

LabelData Schema-Update: title/primary_id/qr_payload werden auf optional (str | None) gesetzt; ContentType-spezifische Pflichtfeld-Validation passiert zentral in LayoutEngine._validate_data().

Scope-Decomposition (4 Sub-Phasen)

Phase Scope Repo Schaetzung
1k.1a Hub Layout-Engine + neue API + Cleanup Label-Printer-Hub ~1 Woche
1k.1b Hangar API-Migration (template_id -> content_type) hangar ~3-5 Tage
1k.1c Hangar Categories DB + CRUD-Editor + Live-Preview + Go-Defaults hangar ~1 Woche
1k.1d Hangar Navigation-Refactor (Administration-Submenu) hangar ~3-4 Tage

Sequentielle Merge-Reihenfolge: a -> b -> c -> d. Jede Phase eigenstaendig deploy-bar.

Review-Verlauf (5 Runden)

  • R1: ops-agent (3 CRITICAL, 5 MEDIUM, 3 LOW, 5 PRAISE) + Gemini (2 MEDIUM) + Copilot (10 inline)
  • R2: Copilot (7) + User-Designentscheidung "YAML-frei via Go-Defaults"
  • R3: Copilot (4, davon 1 Design: template_key behalten)
  • R4: Copilot (4, alles Klarstellungen)
  • R5: Copilot (4, finishing touches)

Alle Findings adressiert. Spec ist ship-ready fuer writing-plans.

Test plan

  • Spec liest sich konsistent (Sektionen widerspruchsfrei)
  • Out-of-Scope-Liste vollstaendig (Sektion 11)
  • Definition of Done pro Sub-Phase ausreichend granular (Sektion 10)
  • Alle Files-Listen (Neu/Modifiziert/Geloescht) sind explizit
  • Migration-SQL deterministisch + SQLite-kompatibel
  • LabelData Schema-Anpassung explizit benannt
  • Pflichtfeld-Matrix pro ContentType in Spec

Closes #81 beim Merge (7e-Spec subsumiert).
Refs #103 (Phase 1k.1 unter Umbrella #101).

Generated with Claude Code.

Vollstaendiges Design fuer Layout-Engine die 21 hartcodierte YAML-Templates
durch eine semantische Engine ersetzt. Tape-unabhaengige ContentTypes
(6 Typen: qr_only, qr_one_line, qr_two_lines, text_one_line,
text_two_lines, qr_with_listing) plus TapeGeometry-Tabelle fuer 7 Brother
Tape-Groessen (3.5/6/9/12/18/24/62mm).

Hard-Cut Migration: keine Legacy-Kompatibilitaet, alle alten Routes und
21 YAML-Templates werden geloescht. TapeMismatchError wird obsolet.

Scope decomposed in 4 Sub-Phasen:
- 1k.1a Hub Layout-Engine + neue API (Python)
- 1k.1b Hangar API-Migration (Go)
- 1k.1c Hangar Categories DB + CRUD-Editor + Live-Preview (Go)
- 1k.1d Hangar Navigation-Refactor (Administration-Submenu)

Subsumiert die Phase 7e-Spec (2026-05-17) die nie implementiert wurde.
Hardware-Baseline: Phase 1i V4-Winner (12mm scan-verifiziert), andere
Tape-Groessen via Pixel-Ratio extrapoliert + Smoke-Test follow-up.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 5, 2026 10:46
@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!

Dieser Pull Request enthält das Design-Dokument für die Phase 1k.1, welche die Layout-Engine des Label-Druck-Systems grundlegend modernisiert. Ziel ist es, die Wartbarkeit durch eine semantische Trennung von Inhalt und physischer Tape-Geometrie zu erhöhen und die bisherige, unflexible Template-Struktur durch eine skalierbare Engine zu ersetzen.

Highlights

  • Layout-Engine Architektur: Einführung einer semantischen Layout-Engine, die 21 hartcodierte YAML-Templates durch eine dynamische Lösung ersetzt, welche auf TapeGeometry und 6 ContentTypes basiert.
  • Tape-Unabhängigkeit: Das System rendert nun basierend auf dem physisch eingelegten Tape, wodurch der 'TapeMismatchError' obsolet wird und die API-Aufrufe vereinfacht werden.
  • Hard-Cut Migration: Vollständige Entfernung der Legacy-Template-Infrastruktur und Umstellung aller Aufrufer auf die neue API, unterstützt durch eine 4-phasige Scope-Decomposition.
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.

@codecov

codecov Bot commented Jun 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.45%. Comparing base (126d2d4) to head (1098473).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #108      +/-   ##
==========================================
- Coverage   89.48%   89.45%   -0.03%     
==========================================
  Files          96       96              
  Lines        4478     4478              
  Branches      388      388              
==========================================
- Hits         4007     4006       -1     
  Misses        369      369              
- Partials      102      103       +1     
Components Coverage Δ
Printer Backends (transport) 86.41% <ø> (ø)
Printer Models (drivers) 88.20% <ø> (ø)
Services 90.58% <ø> (-0.07%) ⬇️
REST API 84.83% <ø> (ø)
Pydantic Schemas 100.00% <ø> (ø)
Integration Plugins 100.00% <ø> (ø)
see 1 file with indirect coverage changes
Flag Coverage Δ
backend 89.45% <ø> (-0.03%) ⬇️

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 126d2d4...1098473. 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.

@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 comprehensive design for a new Layout Engine that replaces hardcoded YAML templates with a semantic, tape-independent architecture using TapeGeometry and ContentType abstractions. The design covers a four-phase migration plan, including backend logic updates, Hangar API changes, a new categories database, and navigation refactoring. The review feedback suggests enhancing the Pydantic model with explicit validation constraints for better type safety and utilizing Alembic's batch operations to ensure SQLite compatibility during database migrations.

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.

Comment on lines +56 to +71
from pydantic import BaseModel, ConfigDict


class TapeGeometry(BaseModel):
"""Render-Parameter pro Tape-Groesse (alle Werte in Pixel)."""
model_config = ConfigDict(frozen=True, extra="forbid")

printable_px: int # Brother Pin-Count fuer die Tape-Groesse
qr_max_px: int # printable_px - 4 (2px Padding pro Seite)
qr_padding_px: int # Padding um den QR-Code
text_start_x: int # X-Position wo Text nach QR beginnt
line_spacing_px: int # Vertikaler Abstand zwischen Text-Zeilen
font_xl: int # primary_id Groesse
font_l: int # title Groesse
font_m: int # listing item Groesse
font_s: int # secondary Groesse (reserviert)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To leverage automatic request-time validation and standard error reporting (e.g., 422 Unprocessable Entity), apply validation constraints such as Field(gt=0) or Field(ge=0) to the pixel and font size fields in the TapeGeometry model.

Suggested change
from pydantic import BaseModel, ConfigDict
class TapeGeometry(BaseModel):
"""Render-Parameter pro Tape-Groesse (alle Werte in Pixel)."""
model_config = ConfigDict(frozen=True, extra="forbid")
printable_px: int # Brother Pin-Count fuer die Tape-Groesse
qr_max_px: int # printable_px - 4 (2px Padding pro Seite)
qr_padding_px: int # Padding um den QR-Code
text_start_x: int # X-Position wo Text nach QR beginnt
line_spacing_px: int # Vertikaler Abstand zwischen Text-Zeilen
font_xl: int # primary_id Groesse
font_l: int # title Groesse
font_m: int # listing item Groesse
font_s: int # secondary Groesse (reserviert)
from pydantic import BaseModel, ConfigDict, Field
class TapeGeometry(BaseModel):
"""Render-Parameter pro Tape-Groesse (alle Werte in Pixel)."""
model_config = ConfigDict(frozen=True, extra="forbid")
printable_px: int = Field(gt=0) # Brother Pin-Count fuer die Tape-Groesse
qr_max_px: int = Field(gt=0) # printable_px - 4 (2px Padding pro Seite)
qr_padding_px: int = Field(ge=0) # Padding um den QR-Code
text_start_x: int = Field(ge=0) # X-Position wo Text nach QR beginnt
line_spacing_px: int = Field(ge=0) # Vertikaler Abstand zwischen Text-Zeilen
font_xl: int = Field(gt=0) # primary_id Groesse
font_l: int = Field(gt=0) # title Groesse
font_m: int = Field(gt=0) # listing item Groesse
font_s: int = Field(gt=0) # secondary Groesse (reserviert)
References
  1. In Pydantic models, use typed fields and validation constraints (such as Field(ge=1)) to leverage automatic parsing, validation, and standard error reporting at request time.

Comment on lines +215 to +218
def upgrade() -> None:
op.add_column("jobs", sa.Column("content_type", sa.String(32)))
op.add_column("jobs", sa.Column("rendered_tape_mm", sa.Float))
op.drop_column("jobs", "template_id")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For SQLite compatibility, use Alembic's batch operations (op.batch_alter_table) when dropping or adding columns, as SQLite does not support standard direct column drops.

Suggested change
def upgrade() -> None:
op.add_column("jobs", sa.Column("content_type", sa.String(32)))
op.add_column("jobs", sa.Column("rendered_tape_mm", sa.Float))
op.drop_column("jobs", "template_id")
def upgrade() -> None:
with op.batch_alter_table("jobs") as batch_op:
batch_op.add_column(sa.Column("content_type", sa.String(32)))
batch_op.add_column(sa.Column("rendered_tape_mm", sa.Float))
batch_op.drop_column("template_id")

@strausmann

Copy link
Copy Markdown
Owner Author

Technische Abnahme: Phase 1k.1 Layout-Engine Spec

Reviewer: ops-agent (technische Abnahme, kein Code geändert)
Datum: 2026-06-05
Branch: spec/phase-1k1-layout-engine


CRITICAL

C1 — Hard-Cut Liste ist unvollständig: main.py, lifespan.py, svg_renderer.py, templates_preview.py

Die Spec listet in Sektion 5 „Gelöschte Files" folgende Files, übersieht aber 5 weitere Abhängigkeiten die aktiv in der Codebase existieren:

File Referenziert via Fehlt in Spec?
backend/app/services/svg_renderer.py importiert DEFAULT_LABEL_WIDTH_PX, TAPE_HEIGHT_PX direkt aus label_renderer.py fehlt
backend/app/api/routes/templates_preview.py eigenständiger Router, in main.py:89 registriert, importiert LabelRenderer + TemplateLoader fehlt
backend/app/main.py (Modifikation) from app.services.label_renderer import LabelRenderer (Zeile 111), from app.services.template_loader import TemplateLoader (Zeile 121), TemplateLoader.load_dir(), seed_templates(s, TemplateLoader), app.include_router(templates_preview_router) — alles muss raus als „modifiziert" implizit vorhanden, aber kein expliziter Eintrag
backend/app/db/lifespan.py (Modifikation) from app.services.template_loader import TemplateLoader (Zeile 40), seed_templates() mit TemplateLoader-Argument fehlt
backend/app/api/error_handlers.py (Modifikation) from app.services.template_loader import TemplateNotFoundError (Zeile 37) — muss gegen neues Error-System getauscht werden fehlt
backend/app/services/print_queue.py (Modifikation) renderer: LabelRenderer, loader: type[TemplateLoader] als Constructor-Parameter, Recovery-Methode _rerender_from_db() nutzt self._loader.get(db_job.template_key) + self._renderer.render(template, ...) — kompletter Recovery-Pfad muss auf LayoutEngine umgestellt werden als „modifiziert" gelistet, aber die nötige Tiefe der Änderung fehlt
backend/app/services/batch_dispatch.py (Modifikation) Zeile 138: await service.get_template_tape_mm(item.template_id)get_template_tape_mm() auf PrintService muss verschwinden oder ersetzt werden fehlt
backend/tests/**/test_svg_renderer* Tests für den SVGRenderer der gelöscht wird fehlt
backend/scripts/generate_template_svgs.py, smoke_first_print.py, smoke_first_print_batch.py referenzieren template_id fehlen in der Spec (low-prio Scripts, aber erwähnenswert)

Empfehlung: Sektion 5 „Gelöschte Files" und „Modifizierte Files" um diese Einträge ergänzen. Besonders der print_queue.py-Recovery-Pfad (_rerender_from_db) ist kritisch: die Methode nutzt TemplateLoader + LabelRenderer tief in der Queue-Logik. Die Spec muss beschreiben wie die Recovery nach der Migration aussieht (LayoutEngine-Instanz als Constructor-Parameter statt renderer+loader?).


C2 — jobs.template_key vs. jobs.template_id: Spaltenname falsch in der Migration

Das Job-Model (SQLModel, backend/app/models/job.py, Zeile 37) hat die Spalte template_key: str, nicht template_id. Die Alembic-Migration in der Spec lautet:

op.drop_column("jobs", "template_id")  # ← FALSCH

Die korrekte Spalte heißt template_key. template_id existiert in der jobs-Tabelle nicht — op.drop_column("jobs", "template_id") würde mit einem Alembic-Fehler scheitern.

Zusätzlich: job.template_key wird in print_service.py als template_key=request.template_id befüllt (Zeile 116 und 158). Nach der Migration muss das auf content_type umgestellt werden. Die neue Job-Model-Spalte sollte dann content_type heißen, nicht mehr template_key.

Empfehlung: In der Spec-Migration ersetzen:

# Vorher (falsch):
op.drop_column("jobs", "template_id")

# Nachher (korrekt):
op.drop_column("jobs", "template_key")
op.add_column("jobs", sa.Column("content_type", sa.String(32)))
op.add_column("jobs", sa.Column("rendered_tape_mm", sa.Float))

Außerdem: Job-Model-Klasse in der Spec explizit als zu änderndes File nennen (backend/app/models/job.py).


C3 — on_tape_mismatch-Feld im PrintRequest: wird es gelöscht oder behalten?

PrintRequest (in backend/app/schemas/print_request.py, Zeile 52) hat das Feld on_tape_mismatch: Literal["fail", "queue"]. Mit Tape-Independence (kein TapeMismatchError mehr im Render-Pfad) ist dieses Feld semantisch obsolet. Die Spec erwähnt das nicht.

Der PAUSED-Job-Pfad in print_service.py (Zeilen 93–147) ist tief mit dem Template-/Tape-Mismatch-Konzept verwoben. Wenn TapeMismatchError wegfällt, entfällt auch dieser gesamte PAUSED-Pfad. Das hat Auswirkungen auf:

  • POST /printer/resume (Route bleibt, aber die Semantik ändert sich)
  • POST /jobs/{job_id}/resume (wird dann nie mehr PAUSED → ggf. überflüssig?)

Empfehlung: In Sektion 5 explizit festlegen: wird on_tape_mismatch aus PrintRequest entfernt? Wird POST /jobs/{job_id}/resume deprecated? Die Spec sagt nur „TapeMismatchError wird ersatzlos gelöscht" — die Request-Schema-Konsequenz fehlt.


MEDIUM

M1 — text_start_x übersteigt Canvas-Breite für 12mm und 18mm

Sektion 3, TAPE_GEOMETRY:

12:  TapeGeometry(printable_px=70, ..., text_start_x=72, ...)
18:  TapeGeometry(printable_px=112, ..., text_start_x=114, ...)
24:  TapeGeometry(printable_px=128, ..., text_start_x=130, ...)

text_start_x ist größer als printable_px um 2px. Das DEFAULT_LABEL_WIDTH_PX = 600 (Längsachse) aus dem bestehenden LabelRenderer gilt für die Längs-/Breiten-Achse, nicht für die Höhen-Achse. Wenn text_start_x als X-Koordinate auf der Längsachse interpretiert wird (was aus dem LabelRenderer-Koordinatensystem folgt), ist das kein Problem — dann startet Text 2px nach dem QR-Code auf der Längsachse.

Wenn aber text_start_x auf der Höhenachse (printable_px) interpretiert wird, wäre der Text außerhalb des Druckbereichs. Die Benennung ist mehrdeutig.

Empfehlung: Einen Kommentar in der Spec und im Code hinzufügen der klärt: text_start_x bezieht sich auf die Längsachse (horizontal, nicht height), analog zur x-Koordinate in LayoutElement. Außerdem: für 62mm ist text_start_x=680 aber qr_max_px=672 — der QR-Code endet bei 672px auf welcher Achse? Das Koordinatenmodell sollte explizit dokumentiert werden.


M2 — Preview-Endpoint: GET mit data_json Query-Param ist problematisch

Sektion 5, neue Route:

GET /api/render/preview?content_type=qr_two_lines&tape_mm=18&data_json={...}

GET-Requests mit JSON-Body in Query-Params haben mehrere Probleme:

  • URL-Länge: Browser-Limits (2048 Zeichen), nginx-Limits (8192 Bytes) — bei qr_with_listing mit 10 Items kann der Query-String schnell 1-2 KB erreichen
  • Die bestehende render_preview-Route ist ein POST (in templates.py:117)
  • HTMX in Phase 1k.1c verwendet GET /admin/print/categories/preview?content_type=...&tape_mm=...&primary_id=...&title=... — das ist ein separater Endpoint mit flachen Parametern (kein data_json-Blob), was inkonsistent ist

Empfehlung: Entweder:

  1. POST /api/render/preview mit JSON-Body (konsistent mit bestehendem render_preview POST) — HTMX kann hx-post nutzen
  2. GET mit flachen Parametern (kein data_json-Blob): ?content_type=...&tape_mm=...&primary_id=...&title=... — konsistent mit dem HTMX-Endpoint in 1k.1c, aber kein items-Support

Option 1 ist für den Hub-API-Consumer (Hangar) vorzuziehen.


M3 — LabelData.source_app wird in der Spec nicht erwähnt

Das bestehende LabelData-Model (Zeile 19 in label_data.py) hat source_app: str als Required-Feld ohne Default. Die Spec erwähnt nur die Erweiterung um items. Was passiert mit source_app in der neuen API?

Bei qr_with_listing hat LabelDataItem kein source_app-Feld — ist das gewünscht? Wenn LabelData.source_app für Analytics genutzt wird (erkennbar aus print_service.py:72: source_app="manual"), müsste das auch für die neue Hangar-API definiert sein.

Empfehlung: In Sektion 2 oder 5 explizit klären ob source_app in LabelData erhalten bleibt, ein Default bekommt, oder entfernt wird.


M4 — qr_with_listing Overflow-Strategie ist unspezifiziert

Sektion 2: „Overflow zeigt +N more". Aber: auf welcher Grundlage wird N berechnet? Die Engine müsste wissen:

  • Wie viele Items passen bei gegebener tape_mm und font_m auf das Label?
  • Ist qr_max_px (Höhe des QR) fix, oder kann er kleiner sein um mehr Item-Zeilen zu ermöglichen?

Für 24mm (printable_px=128) und font_m=24: 128 / 24 = ~5 Zeilen Maximum, minus QR-Bereich. Die Berechnung fehlt in der Spec.

Empfehlung: Eine Formel oder Pseudocode für den Overflow-Schwellwert in Sektion 4 ergänzen, z.B.:

max_items = (geometry.printable_px - geometry.qr_max_px) // (geometry.font_m + geometry.line_spacing_px)

M5 — Coverage-Ziel 90%: für LayoutEngine realistisch, aber Grenzfall

Sektion 9: „90%+ für Phase 1k.1a". Der LabelRenderer hat aktuell TAPE_HEIGHT_PX nur für 4 Tape-Größen (12/18/24/62mm). Die neue LayoutEngine hat 7 Größen (3.5/6/9/12/18/24/62mm). Die _render_*-Methoden sind 6 × 7 = 42 Kombinationen. Wenn alle Unit-Tests für 12mm geschrieben werden und dann Pixel-Ratio für andere Sizes genommen wird, werden die unvalidierten Größen (3.5/6/9mm) nicht durch Hardware-Tests abgedeckt.

90% ist erreichbar wenn für jede der 6 Render-Methoden mindestens 2-3 Tape-Varianten getestet werden. Beim Fokus auf 12mm allein ist 90% nicht erreichbar ohne die anderen Methoden vollständig zu covern.

Empfehlung: In der DoD (Sektion 10) klären: „90% Coverage auf layout_engine.py + tape_geometry.py + content_type.py" (Modul-spezifisch, nicht Gesamt-Coverage).


LOW

L1 — qr_with_listing mit items=(): Spec sagt 422, aber Tuple-Default ist ()

Sektion 2, Validation-Tabelle: „qr_with_listing benötigt items: tuple[LabelDataItem,...] mit mindestens 1 Item — 422 wenn fehlt/leer."

Das Spec-Snippet in Sektion 5 zeigt:

# label_data.py
items: tuple[LabelDataItem, ...] = ()

Mit Default () wird items=() (leere Liste) nicht vom Pydantic-Validator abgefangen — der Default ist valid. Der 422-Check für „leer" muss im LayoutEngine._validate_data() explizit implementiert werden, nicht in Pydantic.

Außerdem: Was passiert wenn items=None explizit übergeben wird? tuple[LabelDataItem, ...] = () erlaubt kein None — das wird von Pydantic auf () gecheckt. Das ist korrekt, aber sollte in der Spec explizit bestätigt werden.

Empfehlung: In der Validation-Tabelle (Sektion 2) präzisieren: „_validate_data in LayoutEngine prüft len(data.items) >= 1 für QR_WITH_LISTING, Pydantic-Default erlaubt ()".


L2 — TapeGeometry.qr_padding_px: Wert wird in qr_max_px bereits eingerechnet, aber Name ist verwirrend

qr_max_px = printable_px - 4 (2px Padding pro Seite). Gleichzeitig gibt es qr_padding_px=2 als separates Feld. Wenn der _render_qr_*-Code qr_max_px als finalen Wert verwendet (was aus dem bestehenden LabelRenderer-Pattern folgt), ist qr_padding_px redundant.

Empfehlung: Entweder qr_padding_px aus dem Modell entfernen (wenn es nur zur Berechnung von qr_max_px dient und nie direkt verwendet wird), oder dokumentieren wo qr_padding_px vom Renderer direkt verwendet wird.


L3 — text_two_lines für Samla-Stirntag: qr_payload ist required in LabelData

Sektion 2: text_two_lines nutzt nur primary_id + title, kein QR. Aber LabelData.qr_payload: str hat keinen Default — es ist Required. Hangar muss also für Samla-Stirntag einen leeren String "" oder einen Dummy-QR-Payload senden.

Empfehlung: Entweder qr_payload: str = "" als Default in LabelData (oder qr_payload: str | None = None), oder in der Spec explizit dokumentieren was Hangar für text_*-ContentTypes als qr_payload senden soll.


PRAISE

P1 — Tape-Independence ist konzeptionell sehr sauber.
Die Eliminierung von TapeMismatchError aus dem Render-Pfad ist die richtige Entscheidung. Hangar sendet semantischen Intent, Hub adaptiert zur geladenen Tape-Größe. Das ist eine klare Verbesserung gegenüber dem bisherigen template_id-Modell mit 21 Varianten.

P2 — Hard-Cut statt Compat-Layer ist die richtige Wahl für diesen Entwicklungsstand.
Kein Legacy-Code, kein Compat-Layer. Der bestehende LabelRenderer ist gut dokumentiert (A-Diagnose, Phase 1i V4-Winner) und dient als Referenz für die Smoke-Tests. Der Pixel-Basis-Vergleich als Akzeptanzkriterium ist präzise und ausreichend für die aktuell Hardware-validierten 12mm-Werte.

P3 — TapeGeometry als Pydantic-Model mit frozen=True + extra="forbid" ist ausgezeichnet.
Unveränderliche Konstanten-Tabelle mit ConfigDict(frozen=True, extra="forbid") verhindert versehentliche Mutation. Das Pattern aus TemplateSchema konsistent weitergeführt.

P4 — Sub-Phasen-Decomposition (1k.1a → b → c → d) ist klar und einzeln deploy-bar.
Jede Phase hat eigene DoD-Checkliste. 1k.1a (Hub) ist vollständig unabhängig von 1k.1b (Hangar). Die Reihenfolge ist logisch.

P5 — match-Statement für ContentType-Dispatch ist idiomatisch und vollständig.
6 case-Zweige ohne case _: raise — der Python-Exhaustiveness-Checker (wenn aktiviert via mypy --warn-unreachable oder ähnlich) würde einen fehlenden Case aufdecken. Wenn ContentType ein StrEnum mit exakt 6 Werten ist, ist das Pattern korrekt und wartbar.


Zusammenfassung Prioritäten vor Implementation

Nr Schwere Kurzform
C1 CRITICAL main.py, lifespan.py, svg_renderer.py, templates_preview.py, batch_dispatch.py fehlen in Hard-Cut-Liste
C2 CRITICAL template_idtemplate_key in Alembic-Migration (falscher Spaltenname)
C3 CRITICAL on_tape_mismatch-Feld und PAUSED-Job-Pfad: obsolet nach Migration?
M1 MEDIUM text_start_x-Koordinatenachse unklar dokumentiert
M2 MEDIUM GET data_json Query-Param bei Preview-Endpoint — URL-Länge + Inkonsistenz
M3 MEDIUM LabelData.source_app Required-Feld: was passiert damit?
M4 MEDIUM qr_with_listing Overflow-Formel fehlt
M5 MEDIUM Coverage-Ziel: modul-spezifisch präzisieren
L1 LOW items=() Default vs. 422-Validierung: liegt in Engine, nicht Pydantic
L2 LOW qr_padding_px redundant zu qr_max_px
L3 LOW qr_payload Required in LabelData bei text_*-ContentTypes

Copilot AI 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.

Pull request overview

Adds a Phase 1k.1 design/spec document for replacing the current template-based label rendering with a semantic Layout-Engine driven by ContentType and TapeGeometry, including planned API, migration, and Hangar follow-up phases.

Changes:

  • Introduces a spec for a hard-cut migration from 21 YAML templates to a centralized Layout-Engine.
  • Defines proposed ContentType set, validation rules, and a TapeGeometry table across tape widths.
  • Outlines backend API/DB migration and Hangar-side follow-up phases (API migration, categories DB/editor, navigation refactor).

**Status:** Draft (zur User-Review)
**Tracking:** strausmann/Label-Printer-Hub#103 (Phase 1k.1 unter Umbrella #101)
**Vorgaenger-Spec:** docs/superpowers/specs/2026-05-17-phase-7e-template-layout-v2-design.md (subsumiert)
**Hardware-Baseline:** Phase 1i V4-Winner (docs/site/operations/protokolle/2026-06-04-phase1i-smoke-test-empirie.md)

Phase 1k.1 ersetzt die 21 hartcodierten YAML-Templates (hangar/grocy/snipeit/spoolman/qr-only x 12/18/24mm + 6 Samla) durch eine semantische **Layout-Engine** mit zwei Achsen:

1. **TapeGeometry** — Tabelle mit allen 7 unterstuetzten Tape-Groessen (3.5/6/9/12/18/24/62mm) und ihren Render-Parametern (printable_px, qr_max, Font-Groessen)
Comment on lines +74 to +76
TAPE_GEOMETRY: dict[float, TapeGeometry] = {
3.5: TapeGeometry(printable_px=24, qr_max_px=20, qr_padding_px=2, text_start_x=26, line_spacing_px=1, font_xl=8, font_l=7, font_m=6, font_s=5),
6: TapeGeometry(printable_px=32, qr_max_px=28, qr_padding_px=2, text_start_x=34, line_spacing_px=2, font_xl=10, font_l=9, font_m=7, font_s=6),
|-------------|---------------------|---------------------------|----------------------------|
| `qr_only` | QR fuellt volle Tape-Hoehe, kein Text | `qr_payload` | qr-only-12mm, qr-only-18mm, qr-only-24mm |
| `qr_one_line` | QR links + 1 Text-Zeile (XL, vertikal zentriert) | `qr_payload`, `primary_id` | (neu, war Sonderfall) |
| `qr_two_lines` | QR links + 2 Text-Zeilen (XL primary_id + L title) | `qr_payload`, `primary_id`, `title` | hangar-furniture-*, grocy-*, snipeit-*, spoolman-* |
| `qr_one_line` | QR links + 1 Text-Zeile (XL, vertikal zentriert) | `qr_payload`, `primary_id` | (neu, war Sonderfall) |
| `qr_two_lines` | QR links + 2 Text-Zeilen (XL primary_id + L title) | `qr_payload`, `primary_id`, `title` | hangar-furniture-*, grocy-*, snipeit-*, spoolman-* |
| `text_one_line` | Voll-Breite Text XL, kein QR | `primary_id` | (neu) |
| `text_two_lines` | 2 Text-Zeilen XL + L, kein QR | `primary_id`, `title` | samla-stirntag-* (teilweise) |
Comment on lines +64 to +65
qr_max_px: int # printable_px - 4 (2px Padding pro Seite)
qr_padding_px: int # Padding um den QR-Code
12: TapeGeometry(printable_px=70, qr_max_px=66, qr_padding_px=2, text_start_x=72, line_spacing_px=4, font_xl=22, font_l=18, font_m=14, font_s=10), # V4-Winner
18: TapeGeometry(printable_px=112, qr_max_px=108, qr_padding_px=2, text_start_x=114, line_spacing_px=6, font_xl=32, font_l=26, font_m=20, font_s=14),
24: TapeGeometry(printable_px=128, qr_max_px=124, qr_padding_px=2, text_start_x=130, line_spacing_px=8, font_xl=36, font_l=30, font_m=24, font_s=18),
62: TapeGeometry(printable_px=696, qr_max_px=672, qr_padding_px=4, text_start_x=680, line_spacing_px=20, font_xl=120, font_l=96, font_m=72, font_s=48), # QL 300 DPI
|-------|---------|-----------|
| `/api/print/{slug}` | POST | Request hat `content_type: str` (Enum), `data: LabelData`, `options: PrintOptions` — `template_id` Feld geloescht |
| `/api/print/{slug}/batch` | POST | items[] mit `content_type`, kein `template_id` |
| `/api/render/preview` | GET | Query: `?content_type=qr_two_lines&tape_mm=18&data_json={...}` |
op.drop_column("jobs", "template_id")
```

Existierende Jobs-Eintraege bekommen content_type=NULL — fuer Historie tolerierbar (Frontend zeigt "(legacy)" wenn NULL).
Comment on lines +109 to +111
tape_mm: float,
content_type: ContentType,
data: LabelData,
Adressiert alle 8 CRITICAL + 7 MEDIUM Findings aus ops-agent, Gemini und
Copilot Reviews.

Strukturelle Aenderungen:
- ContentType-Set: 6 -> 7 (qr_three_lines neu fuer grocy/snipeit/spoolman
  18/24mm + hangar-furniture 18/24mm) — vermeidet Breaking-Change beim
  secondary-Feld
- tape_mm durchgehend int (statt float) — konsistent mit TapeSpec.width_mm
  und SNMP-Parsing
- Kleinste Tape 4mm (statt 3.5mm) — PT-TZe-Realitaet (24 Print-Pins)
- 62mm Werte mathematisch korrigiert: qr_max_px=688 (war 672) mit padding=4

Hard-Cut-Vollstaendigkeit (CRITICAL aus ops-agent):
- print_queue._rerender_from_db Recovery-Pfad als KRITISCH markiert
- svg_renderer, templates_preview, batch_dispatch, main, lifespan,
  error_handlers in Modified/Deleted-Listen ergaenzt
- TapeMismatchError + on_tape_mismatch + PAUSED-State + /jobs/{id}/resume
  + MixedTapeSizesError als obsolete Konzepte tabelliert

Alembic-Korrekturen (CRITICAL aus ops-agent + Gemini + Copilot):
- Spalte ist template_key (nicht template_id)
- op.batch_alter_table fuer SQLite-Kompatibilitaet
- Deterministisches Backfill aus template_key vor drop
- jobs.content_type + rendered_tape_mm im upgrade-Pfad

API-Korrekturen:
- Preview-Endpoint GET -> POST mit JSON-Body (URL-Length fuer
  qr_with_listing)
- TapeGeometry mit Pydantic Field(gt=0)/Field(ge=0) Constraints
- qr_max_px Formel: printable_px - 2 * qr_padding_px

User-Designentscheidung:
- Samla unified: alle 6 Samla-Templates (Stirntag/Deckel x 12/24/62mm)
  -> eine Hangar-Category "Samla" mit content_type=qr_two_lines
- samla-stirntag korrekt als qr_two_lines (hat QR), nicht text_two_lines

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 1 Fixes Applied — Commit 2545467

Alle 8 CRITICAL + 7 MEDIUM Findings aus den drei Reviews adressiert (siehe Sektion 12 "Review-Round 1" im Spec).

CRITICAL (8/8)

# Quelle Adressiert in
C1 ops-agent Sektion 5 — Modified/Deleted Files Listen vervollstaendigt, print_queue._rerender_from_db als KRITISCH markiert
C2 ops-agent Sektion 5 DB-Migration — template_id -> template_key korrigiert
C3 ops-agent Sektion 5 — neue Tabelle "Obsolete Konzepte" (TapeMismatchError, on_tape_mismatch, PAUSED-State, /resume, MixedTapeSizesError)
C4 Copilot CP-2/3/10 Sektion 3 + 4 — tape_mm: int durchgehend, dict[int, TapeGeometry], 4mm statt 3.5mm
C5 Copilot CP-4 Sektion 2 — qr_three_lines als 7. ContentType fuer 3-Zeilen-Layouts mit secondary
C6 Copilot CP-5 Sektion 2 Tabelle — samla-stirntag-* korrekt zu qr_two_lines; User-Designentscheidung "Samla unified" eingearbeitet
C7 Copilot CP-7 Sektion 3 TAPE_GEOMETRY — 62mm qr_max_px=688 (war 672) mit qr_padding_px=4
C8 Copilot CP-1 Verweis auf Issue-Kommentar #103 statt nicht-OS-Pfad

MEDIUM (7/7)

# Quelle Adressiert in
M1 Gemini Sektion 3 — Pydantic Field(gt=0)/Field(ge=0) Constraints in TapeGeometry-Model
M2 Gemini + Copilot Sektion 5 — op.batch_alter_table Wrapper fuer add_column + drop_column
M3 Copilot CP-6 Sektion 3 — qr_max_px = printable_px - 2 * qr_padding_px allgemeine Formel im Header
M4 Copilot CP-8 + ops-agent Sektion 5 Routes-Tabelle — Preview-Endpoint POST mit JSON-Body statt GET
M5 Copilot CP-9 Sektion 5 DB-Migration — deterministischer Backfill aus template_key vor drop
M6 ops-agent Sektion 3 — text_start_x als absolute X-Position erklaert
M7 ops-agent Sektion 2 Validation-Regeln — source_app Pflichtfeld erwaehnt

User-Designentscheidung

Eingearbeitet in Sektion 2 + 6: Samla unified — alle 6 Samla-Templates (Stirntag/Deckel x 12/24/62mm) -> eine Hangar-Category "Samla" mit content_type: qr_two_lines. Keine Unterscheidung zwischen Anbringungsarten.

Diff-Summary

1 file changed, 195 insertions(+), 67 deletions(-)

Bitte Re-Review.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 7 comments.

Comment on lines +171 to +175
+-- tape_geometry.py # TapeGeometry Pydantic-Model + TAPE_GEOMETRY dict
+-- content_type.py # ContentType Enum (6 Werte)
backend/app/services/
+-- layout_engine.py # LayoutEngine.render() + 6 _render_*-Methoden
backend/tests/unit/services/
### Live-Preview-Verhalten

User waehlt im Add/Edit-Form:
1. `content_type` Dropdown (qr_only/qr_one_line/qr_two_lines/text_one_line/text_two_lines/qr_with_listing)
Comment on lines +60 to +62
`qr_max_px` folgt der allgemeinen Formel `printable_px - 2 * qr_padding_px` — damit ist die Geometrie pro Eintrag konsistent und nicht abhaengig von einem hardgecodeten Padding.

`text_start_x` ist die **absolute Pixel-X-Position** ab dem linken Tape-Rand. Sie liegt logisch nach dem QR-Block plus einem Gap von `qr_padding_px`. Bei reinen Text-ContentTypes ohne QR (text_one_line, text_two_lines) wird `text_start_x` ignoriert — Text rendert ab `qr_padding_px` links.
| `internal/hub/layouts.go` | Struct `LayoutMapping`: `TemplateID string` -> `ContentType string`. YAML-Tag `template_id` -> `content_type`. |
| `internal/hub/client.go` | `PrintRequest` struct: `template_id` Feld entfernen, `content_type` hinzufuegen. JSON-Marshaling angepasst. |
| `internal/generator/print_p750w.go` | PrintRequest-Bau: `TemplateID: m.TemplateID` -> `ContentType: m.ContentType` |
| `cmd/hangar/main.go:768` | Preview-Proxy: `/admin/print/preview/{template_id}` -> `/admin/print/preview?content_type=...&tape_mm=...`. Forward auf Hub `/api/render/preview` mit Query-String. |
Comment on lines +416 to +418
GET /admin/print/categories/preview # HTMX-Endpoint: liefert SVG aus Hub
# Query: ?content_type=...&tape_mm=...&primary_id=...&title=...
```
Comment on lines +311 to +312
Jobs mit `template_key=NULL` (bereits in obsoletem Zustand) bleiben mit `content_type=NULL` — fuer Historie tolerierbar (Frontend zeigt "(legacy)" wenn NULL).

Comment on lines +201 to +202
*-- jobs.py # POST /jobs/{job_id}/resume Route loeschen (PAUSED-Pfad obsolet);
# Job-Schema content_type + rendered_tape_mm zurueckgeben
Adressiert 7 Copilot Round-2-Findings + User-Designentscheidung zum
Initial-Seeding der Hangar-Categories.

Copilot R2:
- R2-1: "6 Werte/Methoden" -> "7 Werte/Methoden" in Sektion 5 File-Listen
- R2-2: qr_three_lines im Sektion-7 Dropdown-Liste ergaenzt
- R2-3: text_start_x Formel korrekt (Gap = 2*qr_padding_px, nicht
  qr_padding_px); 62mm text_start_x korrigiert auf 700 (war 696)
- R2-4: 1k.1b Preview-Proxy umgebaut auf POST mit JSON-Body
- R2-5: 1k.1c Categories-Preview-Route POST mit JSON-Body
- R2-6: Alembic-Migration explizit zu Webhook-Template-Keys
  (spoolman/<id>, grocy/<id>) — bekommen content_type=NULL
- R2-7: /jobs/{id}/resume existiert auch in routes/print.py (separate
  Route im PAUSED-Workflow) — beide Routes geloescht

User-Designentscheidung (Round 2):
Initial-Seeding der Hangar-Categories ueber Go-Code DefaultCategories
Slice statt YAML. Reihenfolge: DB-Init -> Moebel-Typen-Seed ->
DefaultCategories-Seed. HUB_LAYOUTS_PATH + hub-layouts.yaml werden in
1k.1c deprecated. Ziel: YAML-freie Konfiguration, Categories
ausschliesslich ueber Admin-UI editierbar.

Sektion 6 1k.1b YAML markiert als Uebergangsloesung. Sektion 7 1k.1c
definiert DefaultCategories als Source-of-Truth fuer frischen Start.
DoD 1k.1c erweitert um DefaultCategories + YAML-Deprecation.

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 2 Fixes Applied — Commit 9c877cb

Alle 7 Copilot Round-2-Findings adressiert + User-Designentscheidung zum Initial-Seeding eingearbeitet.

Round 2 (7/7 adressiert)

# Finding Adressiert in
R2-1 "ContentType Enum (6 Werte)" + "6 render*" -> 7 Sektion 5 File-Listen-Kommentare
R2-2 qr_three_lines fehlte in 1k.1c Dropdown Sektion 7 Live-Preview-Verhalten
R2-3 text_start_x Formel: Gap = 2 * qr_padding_px (war qr_padding_px); 62mm korrigiert 696 -> 700 Sektion 3 Header + TAPE_GEOMETRY
R2-4 1k.1b Preview-Proxy POST statt Query-String Sektion 6 Aenderungen-Tabelle
R2-5 1k.1c Categories-Preview POST mit JSON-Body Sektion 7 Neue Routes
R2-6 Webhook-Template-Keys (spoolman/<id>, grocy/<id>) -> content_type=NULL Sektion 5 Alembic-Migration Hinweis
R2-7 POST /jobs/{id}/resume auch in routes/print.py (PAUSED-Workflow) Sektion 5 Obsolete-Konzepte-Tabelle

User-Designentscheidung — YAML-frei (NEU in Round 2)

Initial-Seeding der Hangar-Categories erfolgt ueber Go-Code DefaultCategories Slice in internal/category/defaults.go, NICHT mehr ueber YAML.

Seed-Reihenfolge beim ersten Start:

  1. DB-Schema initialisieren (GORM auto-migrate)
  2. Moebel-Typen-Seed (bestehend aus internal/catalog/) falls Tabelle leer
  3. DefaultCategories in print_categories schreiben falls Tabelle leer

HUB_LAYOUTS_PATH Env-Variable und hub-layouts.yaml werden in 1k.1c deprecated. Code-Cleanup in Folge-Phase. Ziel: YAML-freie Konfiguration, Categories ausschliesslich ueber Admin-UI editierbar.

Sektion 6 1k.1b YAML-File jetzt als "Uebergangs-Loesung" markiert; Sektion 7 1k.1c komplett umgeschrieben mit DefaultCategories als Source-of-Truth.

Diff-Summary

1 file changed, 88 insertions(+), 32 deletions(-)

Erneute Re-Review bitte fuer commit 9c877cb.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment on lines +13 to +15
1. **TapeGeometry** — Tabelle mit allen 7 unterstuetzten Tape-Groessen (**4**/6/9/12/18/24/62mm — int, kleinste PT-TZe ist 4mm mit 24 Print-Pins) und ihren Render-Parametern (printable_px, qr_max, Font-Groessen)
2. **7 ContentTypes** — semantische Beschreibung was gerendert wird, **tape-unabhaengig** (qr_only, qr_one_line, qr_two_lines, **qr_three_lines** fuer 3-Zeilen-Layouts mit secondary, text_one_line, text_two_lines, qr_with_listing)


Phase 1k.1 ersetzt die 21 hartcodierten YAML-Templates (hangar/grocy/snipeit/spoolman/qr-only x 12/18/24mm + 6 Samla) durch eine semantische **Layout-Engine** mit zwei Achsen:

1. **TapeGeometry** — Tabelle mit allen 7 unterstuetzten Tape-Groessen (**4**/6/9/12/18/24/62mm — int, kleinste PT-TZe ist 4mm mit 24 Print-Pins) und ihren Render-Parametern (printable_px, qr_max, Font-Groessen)
|-------------|---------------------|---------------------------|----------------------------|
| `qr_only` | QR fuellt volle Tape-Hoehe, kein Text | `qr_payload` | qr-only-12mm, qr-only-18mm, qr-only-24mm |
| `qr_one_line` | QR links + 1 Text-Zeile (XL, vertikal zentriert) | `qr_payload`, `primary_id` | (neu, war Sonderfall) |
| `qr_two_lines` | QR links + 2 Text-Zeilen (XL primary_id + L title) | `qr_payload`, `primary_id`, `title` | hangar-furniture-*-12mm, grocy-12mm, snipeit-12mm, spoolman-12mm, **alle Samla-* Templates** (Stirntag + Deckel, 12/24/62mm) |
Comment on lines +295 to +299
# 3) Alte Spalte + Tabelle entfernen
with op.batch_alter_table("jobs") as batch_op:
batch_op.drop_column("template_key")

op.drop_table("templates")
…lungen)

Adressiert 4 Copilot Round-3-Findings auf commit 9c877cb.

- R3-1: PR-Body wird parallel aktualisiert (war 6 ContentTypes / 3.5mm)
- R3-2: Executive Summary "4mm (nicht 3.5mm)" explizit
- R3-3: hangar-furniture-*-12mm Wildcard aufgeloest in konkrete Eintraege
- R3-4 (DESIGN): jobs.template_key BLEIBT als nullable Audit-Spalte
  erhalten. Die Spalte ist als "snapshot string — survives template
  deletion" dokumentiert und enthaelt nicht-rekonstruierbare Werte
  (Webhook-Keys spoolman/<id>, grocy/<id>). Migration NOT NULL ->
  nullable. Neue Jobs ab 1k.1a setzen template_key=NULL +
  content_type/rendered_tape_mm. Frontend zeigt template_key als
  Provenance-Hint fuer historische Jobs.

Migration angepasst:
- batch_op.alter_column("template_key", nullable=True) statt drop
- templates Tabelle wird weiterhin gedroppt (kein TemplateLoader mehr)
- downgrade: alter_column zurueck zu NOT NULL

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 3 Fixes Applied — Commit b10552a

Alle 4 Copilot Round-3-Findings adressiert. PR-Body parallel aktualisiert auf 7 ContentTypes / 4mm.

Round 3 (4/4 adressiert)

# Finding Adressiert in
R3-1 PR-Body veraltet (6 ContentTypes / 3.5mm) PR-Body aktualisiert
R3-2 Executive Summary "4mm (nicht 3.5mm)" explizit Sektion 1
R3-3 hangar-furniture-*-12mm Wildcard aufgeloest Sektion 2 Mapping-Tabelle
R3-4 DESIGN: jobs.template_key BLEIBT als Audit-Spalte Sektion 5 Migration + JobRead API-Schema

Wichtigste Aenderung: template_key als Audit-Snapshot

Statt zu droppen, wird jobs.template_key:

  • Auf nullable gesetzt (alter_column nullable=True)
  • Bleibt fuer historische Jobs gesetzt (snapshot string, survives template deletion)
  • Wird fuer neue Jobs ab 1k.1a NULL gesetzt (ersetzt durch content_type + rendered_tape_mm)
  • Webhook-Keys wie spoolman/<id>, grocy/<id> bleiben rekonstruierbar
  • Frontend zeigt template_key als Provenance-Hint fuer historische Jobs

Diff-Summary

1 file changed, 39 insertions(+), 20 deletions(-)

Bitte finale Re-Review fuer commit b10552a. Wenn nichts mehr kritisches kommt, gehen wir mit dieser Spec in writing-plans fuer Phase 1k.1a.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment on lines +49 to +54
| `qr_one_line` und `text_one_line` benoetigen `primary_id` | 422 wenn fehlt |
| `*_two_lines` benoetigt `primary_id` UND `title` | 422 wenn eines fehlt |
| `qr_three_lines` benoetigt `primary_id` UND `title` UND mindestens 1 `secondary`-Eintrag | 422 wenn eines fehlt |
| `source_app` ist in `LabelData` Pflichtfeld (bestehend, unveraendert) | 422 wenn fehlt |

## 3. TapeGeometry (alle 7 Tape-Groessen)
Comment on lines +99 to +101
12mm-Werte aus Phase 1i V4-Winner sind scan-verifiziert (siehe Issue #103 Issue-Kommentar fuer Detailwerte).
4/6/9/18/24/62mm wurden via Pixel-Ratio extrapoliert (`new_value = 12mm_value * new_printable_px / 70`). Smoke-Test als Follow-up-Issue: jede Tape-Groesse einmal mit `qr_two_lines` drucken, Lesbarkeit pruefen, ggf. Werte korrigieren. User hat 24mm-Tapes und QL-Rollen verfuegbar.

Comment on lines +11 to +15
Phase 1k.1 ersetzt die 21 hartcodierten YAML-Templates (hangar/grocy/snipeit/spoolman/qr-only x 12/18/24mm + 6 Samla) durch eine semantische **Layout-Engine** mit zwei Achsen:

1. **TapeGeometry** — Tabelle mit allen 7 unterstuetzten Tape-Groessen (**4**/6/9/12/18/24/62mm — `int`, kleinste PT-TZe ist **4mm** (24 Print-Pins) — nicht 3.5mm wie in fruehen Drafts) und ihren Render-Parametern (printable_px, qr_max, Font-Groessen)
2. **7 ContentTypes** — semantische Beschreibung was gerendert wird, **tape-unabhaengig** (qr_only, qr_one_line, qr_two_lines, **qr_three_lines** fuer 3-Zeilen-Layouts mit secondary, text_one_line, text_two_lines, qr_with_listing)

Comment on lines +107 to +110
from PIL.Image import Image
from app.schemas.content_type import ContentType
from app.schemas.label_data import LabelData
from app.schemas.tape_geometry import TAPE_GEOMETRY
Adressiert 4 Copilot Round-4-Findings auf commit b10552a — Klarstellungen,
keine Designaenderungen.

- R4-1: LabelData Schema-Anpassung explizit: title/primary_id/qr_payload
  werden optional (str | None), zentrale ContentType-Validation in
  LayoutEngine._validate_data(). Pflichtfeld-Matrix als Tabelle ergaenzt.
- R4-2: Extrapolations-Methodologie pro Feld dokumentiert (Fonts via
  Pixel-Ratio + Clamping, qr_padding konstant, deterministische Formeln
  fuer text_start_x + qr_max_px). Tabelle damit nachvollziehbar pflegbar.
- R4-3: Executive Summary: "initiale Scope" 4/6/9/12/18/24/62mm;
  TapeRegistry kennt zusaetzliche QL-DK-Breiten (29/38/50/54mm) die
  bewusst noch nicht abgedeckt sind -> UnsupportedTapeError; Erweiterung
  als Folge-Phase moeglich.
- R4-4: Import-Konvention `from PIL import Image` + `Image.Image` als Typ
  (konsistent mit Repo).

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 4 Fixes Applied — Commit c37d494

Alle 4 Copilot Round-4-Findings adressiert. Reine Klarstellungen, keine neuen Designaenderungen.

Round 4 (4/4 adressiert)

# Finding Adressiert in
R4-1 LabelData required Felder vs ContentType-Validation Sektion 2 — Schema-Anpassung + Pflichtfeld-Matrix
R4-2 Extrapolations-Methodologie pro Feld unklar Sektion 3 — Methodologie-Sektion erweitert
R4-3 "Alle 7 Tape-Groessen" irrefuehrend (QL kennt mehr) Sektion 1 — initial Scope + UnsupportedTapeError-Notiz
R4-4 Import-Style from PIL.Image Sektion 4 — from PIL import Image + Image.Image

Diff-Summary

1 file changed, 42 insertions(+), 14 deletions(-)

Wenn Round 5 keine neuen kritischen Findings bringt, ist die Spec ship-ready. Wir koennen dann superpowers:writing-plans fuer Phase 1k.1a anstossen.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 4 comments.

Comment on lines +231 to +235
backend/app/services/label_renderer.py # ersetzt durch LayoutEngine
backend/app/services/template_loader.py # Templates obsolet
backend/app/schemas/template.py # v1 Schema obsolet
backend/app/api/routes/templates.py # /api/templates/* komplett weg
backend/app/api/routes/templates_preview.py # /api/templates/{key}/preview-* weg
|---------|---------------------|--------------|
| `TapeMismatchError` | `print_service.py:94`, `:235`, `error_handlers.py` | Klasse + Handler geloescht — Engine rendert immer auf `loaded_tape_mm` |
| `on_tape_mismatch=queue\|fail` PrintRequest-Feld | `routes/print.py`, `routes/batch.py` | Feld geloescht — alle Requests verhalten sich wie "auto-scale" |
| PAUSED-Job State | `print_queue.py`, `JobStateMachine` | State + Transitions geloescht — Jobs sind QUEUED/PRINTING/COMPLETED/FAILED |
Comment on lines +13 to +14
1. **TapeGeometry** — Tabelle mit **initialem Scope** von 7 Tape-Groessen (**4**/6/9/12/18/24/62mm — `int`, kleinste PT-TZe ist **4mm** (24 Print-Pins) — nicht 3.5mm wie in fruehen Drafts) und ihren Render-Parametern (printable_px, qr_max, Font-Groessen). Die bestehende `TapeRegistry` kennt zusaetzliche QL-DK-Breiten (29/38/50/54mm) — Layout-Engine in 1k.1 deckt diese **bewusst noch nicht** ab; entsprechende Print-Requests fuehren zu `UnsupportedTapeError`. Erweiterung der Tabelle ist Constants-Aenderung (kein Code-Refactor) und kann als Folge-Phase ergaenzt werden.
2. **7 ContentTypes** — semantische Beschreibung was gerendert wird, **tape-unabhaengig** (qr_only, qr_one_line, qr_two_lines, **qr_three_lines** fuer 3-Zeilen-Layouts mit secondary, text_one_line, text_two_lines, qr_with_listing)
**Status:** Draft (zur User-Review)
**Tracking:** strausmann/Label-Printer-Hub#103 (Phase 1k.1 unter Umbrella #101)
**Vorgaenger-Spec:** docs/superpowers/specs/2026-05-17-phase-7e-template-layout-v2-design.md (subsumiert)
**Hardware-Baseline:** Phase 1i V4-Winner (docs/site/operations/protokolle/2026-06-04-phase1i-smoke-test-empirie.md)
Adressiert 4 Copilot Round-5-Findings auf commit c37d494. Reine
Klarstellungen, keine Designaenderungen.

- R5-1: Geloeschte-Files-Liste erweitert um models/template.py,
  repositories/templates.py, schemas/template_read.py
- R5-2: PAUSED-Entfernung impliziert NICHT CANCELLED-Entfernung.
  Klarstellung: Jobs sind QUEUED/PRINTING/COMPLETED/FAILED/CANCELLED
- R5-3: PR-Body weiter outdated (GraphQL-Warning beim ersten Update),
  wird erneut gesetzt
- R5-4: Hardware-Baseline-Referenz korrigiert: Issue #103 Kommentar +
  Hinweis auf privates homelab-management Repo

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 5 Fixes Applied — Commit 6a1b88d

Alle 4 Copilot Round-5-Findings adressiert + PR-Body via PATCH-API nachgezogen.

Round 5 (4/4 adressiert)

# Finding Adressiert in
R5-1 Geloeschte-Files Liste unvollstaendig Sektion 5 — models/template.py, repositories/templates.py, schemas/template_read.py ergaenzt
R5-2 CANCELLED-State wurde nicht erwaehnt Sektion 5 Obsolete-Tabelle — Klarstellung: CANCELLED BLEIBT (durch Cancel-Operation, unabhaengig vom PAUSED-Pfad)
R5-3 PR-Body veraltet Body via gh API PATCH neu gesetzt (gh pr edit hatte einen GraphQL-Warning aber schrieb nicht durch)
R5-4 Hardware-Baseline-Pfad nicht existent Spec-Header — Verweis auf Issue #103 Kommentar + Hinweis auf privates homelab-management Repo

Diff-Summary

1 file changed, 16 insertions(+), 2 deletions(-)

Falls Round 6 nichts kritisches mehr bringt, ist die Spec ship-ready und wir gehen zu writing-plans fuer Phase 1k.1a.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

# Phase 1k.1 — Layout-Engine + TapeGeometry + ContentTypes (Design)

**Datum:** 2026-06-05
**Status:** Draft (zur User-Review)

| Error | HTTP | Zweck |
|-------|------|-------|
| `UnsupportedTapeError(tape_mm)` | 422 | tape_mm nicht in TAPE_GEOMETRY (defensive — tritt mit 7 Groessen nicht praktisch auf) |
| `POST /jobs/{job_id}/resume` Route | `routes/jobs.py:230` UND `routes/print.py` (separater Endpoint im on_tape_mismatch-PAUSED-Workflow) | Beide Routes geloescht — Resume war nur fuer PAUSED-Jobs noetig |
| `MixedTapeSizesError` | `batch_dispatch.py`, `routes/batch.py:60+` | Klasse + 400-Mapping geloescht — Batches mit gemischten ContentTypes rendern alle auf gleiche `loaded_tape_mm` |

### Neue/geanderte Routes
Adressiert 3 Copilot Round-6-Findings auf commit 6a1b88d.

- R6-1: Spec-Header Status auf "Approved — Ready for writing-plans"
  (war "Draft (zur User-Review)", widerspruechlich zum Review-Verlauf)
- R6-2: UnsupportedTapeError HTTP 422 -> 409 (konsistent mit bestehenden
  hardware-/preflight-Errors TapeEmpty/CoverOpen/TapeMismatch).
  422 bleibt fuer ContentTypeDataMismatchError (clientseitig korrigierbar)
- R6-3: Route-Prefix-Inkonsistenz dokumentiert (print.py ohne Prefix vs
  batch.py/jobs.py mit /api). Phase 1k.1a normalisiert ALLE Print-/Job-/
  Render-Routes auf /api/ als bewusstes Breaking Change. Andere Routes
  (printers, lookup, webhooks, qr) bleiben unveraendert.

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 6 Fixes Applied — Commit b08c4c2

Alle 3 Copilot Round-6-Findings adressiert. Reine Klarstellungen + Status-Update.

Round 6 (3/3 adressiert)

# Finding Adressiert in
R6-1 Spec-Header "Draft" widerspruechlich zum "ship-ready" Review-Verlauf Header — Status auf "Approved — Ready for writing-plans"
R6-2 UnsupportedTapeError als 422 statt 409 (Konflikt mit bestehenden 409-Mapping) Sektion 4 Error-Tabelle — auf 409 angepasst
R6-3 Route-Prefix-Inkonsistenz (print.py ohne, batch/jobs mit /api) Sektion 5 Routes — bewusstes Breaking Change auf /api/ Prefix dokumentiert

Diff-Summary

1 file changed, 22 insertions(+), 8 deletions(-)

Spec ist jetzt approved. Wenn Round 7 nichts kritisches mehr liefert, starte ich superpowers:writing-plans fuer Phase 1k.1a Hub Layout-Engine.

Copilot AI 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.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 3 comments.

Comment on lines +30 to +38
| ContentType | Layout-Beschreibung | Genutzte LabelData-Felder | Original Templates (vorher) |
|-------------|---------------------|---------------------------|----------------------------|
| `qr_only` | QR fuellt volle Tape-Hoehe, kein Text | `qr_payload` | qr-only-12mm, qr-only-18mm, qr-only-24mm |
| `qr_one_line` | QR links + 1 Text-Zeile (XL, vertikal zentriert) | `qr_payload`, `primary_id` | (neu, war Sonderfall) |
| `qr_two_lines` | QR links + 2 Text-Zeilen (XL primary_id + L title) | `qr_payload`, `primary_id`, `title` | hangar-furniture-12mm, grocy-12mm, snipeit-12mm, spoolman-12mm, samla-stirntag-12mm, samla-stirntag-24mm, samla-stirntag-62mm, samla-deckel-12mm, samla-deckel-24mm, samla-deckel-62mm |
| `qr_three_lines` | QR links + 3 Text-Zeilen (XL primary_id + L title + S secondary[0]) | `qr_payload`, `primary_id`, `title`, `secondary` | hangar-furniture-18mm, hangar-furniture-24mm, grocy-18mm, grocy-24mm, snipeit-18mm, snipeit-24mm, spoolman-18mm, spoolman-24mm |
| `text_one_line` | Voll-Breite Text XL, kein QR | `primary_id` | (neu, kein altes Template) |
| `text_two_lines` | 2 Text-Zeilen XL + L, kein QR | `primary_id`, `title` | (neu, kein altes Template) |
| `qr_with_listing` | QR links + N Item-Zeilen (M-Groesse), Overflow zeigt "+N more" | `qr_payload`, `primary_id` (Header), `items: tuple[LabelDataItem,...]` | (neu, fuer Kallax-Regal-Uebersicht aus 7e-Spec) |

`qr_max_px` folgt der allgemeinen Formel `printable_px - 2 * qr_padding_px` — damit ist die Geometrie pro Eintrag konsistent und nicht abhaengig von einem hardgecodeten Padding.

`text_start_x` ist die **absolute Pixel-X-Position** ab dem linken Tape-Rand. Sie liegt logisch hinter dem QR-Block plus einem Gap von `2 * qr_padding_px` (symmetrisches Padding: einmal vor dem QR, das QR selbst, dann noch einmal das Padding als Trenn-Gap zum Text). Damit gilt die Formel `text_start_x = qr_padding_px + qr_max_px + 2 * qr_padding_px = printable_px + qr_padding_px`. Bei reinen Text-ContentTypes ohne QR (text_one_line, text_two_lines) wird `text_start_x` ignoriert — Text rendert ab `qr_padding_px` links.
"templates",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("key", sa.String, unique=True),
# ... ursprueengliche Spalten
Adressiert Copilot Round-7 (3 Findings, 2 valide + 1 Halluzination).

- R7-1: IGNORIERT — Copilot behauptet doppelte fuehrende Pipes in
  Markdown-Tabellen, verifiziert nicht im File vorhanden (Halluzination)
- R7-2: text_start_x Erklaerung in 3 klare Komponenten umstrukturiert
  (linker QR-Inset / QR-Code / Trenn-Gap) — Mehrdeutigkeit beseitigt
- R7-3: Typo ursprueengliche -> urspruengliche

Spec-Konvergenz: R1=18 -> R2=7 -> R3=4 -> R4=4 -> R5=4 -> R6=3 -> R7=3
(davon 1 Halluzination). Inhalt stabil seit R3.

Status: Approved — Ready for writing-plans (Phase 1k.1a).

Refs #103

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

Copy link
Copy Markdown
Owner Author

Round 7 — Konvergenz erreicht — Commit 6de07cd

# Finding Status
R7-1 Doppelte fuehrende Pipes `
R7-2 text_start_x Erklaerung mehrdeutig Adressiert — in 3 klare Komponenten umstrukturiert (linker QR-Inset / QR-Code / Trenn-Gap)
R7-3 Typo ursprueengliche Adressiert

Convergence-Analyse

Round Findings Trend
1 18 (8 CRIT, 7 MED, 3 LOW) Initial
2 7 Hot-Spots
3 4 Klein
4 4 Klarstellungen
5 4 Finishing
6 3 Polish
7 3 (1 Halluzination) Konvergiert

Inhalt stabil seit Round 3 — nachfolgende Runden waren ausschliesslich Klarstellungen, Konsistenz-Checks und Tippfehler. Spec ist final approved.

Next step: superpowers:writing-plans fuer Phase 1k.1a Hub Layout-Engine.

Implementation-Plan basierend auf approved Spec (commit 6de07cd):
docs/superpowers/specs/2026-06-05-phase-1k1-layout-engine-design.md

25 bite-sized TDD-Tasks:
- Tasks 1-4: Foundation Schemas (TapeGeometry, ContentType, LabelDataItem,
  LabelData mit optionalen Feldern)
- Task 5: Neue Exceptions (UnsupportedTapeError 409, NoTapeLoadedError 409,
  ContentTypeDataMismatchError 422)
- Tasks 6-13: LayoutEngine — Skeleton + 7 _render_* Methoden (qr_only,
  qr_one_line, qr_two_lines (V4-Winner), qr_three_lines, text_one_line,
  text_two_lines, qr_with_listing)
- Task 14: PrintRequest content_type-Schema (template_id raus)
- Tasks 15-17: Service-Refactors (PrintService, PrintQueue Rerender-Pfad
  KRITISCH, BatchDispatch Cleanup)
- Tasks 18-20: Route-Refactors (POST /api/print/{slug}, batch, jobs/resume
  geloescht)
- Task 21: error_handlers — neue Mappings registriert
- Task 22: Alembic Migration (templates drop, jobs.content_type +
  rendered_tape_mm add, template_key nullable, deterministischer
  Backfill via op.batch_alter_table)
- Task 23: Hard-Cut Deletion (13 Files + 21 YAMLs + obsolete Tests)
- Task 24: main.py + lifespan.py Cleanup
- Task 25: Integration-Test + Smoke-Script

Coverage-Ziel 90%+ auf neuen Modulen. Hardware-Smoke nach Deploy: 12mm
V4-Winner visuell identisch.

Refs #103

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@strausmann strausmann changed the title spec: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes docs: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes (Spec + Plan) Jun 9, 2026
@strausmann
strausmann merged commit a5425a5 into main Jun 9, 2026
13 of 14 checks passed
@strausmann
strausmann deleted the spec/phase-1k1-layout-engine branch June 9, 2026 21:47
github-actions Bot pushed a commit that referenced this pull request Jun 10, 2026
## <small>0.10.1 (2026-06-10)</small>

* fix(brother-ql): doppelter 'preflight SNMP failed' Prefix vermieden (Issue #105) (#114) ([e459fd7](e459fd7)), closes [#105](#105) [#114](#114) [#105](#105)
* fix(examples): aktualisiere compose.pangolin.yml und .env.example auf aktuelle Konfiguration (#113) ([a16643e](a16643e)), closes [#113](#113) [#73](#73)
* chore(deps): bump the go-minor-and-patch group across 1 directory with 3 updates (#112) ([3939741](3939741)), closes [#112](#112)
* docs: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes (Spec + Plan) (#108) ([a5425a5](a5425a5)), closes [#108](#108) [#103](#103) [#108](#108) [#103](#103) [#103](#103) [#103](#103) [#103](#103) [#103](#103) [#103](#103) [#103](#103) [#103](#103) [#103](#103)
* ci(deps): bump codecov/codecov-action in the actions-all group (#111) ([2a9454b](2a9454b)), closes [#111](#111)

[skip ci]
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.

Phase 7e: Template Layout System v2 — semantic qr-first + N-text-lines deklarativ

2 participants