Add spoolman-mcp to the list of integrations. It's a Model Context
Protocol server that lets you manage your filament inventory through
AI assistants like Claude.
Weblate opens a PR for every translation sync. These are machine-generated,
always scoped to client/public/locales/, and gated by the full CI suite, so
hand-merging each one is pure friction. This enables GitHub auto-merge on
Weblate-authored PRs so they merge themselves once required checks pass.
A scope-guard step reads the PR's file list from the API and refuses to enable
auto-merge unless every changed file is a .json under client/public/locales/,
so the trust is in the diff, not just the author: a compromised Weblate token
can open a PR but can only be auto-merged if it is confined to translations.
Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
The external database sync scheduler was ignoring the configured
EXTERNAL_DB_SYNC_INTERVAL environment variable and always using the DEFAULT_SYNC_INTERVAL constant (3600 seconds).
Non-English locales are managed exclusively through Weblate; any manual
edit to an existing translation file gets silently overwritten on the
next Weblate sync. This workflow fails any PR (except Weblate's own,
detected via PR author login) that modifies or deletes an existing
non-English file under client/public/locales/. Adding a brand-new
language file is still allowed so contributors can bootstrap a new
language alongside the client/src/i18n.ts entry.
Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
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
- path-to-regexp: force >=8.4.0 (8.4.2) under @ant-design/pro-layout via a
scoped npm override, fixing the ReDoS advisories (GHSA sequential optional
groups + multiple wildcards). Express keeps its own 0.1.x.
- pytest 8.3 -> 9.1.1 and pytest-asyncio 0.23/0.25 -> 1.4.0 in
tests_integration/requirements.txt and the dev group; regenerated uv.lock.
Verified: client build + typecheck pass, npm audit clean, and the full
integration suite (223 tests) passes on postgres, sqlite, mariadb, and
cockroachdb under podman.
Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V
Several DB write functions mutated and then sent their websocket notification (or
just returned) without committing, relying on the request session's teardown
commit. On async postgres that commit races the HTTP response, so a fast
follow-up read could still see the pre-write state — e.g. `DELETE /vendor/{id}`
returns 200 but an immediate `GET` still returns 200. Latent since 2023 (a6d527a);
the async-timing dependency bump widened the window enough to intermittently fail
`test (postgres)` in CI.
Commit inline before notifying, matching the functions that already did
(filament.delete, vendor/spool create/update) and the commit-first invariant from
the earlier websocket-ordering fix:
- vendor.delete, spool.delete
- setting.update, setting.delete
- vendor/filament/spool.clear_extra_field
- spool.rename_location
(use/measure already commit via their wrappers; reads are unaffected.)
Verified via `SPOOLMAN_CONTAINER_ENGINE=podman poe itest`: full 4-DB suite green
plus 7/7 repeat postgres runs with zero flakes (was ~66% failing before).
Brings in the latest #798 commit (388082f) that our earlier merge missed: it
drops --chown from the builder COPYs (matching the Podman build fix already on
master), removes the redundant `usermod -G users app`, and simplifies the
entrypoint to re-exec itself via gosu when root instead of duplicating the
uvicorn invocation. Dockerfile comment conflict resolved in favour of the
Podman-explaining comment.
Brings in jpiccari's unprivileged-Docker support (rootless entrypoint that only
uses gosu when running as root and only remaps app UID/GID when they differ, plus
dropping the redundant `groupmod -g 1000 users` in the runner stage).
Combines cleanly with the Podman build/test enablement already on master
(builder-stage --chown removal, postgres healthcheck, run.py engine selection),
giving full rootless build + run + test support.
Running tests_integration against rootless Podman surfaced three issues; none
affect the Docker/CI path:
- run.py: add a SPOOLMAN_CONTAINER_ENGINE env var (defaults to "docker") so the
build/compose commands can be driven by podman instead.
- Dockerfile: drop `--chown=app:app` from the builder-stage COPYs. The "app"
user only exists in the runner stage; Docker/BuildKit tolerated the dangling
reference but Podman rejects it. Final ownership is unchanged — it is set when
the files are copied into the runner stage.
- docker-compose-postgres.yml: add a pg_isready healthcheck and gate spoolman on
`condition: service_healthy`, matching the mariadb/cockroachdb compose files.
Without it spoolman raced postgres' initdb and died on connection-refused
before running migrations.
Verified: full 4-DB suite (sqlite/postgres/mariadb/cockroachdb) green via
`SPOOLMAN_CONTAINER_ENGINE=podman uv run poe itest` — 223 tests each.
Refresh transitive client dependencies to the latest versions within the
existing package.json semver ranges (package.json unchanged).
Verified: npm ci, tsc --noEmit, eslint, prettier --check, and npm run build all
pass. (3 high-severity advisories remain that require major/breaking bumps via
`npm audit fix --force`; left for a separate, deliberate change.)
The `extra` map on vendors, filaments and spools is intentionally typed as
dict[str, str] where every value is a JSON-encoded string (the client round-
trips them through JSON.parse/stringify). This surprised API consumers, who
saw numeric fields come back as strings.
Spell out the encoding on all three response models via a shared helper so the
generated OpenAPI docs make clear that consumers must JSON-decode each value.
No behaviour change.
requirements.txt was moved to .gitignore when the project switched to uv, so
bare-metal installs lost it. Moonraker's update_manager runs
`pip install -r requirements.txt` against the checkout, so upgrading to 0.23
broke with "Invalid path for option `requirements`".
Track requirements.txt again and regenerate it from uv on every release
(spoolman/bump.py exports it alongside the uv.lock bump), keeping uv as the
single source of truth while staying backwards compatible with installs that
expect a requirements.txt.
The REST endpoints serialize with response_model_exclude_none=True (unset fields
are omitted), but websocket events were serialized with a plain .json() that
includes unset fields as explicit null. The spool list fills a missing price
from the filament price only when it is `undefined`, so a live update carrying
`price: null` (e.g. after a quick weight adjust) blanked the price column until
a full page reload.
Fixed on both sides so the two serialization paths can no longer drift:
- Server: websocket payloads now use exclude_none=True (spoolman/ws.py), giving
live updates and REST responses an identical shape. Note this is a small
websocket wire-format change external consumers will see: unset fields are now
omitted instead of sent as null, matching REST.
- Client: collapseSpool and the price column now fall back for both `null` and
`undefined`, keeping the list resilient regardless of serialization.
This change enables the container to run in unprivileged mode (without
--privileged or additional capabilities) by making permission drops
conditional:
- Changed users group GID from 1000 to 1001 to avoid conflicts with
the app user's primary group (UID 1000)
- Only modify user/group IDs when PUID/PGID environment variables differ
from the default value of 1000, avoiding unnecessary privilege
requirements
- Skip su-exec entirely when running as the default user, since changing
user context requires additional capabilities that unprivileged
containers don't have
This allows the container to work in restricted environments like
Kubernetes with security contexts or rootless Docker while maintaining
backward compatibility for users who customize PUID/PGID.
Fixes Donkie#791