Skip to content

docs(api): pure-vector SVG samples for all 12 seed templates#83

Merged
strausmann merged 1 commit into
mainfrom
docs/template-svg-samples
May 17, 2026
Merged

docs(api): pure-vector SVG samples for all 12 seed templates#83
strausmann merged 1 commit into
mainfrom
docs/template-svg-samples

Conversation

@strausmann
Copy link
Copy Markdown
Owner

Summary

Generates one pure-vector SVG per seed template at docs/site/operations/templates/svg-samples/{key}.svg plus 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:1
  • backend/scripts/generate_template_svgs.py — CLI iterating all seed templates with preview_sample
  • 12 SVG files under docs/site/operations/templates/svg-samples/
  • docs/site/operations/templates/layouts.md — MkDocs page with all 12 embedded
  • mkdocs.yml nav update
  • Makefile: `make docs-svg-samples` target
  • 11 new unit tests, all green

Test plan

  • 696 passed (685 baseline + 11 new SVG tests), 3 skipped, 0 failed
  • ruff + ruff format + mypy clean
  • QR codes pure-vector (qrcode SvgPathImage), text pure <text>, tape outline <rect>
  • All 12 SVGs have title annotation showing template-key + tape-mm + text-line-count

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

Copilot AI review requested due to automatic review settings May 17, 2026 21:38
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

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

This pull request introduces a 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

  • SVG Preview Generation: Added a pure-vector SVG renderer and a CLI script to generate preview samples for all 12 seed templates, improving visual diffing capabilities.
  • API Preview Endpoint: Implemented a new POST /api/render/preview endpoint that renders label previews using template-specific sample data.
  • Printer Metadata API: Added a GET /api/printers/{id} endpoint to retrieve full printer metadata, including the paused state.
  • Frontend Fixes: Resolved issues with the 'Paused' badge display by fixing Go struct generation and updated the printer detail page to display full metadata.
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.

Comment thread backend/app/api/routes/templates.py Fixed
Comment thread backend/app/api/routes/templates.py Fixed
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Codecov Report

❌ Patch coverage is 89.04110% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.09%. Comparing base (52bab83) to head (558bd3c).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
backend/app/services/svg_renderer.py 89.04% 4 Missing and 4 partials ⚠️
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     
Components Coverage Δ
Printer Backends (transport) 87.50% <ø> (ø)
Printer Models (drivers) 91.42% <ø> (ø)
Services 91.87% <89.04%> (-0.23%) ⬇️
REST API 87.04% <ø> (ø)
Pydantic Schemas 100.00% <ø> (ø)
Integration Plugins 100.00% <ø> (ø)
Files with missing lines Coverage Δ
backend/app/services/svg_renderer.py 89.04% <89.04%> (ø)
Flag Coverage Δ
backend 91.09% <89.04%> (-0.05%) ⬇️

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


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 52bab83...558bd3c. 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.

Copy link
Copy Markdown

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

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

Comment thread backend/app/api/routes/templates.py Outdated
Comment on lines +147 to +154
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")
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

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
  1. 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}"
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

The annotation string is inserted directly into the SVG without escaping. If the template_id contains XML special characters (like & or <), the resulting SVG will be invalid. Consider using xml.sax.saxutils.escape or html.escape for all text content.

Comment on lines +191 to +196
text_value = (
text_value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
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

Manual XML escaping is error-prone and incomplete (e.g., it misses the apostrophe '). It is better to use a standard library function like html.escape or xml.sax.saxutils.escape for robustness.

Comment on lines +198 to +201
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>'
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

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.

Suggested change
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>'

Copy link
Copy Markdown

Copilot AI left a comment

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 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 SvgRenderer service + 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_sample data.

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.

Comment on lines +57 to +60
// 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())
}
Comment on lines +66 to +68
source_app=template_app or "generic",
secondary=tuple(preview_sample.get("secondary", ()) or ()),
)
Comment on lines +69 to +73
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
Comment thread backend/app/api/routes/templates.py Outdated
Comment on lines +124 to +129
# 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"}
Comment thread backend/app/schemas/template.py Outdated
Comment on lines +79 to +82
app: str | None
tape_mm: int
elements: tuple[LayoutElement, ...]
preview_sample: dict[str, str | int | float | bool | list[str] | tuple[str, ...]] | None = None
Comment on lines +153 to +157
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>"
Comment on lines +39 to +40
0 on success, 1 if any template lacks a preview_sample (counted as a
warning, not a failure).
Comment on lines +99 to +103
// 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.
Comment on lines +59 to +65
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
@strausmann strausmann force-pushed the docs/template-svg-samples branch 2 times, most recently from dec0212 to 558bd3c Compare May 17, 2026 23:14
@strausmann strausmann merged commit a066dde into main May 17, 2026
27 checks passed
@strausmann strausmann deleted the docs/template-svg-samples branch May 17, 2026 23:24
github-actions Bot pushed a commit that referenced this pull request May 18, 2026
## 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]
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.

3 participants