--- 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: " 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. @C:/Users/SebastienQUEROL/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/SebastienQUEROL/.claude/get-shit-done/templates/summary.md @.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 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: ... ``` 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
  • Drivers
  • ```
    Task 1: Upload endpoint, drivers page route, and integration tests imptune/api/drivers.py, imptune/api/pages.py, imptune/main.py, tests/test_driver_upload.py - 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 **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="
    Error message
    ", 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`
    pytest tests/test_driver_upload.py -v 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.
    Task 2: Drivers page template and HTMX partial imptune/templates/drivers.html, imptune/templates/partials/driver_list.html 1. Create `imptune/templates/partials/` directory (if not exists). 2. Create `imptune/templates/drivers.html` extending base.html: ```html {% extends "base.html" %} {% block content %}

    Drivers

    Upload Driver Package

    Uploading...

    Driver Library

    {% include "partials/driver_list.html" %}
    {% endblock %} ``` 3. Create `imptune/templates/partials/driver_list.html`: - Wrap everything in `
    ` (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 `