feat: driver rename, driver icons, web image + driver search
Driver rename and icons: `Driver.display_name` plus a `DriverIcon` table, both global/shared like the `Driver` row they hang off, so a rename or an icon is what every Owner sees. The rename/icon dialog keeps its forms as siblings (nested forms are invalid HTML) and the icon routes return an `hx-swap-oob` thumbnail refresh rather than re-rendering the table, which would tear the open `<dialog>` out of the DOM. Web image picker: `GET /web/images` renders a pickable grid for a printer or a driver icon, with the search term prefilled from the entity name and editable. Picking one downloads it server-side and normalizes it. Driver download search: `GET /web/drivers` searches for a vendor-wide driver (the term is rewritten into the vendor's real product name for 15 brands) or for the exact model as typed. Links only — nothing is downloaded, and the fragment says the results are unvetted. Icon uploads no longer reject off-size or non-PNG files: `normalize_icon()` letterboxes any decodable raster into a 256x256 PNG. An already-exact 256x256 PNG is returned byte-identical, because icon storage is content-addressed and re-encoding would move the file on every save. `fetch_image()` makes the request from the server, so `assert_fetchable()` refuses any URL resolving to a private, loopback, or link-local address, and re-runs on every redirect. ImpTune sits on the same LAN as the printers it configures; an unguarded fetcher would be a port scanner for anyone who can reach the UI. DuckDuckGo is scraped, not called through an API — no key needed, but fragile, so both search functions swallow parse failures and return [] instead of 500ing a page. `WEB_SEARCH=false` disables every outbound request and hides the controls, for air-gapped installs. Also: one shared `Jinja2Templates` in `templating.py` instead of five per-router instances, so a template global is declared once; `_add_missing_columns()` in `database.py` adds new nullable columns to a pre-existing table, which `create_tables(safe=True)` skips; `db_env` in test_db.py now closes its connection on teardown, or the next test's ORM writes land in the previous test's DB file. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+35
-14
@@ -1,17 +1,14 @@
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from peewee import JOIN, fn
|
||||
|
||||
from imptune.templating import templates
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
|
||||
|
||||
# Group label for printers with no client — the template translates it.
|
||||
UNASSIGNED_GROUP = "Unassigned"
|
||||
|
||||
@@ -57,12 +54,31 @@ def group_printers_by_client(printers) -> dict[str, list]:
|
||||
return ordered
|
||||
|
||||
|
||||
def _driver_data(drivers) -> list[dict]:
|
||||
"""Attach the parsed driver names to each Driver row for template use."""
|
||||
return [
|
||||
{"driver": d, "names": json.loads(d.driver_desc) if d.driver_desc else []}
|
||||
for d in drivers
|
||||
]
|
||||
def build_driver_data(drivers) -> list[dict]:
|
||||
"""Attach parsed driver names + icon presence to each Driver row.
|
||||
|
||||
Shared by every template that lists drivers (library table, printer form
|
||||
select, edit modal) so a rename or a new icon shows up in all of them.
|
||||
"""
|
||||
from imptune.services.icons import driver_icon_ids, driver_search_text
|
||||
from imptune.services.websearch import image_query
|
||||
|
||||
drivers = list(drivers)
|
||||
with_icons = driver_icon_ids(drivers)
|
||||
data = []
|
||||
for d in drivers:
|
||||
names = json.loads(d.driver_desc) if d.driver_desc else []
|
||||
search_text = driver_search_text(d, names)
|
||||
data.append(
|
||||
{
|
||||
"driver": d,
|
||||
"names": names,
|
||||
"has_icon": d.id in with_icons,
|
||||
"search_text": search_text,
|
||||
"image_query": image_query(search_text),
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
@@ -109,13 +125,16 @@ def dashboard(request: Request):
|
||||
def drivers_page(request: Request):
|
||||
from imptune.db.models import Driver
|
||||
|
||||
from imptune.services.websearch import BRAND_SUGGESTIONS
|
||||
|
||||
drivers = list(Driver.select().order_by(Driver.uploaded_at.desc()))
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="drivers.html",
|
||||
context={
|
||||
"driver_data": _driver_data(drivers),
|
||||
"driver_data": build_driver_data(drivers),
|
||||
"usage": printer_counts_by_driver(request.state.owner),
|
||||
"brands": BRAND_SUGGESTIONS,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -145,7 +164,7 @@ def printers_page(request: Request):
|
||||
context={
|
||||
"grouped": grouped,
|
||||
"clients": clients,
|
||||
"driver_data": _driver_data(all_drivers),
|
||||
"driver_data": build_driver_data(all_drivers),
|
||||
"printer_count": printer_count,
|
||||
"ready_count": ready_count,
|
||||
},
|
||||
@@ -161,13 +180,14 @@ def printers_new_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="printers_new.html",
|
||||
context={"clients": clients, "driver_data": _driver_data(all_drivers)},
|
||||
context={"clients": clients, "driver_data": build_driver_data(all_drivers)},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/printers/{printer_id}", response_class=HTMLResponse)
|
||||
def printer_detail(request: Request, printer_id: int):
|
||||
from imptune.db.models import Client, Driver, Icon, Printer
|
||||
from imptune.services.websearch import image_query
|
||||
|
||||
printer = (
|
||||
Printer.select(Printer, Client, Driver)
|
||||
@@ -201,6 +221,7 @@ def printer_detail(request: Request, printer_id: int):
|
||||
"driver_names": driver_names,
|
||||
"has_driver": has_driver,
|
||||
"has_icon": icon is not None,
|
||||
"image_query": image_query(printer.name),
|
||||
"install_cmd": install_cmd,
|
||||
"uninstall_cmd": uninstall_cmd,
|
||||
},
|
||||
@@ -251,7 +272,7 @@ def client_detail(request: Request, client_id: int):
|
||||
"client": client,
|
||||
"grouped": grouped,
|
||||
"clients": clients,
|
||||
"driver_data": _driver_data(all_drivers),
|
||||
"driver_data": build_driver_data(all_drivers),
|
||||
"printer_count": len(printers),
|
||||
"ready_count": sum(1 for p in printers if p.driver_id),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user