13 Commits
Author SHA1 Message Date
Donkie dd6b334a79 Test config.js rendering without importing main
The unit tests are supposed to run with no database, no server and no
built client, but tests/test_configjs.py imported spoolman.main, which
builds the SPA handler at module level and so needs client_v2/build to
exist. That passed locally, where the bundle is lying around, and failed
CI's style job, which never builds the client.

Moves the rendering into client.render_config_js, which the endpoint now
calls, so the test exercises the real code path without the app.
2026-07-31 10:00:47 +02:00
Donkie c9ae367dc0 Say what to do when the client bundle is missing
The clients are build artifacts and are not committed, so a source
checkout has none until `npm run build` has been run in the client
directory -- and a `git pull` that brings new client sources does not
bring a bundle. Spoolman then refuses to start with StaticFiles' own
message, "Directory 'client_v2/build' does not exist", which says
nothing about what is wrong or how to fix it.

This has always been true of client/dist, but it bites harder now: an
existing source install that upgrades into client_v2 has never built
this directory even once, so it hits the wall on the upgrade rather than
on a fresh install. Docker images and spoolman.zip both ship the bundles
pre-built, so neither is affected.

Check before handing the directory to StaticFiles and name the command
to run, derived from the client directory so it is right for whichever
client is mounted.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:59:30 +02:00
Donkie b38468ffe2 client_v2: ship a web app manifest and PWA icons
The legacy client got its manifest and icons from vite-plugin-pwa.
client_v2 has no such plugin, and nothing replaced them: static/ held
only favicon.svg, spoolman.svg and the Ko-fi logo. So the upgrade
silently un-installs the app for everyone who had added Spoolman to a
home screen -- no manifest means no standalone display mode, no
home-screen icon, and no install prompt ever offered again.

The manifest and icons are plain files in static/ rather than plugin
output. The icons are the v1 set, unchanged, so an already-installed
app keeps the icon it had; name, colors and display mode are v1's too,
and start_url resolves to the same URL at a root deploy, which keeps the
app identity stable across the upgrade instead of orphaning the old
install.

Every URL inside the manifest is relative to the manifest itself, so the
one file is correct under any SPOOLMAN_BASE_PATH with no rebuild. Only
the document's link to it needs help: the prerendered pages emit
"./manifest.webmanifest" and resolve fine, but the SPA fallback (200.html,
which serves deep routes like the /spool/show/<id> a printed QR points at)
carries absolute paths, so the existing base-path fixup gains two
patterns for the manifest and the touch icon.

Also register the .webmanifest media type. StaticFiles takes Content-Type
from the stdlib mimetypes registry, whose built-in table only learned
this extension in recent Pythons and otherwise falls back to the host's
/etc/mime.types -- absent in slim images. We support 3.10+, so leaving it
to the environment means some installs serve the manifest as text/plain.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 20:58:49 +02:00
Donkie 2e396d6cda Fail loudly when the SPA fallback base-path rewrite matches nothing
load_and_rewrite_fallback_base_path does blind string replacement on 200.html to
insert the operator's base path into SvelteKit's absolute asset references and its
hardcoded `base: ""`. str.replace on a pattern that isn't there is a silent no-op,
so a SvelteKit upgrade that changes any of those three emission shapes would ship a
fallback document whose assets 404 and whose client-side router builds URLs without
the base path.

That failure is invisible in development and in CI, both of which run at the root
path where this code doesn't execute at all. It would surface only on an operator's
reverse-proxied instance, as a blank page on exactly the routes the rewrite exists
to serve -- direct loads of /spool/show/<id>, the target of printed QR labels.

Check each pattern is present before rewriting and raise naming the ones that are
not. A build/server mismatch is a packaging error, so refusing to start with an
actionable message beats serving a broken page.
2026-07-29 10:35:41 +02:00
Donkie b7772ca743 client_v2: fix base path for direct loads of non-prerendered routes
SvelteKit's adapter-static emits the SPA fallback (200.html) with absolute
asset paths (/_app/...) and a hardcoded base: "", since it can't know the
deploy base path at build time. Prerendered per-route documents use relative
paths and a runtime-computed base, so they work under any base path, but the
fallback is used for non-prerendered dynamic routes (/spool/show/<id>,
/filament/show/<id>) -- exactly the URLs printed into QR labels. Under a base
path those direct loads 404'd every asset and rendered a blank page.

Rewrite the fallback server-side using the base path the backend already
knows, mirroring the legacy-client asset rewrite: prefix /_app/ and /favicon
references and set the runtime base. Prerendered docs are still served
verbatim, and the legacy client is unaffected (its tweaked_html is always
produced when rewrite_asset_paths is on, so the file_response guard change is
equivalent there).
2026-07-25 17:49:11 +02:00
Donkie 638466bc70 Bake client_v2 into the image and serve it by default
Build the new Svelte client (client_v2) in CI, bake it into the Docker
image alongside the legacy React client, and serve it by default. Set
SPOOLMAN_LEGACY_CLIENT=TRUE to fall back to the old client.

SinglePageApplication is generalized to serve either client: the React
client keeps its index.html + base-path rewriting, while the Svelte
client uses a 200.html SPA fallback, serves prerendered per-route pages,
and serves relative-path assets verbatim. The tweaked-HTML store is moved
off self.html, which StaticFiles uses as its HTML-mode flag.

The Playwright frontend suite targets the React DOM, so it's pinned to
the legacy client via SPOOLMAN_LEGACY_CLIENT for now.

Claude-Session: https://claude.ai/code/session_01VQQtwPBPiMCoZCpBbbqS64
2026-07-18 23:05:00 +02:00
Donkie d65b50d5a2 Use paraglide for i18n in client_v2. Update base path handling 2026-07-16 18:41:01 +02:00
Donkie d9fd44eab9 Add Playwright frontend integration tests; fix broken SPA static serving
Add a browser-driven integration suite (tests_frontend/) that runs against
the real production image + PostgreSQL and drives the UI with Playwright:
a smoke test that navigates every page via the sidebar with no console
errors, and a CRUD test that creates a vendor -> filament -> spool entirely
through the UI. Wired up as `poe itest-frontend` and a new `test-frontend`
CI job (reusing the image built for the backend tests, gating releases).

Navigation goes through real UI buttons (language-independent link targets)
and the app language is forced to English so label matchers are stable
regardless of the runner's browser locale.

This immediately caught a real regression: Starlette 1.3.1 (pulled by a
recent dep bump) removed the `method` kwarg from FileResponse, so
SinglePageApplication 500'd on every static asset and the whole UI was dead.
Drop the removed kwarg — Starlette now derives HEAD handling from the scope.

Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
2026-07-07 09:12:42 +02:00
Donkie 0f5775c2d4 Enforced eslint and prettier, added lefthook config 2026-01-07 23:02:32 +01:00
Donkie b942d1f8a1 Replaced pdm with uv, updated backend deps, python 3.10 minimum, simplified install script 2026-01-07 22:01:02 +01:00
Donkie 26f5eaa2f7 Fixed base path issues 2024-05-22 20:15:39 +02:00
Donkie bc32f1e890 Now supports running under a sub path
Just set the SPOOLMAN_BASE_PATH environment variable and it should work.

Resolves #95
2024-05-10 11:37:09 +02:00
Donkie 9e46de9007 Moved SPA definition to a new file 2023-09-04 20:22:31 +02:00