docs: Phase 1k.1 Layout-Engine + TapeGeometry + ContentTypes (Spec + Plan)#108
Conversation
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>
Summary of ChangesHello, 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
Using Gemini Code AssistThe 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
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 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
|
Codecov Report✅ All modified and coverable lines are covered by tests. 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
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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
- 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.
| 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") |
There was a problem hiding this comment.
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.
| 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") |
Technische Abnahme: Phase 1k.1 Layout-Engine SpecReviewer: ops-agent (technische Abnahme, kein Code geändert) CRITICALC1 — Hard-Cut Liste ist unvollständig: Die Spec listet in Sektion 5 „Gelöschte Files" folgende Files, übersieht aber 5 weitere Abhängigkeiten die aktiv in der Codebase existieren:
Empfehlung: Sektion 5 „Gelöschte Files" und „Modifizierte Files" um diese Einträge ergänzen. Besonders der C2 — Das op.drop_column("jobs", "template_id") # ← FALSCHDie korrekte Spalte heißt Zusätzlich: 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: C3 —
Der PAUSED-Job-Pfad in
Empfehlung: In Sektion 5 explizit festlegen: wird MEDIUMM1 — Sektion 3, 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, ...)
Wenn aber Empfehlung: Einen Kommentar in der Spec und im Code hinzufügen der klärt: M2 — Preview-Endpoint: GET mit Sektion 5, neue Route: GET-Requests mit JSON-Body in Query-Params haben mehrere Probleme:
Empfehlung: Entweder:
Option 1 ist für den Hub-API-Consumer (Hangar) vorzuziehen. M3 — Das bestehende Bei Empfehlung: In Sektion 2 oder 5 explizit klären ob M4 — Sektion 2: „Overflow zeigt
Für 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 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 LOWL1 — Sektion 2, Validation-Tabelle: „ Das Spec-Snippet in Sektion 5 zeigt: # label_data.py
items: tuple[LabelDataItem, ...] = ()Mit Default Außerdem: Was passiert wenn Empfehlung: In der Validation-Tabelle (Sektion 2) präzisieren: „ L2 —
Empfehlung: Entweder L3 — Sektion 2: Empfehlung: Entweder PRAISEP1 — Tape-Independence ist konzeptionell sehr sauber. P2 — Hard-Cut statt Compat-Layer ist die richtige Wahl für diesen Entwicklungsstand. P3 — P4 — Sub-Phasen-Decomposition (1k.1a → b → c → d) ist klar und einzeln deploy-bar. P5 — Zusammenfassung Prioritäten vor Implementation
|
There was a problem hiding this comment.
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
ContentTypeset, validation rules, and aTapeGeometrytable 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) |
| 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) | |
| 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). |
| 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>
Round 1 Fixes Applied — Commit 2545467Alle 8 CRITICAL + 7 MEDIUM Findings aus den drei Reviews adressiert (siehe Sektion 12 "Review-Round 1" im Spec). CRITICAL (8/8)
MEDIUM (7/7)
User-DesignentscheidungEingearbeitet in Sektion 2 + 6: Samla unified — alle 6 Samla-Templates (Stirntag/Deckel x 12/24/62mm) -> eine Hangar-Category "Samla" mit Diff-SummaryBitte Re-Review. |
| +-- 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) |
| `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. | |
| GET /admin/print/categories/preview # HTMX-Endpoint: liefert SVG aus Hub | ||
| # Query: ?content_type=...&tape_mm=...&primary_id=...&title=... | ||
| ``` |
| Jobs mit `template_key=NULL` (bereits in obsoletem Zustand) bleiben mit `content_type=NULL` — fuer Historie tolerierbar (Frontend zeigt "(legacy)" wenn NULL). | ||
|
|
| *-- 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>
Round 2 Fixes Applied — Commit 9c877cbAlle 7 Copilot Round-2-Findings adressiert + User-Designentscheidung zum Initial-Seeding eingearbeitet. Round 2 (7/7 adressiert)
User-Designentscheidung — YAML-frei (NEU in Round 2)Initial-Seeding der Hangar-Categories erfolgt ueber Go-Code Seed-Reihenfolge beim ersten Start:
Sektion 6 1k.1b YAML-File jetzt als "Uebergangs-Loesung" markiert; Sektion 7 1k.1c komplett umgeschrieben mit Diff-SummaryErneute Re-Review bitte fuer commit 9c877cb. |
| 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) | |
| # 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>
Round 3 Fixes Applied — Commit b10552aAlle 4 Copilot Round-3-Findings adressiert. PR-Body parallel aktualisiert auf 7 ContentTypes / 4mm. Round 3 (4/4 adressiert)
Wichtigste Aenderung: template_key als Audit-SnapshotStatt zu droppen, wird
Diff-SummaryBitte finale Re-Review fuer commit b10552a. Wenn nichts mehr kritisches kommt, gehen wir mit dieser Spec in writing-plans fuer Phase 1k.1a. |
| | `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) |
| 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. | ||
|
|
| 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) | ||
|
|
| 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>
Round 4 Fixes Applied — Commit c37d494Alle 4 Copilot Round-4-Findings adressiert. Reine Klarstellungen, keine neuen Designaenderungen. Round 4 (4/4 adressiert)
Diff-SummaryWenn Round 5 keine neuen kritischen Findings bringt, ist die Spec ship-ready. Wir koennen dann |
| 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 | |
| 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>
Round 5 Fixes Applied — Commit 6a1b88dAlle 4 Copilot Round-5-Findings adressiert + PR-Body via PATCH-API nachgezogen. Round 5 (4/4 adressiert)
Diff-SummaryFalls Round 6 nichts kritisches mehr bringt, ist die Spec ship-ready und wir gehen zu writing-plans fuer Phase 1k.1a. |
| # 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>
Round 6 Fixes Applied — Commit b08c4c2Alle 3 Copilot Round-6-Findings adressiert. Reine Klarstellungen + Status-Update. Round 6 (3/3 adressiert)
Diff-SummarySpec ist jetzt approved. Wenn Round 7 nichts kritisches mehr liefert, starte ich |
| | 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>
Round 7 — Konvergenz erreicht — Commit 6de07cd
Convergence-Analyse
Inhalt stabil seit Round 3 — nachfolgende Runden waren ausschliesslich Klarstellungen, Konsistenz-Checks und Tippfehler. Spec ist final approved. Next step: |
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>
## <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]
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_keybleibt 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(keintape_mm), Hub liestpreflight.loaded_tape_mmund rendert passend.TapeMismatchErrorwird obsolet.YAML-frei: Hangar Categories werden nicht mehr aus YAML, sondern aus Go-Code
DefaultCategoriesSlice initial geseedet.HUB_LAYOUTS_PATHwird in 1k.1c deprecated.LabelData Schema-Update: title/primary_id/qr_payload werden auf optional (
str | None) gesetzt; ContentType-spezifische Pflichtfeld-Validation passiert zentral inLayoutEngine._validate_data().Scope-Decomposition (4 Sub-Phasen)
Sequentielle Merge-Reihenfolge: a -> b -> c -> d. Jede Phase eigenstaendig deploy-bar.
Review-Verlauf (5 Runden)
Alle Findings adressiert. Spec ist ship-ready fuer writing-plans.
Test plan
Closes #81 beim Merge (7e-Spec subsumiert).
Refs #103 (Phase 1k.1 unter Umbrella #101).
Generated with Claude Code.