docs(02-driver-management): create phase plan

Two plans: INF parser TDD (wave 1), upload endpoint + UI (wave 2).
Covers DRV-01 through DRV-05.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 11:48:02 +02:00
co-authored by Claude Opus 4.6
parent 7930619404
commit 9f5ee24811
3 changed files with 528 additions and 5 deletions
+4 -5
View File
@@ -45,12 +45,11 @@ Plans:
2. After upload, user sees a dropdown of driver names extracted from the INF (DriverDesc values), not a text field 2. After upload, user sees a dropdown of driver names extracted from the INF (DriverDesc values), not a text field
3. Uploaded driver packages survive container restarts (persisted to Docker volume) 3. Uploaded driver packages survive container restarts (persisted to Docker volume)
4. System flags files in the driver package that are not referenced by the INF, with a count or list 4. System flags files in the driver package that are not referenced by the INF, with a count or list
**Plans**: TBD **Plans**: 2 plans
Plans: Plans:
- [ ] 02-01: Driver upload endpoint and volume storage (SHA256-keyed, deduplication) - [ ] 02-01-PLAN.md — INF parser with TDD (encoding detection, token resolution, multi-model, unused files)
- [ ] 02-02: INF parser (DriverDesc extraction, multi-model INF support) - [ ] 02-02-PLAN.md — Driver upload endpoint, persistence, drivers page UI with HTMX
- [ ] 02-03: Driver library UI (upload form, driver list, unused-file hints)
### Phase 3: Printer Configuration ### Phase 3: Printer Configuration
**Goal**: Technicians can configure all printer parameters, assign printers to clients, and retrieve saved configs without re-uploading drivers **Goal**: Technicians can configure all printer parameters, assign printers to clients, and retrieve saved configs without re-uploading drivers
@@ -109,7 +108,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Foundation | 3/3 | Complete | 2026-04-10 | | 1. Foundation | 3/3 | Complete | 2026-04-10 |
| 2. Driver Management | 0/3 | Not started | - | | 2. Driver Management | 0/2 | Not started | - |
| 3. Printer Configuration | 0/3 | Not started | - | | 3. Printer Configuration | 0/3 | Not started | - |
| 4. Script Generation | 0/3 | Not started | - | | 4. Script Generation | 0/3 | Not started | - |
| 5. Package Export | 0/3 | Not started | - | | 5. Package Export | 0/3 | Not started | - |
@@ -0,0 +1,213 @@
---
phase: 02-driver-management
plan: "01"
type: tdd
wave: 1
depends_on: []
files_modified:
- imptune/services/__init__.py
- imptune/services/inf_parser.py
- tests/test_inf_parser.py
- tests/fixtures/sample.inf
- tests/fixtures/sample_utf16.inf
- tests/fixtures/sample_multi_model.inf
autonomous: true
requirements: [DRV-02, DRV-05]
must_haves:
truths:
- "parse_inf extracts DriverDesc values from a simple INF with literal names"
- "parse_inf resolves %TOKEN% references via the [Strings] section"
- "parse_inf handles UTF-16 LE BOM, UTF-8 BOM, and ANSI (cp1252) encoded INF files"
- "parse_inf deduplicates driver names from multi-model INFs (NTamd64 + undecorated)"
- "parse_inf returns a list of unused files not referenced in the INF text"
- "parse_inf detects architecture from section decorations (x64, x86, arm64)"
- "parse_inf detects presence of .cat file in ZIP member list"
artifacts:
- path: "imptune/services/inf_parser.py"
provides: "ParsedInf dataclass and parse_inf() + _detect_encoding() functions"
exports: ["ParsedInf", "parse_inf", "_detect_encoding"]
- path: "tests/test_inf_parser.py"
provides: "Unit tests covering all DRV-02 and DRV-05 behaviors"
min_lines: 80
- path: "tests/fixtures/sample.inf"
provides: "Minimal valid INF with %TOKEN% values and [Strings] section"
- path: "tests/fixtures/sample_utf16.inf"
provides: "UTF-16 LE encoded INF for encoding detection test"
- path: "tests/fixtures/sample_multi_model.inf"
provides: "INF with NTamd64 and undecorated Models sections"
key_links:
- from: "imptune/services/inf_parser.py"
to: "configparser.RawConfigParser"
via: "stdlib import"
pattern: "RawConfigParser.*strict=False"
- from: "imptune/services/inf_parser.py"
to: "[Strings] section"
via: "_resolve_tokens regex expansion"
pattern: "re\\.sub.*%([^%]+)%"
---
<objective>
Create the INF parser service that extracts driver names (DriverDesc) from Windows INF files, with encoding auto-detection, %TOKEN% resolution, multi-model support, and unused-file detection.
Purpose: This is the core novel logic of Phase 2. The INF parser is a pure function with defined I/O — ideal for TDD. All other Phase 2 work (upload endpoint, UI) consumes this parser's output.
Output: `imptune/services/inf_parser.py` with `ParsedInf` dataclass and `parse_inf()` function, plus comprehensive unit tests and INF fixture files.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-driver-management/02-RESEARCH.md
<interfaces>
<!-- Existing codebase interfaces this plan needs -->
From imptune/config.py:
```python
DATA_DIR = os.environ.get("DATA_DIR", "/data")
DRIVERS_DIR = str(Path(DATA_DIR) / "drivers")
```
From imptune/storage/driver_store.py:
```python
class DriverStore:
def save(self, data: bytes) -> str: ... # returns SHA256 hex digest
def get_path(self, sha256: str) -> Path: ...
def exists(self, sha256: str) -> bool: ...
```
From imptune/db/models.py:
```python
class Driver(BaseModel):
sha256 = CharField(unique=True, index=True)
original_filename = CharField()
size_bytes = IntegerField()
uploaded_at = DateTimeField(default=datetime.utcnow)
driver_desc = CharField(null=True) # Store json.dumps(list) for multi-model
inf_filename = CharField(null=True)
architecture = CharField(null=True) # 'x64', 'x86', 'arm64', or None
has_cat_file = BooleanField(default=False)
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: INF parser with TDD (RED then GREEN)</name>
<files>imptune/services/__init__.py, imptune/services/inf_parser.py, tests/test_inf_parser.py, tests/fixtures/sample.inf, tests/fixtures/sample_utf16.inf, tests/fixtures/sample_multi_model.inf</files>
<behavior>
- test_detect_encoding_utf16le: _detect_encoding(b'\xff\xfe...') returns 'utf-16'
- test_detect_encoding_utf8bom: _detect_encoding(b'\xef\xbb\xbf...') returns 'utf-8-sig'
- test_detect_encoding_ansi: _detect_encoding(b'[Version]...') returns 'cp1252'
- test_simple_driver_desc: parse_inf with literal DriverDesc in [Models] section returns those names in driver_names
- test_token_resolution: parse_inf with %HP_DRIVER% in [Models] and HP_DRIVER="HP LaserJet" in [Strings] resolves to "HP LaserJet"
- test_utf16_encoding: Reading a UTF-16 LE BOM fixture, decoding with _detect_encoding, and passing to parse_inf produces correct driver_names
- test_multi_model_inf: INF with both [Mfg.NTamd64] and [Mfg] sections returns deduplicated driver_names; architecture='x64' when NTamd64 is present alone
- test_architecture_detection: NTamd64 -> 'x64', NTarm64 -> 'arm64', undecorated only -> 'x86', mixed -> None
- test_cat_file_detection: zip_names containing 'driver.cat' -> has_cat_file=True; without -> False
- test_unused_files: ZIP members ['driver.inf', 'driver.dll', 'readme.txt'] where INF text mentions 'driver.inf' and 'driver.dll' but not 'readme.txt' -> unused_files=['readme.txt']
- test_empty_models_section: INF with [Manufacturer] but empty Models section returns empty driver_names list
</behavior>
<action>
**Phase: RED**
1. Create `tests/fixtures/` directory if it does not exist.
2. Create `tests/fixtures/sample.inf` — minimal valid INF with %TOKEN% values:
```ini
[Version]
Signature="$Windows NT$"
Class=Printer
Provider=%MFG%
[Manufacturer]
%MFG%=Models,NTamd64
[Models.NTamd64]
%DRIVER_NAME%=Install,{GUID}
[Strings]
MFG="Test Manufacturer"
DRIVER_NAME="Test LaserJet Pro"
```
3. Create `tests/fixtures/sample_utf16.inf` — same content as sample.inf but encoded as UTF-16 LE with BOM. Write using Python: `content.encode('utf-16-le')` prepended with `b'\xff\xfe'`. Actually, create this fixture programmatically within the test (or as a conftest fixture) since writing binary fixtures from plan text is fragile.
4. Create `tests/fixtures/sample_multi_model.inf` — INF with both decorated and undecorated sections:
```ini
[Version]
Signature="$Windows NT$"
Class=Printer
[Manufacturer]
%MFG%=Models,Models.NTamd64
[Models]
%DRIVER_A%=InstallA,{GUID1}
[Models.NTamd64]
%DRIVER_A%=InstallA,{GUID1}
%DRIVER_B%=InstallB,{GUID2}
[Strings]
MFG="Multi Corp"
DRIVER_A="Multi Printer 1000"
DRIVER_B="Multi Printer 2000"
```
5. Create `imptune/services/__init__.py` — empty package marker.
6. Create `tests/test_inf_parser.py` with all 11 test functions listed in behavior. Tests import from `imptune.services.inf_parser` and call `parse_inf()` / `_detect_encoding()`. Each test asserts specific expected outputs. For the UTF-16 test, generate the fixture bytes inline: `sample_text.encode('utf-16')`.
7. Run `pytest tests/test_inf_parser.py -x` — all tests MUST FAIL (ImportError or assertion errors). Commit: `test(02-01): add failing tests for INF parser`
**Phase: GREEN**
8. Create `imptune/services/inf_parser.py` implementing:
- `ParsedInf` dataclass with fields: `driver_names: list[str]`, `inf_filename: str`, `architecture: str | None`, `has_cat_file: bool`, `unused_files: list[str]`
- `_detect_encoding(raw: bytes) -> str` — BOM sniffing (UTF-16 BOM -> 'utf-16', UTF-8 BOM -> 'utf-8-sig', else -> 'cp1252')
- `_resolve_tokens(value: str, strings: dict[str, str]) -> str` — regex `%TOKEN%` expansion from strings dict
- `parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedInf` — uses `configparser.RawConfigParser(strict=False, comment_prefixes=(';', '#'), delimiters=('=',))`, reads [Manufacturer] to find Models section names, iterates all matching sections (decorated: .NTamd64, .NTarm64, .NTx86; undecorated), extracts left-hand keys as device-descriptions, resolves tokens, deduplicates with set(), detects architecture from section suffix, detects .cat in zip_names, computes unused files by checking if each zip member's basename appears in inf_text (case-insensitive)
Follow the exact code patterns from 02-RESEARCH.md "Pattern 2: INF DriverDesc Extraction". Key points:
- Use `RawConfigParser` (NOT `ConfigParser` — avoids %(interpolation)s interference)
- `strict=False` to handle duplicate keys in real INFs
- Strings dict keys must be lowercased (configparser lowercases keys by default)
- Strip surrounding double-quotes from [Strings] values
- Architecture: if exactly one arch hint in set -> return it; multiple -> None
- `sorted(driver_names)` for deterministic dropdown order
9. Run `pytest tests/test_inf_parser.py -x` — all tests MUST PASS. Commit: `feat(02-01): implement INF parser with encoding detection and token resolution`
</action>
<verify>
<automated>pytest tests/test_inf_parser.py -v</automated>
</verify>
<done>All 11 tests pass. ParsedInf dataclass and parse_inf() function correctly extract driver names from simple, tokenized, UTF-16, and multi-model INF files. Unused files detected. Architecture and .cat presence detected.</done>
</task>
</tasks>
<verification>
```bash
pytest tests/test_inf_parser.py -v
pytest tests/ -x -q # no regressions in existing tests
```
</verification>
<success_criteria>
- parse_inf() extracts DriverDesc from all 3 fixture types (simple, UTF-16, multi-model)
- %TOKEN% references resolved to human-readable names
- Unused files correctly identified
- All 11+ unit tests green, zero regressions in existing suite
</success_criteria>
<output>
After completion, create `.planning/phases/02-driver-management/02-01-SUMMARY.md`
</output>
@@ -0,0 +1,311 @@
---
phase: 02-driver-management
plan: "02"
type: execute
wave: 2
depends_on: ["02-01"]
files_modified:
- imptune/api/drivers.py
- imptune/api/pages.py
- imptune/main.py
- imptune/templates/drivers.html
- imptune/templates/partials/driver_list.html
- tests/test_driver_upload.py
autonomous: true
requirements: [DRV-01, DRV-03, DRV-04, DRV-05]
must_haves:
truths:
- "User can upload a ZIP file via the /drivers page and receive a success response"
- "After upload, the response contains a populated select dropdown with driver names from the INF"
- "Uploading a non-ZIP file or a ZIP with no INF returns a 400 error displayed in-page"
- "Uploaded driver file is persisted to DRIVERS_DIR via DriverStore (survives restart)"
- "Re-uploading the same ZIP does not create a duplicate Driver record (SHA256 dedup)"
- "Upload response shows count of unused files not referenced by the INF"
- "GET /drivers renders the drivers page with upload form and existing driver list"
artifacts:
- path: "imptune/api/drivers.py"
provides: "POST /drivers/upload endpoint returning HTMX partial"
exports: ["router"]
- path: "imptune/templates/drivers.html"
provides: "Drivers page with upload form and driver list container"
contains: "hx-post"
- path: "imptune/templates/partials/driver_list.html"
provides: "HTMX partial fragment with driver table and select dropdown"
contains: "<select"
- path: "tests/test_driver_upload.py"
provides: "Integration tests for upload endpoint and drivers page"
min_lines: 80
key_links:
- from: "imptune/api/drivers.py"
to: "imptune/services/inf_parser.py"
via: "import parse_inf, _detect_encoding"
pattern: "from imptune\\.services\\.inf_parser import"
- from: "imptune/api/drivers.py"
to: "imptune/storage/driver_store.py"
via: "DriverStore(DRIVERS_DIR).save(data)"
pattern: "DriverStore.*save"
- from: "imptune/api/drivers.py"
to: "imptune/db/models.py"
via: "Driver.get_or_create(sha256=...)"
pattern: "Driver\\.get_or_create"
- from: "imptune/templates/drivers.html"
to: "/drivers/upload"
via: "hx-post with multipart/form-data"
pattern: "hx-post.*drivers/upload"
- from: "imptune/main.py"
to: "imptune/api/drivers.py"
via: "app.include_router(drivers.router)"
pattern: "include_router.*drivers"
---
<objective>
Create the driver upload endpoint, drivers page, and HTMX-driven UI that lets technicians upload driver ZIPs, see parsed driver names in a dropdown, and view unused-file hints.
Purpose: This wires the INF parser (from plan 02-01) into a working upload flow with persistence and UI feedback. After this plan, the full DRV-01 through DRV-05 feature set is functional.
Output: Upload API endpoint, drivers page template, HTMX partial for driver list, integration tests.
</objective>
<execution_context>
@C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md
@C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-driver-management/02-RESEARCH.md
@.planning/phases/02-driver-management/02-01-SUMMARY.md
<interfaces>
<!-- From Plan 02-01 (INF parser) -->
From imptune/services/inf_parser.py:
```python
from dataclasses import dataclass
@dataclass
class ParsedInf:
driver_names: list[str] # resolved DriverDesc values, deduplicated, sorted
inf_filename: str # which .inf file inside the ZIP
architecture: str | None # 'x64', 'x86', 'arm64', or None
has_cat_file: bool # whether a .cat file exists in the ZIP
unused_files: list[str] # ZIP members not referenced by the INF
def _detect_encoding(raw: bytes) -> str: ...
def parse_inf(inf_text: str, inf_filename: str, zip_names: list[str]) -> ParsedInf: ...
```
<!-- Existing Phase 1 interfaces -->
From imptune/config.py:
```python
DATA_DIR = os.environ.get("DATA_DIR", "/data")
DRIVERS_DIR = str(Path(DATA_DIR) / "drivers")
```
From imptune/storage/driver_store.py:
```python
class DriverStore:
def __init__(self, base_dir: str) -> None: ...
def save(self, data: bytes) -> str: ... # returns SHA256 hex
```
From imptune/db/models.py:
```python
class Driver(BaseModel):
sha256 = CharField(unique=True, index=True)
original_filename = CharField()
size_bytes = IntegerField()
uploaded_at = DateTimeField(default=datetime.utcnow)
driver_desc = CharField(null=True) # json.dumps(list) for multi-model
inf_filename = CharField(null=True)
architecture = CharField(null=True)
has_cat_file = BooleanField(default=False)
```
From imptune/main.py:
```python
app = FastAPI(title="ImpTune", lifespan=lifespan)
app.include_router(health.router)
app.include_router(pages.router)
# Add: app.include_router(drivers.router)
```
From imptune/api/pages.py:
```python
router = APIRouter()
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
```
From imptune/templates/base.html:
```html
<!-- Sidebar already has /drivers link -->
<li><a href="/drivers" ...>Drivers</a></li>
<!-- Content block: {% block content %}{% endblock %} -->
```
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Upload endpoint, drivers page route, and integration tests</name>
<files>imptune/api/drivers.py, imptune/api/pages.py, imptune/main.py, tests/test_driver_upload.py</files>
<behavior>
- test_drivers_page: GET /drivers returns 200 with HTML containing upload form (input type="file", hx-post="/drivers/upload")
- test_upload_valid_zip: POST /drivers/upload with a valid ZIP containing sample.inf returns 200, HTML contains driver name from INF
- test_upload_non_zip: POST /drivers/upload with a .txt file returns 400
- test_upload_no_inf: POST /drivers/upload with a ZIP containing no .inf returns 400
- test_upload_returns_select: POST /drivers/upload with valid ZIP returns HTML containing a select element with driver names as options
- test_driver_persisted: After upload, Driver.select().where(Driver.sha256==expected).count() == 1, and DriverStore file exists on disk
- test_dedup_upload: Uploading same ZIP twice creates only one Driver record
- test_unused_files_in_response: Upload a ZIP with an extra file not in INF text; response HTML contains "unused" or the count
</behavior>
<action>
**RED phase first:**
1. Create `tests/test_driver_upload.py` with all 8 integration tests. Tests use the `client` fixture from conftest.py. For test fixtures, create valid ZIP bytes in-memory using `zipfile.ZipFile(io.BytesIO(), 'w')`:
- Build a helper `_make_driver_zip(inf_content: str, extra_files: dict[str, bytes] = None) -> bytes` that creates a ZIP with the INF and optional extra files
- Use the same sample INF content from tests/fixtures/sample.inf (read it or inline it)
- For `test_upload_non_zip`, send raw text bytes with filename="test.zip"
- For `test_upload_no_inf`, create a ZIP with only a .txt file
- For `test_unused_files_in_response`, add a "readme.txt" to the ZIP that the INF does not reference
- All tests use `client.post("/drivers/upload", files={"file": ("driver.zip", zip_bytes, "application/zip")})`
- Import `Driver` from `imptune.db.models` and `init_db` from `imptune.db.database` for persistence checks. Call `init_db()` in tests that check DB state (the `client` fixture triggers lifespan which calls init_db).
2. Run `pytest tests/test_driver_upload.py -x` — all MUST FAIL. Commit: `test(02-02): add failing integration tests for driver upload`
**GREEN phase:**
3. Create `imptune/api/drivers.py`:
- `router = APIRouter(prefix="/drivers")`
- `templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))`
- `MAX_UPLOAD_BYTES = 100 * 1024 * 1024`
- `POST /upload` endpoint (sync def, not async — Peewee is sync):
- Read file bytes, validate size <= 100MB
- Validate filename ends with `.zip`
- Validate `zipfile.is_zipfile(io.BytesIO(data))`
- Open ZIP, validate no zip-slip paths (reject `..` or absolute paths)
- Find `.inf` files in namelist; raise 400 if none
- Prefer INF whose path contains `amd64`/`x64` if multiple exist; else first alphabetically
- Read INF bytes, detect encoding with `_detect_encoding()`, decode
- Call `parse_inf(inf_text, inf_filename, zip_names)`
- Save via `DriverStore(DRIVERS_DIR).save(data)`
- Upsert `Driver.get_or_create(sha256=sha256, defaults={...})` — store `json.dumps(parsed.driver_names)` in `driver_desc`
- Query all drivers: `Driver.select().order_by(Driver.uploaded_at.desc())`
- Return `templates.TemplateResponse(request=request, name="partials/driver_list.html", context={...})`
- On validation errors, return HTMX-friendly error: `HTMLResponse(content="<div id='driver-list' class='error'>Error message</div>", status_code=400)` — so HTMX can swap the error into the target area
4. Add GET /drivers route to `imptune/api/pages.py`:
```python
@router.get("/drivers", response_class=HTMLResponse)
def drivers_page(request: Request):
from imptune.db.models import Driver
drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
return templates.TemplateResponse(
request=request, name="drivers.html",
context={"drivers": drivers}
)
```
5. Register the drivers router in `imptune/main.py`:
- Add `from imptune.api import drivers` to imports
- Add `app.include_router(drivers.router)` after the pages router
6. Run `pytest tests/test_driver_upload.py -x` — all MUST PASS. Commit: `feat(02-02): add driver upload endpoint with INF parsing and dedup`
</action>
<verify>
<automated>pytest tests/test_driver_upload.py -v</automated>
</verify>
<done>All 8 integration tests pass. POST /drivers/upload accepts ZIPs, parses INFs, persists via DriverStore + Peewee, returns HTMX partial. GET /drivers renders the page. Error cases return 400.</done>
</task>
<task type="auto">
<name>Task 2: Drivers page template and HTMX partial</name>
<files>imptune/templates/drivers.html, imptune/templates/partials/driver_list.html</files>
<action>
1. Create `imptune/templates/partials/` directory (if not exists).
2. Create `imptune/templates/drivers.html` extending base.html:
```html
{% extends "base.html" %}
{% block content %}
<h1>Drivers</h1>
<section>
<h2>Upload Driver Package</h2>
<form
hx-post="/drivers/upload"
hx-encoding="multipart/form-data"
hx-target="#driver-list"
hx-swap="outerHTML"
hx-indicator="#upload-spinner"
>
<label for="driver-file">Driver Package (ZIP containing .inf + driver files)</label>
<input type="file" id="driver-file" name="file" accept=".zip" required>
<button type="submit">Upload</button>
<span id="upload-spinner" class="htmx-indicator" aria-busy="true">Uploading...</span>
</form>
</section>
<section>
<h2>Driver Library</h2>
<div id="driver-list">
{% include "partials/driver_list.html" %}
</div>
</section>
{% endblock %}
```
3. Create `imptune/templates/partials/driver_list.html`:
- Wrap everything in `<div id="driver-list">` (for HTMX outerHTML swap)
- If `drivers` list is empty, show "No drivers uploaded yet."
- If `drivers` exist, render a table with columns: Filename, Driver Name(s), Architecture, Uploaded, Unused Files
- For each driver, parse `driver.driver_desc` as JSON to get the list of driver names. Display as a `<select>` dropdown if multiple names, or plain text if single name. Use Jinja2: `{% set names = driver.driver_desc | tojson | default('[]') %}` — actually, since driver_desc is already a JSON string, parse it in template or pass parsed data from the route.
- Show unused files count if `parsed` context variable is available (on fresh upload): "N files may be unused" with a details/summary for the list
- For the "new_driver" highlight (if present in context), add a CSS class to indicate success
The partial must work both as an include (initial page load, no `parsed` variable) and as a standalone HTMX response (after upload, `parsed` available).
Template approach for driver names: In the route, pass `driver_names_map` — a dict mapping driver.id to the parsed list. Or simpler: add a property/method. Simplest approach for Jinja2: use a custom filter or pass a helper. Actually, simplest: in the route handler, build a list of dicts with pre-parsed data:
```python
import json
driver_data = []
for d in drivers:
names = json.loads(d.driver_desc) if d.driver_desc else []
driver_data.append({"driver": d, "names": names})
```
Pass `driver_data` to template. Template iterates `driver_data` and renders `item.names` as select options.
Update both the drivers.py upload endpoint AND the pages.py GET /drivers route to pass `driver_data` in this format.
4. Run full test suite to verify no regressions: `pytest tests/ -x -q`
</action>
<verify>
<automated>pytest tests/ -v</automated>
</verify>
<done>GET /drivers renders a page with upload form and driver table. After upload, HTMX swaps in updated driver list with select dropdown containing parsed driver names. Unused file count visible. All tests green.</done>
</task>
</tasks>
<verification>
```bash
pytest tests/ -v # Full suite green
pytest tests/test_driver_upload.py -v # All upload integration tests
pytest tests/test_inf_parser.py -v # All parser unit tests
```
</verification>
<success_criteria>
- POST /drivers/upload with valid driver ZIP returns 200 with HTML containing driver name dropdown
- POST /drivers/upload with invalid input returns 400 with clear error
- Driver records persisted in SQLite with json.dumps(driver_names) in driver_desc
- Driver files persisted in DRIVERS_DIR via SHA256 content-addressed storage
- Duplicate uploads produce no duplicate records
- GET /drivers renders upload form and existing driver list
- Unused files flagged in upload response
- All integration + unit tests pass, zero regressions
</success_criteria>
<output>
After completion, create `.planning/phases/02-driver-management/02-02-SUMMARY.md`
</output>