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:
+102
-1
@@ -1263,6 +1263,106 @@ div.error > p {
|
||||
|
||||
.icon-preview .meta { font-size: .812rem; color: var(--im-text-dim); }
|
||||
|
||||
.error-note {
|
||||
margin: 0 0 .8rem;
|
||||
font-size: .875rem;
|
||||
color: var(--im-danger);
|
||||
}
|
||||
|
||||
/* Driver-library thumbnail: fixed box whether or not an icon exists, so the
|
||||
name column does not shift when one is added out of band. */
|
||||
.cell-with-thumb {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .6rem;
|
||||
}
|
||||
|
||||
.driver-thumb {
|
||||
flex: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-top: .1rem;
|
||||
border-radius: var(--im-r-sm);
|
||||
background: var(--im-surface-3);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.driver-thumb img { width: 24px; height: 24px; display: block; }
|
||||
|
||||
/* ==========================================================================
|
||||
Web lookups — image picker and driver-page results
|
||||
========================================================================== */
|
||||
|
||||
.web-picker {
|
||||
margin-top: .9rem;
|
||||
padding-top: .9rem;
|
||||
border-top: 1px dashed var(--im-line);
|
||||
}
|
||||
|
||||
.web-picker label { margin: 0 0 .5rem; }
|
||||
.web-picker .btn-row { margin: 0; }
|
||||
|
||||
.image-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
||||
gap: .5rem;
|
||||
margin: .6rem 0;
|
||||
}
|
||||
|
||||
/* Each result is a button, not an <img> with a click handler — keyboard
|
||||
selection and focus rings come for free. */
|
||||
.image-option {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
padding: 0;
|
||||
border: 1px solid var(--im-line);
|
||||
border-radius: var(--im-r-sm);
|
||||
background: var(--im-surface-3);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-option:hover,
|
||||
.image-option:focus-visible { border-color: var(--im-accent); }
|
||||
|
||||
.image-option img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.image-option .image-meta {
|
||||
position: absolute;
|
||||
inset: auto 0 0 0;
|
||||
padding: .1rem .25rem;
|
||||
font-size: .625rem;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, .55);
|
||||
}
|
||||
|
||||
.link-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .7rem;
|
||||
}
|
||||
|
||||
.link-list li {
|
||||
padding-bottom: .7rem;
|
||||
border-bottom: 1px solid var(--im-line);
|
||||
}
|
||||
|
||||
.link-list li:last-child { border-bottom: 0; padding-bottom: 0; }
|
||||
.link-list a { font-size: .906rem; font-weight: 560; }
|
||||
.link-list p { margin: .2rem 0 0; font-size: .812rem; }
|
||||
.link-list .cell-sub { display: block; }
|
||||
|
||||
/* ==========================================================================
|
||||
Dialogs
|
||||
========================================================================== */
|
||||
@@ -1406,7 +1506,8 @@ details.more-pills > summary::marker { content: ""; }
|
||||
sidebar there is only room for the file and its parsed names. */
|
||||
.form-aside .data-table .col-arch,
|
||||
.form-aside .data-table .col-used,
|
||||
.form-aside .data-table .col-added { display: none; }
|
||||
.form-aside .data-table .col-added,
|
||||
.form-aside .data-table .col-actions { display: none; }
|
||||
|
||||
.uploader label { margin: 0; }
|
||||
.uploader .hint { margin-top: 0; }
|
||||
|
||||
Reference in New Issue
Block a user