feat(02-02): add driver upload endpoint with INF parsing and dedup

- POST /drivers/upload: validates ZIP, parses INF, persists via DriverStore + Peewee
- SHA256 dedup: get_or_create prevents duplicate Driver records
- GET /drivers route added to pages.py with driver_data context
- drivers.router registered in main.py
- drivers.html template with HTMX upload form
- partials/driver_list.html HTMX target with select dropdown and unused-file notice
- Fixed conftest client fixture to use context manager (triggers lifespan/init_db)
- [Rule 3] conftest: TestClient context manager required for lifespan trigger
- [Rule 1] drivers.py: dynamic DRIVERS_DIR read so monkeypatch works in tests
This commit is contained in:
2026-04-10 12:02:00 +02:00
parent 8ecfbf25a7
commit c648fc5793
6 changed files with 212 additions and 2 deletions
+25
View File
@@ -0,0 +1,25 @@
{% 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>
{% include "partials/driver_list.html" %}
</section>
{% endblock %}
@@ -0,0 +1,50 @@
<div id="driver-list">
{% if parsed is defined and parsed.unused_files %}
<p class="notice">
{{ parsed.unused_files | length }} file(s) may be unused (not referenced by the INF):
<details>
<summary>Show unused files</summary>
<ul>
{% for f in parsed.unused_files %}
<li>{{ f }}</li>
{% endfor %}
</ul>
</details>
</p>
{% endif %}
{% if driver_data %}
<table>
<thead>
<tr>
<th>Filename</th>
<th>Driver Name(s)</th>
<th>Architecture</th>
<th>Uploaded</th>
</tr>
</thead>
<tbody>
{% for item in driver_data %}
<tr>
<td>{{ item.driver.original_filename }}</td>
<td>
{% if item.names %}
<select aria-label="Driver names">
{% for name in item.names %}
<option>{{ name }}</option>
{% endfor %}
</select>
{% else %}
<em>Unknown</em>
{% endif %}
</td>
<td>{{ item.driver.architecture or "Unknown" }}</td>
<td>{{ item.driver.uploaded_at }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No drivers uploaded yet.</p>
{% endif %}
</div>