Backup folders on network shares often refuse SQLite's file locks (CIFS
without nobrl, NFS without lockd). SQLite reports a refused lock as
SQLITE_BUSY, so every backup to such a folder retried until the 60 second
give-up and blamed a locked database, while the live database was not
locked at all (#1246).
The target is a scratch .pending file that nothing else opens, so open it
with nolock=1. The give-up message now names the live database, since that
is the only file a backup can still wait on.
sqlite3's Connection.backup() retries forever while the source is busy,
calling progress with 0 of 0 pages each time. The backup ran synchronously
on the event loop, so a lock that did not clear froze every request and
spammed "Copied 0 of 0 pages." into the log (#1191).
Run backups in a worker thread, give up after the source has been locked
for 60 seconds, and serialize backups now that they can overlap. Per-page
progress drops to debug.
Escape now closes the Filter, Group and Sort menus when they were opened
with a click, which left focus on their button where the menu never saw
the key.
The pager's page numbers and page-size picker wrap as one, so the
catalog's longer summary line no longer strands the picker on a line of
its own on phones.
"Show as a list" in the manufacturer inspector filters by the
manufacturer's id rather than its name, so a namesake stays out.
The filament and manufacturer lists now sort by every field of the
filament or manufacturer, their custom fields included, plus spool and
filament counts, and offer the same filters as the spool view. A filter
on the spools themselves leaves out the filaments with none, as it does
there.
/spool/group takes the matching sort keys: filament.<column>,
filament.vendor.<column>, filament.extra.<key>,
filament.vendor.extra.<key> and group.filament_count. Keys it can't
resolve are still ignored.
Rows show the id as #id on the left like spool rows, and the Library
tab menu loses its descriptive sublines.
The Library gets two power-user sub-views, /?view=filaments and
/?view=manufacturers, reached from a menu on the Library tab, the search
panel and the manufacturer inspector. Rows are lists rather than tables so
translated labels and long names never widen them. The spool view is
unchanged.
The group endpoint gains what the lists need, all additive: a
group.vendor_name sort, include_empty for group_by=vendor, filament_count
on vendor groups, an opt-in preview of a group's first spools or
filaments, and X-Total-Spools / X-Total-Remaining-Weight headers.
A screenshots Playwright project seeds a fixed dataset on the fresh CI
database and captures the main pages in light/dark at desktop and phone
widths. The Argos reporter uploads them from CI and posts the diff against
master on the PR. Fork PRs upload tokenless.
Also scope the transparency spec's swatch locator to the inspector header,
it picked the first list row once other spools sorted ahead of its own.
A spool label's custom QR target can now use fields like {filament.id},
so it can open the filament page while still printing spool fields such
as the lot number. Closes#1152.
Headless Chromium has no Web NFC, so install a small fake of the surface
client_v2 uses (NDEFReader, scan, reading event, nfc permission) and cover
auto-navigate from a phone read, the unknown-tag toast, starting once
permission is granted, and the link dialog's own read.
The server checks the latest GitHub release in the background at startup
and every 12h, logs a warning when a newer version exists, and exposes
latest_version/update_available on /info. The client_v2 footer shows a
link to the new release. Disable with SPOOLMAN_UPDATE_CHECK=FALSE.
Now that every branch builds on push, opening a PR from one ran the whole
workflow twice for the same commit.
Concurrency cannot dedupe these: push and pull_request carry different refs, and
forcing them into one group only races them, killing whichever run started
first. That is the wrong outcome roughly half the time, since publish-images
runs on push and not on pull_request, so the branch image would vanish.
Guard the three jobs that have no `needs` instead. A skipped dependency skips its
dependents, so this cascades to all twelve. Forks are unaffected: a fork's push
fires no event in this repo, so its pull_request run is the only one there is.
Branch pushes matched no trigger, so an unmerged fix could not be handed to a
bug reporter as an image. Push now covers all branches except weblate (constant
locale-only commits) and dependabot (arrives as PRs anyway).
publish-images was already gated only on "not a pull request", so it picks
branch pushes up for free; it just had no tag to publish them under. Adding
type=ref,event=branch to the GHCR metadata gives one, skipping master since that
is already :edge. Docker Hub deliberately keeps only edge and tagged releases,
so users browsing it do not see half-finished branches.
Concurrency now cancels superseded runs on any ref except master and tags, which
publish images and must not be interrupted mid-manifest.
Note: once a branch has an open PR, push and pull_request both fire and it runs
twice.
Review feedback on #1184. A native select replaces the cycle button, still
shown only when more than one camera is listed. Picking directly suits
desktops, where webcams have real names, and on Android the select opens the
system's full-screen picker. It also removes a trap in the cycle: qr-scanner
opens a default camera when the requested one fails, and cycling on from that
default could never reach the cameras after the failing one. Every camera is
now one choice away, and the select shows the camera actually streaming.
Cameras with no label are listed as "Camera 1", "Camera 2" and so on. The
list now comes from enumerateDevices() directly, because qr-scanner's
listCameras() fills gaps with an untranslated "Default Camera" / "Camera N"
that can't be told apart from a real device name.
A failed switch now shows the same classifyStartFailure() message as a failed
start, instead of leaving the stage black with only a console error. The
picker stays up so another camera can be tried. After a failed open the
scanner is stopped, and setCamera() only restarts a running scanner (or does
nothing for the same id), so a retry starts it explicitly.
The caption and its n/N position are gone, since the select names the
current camera itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Auto-navigate only listened to the scan relay, so a tag tapped against an
Android phone running the client did nothing outside the link-tag dialog.
While auto-navigate is on, also run a background Web NFC reader and feed
its reads through the same handler, looking the holder up by UID.
Persist uv's cache of source-built wheels (asyncpg, greenlet, psycopg2,
cffi, ...) across CI runs via actions/cache + buildkit-cache-dance, so a
lockfile or base-image change no longer recompiles everything under QEMU
(~11 min -> ~3 min). Move the greenlet patchelf step into the cached
dependency layer so source-only builds skip an apt-get under QEMU, and
drop python3-dev, which only pulled in Debian's unrelated python3.11.
String filters split their value on every comma before checking for quotes, so an exact
match like location="Top shelf, Rack 3" became two fuzzy terms that matched nothing.
The dashboard and library filters always send quoted values, so a location with a comma
showed as empty. Split with a quote-aware helper shared by the built-in string filters
and extra-field filters. Unquoted values split exactly as before.
Fixes#1169
Two more problems from #1149.
Sheet mode set @page size to the paper's exact dimensions. A 62x34 page makes
the browser send it as landscape, and Chromium hides the orientation picker
when the page CSS sets a size. Label printer drivers disagree on which way
round a roll is: a Brother QL needed landscape, a Niimbot B1 needed portrait,
so any fixed choice prints rotated for someone. Sheet mode now uses size: auto
like v1 did, and the page is still laid out in mm. Label mode keeps its exact
size.
The v1 migration derived the label size from the raw margins. v1 users paired
-20mm margins with a 1.5mm printer margin to pull an oversized cell back onto a
62x34 page, which migrated as a 102x74 label. Margins are now clamped at
migration; the safe-zone alone gives the same content inset (max(safe, margin)
in both versions), and the label comes out at the paper size.
Two bugs in the sheet-mode safe-zone inset, both reported in #1149.
The label raster has a fixed aspect ratio, but labelImg sized its width and
height independently, so a safe-zone squashed the label (1.5mm top/bottom on a
34mm label is a 9% vertical squeeze). v1 padded a box of reflowing HTML, where
that was harmless; a raster has to scale uniformly, so the inset box now gets
object-fit: contain.
Negative page margins made it much worse. v1 accepted them and used them to size
the grid cells; in v2 the label size comes from the design and the margin only
positions an absolutely placed grid inside an overflow:hidden page, so a negative
margin can only clip. It also inflated overSafe (safe - margin), which turned a
1.5mm safe-zone into a 21.5mm inset: at margin -20 the label rendered 22x12mm in
the corner of a 62x34mm page, and sheetGrid invented a second row off the paper.
Margins are now clamped to zero on read, so existing presets carrying v1's
negative values are fixed without the user editing them, and the margin inputs
refuse new negative values.
The scanner asks for the 'environment' camera and takes whichever rear camera
the browser hands it. On a phone with several lenses that can be the
telephoto: an Oppo X9 Ultra lists its rear cameras as camera 5, 4, 3, 2, 0 and
Chrome opens the zoom lens, which only fits a label in frame from across the
room. On a desktop the same thing happens with a virtual webcam, and once the
permission is saved the browser never offers the choice again (#693).
When more than one camera is listed, a switch button over the video steps to
the next one and wraps around. Each switch briefly shows the camera's name and
position, since phone lenses are only told apart by numbers. The camera the
user settles on is remembered per browser and opened directly next time.
A remembered camera that is no longer listed falls back to 'environment'.
That is checked before starting, because qr-scanner retries an unavailable
deviceId with no constraint at all, which can land on the front camera.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>