feat(panel): printer pairing QR export — per-printer QR PNG + tenant CSV - #109
Conversation
…nt registry) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds downloadPrinterPairingQr/downloadPrinterPairingCsv, fetching the Task 3-4 pairing-qr.png / pairing-export.csv endpoints via the shared authed api client and saving the returned Blob via a temporary anchor click, mirroring attendees/exportCsv.ts's downloadCsv mechanics.
|
Warning Review limit reached
Next review available in: 38 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughAdds tenant-scoped network printer pairing exports: individual 512px QR PNG downloads and bulk UTF-8 BOM CSV exports. The backend validates payloads and sanitizes CSV fields, while the equipment panel exposes corresponding download actions with generated API contracts, localized labels, and tests. ChangesPrinter pairing export
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Operator
participant EquipmentPage
participant pairingExport
participant PrinterPairingAPI
participant PGStore
Operator->>EquipmentPage: Select QR or CSV export action
EquipmentPage->>pairingExport: Start authenticated download
pairingExport->>PrinterPairingAPI: Request PNG or tenant CSV blob
PrinterPairingAPI->>PGStore: Load tenant-scoped printer data
PGStore-->>PrinterPairingAPI: Network printer records
PrinterPairingAPI-->>pairingExport: Pairing file blob
pairingExport-->>Operator: Save downloaded file
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
backend/internal/store/pg_store_equipment_test.go (1)
717-749: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse the required table-driven parallel test pattern.
Convert this unit test to a table case and call
t.Parallel(); the repository requires both for*_test.gotests.As per coding guidelines, “Write unit tests using table-driven patterns and parallel execution.”
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@backend/internal/store/pg_store_equipment_test.go` around lines 717 - 749, Convert TestListEquipmentPrintersForTenant_ReturnsNetworkPrintersWithHostname into a table-driven test with the existing scenario represented as a test case, call t.Parallel() at the test or subtest level as appropriate, and run the case through t.Run while preserving all current assertions and mock expectations.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/internal/handler/printer_pairing_export.go`:
- Around line 43-57: Normalize shape.IP once by trimming whitespace, validate
the normalized value for emptiness and valid IP syntax, and use that normalized
value when populating the QR payload’s IP field in the printer pairing export
flow. Add coverage for whitespace-padded valid input and invalid IP input.
- Around line 91-126: Instrument
backend/internal/handler/printer_pairing_export.go lines 91-126 in
GetPrinterPairingQR and lines 134-202 in the sibling export handler with
request-context spans. Add tenant/device or export attributes as appropriate,
propagate the span context through downstream calls, and record/fail the span
for storage, encoding, CSV-writing, and QR-generation errors before returning
the existing responses.
In `@docs/superpowers/plans/2026-07-23-printer-pairing-qr-export.md`:
- Around line 1056-1061: Remove stale network-printer gating from the export
plan and design. In
docs/superpowers/plans/2026-07-23-printer-pairing-qr-export.md:1056-1061,
replace the disabled-button test with enabled export behavior producing an
empty/header-only CSV; at 1112-1131 remove hasNetworkPrinter and the disabled
prop; at 1187-1189 update the no-printers edge-case summary. In
docs/superpowers/specs/2026-07-23-printer-pairing-qr-export-design.md:134-140
and 189-190, state that tenant-wide CSV export remains available with headers
only and remove disabled/hidden behavior.
In `@docs/superpowers/specs/2026-07-23-printer-pairing-qr-export-design.md`:
- Around line 74-102: Update the design contract for buildPrinterQRPayload to
show only the models.EquipmentDevice parameter and remove the hostname argument,
since hostname is CSV-only display data. Revise the filename description to
state that slugForFilename removes non-ASCII characters, with device_id as the
fallback, rather than claiming Cyrillic transliteration.
In `@panel/src/features/equipment/DeviceCard.tsx`:
- Around line 282-286: Move the pairing-QR download item using
onDownloadPairingQr and the network device.kind check outside the !agentDown
action subtree in DeviceCard, while preserving its existing label and handler.
Keep it available during agent outages, independently of live-agent actions.
In `@panel/src/features/equipment/EquipmentPage.tsx`:
- Around line 465-473: Update the download handlers in EquipmentPage.tsx at
lines 465-473 and 554-556 to catch rejections from downloadPrinterPairingCsv and
the QR download helper instead of discarding them with void. In both sites,
display the same localized error feedback using the existing translation and
notification mechanisms.
---
Nitpick comments:
In `@backend/internal/store/pg_store_equipment_test.go`:
- Around line 717-749: Convert
TestListEquipmentPrintersForTenant_ReturnsNetworkPrintersWithHostname into a
table-driven test with the existing scenario represented as a test case, call
t.Parallel() at the test or subtest level as appropriate, and run the case
through t.Run while preserving all current assertions and mock expectations.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: d7dc0d5b-524f-4492-9b8b-384f509f2ec7
📒 Files selected for processing (19)
backend/internal/handler/handler.gobackend/internal/handler/printer_pairing_export.gobackend/internal/handler/printer_pairing_export_test.gobackend/internal/handler/testsupport_test.gobackend/internal/models/equipment.gobackend/internal/store/interface.gobackend/internal/store/pg_store_equipment.gobackend/internal/store/pg_store_equipment_test.gobackend/openapi.yamldocs/superpowers/plans/2026-07-23-printer-pairing-qr-export.mddocs/superpowers/specs/2026-07-23-printer-pairing-qr-export-design.mdpanel/src/features/equipment/DeviceCard.tsxpanel/src/features/equipment/EquipmentPage.test.tsxpanel/src/features/equipment/EquipmentPage.tsxpanel/src/features/equipment/pairingExport.test.tspanel/src/features/equipment/pairingExport.tspanel/src/shared/api/schema.d.tspanel/src/shared/i18n/en.jsonpanel/src/shared/i18n/ru.json
| func (h *Handler) GetPrinterPairingQR(c echo.Context) error { | ||
| deviceID, err := uuid.Parse(c.Param("device_id")) | ||
| if err != nil { | ||
| return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid device ID"}) | ||
| } | ||
| tenantID, err := tenantIDFromContext(c) | ||
| if err != nil { | ||
| return writeErr(c, err) | ||
| } | ||
|
|
||
| device, err := h.Store.GetEquipmentDeviceForTenant(c.Request().Context(), tenantID, deviceID) | ||
| if err != nil { | ||
| return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Internal error"}) | ||
| } | ||
| if device == nil { | ||
| return c.JSON(http.StatusNotFound, map[string]string{"error": "Device not found"}) | ||
| } | ||
|
|
||
| payload, err := buildPrinterQRPayload(*device) | ||
| if err != nil { | ||
| return c.JSON(http.StatusUnprocessableEntity, map[string]string{"error": err.Error()}) | ||
| } | ||
|
|
||
| jsonData, err := json.Marshal(payload) | ||
| if err != nil { | ||
| return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to encode printer data"}) | ||
| } | ||
| png, err := qrcode.Encode(string(jsonData), qrcode.Medium, 512) | ||
| if err != nil { | ||
| return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to generate QR code"}) | ||
| } | ||
|
|
||
| c.Response().Header().Set("Content-Disposition", | ||
| fmt.Sprintf(`attachment; filename="%s-pairing-qr.png"`, slugForFilename(device.DisplayName, device.ID))) | ||
| return c.Blob(http.StatusOK, "image/png", png) | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n --glob '*.go' \
'otel\.Tracer|trace\.SpanFromContext|span\.RecordError|span\.SetStatus' \
backend/internalRepository: thevladbog/idento
Length of output: 155
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== candidate file outline =="
ast-grep outline backend/internal/handler/printer_pairing_export.go || true
echo
echo "== handler imports and relevant handler implementation =="
sed -n '1,240p' backend/internal/handler/printer_pairing_export.go
echo
echo "== tracing references across backend =="
rg -n --glob '*.go' '(\bobot?|otel|trace|Span|RecordError|SetStatus|NewTracer|NewTracerProvider)' backend || true
echo
echo "== imports context/json/qrcode/buildPrinterQRPayload/tenantIDFromContext usages =="
rg -n --glob '*.go' 'trace\.Tracer|SpanFromContext|otel|NewTracer|RecordError|SetStatus|buildPrinterQRPayload|tenantIDFromContext|Request\(\)\.Context\(\)' backend/internal || trueRepository: thevladbog/idento
Length of output: 37147
Instrument the printer pairing export endpoints with tracing.
Both handlers now create new observability-critical request flows but still do not start a span or record errors. Add spans propagated on request context, with tenant/device or export attributes, and fail/span errors for storage, encoding, CSV writing, and QR-generation failures.
📍 Affects 1 file
backend/internal/handler/printer_pairing_export.go#L91-L126(this comment)backend/internal/handler/printer_pairing_export.go#L134-L202
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/internal/handler/printer_pairing_export.go` around lines 91 - 126,
Instrument backend/internal/handler/printer_pairing_export.go lines 91-126 in
GetPrinterPairingQR and lines 134-202 in the sibling export handler with
request-context spans. Add tenant/device or export attributes as appropriate,
propagate the span context through downstream calls, and record/fail the span
for storage, encoding, CSV-writing, and QR-generation errors before returning
the existing responses.
Source: Coding guidelines
| {onDownloadPairingQr && device.kind === "network" ? ( | ||
| <DropdownMenuItem onSelect={() => onDownloadPairingQr(device)}> | ||
| {t("equipmentDownloadPairingQr")} | ||
| </DropdownMenuItem> | ||
| ) : null} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep pairing-QR download available when the agent is offline.
This item is inside the !agentDown action subtree, so it disappears during an agent outage. The backend generates the QR from the tenant-scoped stored printer config; expose this download independently of live-agent actions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@panel/src/features/equipment/DeviceCard.tsx` around lines 282 - 286, Move the
pairing-QR download item using onDownloadPairingQr and the network device.kind
check outside the !agentDown action subtree in DeviceCard, while preserving its
existing label and handler. Keep it available during agent outages,
independently of live-agent actions.
| <Button | ||
| type="button" | ||
| variant="ghost" | ||
| onClick={() => { | ||
| void downloadPrinterPairingCsv(); | ||
| }} | ||
| > | ||
| {t("equipmentExportPrinters")} | ||
| </Button> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Handle download failures instead of discarding them.
Both helpers can reject on API failures or empty responses, but void leaves those rejections unhandled and the operator gets no feedback.
panel/src/features/equipment/EquipmentPage.tsx#L465-L473: catch CSV download failures and show localized error feedback.panel/src/features/equipment/EquipmentPage.tsx#L554-L556: catch QR download failures and show the same localized error feedback.
📍 Affects 1 file
panel/src/features/equipment/EquipmentPage.tsx#L465-L473(this comment)panel/src/features/equipment/EquipmentPage.tsx#L554-L556
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@panel/src/features/equipment/EquipmentPage.tsx` around lines 465 - 473,
Update the download handlers in EquipmentPage.tsx at lines 465-473 and 554-556
to catch rejections from downloadPrinterPairingCsv and the QR download helper
instead of discarding them with void. In both sites, display the same localized
error feedback using the existing translation and notification mechanisms.
…in QR payload
- Add validateResponse-based contract tests for pairing-qr.png and
pairing-export.csv — satisfies the OPENAPI_COVERAGE=1 gate that failed
'validate / Test Go' (every documented operation needs a contract test).
- Trim config.ip before it reaches the QR payload so a stored padded value
(' 10.0.0.5 ') can't produce an unusable mobile endpoint.
- Align design spec with shipped behavior: buildPrinterQRPayload takes only
the device (hostname is a CSV column), the CSV export button is always
enabled (tenant-wide), and the filename slug drops non-ASCII (no translit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rs (PR #109 follow-ups) (#110) * docs: design for equipment pairing-QR UX follow-ups (CodeRabbit PR #109) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(panel): keep pairing-QR reachable offline + surface download errors (PR #109 follow-ups) Two deferred CodeRabbit findings from PR #109 (printer pairing QR export), panel-only UX polish in the Equipment hub; backend unchanged. Finding 1 — DeviceCard.tsx: the per-device 'Download pairing QR' row action lived inside the card's {!agentDown} degraded branch, so an agent outage hid it. The QR is generated server-side from the tenant-scoped stored config and needs no live agent. Render the row menu whenever it would hold >=1 item and gate each item: only 'Download pairing QR' (network-only) is agent-independent; Rename/Edit address/Set-default/Delete stay hidden while the agent is down, so the intentional 'unreachable' degraded state is preserved. A system printer (no QR) still shows no menu offline. Finding 2 — EquipmentPage.tsx: both download call sites used void, swallowing the helpers' ApiError/empty-response rejections. Surface them via a dismissible destructive banner (role=alert), reusing the existing mirror-warning banner convention rather than adding a toast primitive. Each fresh attempt clears the stale error; Close dismisses it. New i18n key equipmentDownloadError (EN + RU). Tests: agent-down network printer keeps QR reachable while agent-dependent items stay hidden; a system printer shows no offline menu; a rejected QR/CSV download surfaces the banner and Close dismisses it. Mock stubs now resolve so the new .catch() chains work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: CI Bot <ci@example.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…n flake) (#111) The three 'PUT is pending' tests in BadgeEditorPage.test.tsx gated the save PUT with a fixed 60ms MSW delay, then synchronously asserted the guard buttons were disabled. On a loaded CI runner the 60ms could elapse before those assertions ran, re-enabling the buttons ('Received element is not disabled') — a flake that reddened main after the (unrelated) #109/#110 squash-merges. Replace the timing window with a manually-released gate (armPutGate) so the PUT stays pending until the test explicitly releases it. Co-authored-by: CI Bot <ci@example.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
What & why
Lets operators export the pairing data of registered network (ethernet) printers from the panel's Equipment hub, so labels with connection QR codes can be mass-produced in an external label tool (BarTender / NiceLabel / ZebraDesigner). Scanning such a QR in the mobile app connects the phone to that printer — that scan-to-connect half already exists (
PrinterQRData+ camera), so this PR only adds the export half.Design spec:
docs/superpowers/specs/2026-07-23-printer-pairing-qr-export-design.mdPlan:
docs/superpowers/plans/2026-07-23-printer-pairing-qr-export.mdWhat's in it
Backend
buildPrinterQRPayload— single source of truth mapping anequipment_devicesrow → the exactPrinterQRDataJSON the mobile app parses.ListEquipmentPrintersForTenantstore method — allkind=networkprinters across the tenant's machines + hostname.GET /api/equipment/devices/:device_id/pairing-qr.png— PNG QR for one network printer (422 for non-network, 404 for missing/foreign).GET /api/equipment/printers/pairing-export.csv— tenant-wide CSV, UTF-8 BOM, columnsname,machine,printer_type,ip,port,dpi,qr_payload,device_id. Theqr_payloadcolumn is the ready JSON to bind the QR in a label tool.schema.d.ts) regenerated.Panel
pairingExport.ts— authed downloads via the sharedapiclient (parseAs: "blob") → save.Load-bearing invariant
The CSV
qr_payloadand the PNG encode byte-identicalPrinterQRDatafor the same device (both viabuildPrinterQRPayload) — verified by a round-trip test — so a scanned label reliably connects the mobile app.Scope
Network/ethernet printers only (they have a reachable
ip:port). Bluetooth (not in the registry) and system/CUPS (unreachable from mobile) are out of scope by design.Testing
go test ./...green, build clean.pairingExportunit tests + Equipment UI wiring tests;npm run typecheckclean; equipment + i18n suites green (173 tests).Review notes
Built via spec → plan → per-task implement+review. A whole-branch review flagged one non-blocking issue — the CSV export button was gated on the current machine while the export is tenant-wide — fixed in
0e6504a(button always enabled; export handles the empty-tenant case), along with a PNG OpenAPI403and two tenant-scoping tests.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation
Tests