Files
ImpTune/.planning/phases/02-driver-management/02-02-PLAN.md
T
kawaandClaude Opus 4.6 9f5ee24811 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>
2026-04-10 11:48:02 +02:00

14 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
02-driver-management 02 execute 2
02-01
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
true
DRV-01
DRV-03
DRV-04
DRV-05
truths artifacts key_links
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
path provides exports
imptune/api/drivers.py POST /drivers/upload endpoint returning HTMX partial
router
path provides contains
imptune/templates/drivers.html Drivers page with upload form and driver list container hx-post
path provides contains
imptune/templates/partials/driver_list.html HTMX partial fragment with driver table and select dropdown <select
path provides min_lines
tests/test_driver_upload.py Integration tests for upload endpoint and drivers page 80
from to via pattern
imptune/api/drivers.py imptune/services/inf_parser.py import parse_inf, _detect_encoding from imptune.services.inf_parser import
from to via pattern
imptune/api/drivers.py imptune/storage/driver_store.py DriverStore(DRIVERS_DIR).save(data) DriverStore.*save
from to via pattern
imptune/api/drivers.py imptune/db/models.py Driver.get_or_create(sha256=...) Driver.get_or_create
from to via pattern
imptune/templates/drivers.html /drivers/upload hx-post with multipart/form-data hx-post.*drivers/upload
from to via pattern
imptune/main.py imptune/api/drivers.py app.include_router(drivers.router) include_router.*drivers
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.

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

@.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: ...


<!-- 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:

class DriverStore:
    def __init__(self, base_dir: str) -> None: ...
    def save(self, data: bytes) -> str: ...  # returns SHA256 hex

From imptune/db/models.py:

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:

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:

router = APIRouter()
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))

From imptune/templates/base.html:

<!-- Sidebar already has /drivers link -->
<li><a href="/drivers" ...>Drivers</a></li>
<!-- Content block: {% block content %}{% endblock %} -->
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:

  1. 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
  2. Add GET /drivers route to imptune/api/pages.py:

    @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}
        )
    
  3. 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
  4. 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).
  1. Create imptune/templates/drivers.html extending base.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 %}
    
  2. 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:

    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.

  3. Run full test suite to verify no regressions: pytest tests/ -x -q pytest tests/ -v 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.

```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 ```

<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>
After completion, create `.planning/phases/02-driver-management/02-02-SUMMARY.md`