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 |
|
|
true |
|
|
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 %} -->
-
Create
tests/test_driver_upload.pywith all 8 integration tests. Tests use theclientfixture from conftest.py. For test fixtures, create valid ZIP bytes in-memory usingzipfile.ZipFile(io.BytesIO(), 'w'):- Build a helper
_make_driver_zip(inf_content: str, extra_files: dict[str, bytes] = None) -> bytesthat 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
Driverfromimptune.db.modelsandinit_dbfromimptune.db.databasefor persistence checks. Callinit_db()in tests that check DB state (theclientfixture triggers lifespan which calls init_db).
- Build a helper
-
Run
pytest tests/test_driver_upload.py -x— all MUST FAIL. Commit:test(02-02): add failing integration tests for driver upload
GREEN phase:
-
Create
imptune/api/drivers.py:router = APIRouter(prefix="/drivers")templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))MAX_UPLOAD_BYTES = 100 * 1024 * 1024POST /uploadendpoint (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
.inffiles in namelist; raise 400 if none - Prefer INF whose path contains
amd64/x64if 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={...})— storejson.dumps(parsed.driver_names)indriver_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
-
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} ) -
Register the drivers router in
imptune/main.py:- Add
from imptune.api import driversto imports - Add
app.include_router(drivers.router)after the pages router
- Add
-
Run
pytest tests/test_driver_upload.py -x— all MUST PASS. Commit:feat(02-02): add driver upload endpoint with INF parsing and deduppytest 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.
-
Create
imptune/templates/drivers.htmlextending 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 %} -
Create
imptune/templates/partials/driver_list.html:- Wrap everything in
<div id="driver-list">(for HTMX outerHTML swap) - If
driverslist is empty, show "No drivers uploaded yet." - If
driversexist, render a table with columns: Filename, Driver Name(s), Architecture, Uploaded, Unused Files - For each driver, parse
driver.driver_descas 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
parsedcontext 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
parsedvariable) and as a standalone HTMX response (after upload,parsedavailable).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_datato template. Template iteratesdriver_dataand rendersitem.namesas select options.Update both the drivers.py upload endpoint AND the pages.py GET /drivers route to pass
driver_datain this format. - Wrap everything in
-
Run full test suite to verify no regressions:
pytest tests/ -x -qpytest 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.
<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>