- SUMMARY.md created: one-line fix, two new tests, full suite green - STATE.md updated: Phase 15 complete, decisions logged - ROADMAP.md: Phase 15 marked 1/1 Complete 2026-04-16 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
Phase 15: UX Driver Upload Feedback Fix - Research
Researched: 2026-04-16 Domain: HTMX OOB swap, Jinja2 templates, driver upload flow Confidence: HIGH
Summary
The integration gap is fully diagnosed by reading the actual source files. No speculative work is needed.
Phase 9 (plan 09-01) wired the driver upload OOB flow correctly: POST /drivers/upload?caller=printer_form returns driver_upload_with_oob.html, which contains two swap targets: (1) the primary #driver-list outerHTML swap, and (2) an OOB <select id="printer-form-driver-select"> that refreshes the driver dropdown. The OOB select refresh works as intended.
The gap: Phase 11 (plan 11-01) created /printers/new using printers_new.html — a new template with the form markup inlined (not using printer_form.html). That template includes the HTMX upload sub-form (hx-post="/drivers/upload", hx-target="#driver-list") and a hidden <div id="driver-list" style="display:none">. When the upload returns the driver_upload_with_oob.html response, HTMX swaps the returned <div id="driver-list"> (the driver list table) into the hidden target element. Because the target div has style="display:none", the upload confirmation (driver name, table row, unused files notice) is invisible. The OOB select refresh still works because HTMX processes OOB elements independently of the primary swap.
Primary recommendation: Remove style="display:none" from the <div id="driver-list"> in printers_new.html so the primary swap content (upload confirmation) becomes visible. Optionally add a heading/label above it for clarity. Add a smoke test asserting visible confirmation content appears after upload.
<phase_requirements>
Phase Requirements
| ID | Description | Research Support |
|---|---|---|
| UX-01 | After a new driver is uploaded on the printer form, the DriverDesc dropdown refreshes automatically (no manual page reload) — verified live in browser | OOB select refresh already works; fix is making the upload confirmation visible by un-hiding #driver-list on /printers/new |
| </phase_requirements> |
Standard Stack
Core
| Library | Version | Purpose | Why Standard |
|---|---|---|---|
| HTMX | 1.x (CDN, project-standard) | OOB swap, partial HTML responses | Already in use; driver upload flow depends on it |
| Jinja2 | project-standard (FastAPI) | Template rendering | Entire template layer uses Jinja2 |
| pytest + httpx TestClient | project-standard | Integration tests | All existing tests use this pattern |
No new dependencies are needed. This is a one-line template fix plus a test.
Installation: No installation needed.
Architecture Patterns
How the Current OOB Upload Flow Works (Phase 9 implementation)
Browser Server
| |
|-- POST /drivers/upload ------->|
| (multipart: file + caller=printer_form)
| |
|<-- 200 driver_upload_with_oob.html --|
| Body contains TWO elements:
| 1. <div id="driver-list">...</div> <- primary swap (hx-target="#driver-list", hx-swap="outerHTML")
| 2. <select id="printer-form-driver-select" hx-swap-oob="true">...</select>
|
HTMX processes response:
- Primary: replaces <div id="driver-list"> in DOM with the returned div
- OOB: replaces <select id="printer-form-driver-select"> in DOM with the returned select
The primary swap target in printers_new.html:
<!-- CURRENT (broken): hidden div — swap happens but content invisible -->
<div id="driver-list" style="display:none"></div>
<!-- FIX: remove display:none so swapped-in driver list table is visible -->
<div id="driver-list"></div>
Recommended Project Structure (unchanged)
imptune/templates/
├── printers_new.html # MODIFY: remove style="display:none" from #driver-list
├── partials/
│ ├── driver_list.html # unchanged
│ └── driver_upload_with_oob.html # unchanged
tests/
├── test_printer_form.py # ADD: smoke test for visible upload confirmation
Pattern 1: HTMX OOB Swap (established in Phase 9)
What: Server response body includes a primary fragment AND sibling elements with hx-swap-oob="true". HTMX applies the primary swap to hx-target, then independently finds each OOB element by id and swaps it.
When to use: When a single action must update multiple DOM regions.
Example:
<!-- Source: driver_upload_with_oob.html -->
{% include "partials/driver_list.html" %}
<select name="driver_id" id="printer-form-driver-select" hx-swap-oob="true">
...
</select>
The driver_list.html partial renders <div id="driver-list"> — this is the primary swap content. It is returned as the response body. The <select> with hx-swap-oob="true" is the secondary update.
Anti-Patterns to Avoid
- Re-architecting the OOB flow: The upload handler and
driver_upload_with_oob.htmlwork correctly. Do not change them. - Using
style="display:none"on HTMX swap targets: HTMX outerHTML swap replaces the element including its style. The replacement element (<div id="driver-list">fromdriver_list.html) has no display:none. But the initial state of the div before upload is invisible, so users see nothing until the swap occurs — and once swapped in, content IS visible. The real issue is UX: users see no feedback that upload was received at all — the driver table appears out of thin air in a previously empty, invisible area. Making the#driver-listdiv visible (even empty) also makes the post-upload table appear in an expected location. - Nesting upload form inside printer form: Already handled in Phase 9. The upload form is a sibling. Do not change this.
Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---|---|---|---|
| Multi-region DOM update | Custom JS event dispatch | HTMX OOB swap | Already implemented; proven in Phase 9 |
| Test HTTP responses | Manual request construction | httpx TestClient (conftest client fixture) |
Standard pattern across all test files |
Common Pitfalls
Pitfall 1: Modifying the Wrong Template
What goes wrong: Editing printer_form.html instead of printers_new.html.
Why it happens: Phase 9 added the #driver-list hidden anchor to printer_form.html. Phase 11 created printers_new.html with its own inline copy of the form (not using {% include "partials/printer_form.html" %}). The two templates now diverge — printers_new.html has its own inline <div id="driver-list" style="display:none"></div> at line 98.
How to avoid: The target file is imptune/templates/printers_new.html, line 98. printer_form.html still has its own hidden anchor for contexts where it is used as a partial (though it is no longer used on /printers/new).
Warning signs: If the fix is in printer_form.html but the test checks /printers/new, the test will still fail.
Pitfall 2: Removing the #driver-list Anchor Entirely
What goes wrong: Deleting <div id="driver-list"> from printers_new.html causes HTMX to silently fail the primary swap (no target found in DOM).
Why it happens: HTMX hx-target="#driver-list" requires the element to exist. If the anchor is absent, the outerHTML swap finds nothing and discards the response.
How to avoid: Keep <div id="driver-list"></div> but remove style="display:none".
Warning signs: After upload, the OOB select refreshes but the driver list table never appears.
Pitfall 3: UX Without Label/Context
What goes wrong: The driver list table appears without heading, making it confusing why a table of all uploaded drivers suddenly appears.
Why it happens: driver_list.html just renders a <div id="driver-list"> with a table — no surrounding context label.
How to avoid: Add a heading or label above the #driver-list div in printers_new.html (e.g. "Available Drivers" or a localized key). This makes the feedback contextually clear.
Warning signs: Technicians don't know what the table means after upload.
Pitfall 4: Test Asserts Wrong Route
What goes wrong: Smoke test hits /drivers/upload without caller=printer_form and checks standalone behavior instead of the printer form integration.
Why it happens: Confusion between the two upload paths.
How to avoid: Test must POST to /drivers/upload with data={"caller": "printer_form"} and assert visible confirmation in the response fragment that gets swapped into #driver-list.
Code Examples
Current State of printers_new.html (lines 87-99)
<!-- Source: imptune/templates/printers_new.html, lines 87-99 -->
<form hx-post="/drivers/upload"
hx-target="#driver-list"
hx-encoding="multipart/form-data"
hx-swap="outerHTML">
<input type="hidden" name="caller" value="printer_form">
<label>
<span x-text="$store.i18n.t('upload_new_driver')">Upload New Driver</span>
<input type="file" name="file" accept=".zip" required>
</label>
<button type="submit" class="secondary" x-text="$store.i18n.t('upload_driver')">Upload Driver</button>
</form>
<div id="driver-list" style="display:none"></div> <!-- BUG: hidden -->
</div>
Fix (single-line change)
<div id="driver-list"></div> <!-- FIX: visible, HTMX will outerHTML-swap driver list here -->
Upload Handler (unchanged — already correct)
# Source: imptune/api/drivers.py, lines 113-122
if caller == "printer_form":
return templates.TemplateResponse(
request=request,
name="partials/driver_upload_with_oob.html",
context={
"driver_data": driver_data,
"new_driver_id": new_driver.id,
"parsed": parsed,
},
)
OOB Template (unchanged — already correct)
<!-- Source: imptune/templates/partials/driver_upload_with_oob.html -->
{% include "partials/driver_list.html" %}
<select name="driver_id" id="printer-form-driver-select" hx-swap-oob="true">
<option value="">-- No driver --</option>
{% for item in driver_data %}
<option value="{{ item.driver.id }}"
{% if item.driver.id == new_driver_id %}selected{% endif %}>
{{ item.driver.original_filename }} ({{ item.names | join(', ') }})
</option>
{% endfor %}
</select>
Test Pattern (existing style from test_printer_form.py and test_driver_upload.py)
# Pattern: POST upload with caller, check HTML for confirmation content
def test_upload_feedback_visible_on_printers_new(client: TestClient) -> None:
"""After upload on /printers/new flow, driver name appears in the OOB response fragment."""
zip_bytes = _make_driver_zip_with_cat()
resp = client.post(
"/drivers/upload",
files={"file": ("driver.zip", zip_bytes, "application/zip")},
data={"caller": "printer_form"},
)
assert resp.status_code == 200
# The primary #driver-list fragment must contain the driver name (upload confirmation)
assert "Test LaserJet Pro" in resp.text
# OOB select still refreshes
assert 'hx-swap-oob="true"' in resp.text
Note: this test verifies server-side response content (what HTMX receives). The visibility fix (removing style="display:none") is a template change verified by inspecting the rendered /printers/new HTML.
def test_printers_new_driver_list_visible(client: TestClient) -> None:
"""GET /printers/new: #driver-list anchor must NOT have display:none."""
resp = client.get("/printers/new")
assert resp.status_code == 200
assert 'id="driver-list"' in resp.text
# The anchor must exist but must NOT be hidden
assert 'id="driver-list" style="display:none"' not in resp.text
State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|---|---|---|---|
| Inline form on /printers | Dedicated /printers/new page | Phase 11 | printers_new.html is the only template to fix |
| printer_form.html partial on /printers | Inlined form markup in printers_new.html | Phase 11 | Two diverged templates; fix goes in printers_new.html only |
| No OOB upload flow | caller=printer_form sentinel + driver_upload_with_oob.html | Phase 9 | OOB mechanism works; visibility is the only gap |
Deprecated/outdated:
- The Phase 9 plan notes about adding a hidden anchor: correct at the time, but the hidden anchor is now the bug in the Phase 11 context.
Open Questions
-
Should
printer_form.htmlalso get its hidden anchor removed?- What we know:
printer_form.htmlis no longer included in/printers/new(Phase 11 inlined the markup). It may still be used in other contexts (e.g. edit modal). - What's unclear: whether any route still renders
printer_form.htmlas a standalone partial. - Recommendation: Check if
printer_form.htmlis referenced anywhere besides the edit modal. Scope of Phase 15 isprinters_new.htmlonly — do not changeprinter_form.htmlunless the edit modal context also needs visible feedback.
- What we know:
-
i18n key for "Available Drivers" heading
- What we know: Phase 12 added full i18n coverage; all strings use Alpine i18n store.
- What's unclear: whether an i18n key for a "Drivers uploaded" or "Available Drivers" label already exists.
- Recommendation: Check
imptune/static/i18n/for existing keys before adding a new one. If a label is out of scope, omit it and just un-hide the div — the driver list table is self-explanatory.
Validation Architecture
Test Framework
| Property | Value |
|---|---|
| Framework | pytest (project standard) |
| Config file | pyproject.toml or pytest.ini (inferred from project) |
| Quick run command | pytest tests/test_printer_form.py tests/test_driver_upload.py -x -q |
| Full suite command | pytest tests/ -x -q --ignore=tests/e2e |
Phase Requirements -> Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|---|---|---|---|---|
| UX-01 | Upload confirmation visible after driver upload on /printers/new | integration | pytest tests/test_printer_form.py -x -q |
Partial — new test needed |
| UX-01 | #driver-list anchor not hidden on GET /printers/new | integration | pytest tests/test_printer_form.py -x -q |
Partial — new assertion needed |
| UX-01 | OOB select still refreshes (no regression) | integration | pytest tests/test_driver_upload.py::test_upload_returns_oob_when_called_from_form -x |
YES (test_driver_upload.py) |
Sampling Rate
- Per task commit:
pytest tests/test_printer_form.py tests/test_driver_upload.py -x -q - Per wave merge:
pytest tests/ -x -q --ignore=tests/e2e - Phase gate: Full suite green before marking phase complete
Wave 0 Gaps
tests/test_printer_form.py— addtest_printers_new_driver_list_visible(asserts nostyle="display:none"on#driver-list)tests/test_printer_form.py— addtest_upload_feedback_visible_on_printers_new(asserts driver name appears in upload response)
(Existing test infrastructure covers everything else — only these two assertions are missing)
Sources
Primary (HIGH confidence)
imptune/templates/printers_new.html— confirmed<div id="driver-list" style="display:none">at line 98imptune/templates/partials/driver_upload_with_oob.html— confirmed correct OOB templateimptune/api/drivers.py— confirmed correct handler branching oncaller == "printer_form"tests/test_driver_upload.py— confirmed OOB contract tests already exist and passtests/test_printer_form.py— confirmed existing printer form tests
Secondary (MEDIUM confidence)
- Phase 09-01 SUMMARY.md — decision to use hidden anchor: "Hidden
<div id="driver-list" style="display:none">added to printer form to provide HTMX outerHTML swap target" - Phase 11-01 SUMMARY.md — confirmed
printers_new.htmlinlines form markup (Option A), not{% include "partials/printer_form.html" %}
Tertiary (LOW confidence)
- None
Metadata
Confidence breakdown:
- Root cause: HIGH — confirmed by reading
printers_new.htmlline 98 directly - Fix: HIGH — single-line template change, no architectural risk
- Test strategy: HIGH — established pattern in test_printer_form.py and test_driver_upload.py
- Regression risk: LOW — OOB select path is unchanged; only the div visibility changes
Research date: 2026-04-16 Valid until: N/A — project-internal, stable until printers_new.html is changed