12 KiB
12 KiB
Phase 9: UX Tech Debt Closure - Context
Gathered: 2026-04-13 Status: Ready for planning
## Phase BoundaryClose the three carried-over UX defects from v1.0 (UX-01, UX-02, UX-03) so the build rolled out in Phase 11 is the polished one technicians actually use. Also fix the POST /drivers/upload HTTP 500 surfaced during Phase 8 kickoff — it's a hard blocker for UX-01 and is bundled into that work. No new product capabilities, no refactors outside the touched files.
Plan structure (3 plans, sequential)
- 09-01 — UX-01 + driver upload 500 fix (bundled). Reproduce the 500, write a failing pytest first (TDD), fix root cause, then add inline driver upload input to the printer form template, wire HTMX OOB swap so the printer form's
<select name="driver_id">refreshes after upload, and auto-select the newly uploaded driver. Single plan because the 500 fix and the inline-upload work touch the same endpoint/template pair — splitting them would mean touchingimptune/api/drivers.pytwice with an intermediate broken state. - 09-02 — UX-02 live verification via Playwright headless test. Add Playwright as a dev dependency, write a headless test that loads the printer form, types an IP address, and asserts the
port_nameinput auto-fills withIP_<dotted_underscore>. The test itself IS the evidence cited in09-VALIDATION.mdfor UX-02. - 09-03 — UX-03 per-script download links. Add 3 new routes
GET /printers/{id}/scripts/install.ps1,.../uninstall.ps1,.../detect.ps1that regenerate the .ps1 text on the fly from the saved printer config (same generators used by package export) and return it with appropriateContent-Disposition. Wire 3 direct-download links intoprinter_detail.htmlnext to the existing package export buttons. - Total plans: 3 (not 4). Phase 8 flagged the 500 as blocking UX-01, so it rides the same plan as UX-01 rather than a standalone bugfix plan.
UX-01: Driver upload flow (inline in printer form)
- Requirement re-read: UX-01 says "After a new driver is uploaded on the printer form, the DriverDesc dropdown refreshes automatically." The v1.0 printer form has NO inline upload — that's the tech debt. Adding it IS in scope; it's what the requirement asks for.
- Current v1.0 flow (broken/missing): Technician visits
/driversseparately, uploads driver, navigates back to the printer form, then sees the driver only if the page is reloaded. This is what we're replacing. - New flow: Inline driver upload control inside
imptune/templates/partials/printer_form.html, next to or above the<select name="driver_id">. - Refresh mechanism: HTMX Out-of-Band (OOB) swap.
POST /drivers/uploadresponse is extended so that when called from the printer form (detected viaHX-Targetheader or a posted sentinel field), it returns BOTH the existing#driver-listfragment AND an OOB-swap fragment replacing the printer form's driver<select>. Keeps the existing/driverspage behavior untouched. - Auto-select behavior: Newly uploaded driver becomes the
selectedoption in the refreshed<select>. Saves a click in the common case ("I'm uploading this driver for this printer"). No confirmation prompt; keep it frictionless. - Existing selection preservation: Not applicable — if the technician was mid-form and had already picked a different driver, auto-select overrides it. This is the user-requested behavior.
UX-01: 500 bug fix
- Reported: 2026-04-13 during Phase 8 kickoff. Repro: upload a driver from the
/driverspage, server returns HTTP 500. - Blocker status: Confirmed blocker for UX-01 regardless of which page triggers upload — any 500 on the upload path fails Phase 9 acceptance. Non-optional.
- TDD flow (mandatory): (1) Reproduce against the running app, capture the traceback. (2) Write a failing
pytestcase intests/test_driver_upload.pymatching the repro. (3) Confirm red. (4) Fix the root cause inimptune/api/drivers.py(or deeper — INF parser, DriverStore, Peewee layer). (5) Confirm green. (6) Commit the failing test and the fix atomically per GSD conventions. - Error-handling scope: Fix the specific root cause of THIS 500. Do NOT rewrite the handler to swallow all exceptions into 400 responses — that would mask future bugs. Existing
_error_response()helper returns 400 for validated failures; unhandled exceptions should remain loud but the reported repro must not be one of them.
UX-02: PRNT-03 live verification
- Mechanism: Playwright headless test, committed to
tests/. Not a manual screenshot. - Why: Permanent regression guard costs one-time setup, then self-maintains. A screenshot decays the moment the template changes; a Playwright test fails loudly in CI.
- Scope of the test: Load
/printers/new(or equivalent printer-form route), type an IP intoinput[name="ip_address"], assertinput[name="port_name"]now containsIP_<dotted_underscore>matching the Alpine.js handler inprinter_form.html:12-17. - Evidence for VALIDATION.md: The Playwright test file path + pytest command. The recorded-green test run at the commit that closes UX-02 is the evidence. No screenshot needed in the validation record — the test IS the record.
- Dev dependency addition: Playwright is a new dev dep. Add to
requirements-dev.txt. No Playwright in production image (headless browsers violate the "single container, minimal deps" constraint). Test-only.
UX-03: Per-script download links
- Route shape: Three distinct GET routes, one per script kind.
GET /printers/{id}/scripts/install.ps1GET /printers/{id}/scripts/uninstall.ps1GET /printers/{id}/scripts/detect.ps1
- Why 3 routes over
?kind=param: Discoverable URLs (a technician can share/printers/42/scripts/install.ps1directly), trivial to bookmark, maps naturally toContent-Disposition: attachment; filename=install.ps1. Route sprawl is minimal (3 lines in the router). - Content generation: Reuse the existing script generators (
imptune/services/— the ones that feed package export). Plain-string generator contract, no new ORM access pattern needed. - Response headers:
Content-Type: text/plain; charset=utf-8,Content-Disposition: attachment; filename="<kind>.ps1". Download, not inline-view (avoids browsers rendering.ps1as text and copy-paste losing CRLFs). - Template placement: In imptune/templates/printer_detail.html:48-51, add a new
<h2>Scripts</h2>block above or below the existing Export section with 3<a role="button">links. Existing package export buttons stay untouched (UX-03 says "in addition to the existing package export buttons").
Claude's Discretion
- Exact Alpine.js / HTMX OOB wiring syntax for the driver refresh.
- Whether to inline the upload control inside the printer form or stack it above — visual judgment during implementation.
- Playwright config file location and browser choice (chromium is the default, fine).
- Exact wording of the 3 download link labels ("Install script", "Install (.ps1)", etc.).
- Whether the 3 new script routes live in
imptune/api/printers.pyor a newimptune/api/scripts.py— router organization call. - Test fixture format for the 500 repro (real driver ZIP vs. synthetic ZIP) — whichever reproduces fastest.
<code_context>
Existing Code Insights
Reusable Assets
- imptune/api/drivers.py:35-115 —
upload_driverhandler. Already returns HTMX partials and uses_error_response()for validated 400s. Extend to emit OOB fragment for printer-form callers; find and fix the uncaught exception causing the 500. - imptune/templates/partials/driver_list.html — existing partial returned by upload. Inspect to understand current structure before layering OOB output.
- imptune/templates/partials/printer_form.html:29-39 — driver
<select>. Target for OOB swap. The whole label block (or just the<select>) becomes the OOB-swap root. - imptune/templates/partials/printer_form.html:1 — existing Alpine.js
x-datablock. Add upload control inside the same form scope to share Alpine state if needed. - imptune/templates/printer_detail.html:48-51 — Export section where script download links will be added.
- Script generators under
imptune/services/— plain-string contract per PROJECT.md Key Decisions; safe to call from new routes without DB coupling. tests/test_driver_upload.py— existing test file, extend with the 500 regression case.imptune/api/printers.py— existing printer router, candidate home for the 3 new script download routes.
Established Patterns
- HTMX partial responses with HTTP 4xx for validated failures (
_error_response()) — reuse pattern for any new validation in the inline-upload path. - Jinja2 templates served via
fastapi.templating.Jinja2Templates(directory=templates_dir)— add new blocks, no router-level template refactor needed. - Content-addressed storage (SHA256) for drivers — no change; the 500 bugfix should not alter this.
- Alpine.js
x-datainline state in printer_form.html — pattern for new inline-upload local state (e.g.,uploading: false). - Existing pytest + httpx test pattern per PROJECT.md — 500 repro test follows same structure.
Integration Points
- OOB swap contract: The HTMX OOB fragment must use
<select name="driver_id" hx-swap-oob="true" id="printer-form-driver-select">(or equivalent); requires adding a stableidto the<select>in printer_form.html if it doesn't have one. - Playwright integration: New
tests/e2e/(or similar) subdir holding Playwright specs. Pytest invokes them viapytest-playwrightplugin. App must be reachable on a test port during the run — fixture starts FastAPI via uvicorn in a subprocess or thread. - Script route dependency: New download routes need to load
Printerby id and call the same generator functions package export uses. No new business logic, pure re-use. - No source files outside
imptune/api/,imptune/templates/, andtests/should need editing for this phase (plusrequirements-dev.txtfor Playwright).
</code_context>
## Specific Ideas- User re-framed UX-01 mid-discussion: adding an inline upload to the printer form was initially flagged as scope creep, but re-reading REQUIREMENTS.md UX-01 confirmed the requirement literally says "uploaded on the printer form" — so the inline upload IS the ask, not an add-on.
- User explicitly chose auto-select-new-driver behavior after upload ("Dropdown shows + auto-selects new") — the common case is "I uploaded this driver for this printer", so save the click.
- User treats the 500 as non-negotiable ("Yes — must be fixed") regardless of whether it's edge-case or happy-path. No conditional shipping.
- TDD is required for the 500 fix — failing test first, red-green-commit, not fix-then-test.
- Playwright chosen over manual screenshot because a committed test survives template edits; a screenshot doesn't.
- Three distinct GET routes preferred over
?kind=for discoverability and cleanContent-Dispositionfilenames.
- Hardening
/drivers/uploadagainst unknown future exceptions (global try/except, structured error pages) — out of scope. Fix the reported 500 only; broader resilience is v1.2 territory. - A "Download all scripts as ZIP" button alongside the 3 individual links — tempting but not asked for by UX-03. Log for v1.2 if technicians request it after rollout.
- Playwright tests for anything beyond UX-02 (e.g., full printer CRUD flow, driver upload E2E) — Phase 9 adds Playwright only for UX-02; expanding coverage is a Phase 11 feedback-driven decision.
- Refactoring
upload_driverinto a service layer to separate validation from HTTP concerns — not needed for this fix; avoid scope creep. - Fixing
/drivers/uploadto return JSON for programmatic clients — v1.0 is HTMX-only; no programmatic consumers exist.
Phase: 09-ux-tech-debt-closure Context gathered: 2026-04-13