Releases: xiao-villamor/PrintStash
Release list
v0.11.1 — Vault audit, capture, per-printer access, and i18n completion
This patch-version release is an explicit exception to normal 0.x patch policy: it adds features and append-only database migrations. Back up before upgrading.
Added
- Vault Maintenance. Superusers can run persisted Quick or Full audits, inspect severity-grouped findings, cancel hashing between objects, repair eligible thumbnail/Metadata/Revision findings, and verify backup archive structure and manifest membership.
- Pending Imports and Quick Capture. URL captures persist across restarts with owner-scoped review, retry, Collection/tag assignment, archive/file selection, and a minimal Manifest V3 browser helper that sends only current page URL/title.
- Structured Model filters. Artifact type, material, slicer, printer model, Revision status, print history/outcome, storage location, and upload date extend canonical URLs, facet counts, and Saved Views.
- Per-printer access control. Superusers can grant view, print, control, or admin roles independently for each printer. Printer, fleet, live WebSocket, remote-file, queue, routing, and maintenance APIs enforce the effective role; connection details and unavailable actions stay hidden in the UI.
Changed
- Backup creation now fails if a database-owned blob is unreadable, missing, changes size, or vanishes while streaming instead of silently producing a smaller archive.
- Completed Spanish translations across the Model detail print-settings panel, card-metric and metadata-field pickers, and remaining Settings surfaces left over from 0.11.0's localization foundation.
Fixed
- Clearing all Structured Model filters at once now removes every active filter and date range from the URL instead of leaving the last-applied one in place.
- The mobile bottom navigation no longer pushes "Profiles" into the overflow "More" sheet for administrators; it stays one tap away alongside Vault, Pending, and Printers.
Security
- Pending Import URLs strip secret-shaped query parameters and reject embedded URL credentials; persisted manifests and API responses omit staging paths, cookies, and resolved download credentials.
- Vault-audit and repair endpoints are superuser-only, Pending Imports are owner-scoped, and facet counts use the same Collection RBAC scope as Model browsing.
- Printer grants are checked server-side on every REST and WebSocket boundary. Printing also requires existing Collection edit access to the source Model, and API keys inherit their owning user's printer grants.
Full changelog: v0.11.0...v0.11.1
v0.11.0 — Fleet, Scheduling, SSO, PWA, and i18n
Added
- Fleet queue and routing. Administrators can queue Vault G-code across configured printers using manual, default-printer, or deterministic least-busy routing, then reorder, reroute, cancel, and retry eligible failed dispatches.
- Maintenance-aware scheduling. Printers can be placed in soft-drain mode, assigned one-off maintenance windows, and given a simple maintenance log without interrupting active prints.
- Persistent background progress. Import and scan job status now survives application restarts; interrupted non-replayable work is marked failed and retryable instead of disappearing or hanging indefinitely.
- Fleet operations UI. Printer management now includes Fleet, Queue, and Maintenance views, and Model send dialogs can add G-code to the fleet queue.
- OIDC / SSO login. Optional generic OpenID Connect supports Authentik, Authelia, and similar providers with discovery, authorization-code PKCE, validated ID tokens, collision-safe just-in-time users, configurable admin-group mapping, fallback local login, and encrypted setup under Settings → SSO.
- Installable offline shell. PrintStash now includes a web-app manifest and service worker so it can be installed as a PWA and reopen its cached shell when the server is temporarily unreachable.
- Localization foundation. A typed, persisted locale system ships with English and Spanish catalogs, a global language switch, and translated login, desktop, mobile, profile, and Settings navigation surfaces.
Changed
- Immediate sends and scheduled dispatches share one storage-to-provider transfer seam while preserving provider capability checks and local-first storage behavior.
- Printer jobs expose additive routing, queue, provider identity, dispatch-attempt, and blocked-reason fields without removing existing history fields.
- The previously planned 0.12 Auth and Platform scope is included in 0.11.0.
- Fleet queue reads keep active work visible while limiting terminal history to 20 rows by default, with bounded pagination up to 100 rows in the UI and API.
- The local
TaskQueuenow wakes database-backed fleet dispatch immediately while timeout polling preserves queued work across restarts without Redis or another external service.
Fixed
- Moonraker WebSocket connections now send the required
server.connection.identifyhandshake before subscribing when an API key is configured, instead of silently subscribing unauthenticated. - Bambu LAN MQTT commands wait for the printer's own report before treating a print/pause/cancel/resume as successful, and status queries read the pushed MQTT report instead of a nonexistent local HTTPS status endpoint.
- PrusaLink file listing, job state, and start-print now match the real v1 API: recursive folder listings, telemetry read from
printerfields, and starting a file viaPOST /api/v1/files/local/{path}instead of the OctoPrint-style select/print body. - Cancelling a printer job now also marks its active PrintStash job row as cancelled, instead of leaving it stuck in a started/printing/paused state after the provider cancel succeeds.
Security
- Dispatches interrupted between provider upload/start and the final database write are marked as an unknown outcome and cannot be retried automatically, preventing accidental duplicate prints after restart.
- OIDC ID tokens accept only asymmetric RS/ES signing algorithms, and HTTPS callback configurations force Secure state, nonce, verifier, and session cookies.
Operations
- Prometheus now reports fleet jobs, blocked work, dispatcher outcomes, scheduler liveness, and last-tick time; authenticated health details expose the same scheduler state.
- PWA registration bypasses HTTP caching for service-worker updates, replaces old cache generations, activates waiting workers, and revalidates cached static assets while preserving offline navigation fallback.
Full changelog: v0.10.0...v0.11.0
v0.10.0 — Library Workflow Polish
Added
- Bulk library editing. Select up to 500 Models to move collections, add or remove tags, move to trash, or update labels across selected G-code revisions with atomic permission preflight.
- Saved views and favorites. Star Models per user and save common collection, tag, search, printer-presence, and favorites filters. Applying a saved view restores canonical URL filters.
- Richer Artifact comparison. Compare any two live Artifacts within one Model, including file, mesh, slicer, material, revision, and measured print-outcome differences.
- Portable library migration. Export a versioned ZIP containing accessible Models, original Artifact blobs, metadata, taxonomy, print history, favorites, and saved views; admins can safely import it into another installation with hash validation and deduplication.
- Configured-root folder import. Admins can trigger a targeted external-library scan below an already configured root without granting access to arbitrary server paths.
Security
- Portable imports reject path traversal, missing blobs, unsupported manifests, oversized archives, and SHA-256 mismatches before database or storage writes.
Changed
- Model detail now includes actual print counts, success rate, duration, filament, and cost when comparing Artifacts.
- Daily library workflow polish. Saved views now support search, recent ordering, update, rename, duplicate, and delete; mixed Model/folder selection adapts available bulk actions; destination search prevents invalid folder moves; sorting, compact density, keyboard shortcuts, recent folders, and undo reduce repetitive library work.
- Collection and tag management now lives in Vault, model editing, upload, and Settings; the redundant Catalog page and navigation entry have been removed.
Fixed
- Artifact comparison rounds percentages and filament totals to readable precision instead of exposing floating-point artifacts, and the vault sort picker now uses the shared styled menu.
- Header and recent-folder menus now stay above adjacent vault surfaces instead of being obscured by sticky controls or the collection sidebar.
Full changelog: v0.9.0...v0.10.0
v0.9.0 — Provider maturity
Added
- PrusaLink local FDM support (beta). Connect modern PrusaLink with Digest
username/password authentication or legacy installations with an API key.
PrintStash supports live status, G-code upload, explicit start, remote file
inventory/deletion, and pause/resume/cancel without using Prusa Connect cloud. - Elegoo Neptune 4-family setup. Neptune 4, Pro, Plus, and Max can be added
through a dedicated preset backed by the existing Moonraker/Klipper provider,
keeping one tested transport and an identifiable printer variant. - Elegoo Centauri Carbon support (beta). Original Carbon printers connect
locally over SDCP/WebSocket, while Carbon 2 uses its access-code-protected
local MQTT API. Both provide status and start/pause/resume/cancel controls.
Upload and file inventory stay disabled because current firmware exposes no
safe confirmed file API. - OctoPrint support (beta). Connect a local OctoPrint/OctoPi instance with
an API key for live status, G-code upload, explicit start, remote file
inventory/deletion, and pause/resume/cancel. - Provider-aware printer setup. The add-printer flow now offers Moonraker,
Elegoo Neptune 4, PrusaLink, OctoPrint, and Bambu LAN with only the
credentials required by each integration. - Printer model detection on the printer card. Bambu (from its serial
prefix) and Elegoo Neptune 4/Centauri (from the setup preset) show a
best-effort hardware model automatically. Any printer's model can be set or
corrected from a curated picklist to avoid typos, with an "Other" option
for custom builds. - Bambu LAN Vault send support (beta). PrintStash can upload plain-text
G-code to a Bambu printer's local cache over implicit FTPS, then start the
uploaded file through the local MQTT connection. Upload-only remains the
default; starting requires the explicit Start print immediately option. - Provider-safe Bambu send guard. Bambu sends now verify that the printer
reports an idle/ready state before creating a transfer job, so an active or
unreachable printer is rejected without leaving a job stuck uploading. - Provider capability matrix. Printer integrations declare supported
actions from one shared capability set (status, upload, start, controls,
file inventory, raw G-code, and measured consumption). API diagnostics and
frontend actions are derived from that data instead of provider-specific UI
checks. - Provider conformance suite. Every printer integration is exercised by one
shared test pack: it must register, reject incomplete credentials, refuse the
actions it does not declare without touching the network, and keep transport
failures behind the common provider error surface. Adding a printer backend is
now a single small class, so future providers arrive faster and behave
consistently from the first release. - Printer management refresh. Printer cards can optionally show model
artwork, detected models can be corrected inline, and provider badges now use
the shared visual language. Printer detail has a clearer idle/offline Status
view, compact temperature telemetry, and a new Settings tab for editing names,
groups, notes, model overrides, connection addresses, and provider credentials. - Dashboard-wide UI polish. Document pages now share one responsive width
and heading pattern; Settings, Statistics, Profiles, Organize, navigation,
empty states, errors, and destructive confirmations use the same semantic
surfaces and controls. Browser tabs also identify the current PrintStash page. - Consistent motion and feedback. Model grids settle in with a short capped
stagger, the batch toolbar rises and fades with selection, tabs move on the
compositor, and modals, drawers, menus, and toasts use the same fast motion
scale with symmetric close animations and reduced-motion support.
Fixed
-
Predictable reconnect behavior. Initial connection and subscription
failures now use bounded exponential backoff, while a recovered provider
clears its stale offline/error state on the first successful status update. -
Bambu status failures reach the printer hub. Polling errors no longer stay
hidden inside the provider loop, preventing stale online state during a LAN
outage and allowing the shared reconnect policy to take over. -
Mixed-provider send controls are capability-aware. The send dialog keeps
upload-only printers selectable but disables Send & Print when any
selected printer cannot start the uploaded file. -
Consistent page width. Printers, Statistics, Profiles, Settings, Organize
and printer detail each defined their own scroll container and content width,
so navigating between them shifted the layout sideways. They now share one
PageContainerprimitive with a single content width. -
The batch selection toolbar animates. Selecting models popped the toolbar
into existence with no transition; it now rises and fades, and reverses
cleanly when a selection is toggled rapidly. -
Printer creation ignores duplicate submits. Rapidly pressing Add
printer can no longer create multiple database rows and unexpectedly advance
the printer URL ID. -
Navigation and overlays are more accessible. Active navigation links now
expose their current-page state, focus behavior is consistent, and shared
confirmation dialogs replace browser-native destructive prompts.
Performance
- Live job database writes are bounded. Unchanged progress updates are
coalesced to a five-second interval. Three consecutive job-sync database
failures open a per-printer circuit breaker with increasing retry delays,
preventing a database outage from becoming a write storm. - Theme switching no longer restyles the whole document. The theme swap
applied a transition to every element and pseudo-element including
box-shadow; it is now limited to color properties on the standard timing
scale. Toasts and the tab indicator were also moved onto GPU-composited
properties and the shared motion tokens.
Ops
- Node.js 24 is pinned for frontend development and release builds, matching
CI and the production frontend image. - Provider documentation, limitations, diagnostics, manual hardware checks,
and the public roadmap now describe the 0.9.0 capability surface.
Full changelog: v0.8.5...v0.9.0
v0.8.5 — audit-log security & release hardening
Security
- Login and refresh are now rate-limited (10 requests/minute per IP,
separately for each endpoint), reusing the same limiter the public share
API already had. - Backup and restore are now audit-logged (
backup.create,
restore.start,restore.complete,restore.failed), closing the one gap
in audit coverage — everything else already flowed through the ORM's
change-tracking hook. - Reverse-proxy IP handling documented. If
apisits behind a reverse
proxy, set the newFORWARDED_ALLOW_IPScompose variable to the proxy's
address so rate limiting and audit logs see the real client IP instead of
the proxy's. Seedocs/known-limitations.md. - Audit-log diffs no longer leak secrets. The change-tracking hook wrote
unredacted before/after values for every changed column, so printer API
keys, S3/Spoolman/MakerWorld credentials, the auto-generated JWT secret, and
password/token hashes were readable viaGET /admin/audit. Redacted by
field name going forward; a migration scrubs previously-captured rows.
Fixed
- S3/R2 backends could fail to boot on first run.
_ensure_bucket's
missing-bucket check compared against a response field
(Error.StatusCode) that aClientErrornever sets, so the auto-create
path never ran on a real S3-compatible store (including MinIO) — startup
failed instead of creating the bucket. Found by adding integration tests
against a real MinIO instance. - A rejected send-to-printer could leave a job stuck "uploading" forever.
The print job was markedUPLOADINGbefore checking the printer's provider
supported the upload; a 409 on an unsupported provider left it orphaned in
that state. The check now runs before the job is created. - "Send to printer" could keep a deleted revision selected in the picker
after it was trashed elsewhere; it now falls back to the current
recommended revision. Failed sends now show which printer failed and why,
instead of only a success count. - The trash/notification cleanup loop skipped its first run for up to an
hour on every boot (it slept before its first tick), so short-lived
containers could go a whole redeploy cycle without expiring trash or
pruning old notification deliveries.
Performance
- New model versioning is a single
SELECT MAX()instead of loading
every file row for the model. - The live-print DB sync skips its lookup query on repeat ticks — the
active job is now cached per printer and re-queried only when the tracked
file changes or the print finishes, instead of running a filtered query on
every status update (several per second per printer). - Collection and model-detail permission checks are batched, replacing
a per-row lookup with one query for the whole page — most noticeable in
the sidebar on installs with many collections. - Filament/printer profile usage counts scan thinner rows, selecting only
the matched columns instead of hydrating full metadata records for every
live file.
Ops
- CI now runs
banditandpip-auditon every PR, and a migration-upgrade
job that seeds a database from the oldest supported release and replays
every migration to head. Release tags are validated as strict semver before
publishing.
Full changelog: v0.8.4...v0.8.5
v0.8.4 — integrity, security & performance hardening
Behavior changes on upgrade — read before installing.
Security
- JWT secret can no longer boot with the shipped default. The app now generates and persists a real secret on first boot; if
VAULT_JWT_SECRETwas left at the placeholder value, existing sessions are invalidated once on the upgrade boot. Set your own withopenssl rand -hex 32if you manage it explicitly. - SQLite foreign keys are now enforced. The upgrade repairs any orphaned rows first (clears a dangling attribution, or drops a row whose required parent no longer exists) so enforcement doesn't turn a later delete into a crash.
- Outbound fetches are pinned to the address the SSRF guard validated, closing a DNS-rebind gap between validation and connection.
Fixed
- Printer history re-import could duplicate every past job. Comparing imported filenames case-sensitively (and, in one path, against only the first character of the stored name) meant a printer that changed filename casing between polls — or any poll at all — could re-import its whole history. Comparison is now a real case-insensitive match on both sides.
- Backup restore could race the background jobs. Restoring while GC, an external library scan, or a live print sync were in flight could leave the database mid-swap under them. Restore now waits briefly for in-flight ingestion to finish and refuses with a 409 if it doesn't; GC and printer sync skip their tick while a restore is in progress.
Performance
- Collection permission checks and model-list role resolution now run as a single SQL query instead of a per-row Python scan.
- Print-job ingestion is atomic — a failed thumbnail no longer leaves a half-built model behind.
- The printer WebSocket fan-out no longer blocks on a slow client. One stalled browser tab used to stall the update for every other subscriber to that printer; sends now run concurrently with a timeout, and a slow or dead socket is dropped instead of blocking the rest.
- The dashboard's storage-usage figure is cached for 60 seconds instead of walking the filesystem on every load.
- Print statistics no longer re-hydrate every completed job on every dashboard load. Cost and effective filament grams are now resolved once, when a print completes, and summed directly. Behavior change: a print's cost is frozen at completion time — editing a filament profile's price afterwards no longer revises the cost of prints that already finished.
Full changelog: v0.8.3...v0.8.4
v0.8.3 — data-loss hotfix: document files
Upgrade immediately if you use Documents.
Fixed
-
Hourly cleanup could delete document files. The background job that removes unreferenced files only recognised model files as "in use", so uploaded documents (PDFs and other binaries) were treated as leftovers and deleted. Only local storage was affected; S3/R2 installs were not.
Documents deleted by an earlier version cannot be recovered by upgrading — restore them from a backup taken before the loss, if you have one.
-
Backups omitted document files. For the same reason, document blobs were never written into the backup archive, so restoring a backup silently lost them. New backups include them.
-
S3/R2 existence checks hid errors. A credentials or permissions failure was reported as "this file does not exist" instead of raising. Combined with the cleanup job, a transient auth error could have been read as "every file is unreferenced".
Under the hood
The cleanup sweep and the backup manifest each answered "which files does the database still own?" separately, and only one of them knew about documents. Both now share a single census (all_owned_blob_keys), so they cannot disagree again. Regression tests cover a document surviving a cleanup cycle, a trashed model keeping its bytes until it is purged, genuine leftovers still being collected, and a document round-tripping through backup and restore.
Full changelog: https://github.com/xiao-villamor/PrintStash/blob/main/CHANGELOG.md
v0.8.2
Added
- Grid drag-to-move. Drag a model card onto a folder in the grid view to move it there.
- Trademark policy. Added
TRADEMARKS.mdto protect the PrintStash name and branding; the project stays licensed under AGPLv3.
Fixed
- Remember-me login. Fixed the remember-me checkbox not persisting the session correctly, and shortened the token lifetime for accounts that don't opt in.
- Query client. Dropped a duplicate collections invalidation that could cause extra refetches.
- Docker Compose (light). Repaired
docker-compose.light.yml.
v0.8.1 — Printer controls & mobile polish
Added
- Live printer controls (Moonraker). The printer Status tab now lets you set the hotend and bed target temperature, apply one-tap PLA/PETG/ABS preheat presets or Cooldown, Home all axes, and trigger an Emergency stop. Controls are gated behind a new `can_send_gcode` capability, so they appear only for providers that accept G-code (hidden for Bambu LAN).
Fixed
- Mobile scrolling. Pages use the dynamic viewport height (`dvh`) so they scroll fully under the mobile browser chrome; the model detail page scrolls when its right-hand panel overflows; and the vault, model, and document pages keep their last content clear of the fixed bottom navigation bar.
- Thumbnails. Brighter, higher-contrast mesh lighting on the dark theme.
Full changelog: v0.8.0...v0.8.1
v0.8.0
Added
- Spoolman integration (optional, OFF by default). Connect a self-hosted
Spoolman instance under
Settings → Spoolman with a base URL and optional API key, behind a master
switch. A Test connection action and a live status badge confirm reachability,
and the connection is reported in/health(informational — a Spoolman outage
never marks the service degraded or blocks a print). - Spool inventory display. The Spoolman card lists spools with their
remaining weight, pulled live from Spoolman (the source of truth). - Filament preset sync (Spoolman → PrintStash, one-way). A "Sync from
Spoolman" action on the Profiles page imports Spoolman filaments as
FilamentProfilepresets keyed byspoolman_filament_id(deriving$/kgfrom
price / weight, plus material, brand, density, and diameter). Synced presets
are read-only in PrintStash (edit them in Spoolman); the first sync adopts
matching local presets by name+material instead of duplicating, and filaments
removed in Spoolman are unlinked (reverted to editable local), never deleted.
Sync also runs automatically when the integration is enabled. This keeps
filament data in one source of truth instead of drifting across two apps. - Exact per-print cost & weight from the selected spool. When a print used a
synced spool, its cost comes from that filament's real Spoolman price and its
grams use the spool's real density/diameter (mm_to_gramsoverride) instead of
the static per-material table — replacing fuzzy metadata matching for those
prints. - Per-print spool selection. Choose which spool a print consumes when
sending a job to a printer (send-to-printer) or logging a print manually.
The spool is persisted on the print record (PrintJob.spool_id/spool_name)
and shown in the model's print history. - Consumption write-back. On a Moonraker-measured print completion,
PrintStash decrements the selected Spoolman spool by the measured
filament_used_g(server-side via Spoolman's/spool/{id}/use), reusing the
existingprint_results+mm_to_gramspipeline. Once per job, after the job
is committed, so it never blocks the print path. Bambu reports no live
consumption, so its prints are skipped. - Double-count safety. Before writing consumption back, PrintStash checks
Spoolman's active spool at completion time; if Moonraker's own Spoolman
integration is already decrementing it, PrintStash skips its own write so a
print is never counted twice — even if you never opened the settings card.
The UI also warns when this is detected. An operator who has disabled
Moonraker's hook can set a "write back anyway" override to make PrintStash
own the consumption. - Collection documents & READMEs. Attach documentation to any collection:
write Markdown in an in-app editor (live preview, paste-or-drop image embeds)
or upload PDFs and other files. New Markdown docs open straight in the editor
and aren't created until you save. PDFs render inline in a themed pdf.js viewer
(page navigation, zoom, fit-to-width) that matches the app instead of the
browser's default chrome. Each collection can also carry a README shown at the
top of its page. Access follows the collection's role (view / edit / admin).
Changed
- Mobile UI refresh. Rebuilt the bottom navigation as a five-slot
Material-style bar (four destination tabs plus an always-present "More"
sheet) instead of cramming up to seven items across the width — readable
labels, a clear active indicator, and the account actions (username +
Log out) moved into the More sheet. - Search on mobile. The top-bar search is now available on small screens;
the redundant user-avatar menu is hidden there since navigation and account
actions live in the bottom nav. - Library header on mobile. The "All Models" header stacks its title and
actions on narrow screens (so the heading no longer wraps), with tighter
padding and smaller folder cards.
Fixed
- Dark-mode Catalog stat cards no longer show bright/stray dividers — the
Collections/Tags summary now uses theme-aware hairline separators. - The internal
__external__sentinel model (placeholder for external print
jobs) no longer leaks into the "All Models" grid — the library browse now
excludes it, matching the header count that already did. - Batch tag/move no longer creates a tag or destination collection as a side
effect when every selected model fails the permission check; taxonomy is only
created once at least one model qualifies. Batch permission checks were also
collapsed from a per-model grants query into a single pass. - Upload drag-and-drop is steadier: the drop highlight no longer flickers when
the cursor crosses items inside the dropzone, the copy cursor shows while
dragging, and a folder that fails to read now surfaces an error instead of
silently doing nothing. - Spoolman Test connection now probes the values typed into the form, so you
can verify a connection before saving — it no longer tests only the last-saved
config. Save and Test now show clear success/error feedback (a "Saved."
/ "Connected — Spoolman vX" line and an immediate status-badge update) instead
of appearing to do nothing, and a failed connection names the error instead of
showing a bare "transport error:". - The PrintStash logo and the document Back link now return to the Documents
tab when that's where you were, rather than always resetting to Models.
Internal
- New
app.services.spoolman(SpoolmanClientover the shared httpx pool, plus
a blockinguse_spool_weight_syncfor the worker-thread finish tick),
app.api.v1.spoolmanrouter (superuser, secret-masked config), a
record_spool_usagehelper inprint_results, a_spoolman_probehealth
component, and thespoolman_*switches onSystemConfigwith an Alembic
migration. Covered by client/helper/API unit tests and a frontend API-contract
test; e2e mock routes added for the Spoolman endpoints. - Consumption write-back gained a write-time double-count guard: a blocking
active_spool_syncprobe inapp.services.spoolmanand a
spoolman_write_forceoverride onSystemConfig(third Alembic migration).
record_spool_usageskips the decrement when Spoolman reports an active spool
unless the override is set. Covered by unit tests. app.services.filament_sync(Spoolman→preset reconcile),spoolman_filament_id
/density_g_cm3/diameter_mmonFilamentProfileandspool_filament_idon
PrintJob(second Alembic migration), adensityoverride onmm_to_grams,
andmodel_views.filament_cost_for_jobfor spool-exact cost. Read-only
enforcement on linked presets in the filaments API.- Collection documents:
app.api.v1.documents(CRUD, file upload, Markdown image
embeds) with aDocumentmodel and a collectionreadmefield across two
Alembic migrations. Frontend addsreact-pdf/pdfjs-dist(lazy-loaded, code
split) for the inline PDF viewer; the nginx config now serves.mjsas
application/javascriptso the pdf.js module worker loads (browsers reject a
module worker served asapplication/octet-stream).POST /spoolman/test
gained optionalbase_url/api_keyoverrides so the UI can probe unsaved
values. Covered by document/README and Spoolman API tests.