Files
ImpTune/.planning/phases/09-ux-tech-debt-closure/09-CONTEXT.md
T
2026-04-13 10:16:54 +02:00

12 KiB

Phase 9: UX Tech Debt Closure - Context

Gathered: 2026-04-13 Status: Ready for planning

## Phase Boundary

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

## Implementation Decisions

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 touching imptune/api/drivers.py twice 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_name input auto-fills with IP_<dotted_underscore>. The test itself IS the evidence cited in 09-VALIDATION.md for UX-02.
  • 09-03 — UX-03 per-script download links. Add 3 new routes GET /printers/{id}/scripts/install.ps1, .../uninstall.ps1, .../detect.ps1 that regenerate the .ps1 text on the fly from the saved printer config (same generators used by package export) and return it with appropriate Content-Disposition. Wire 3 direct-download links into printer_detail.html next 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 /drivers separately, 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/upload response is extended so that when called from the printer form (detected via HX-Target header or a posted sentinel field), it returns BOTH the existing #driver-list fragment AND an OOB-swap fragment replacing the printer form's driver <select>. Keeps the existing /drivers page behavior untouched.
  • Auto-select behavior: Newly uploaded driver becomes the selected option 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 /drivers page, 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 pytest case in tests/test_driver_upload.py matching the repro. (3) Confirm red. (4) Fix the root cause in imptune/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 into input[name="ip_address"], assert input[name="port_name"] now contains IP_<dotted_underscore> matching the Alpine.js handler in printer_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.
  • Route shape: Three distinct GET routes, one per script kind.
    • GET /printers/{id}/scripts/install.ps1
    • GET /printers/{id}/scripts/uninstall.ps1
    • GET /printers/{id}/scripts/detect.ps1
  • Why 3 routes over ?kind= param: Discoverable URLs (a technician can share /printers/42/scripts/install.ps1 directly), trivial to bookmark, maps naturally to Content-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 .ps1 as 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.py or a new imptune/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-115upload_driver handler. 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-data block. 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-data inline 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 stable id to 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 via pytest-playwright plugin. 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 Printer by 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/, and tests/ should need editing for this phase (plus requirements-dev.txt for 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 clean Content-Disposition filenames.
## Deferred Ideas
  • Hardening /drivers/upload against 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_driver into a service layer to separate validation from HTTP concerns — not needed for this fix; avoid scope creep.
  • Fixing /drivers/upload to 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