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:
2026-08-05 10:44:47 +02:00
co-authored by Claude Opus 5
parent 3f9cd1f266
commit 2c06806814
34 changed files with 2516 additions and 162 deletions
+58
View File
@@ -224,6 +224,35 @@
replace_icon: 'Remplacer licône',
no_icon: 'Aucune icône. Intune affichera licône par défaut.',
hint_icon: 'PNG, exactement 256 × 256, 750 Ko maximum.',
hint_any_image: 'Nimporte quelle image — redimensionnée en PNG 256 × 256. 750 Ko maximum.',
// Recherche dimages sur le web
search_web_image: 'Chercher une image sur le web',
search_btn: 'Chercher',
searching: 'Recherche…',
image_results_for: 'Résultats pour',
image_pick_hint: 'Cliquez sur une image : elle est téléchargée puis redimensionnée en PNG 256 × 256.',
no_image_results: 'Aucun résultat. Essayez un autre terme.',
web_search_off: 'Les recherches web sont désactivées sur ce serveur.',
unknown_target: 'Cible de recherche inconnue.',
printer_not_found: 'Imprimante introuvable.',
driver_not_found: 'Pilote introuvable.',
type_to_search_image: 'Saisissez un terme à rechercher.',
type_to_search_driver: 'Saisissez une marque ou un modèle dimprimante.',
// Pilotes : renommage et icône
edit_driver_title: 'Modifier le pilote',
section_driver_name: 'Nom',
driver_display_name: 'Nom affiché',
hint_driver_rename: 'Visible par tout le monde — la bibliothèque de pilotes est partagée. Laissez vide pour afficher le nom du ZIP.',
no_driver_icon: 'Aucune icône.',
// Recherche de pilotes sur le web
find_driver_online: 'Trouver un pilote en ligne',
find_driver_intro: 'Cherchez un pilote universel du constructeur, ou un modèle précis. ImpTune naffiche que des liens : téléchargez le ZIP puis téléversez-le ci-dessus.',
driver_search_label: 'Marque ou modèle dimprimante',
search_generic_btn: 'Chercher le pilote universel',
search_exact_btn: 'Chercher le modèle exact',
searched_for: 'Recherche effectuée pour',
driver_search_warning: 'Résultats web bruts, sans filtrage ni validation. Ne téléchargez un pilote que depuis le site du constructeur, puis téléversez le ZIP ci-dessus.',
no_driver_results: 'Aucun résultat. Essayez le nom exact du modèle.',
back_to_printers_btn: 'Retour aux imprimantes',
export_locked: 'Assignez un pilote à cette imprimante pour débloquer les scripts et lexport.',
// Edit modal
@@ -410,6 +439,35 @@
replace_icon: 'Replace icon',
no_icon: 'No icon. Intune will show its default.',
hint_icon: 'PNG, exactly 256 × 256, 750 KB max.',
hint_any_image: 'Any image — resized to 256 × 256 PNG. 750 KB max.',
// Web image search
search_web_image: 'Search the web for an image',
search_btn: 'Search',
searching: 'Searching…',
image_results_for: 'Results for',
image_pick_hint: 'Click an image — it is downloaded and resized to 256 × 256 PNG.',
no_image_results: 'No results. Try another search term.',
web_search_off: 'Web lookups are disabled on this server.',
unknown_target: 'Unknown search target.',
printer_not_found: 'Printer not found.',
driver_not_found: 'Driver not found.',
type_to_search_image: 'Type something to search for.',
type_to_search_driver: 'Type a printer model or brand to search for.',
// Driver rename + icon
edit_driver_title: 'Edit driver',
section_driver_name: 'Name',
driver_display_name: 'Display name',
hint_driver_rename: 'Shared with everyone — the driver library is global. Leave empty to show the ZIP filename.',
no_driver_icon: 'No icon yet.',
// Driver web search
find_driver_online: 'Find a driver online',
find_driver_intro: 'Search for a vendor-wide driver, or for an exact model. ImpTune only shows links — you download the ZIP and upload it above.',
driver_search_label: 'Brand or printer model',
search_generic_btn: 'Find generic driver',
search_exact_btn: 'Search exact model',
searched_for: 'Searched for',
driver_search_warning: 'Unfiltered web results, not a vetted list. Download drivers only from the manufacturers own site, then upload the ZIP above.',
no_driver_results: 'No results. Try the exact model name instead.',
back_to_printers_btn: 'Back to printers',
export_locked: 'Assign a driver to this printer to unlock scripts and export.',
// Edit modal