docs(api): pure-vector SVG samples for all 12 seed templates#83
Conversation
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! This pull request introduces a new pure-vector SVG rendering system for label templates, providing a reliable visual basis for layout development. It also addresses several backend and frontend gaps, including a missing printer detail API endpoint and a bug in the boolean state handling for printer status, ensuring consistent and accurate UI representation. 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❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #83 +/- ##
==========================================
- Coverage 91.14% 91.09% -0.05%
==========================================
Files 70 71 +1
Lines 3116 3189 +73
Branches 263 271 +8
==========================================
+ Hits 2840 2905 +65
- Misses 212 216 +4
- Partials 64 68 +4
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Code Review
This pull request implements a template preview system featuring a new backend endpoint and an SVG rendering service, while also resolving bugs related to missing printer metadata and incorrect 'Paused' badge rendering. The feedback identifies opportunities to improve the robustness and performance of the new rendering logic. Specifically, synchronous image processing should be offloaded to threads to avoid blocking the event loop, and the SVG renderer should utilize standard library functions for XML escaping and idiomatic SVG attributes for precise text alignment.
| img = renderer.render(template_schema, sample_data) | ||
| except ValueError as exc: | ||
| _log.warning("render_preview: render failed for key=%r: %s", key, exc) | ||
| raise HTTPException(status_code=422, detail=str(exc)) from exc | ||
|
|
||
| # Convert PIL image to PNG bytes | ||
| buf = io.BytesIO() | ||
| img.save(buf, format="PNG") |
There was a problem hiding this comment.
The rendering process (renderer.render and img.save) involves synchronous operations that block the event loop in this async function. To prevent performance degradation, wrap these operations in asyncio.to_thread as per repository guidelines for blocking I/O.
References
- Wrap synchronous blocking I/O operations, such as Alembic commands, in asyncio.to_thread when calling them from asynchronous functions to avoid blocking the event loop.
| f' <text x="4" y="{_ANNOTATION_HEIGHT_PX - 4}"' | ||
| f' font-family="Liberation Sans, DejaVu Sans, sans-serif"' | ||
| f' font-size="10" fill="#666">' | ||
| f"{annotation}" |
| text_value = ( | ||
| text_value.replace("&", "&") | ||
| .replace("<", "<") | ||
| .replace(">", ">") | ||
| .replace('"', """) | ||
| ) |
| f' <text x="{ex}" y="{svg_y + font_size}"' | ||
| f' font-family="Liberation Sans, DejaVu Sans, sans-serif"' | ||
| f' font-size="{font_size}"' | ||
| f' fill="black">{text_value}</text>' |
There was a problem hiding this comment.
Adding font_size to the y coordinate is an approximation for top-left alignment. A more precise and idiomatic SVG approach is to use dominant-baseline="hanging", which aligns the top of the text with the specified y coordinate.
| f' <text x="{ex}" y="{svg_y + font_size}"' | |
| f' font-family="Liberation Sans, DejaVu Sans, sans-serif"' | |
| f' font-size="{font_size}"' | |
| f' fill="black">{text_value}</text>' | |
| f' <text x="{ex}" y="{svg_y}" dominant-baseline="hanging"' | |
| f' font-family="Liberation Sans, DejaVu Sans, sans-serif"' | |
| f' font-size="{font_size}"' | |
| f' fill="black">{text_value}</text>' |
There was a problem hiding this comment.
Pull request overview
Adds a pure-vector SVG rendering pipeline for seed templates (to make layout diffs reviewable) and wires up supporting preview/metadata endpoints and UI plumbing so templates/printers have consistent “detail + preview” surfaces.
Changes:
- Added
SvgRendererservice + a CLI script/Make target to generate versioned SVG samples for seed templates. - Added/extended backend + frontend APIs to fetch printer detail metadata and render template previews.
- Added docs page embedding all generated SVG samples and updated seed templates with
preview_sampledata.
Reviewed changes
Copilot reviewed 32 out of 44 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| Makefile | Adds docs-svg-samples target to regenerate SVG previews. |
| backend/app/api/routes/printers.py | Adds GET /api/printers/{printer_id} printer detail endpoint. |
| backend/app/api/routes/templates.py | Adds POST /api/render/preview endpoint and router wiring. |
| backend/app/main.py | Registers the new templates render_router. |
| backend/app/schemas/printer.py | Makes paused required in PrinterRead. |
| backend/app/schemas/template.py | Adds preview_sample field to template schema. |
| backend/app/services/svg_renderer.py | New pure-vector SVG renderer mirroring LabelRenderer coordinates. |
| backend/scripts/generate_template_svgs.py | New CLI to generate SVG samples from seed templates. |
| backend/tests/unit/api/test_printers_routes.py | Adds tests for the new printer detail endpoint. |
| backend/tests/unit/api/test_templates_routes.py | Adds tests for preview rendering endpoint behavior. |
| backend/tests/unit/services/test_svg_renderer.py | Adds unit tests validating SVG structure/vector-only output. |
| backend/app/seed/templates/grocy-12mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/grocy-18mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/grocy-24mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/qr-only-12mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/qr-only-18mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/qr-only-24mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/snipeit-12mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/snipeit-18mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/snipeit-24mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/spoolman-12mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/spoolman-18mm.yaml | Adds preview_sample for SVG/preview generation. |
| backend/app/seed/templates/spoolman-24mm.yaml | Adds preview_sample for SVG/preview generation. |
| docs/site/operations/templates/layouts.md | New docs page embedding all SVG samples + technical notes. |
| docs/site/operations/templates/svg-samples/grocy-12mm.svg | Generated SVG sample for Grocy 12mm template. |
| docs/site/operations/templates/svg-samples/grocy-18mm.svg | Generated SVG sample for Grocy 18mm template. |
| docs/site/operations/templates/svg-samples/grocy-24mm.svg | Generated SVG sample for Grocy 24mm template. |
| docs/site/operations/templates/svg-samples/qr-only-12mm.svg | Generated SVG sample for QR-only 12mm template. |
| docs/site/operations/templates/svg-samples/qr-only-18mm.svg | Generated SVG sample for QR-only 18mm template. |
| docs/site/operations/templates/svg-samples/qr-only-24mm.svg | Generated SVG sample for QR-only 24mm template. |
| docs/site/operations/templates/svg-samples/snipeit-12mm.svg | Generated SVG sample for Snipe-IT 12mm template. |
| docs/site/operations/templates/svg-samples/snipeit-18mm.svg | Generated SVG sample for Snipe-IT 18mm template. |
| docs/site/operations/templates/svg-samples/snipeit-24mm.svg | Generated SVG sample for Snipe-IT 24mm template. |
| docs/site/operations/templates/svg-samples/spoolman-12mm.svg | Generated SVG sample for Spoolman 12mm template. |
| docs/site/operations/templates/svg-samples/spoolman-18mm.svg | Generated SVG sample for Spoolman 18mm template. |
| docs/site/operations/templates/svg-samples/spoolman-24mm.svg | Generated SVG sample for Spoolman 24mm template. |
| frontend/internal/api/openapi.snapshot.json | Updates OpenAPI snapshot for new endpoints/required fields. |
| frontend/internal/api/client.gen.go | Regenerated client/types (e.g., Paused bool, new printer detail call). |
| frontend/internal/api/client.go | Adds GetPrinterDetail() wrapper. |
| frontend/internal/api/client_test.go | Adds regression test for paused bool decoding behavior. |
| frontend/internal/handlers/printer.go | Fetches printer detail in parallel and passes metadata to template. |
| frontend/internal/handlers/printer_test.go | Adds regression test for printer detail metadata (currently weak assertion). |
| frontend/internal/handlers/dashboard_test.go | Adds regression test for paused badge (currently weak assertion). |
| frontend/web/templates/printer.html | Renders printer metadata block when .Printer is present. |
| // Verify page renders without error — metadata fields verified at template level. | ||
| if !strings.Contains(w.Body.String(), "printer-detail") { | ||
| t.Errorf("body missing 'printer-detail', got: %s", w.Body.String()) | ||
| } |
| source_app=template_app or "generic", | ||
| secondary=tuple(preview_sample.get("secondary", ()) or ()), | ||
| ) |
| except Exception as exc: # ValidationError or coercion error | ||
| raise HTTPException( | ||
| status_code=422, | ||
| detail=(f"Template {template_key!r} has an invalid preview_sample block: {exc}"), | ||
| ) from exc |
| # the TemplateSchema field values. Supplement missing fields from the row's | ||
| # top-level columns (id→key, tape_mm→tape_width_mm, etc.) so that rows | ||
| # created before the definition was normalised can still render. | ||
| # ``preview_sample`` is not a TemplateSchema field — strip it before | ||
| # passing to the schema constructor. | ||
| schema_dict = {k: v for k, v in definition.items() if k != "preview_sample"} |
| app: str | None | ||
| tape_mm: int | ||
| elements: tuple[LayoutElement, ...] | ||
| preview_sample: dict[str, str | int | float | bool | list[str] | tuple[str, ...]] | None = None |
| f' <text x="4" y="{_ANNOTATION_HEIGHT_PX - 4}"' | ||
| f' font-family="Liberation Sans, DejaVu Sans, sans-serif"' | ||
| f' font-size="10" fill="#666">' | ||
| f"{annotation}" | ||
| f"</text>" |
| 0 on success, 1 if any template lacks a preview_sample (counted as a | ||
| warning, not a failure). |
| // The stub dashboard-content template renders <span>Name</span> for each printer. | ||
| // The real badge logic lives in the real template; here we verify the data | ||
| // round-trips correctly: Paused must be a plain bool so the handler's data | ||
| // struct can be inspected via the stub template that accesses .Printers. | ||
| // The mock backend sends paused=false for PT-P750W and paused=true for QL-800. |
| try: | ||
| # source_app is filled from the template's own ``app`` field — falls | ||
| # back to "generic" for templates without an integration. | ||
| return LabelData( | ||
| primary_id=str(preview_sample.get("primary_id", "")), | ||
| title=str(preview_sample.get("title", "")), | ||
| qr_payload=str(preview_sample.get("qr_payload", "")), |
Adds backend/app/services/svg_renderer.py which produces SVG strings that mirror the LabelRenderer's pixel coordinate system 1:1. Text elements are pure <text>, QR codes use qrcode's SvgPathImage factory (box_size=1, border=0) with the <path> extracted and scaled via transform="translate(x,y) scale(factor)" — no raster embeds at all. Each SVG has a gray tape outline + 18px annotation strip above the tape showing template key, tape width, and text-line count. CLI script backend/scripts/generate_template_svgs.py iterates all seed templates that have preview_sample and writes one SVG per template to docs/site/operations/templates/svg-samples/. Makefile target docs-svg-samples wraps the script. Docs page docs/site/operations/templates/layouts.md embeds all 12 SVGs via <object> tags grouped by tape width for side-by-side comparison. 11 new unit tests in test_svg_renderer.py cover: valid XML, no raster embeds, correct viewBox per tape_mm, tape outline rect, QR as <path>, text value rendering, annotation strip, list join with ' | ', XML escaping, unsupported tape_mm error, and distinct heights per tape. These SVGs are the visual basis for Phase 7e #81 (semantic qr-first + N-text-lines layout system v2). Refs #22
dec0212 to
558bd3c
Compare
## 0.6.0 (2026-05-18) * docs(api): address PR #79 bot-review — privacy sanitise + protocol self + consistency fixes ([61602d0](61602d0)), closes [#79](#79) [#22](#22) * docs(api): pure-vector SVG samples for all 12 seed templates (#83) ([a066dde](a066dde)), closes [#83](#83) [#81](#81) [#22](#22) * docs(phase-7b): foundation design spec — init-robustness + health-split + pangolin-bypass (#74) ([c5a7964](c5a7964)), closes [#74](#74) [fosrl/pangolin#3099](fosrl/pangolin#3099) [#22](#22) [#22](#22) * docs(phase-7d): foundation design — print API + QR tab + hangar plugin (#79) ([cdaedeb](cdaedeb)), closes [#79](#79) [strausmann/hangar#63](https://github.com/strausmann/hangar/issues/63) [#22](#22) * fix: 3 production bugs from local smoke-test + dev/ folder ([c0fc903](c0fc903)), closes [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) * fix: Phase 6b code-cleanup — 6 audit findings + plugin pattern wired ([f77aa44](f77aa44)), closes [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) [#67](#67) * fix(api): Phase 7b.1 — upsert name-collision + /readiness proxy gap (#77) ([4e74a03](4e74a03)), closes [#77](#77) [#76](#76) [#22](#22) [#76](#76) [#22](#22) [#77](#77) [#1](#1) [#76](#76) [#22](#22) * fix(ui): printer detail metadata + template preview + paused-bool gap (#82) ([52bab83](52bab83)), closes [#82](#82) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) * feat(api): Phase 7b foundation — init, datetime-TZ, /readiness, status cache, proxy widening (#75) ([784decc](784decc)), closes [#75](#75) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [#22](#22) [HI#priority](https://github.com/HI/issues/priority) [#75](#75) [#22](#22) * feat(ui): proxy legacy /print to backend (restores First-Print smoke curl) (#84) ([8ef36ed](8ef36ed)), closes [#84](#84) [#22](#22) [#22](#22) [#84](#84) [#22](#22) [skip ci]
Summary
Generates one pure-vector SVG per seed template at
docs/site/operations/templates/svg-samples/{key}.svgplus a MkDocs page that embeds all 12 side-by-side via `` tags.Builds on top of PR #82 (preview_sample data in every seed template). When #82 merges, this branch will rebase cleanly to main.
Why
User + Claude need a git-diff-able visual basis for Phase 7e #81 layout-conversation. PNG bytes change wholesale on any rendering tweak — pure SVG diffs show actual structural changes.
What's in
backend/app/services/svg_renderer.py— pure-vector renderer mirroring LabelRenderer's pixel coordinate system 1:1backend/scripts/generate_template_svgs.py— CLI iterating all seed templates with preview_sampledocs/site/operations/templates/svg-samples/docs/site/operations/templates/layouts.md— MkDocs page with all 12 embeddedmkdocs.ymlnav updateMakefile: `make docs-svg-samples` targetTest plan
<text>, tape outline<rect>Follow-up
Phase 7e #81 will replace this with a Schema-v2 renderer that has semantic layout fields. These SVGs are the discussion basis for that brainstorming.
Refs #22