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
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
**Plans**: TBD
**Plans**: 2 plans
Plans:
- [ ] 02-01: Driver upload endpoint and volume storage (SHA256-keyed, deduplication)
- [ ] 02-02: INF parser (DriverDesc extraction, multi-model INF support)
- [ ] 02-03: Driver library UI (upload form, driver list, unused-file hints)
- [ ] 02-01-PLAN.md — INF parser with TDD (encoding detection, token resolution, multi-model, unused files)
- [ ] 02-02-PLAN.md — Driver upload endpoint, persistence, drivers page UI with HTMX
### Phase 3: Printer Configuration
**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 |
|-------|----------------|--------|-----------|
| 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 | - |
| 4. Script Generation | 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>