Skip to content

Commit 147bab5

Browse files
akira69jpiccarivelziDonkiechruoss
authored
Sync PR867 branch to english-only + latest master (#17)
* Add conditional logic to drop permissions for unprivileged Docker 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 * Allow archived spool export * Tidy up docker file and entrypoint * Fix spool price disappearing from the list on live updates (Donkie#814) 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. * Commit generated requirements.txt for bare-metal/Moonraker installs (Donkie#830) 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. * Document that extra field values are JSON-encoded strings (Donkie#849) 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. * Update Python dependencies (uv lock --upgrade) Refresh all backend dependencies to the latest versions allowed by the pyproject constraints and regenerate requirements.txt to match. Notable bumps: starlette 0.50 -> 1.3, fastapi 0.128 -> 0.139, cryptography 46 -> 49, uvicorn 0.40 -> 0.50, pydantic 2.12 -> 2.13, sqlalchemy 2.0.45 -> 2.0.51, alembic 1.17 -> 1.18. Locally verified: uv sync, ruff check, uv lock --check, app import, and a live startup smoke (SQLite) — migrations run, /health + /info respond, vendor CRUD works, and a websocket update event is delivered correctly on starlette 1.x. The Docker-based 4-DB integration suite still needs to run (CI). * Update client dependencies (npm update) 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.) * Make the integration test suite runnable under Podman 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. * Fix commit-before-notify races in write paths (flaky postgres deletes) 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). * Bump deps to clear Dependabot alerts (path-to-regexp, pytest) - 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 * 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 * Add CI guard blocking manual edits to non-English translations 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 * Update sync interval for external database synchronization (Donkie#943) 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). * Auto-merge Weblate translation PRs on green CI 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 * Remove hashes from requirements.txt * Bump uuid from 13.0.2 to 14.0.0 in /client (Donkie#927) Bumps [uuid](https://github.com/uuidjs/uuid) from 13.0.2 to 14.0.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md) - [Commits](uuidjs/uuid@v13.0.2...v14.0.0) --- updated-dependencies: - dependency-name: uuid dependency-version: 14.0.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs: add MCP server to integrations list (Donkie#890) 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. * feat(scanner): accept Data Matrix and other 2D codes (Donkie#887) Widen the code scanner's accepted formats beyond qr_code to the common 2D matrix codes (micro/rm QR, Data Matrix, Aztec, PDF417) so manually-generated labels using those symbologies can be scanned. Payloads that don't match the spoolman spool format are ignored, so this is a safe superset. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * feat(theme): add 'System' option that follows OS/browser theme (Donkie#947) Replace the light/dark switch with a 3-way System/Light/Dark control. The new 'System' preference follows prefers-color-scheme live via a matchMedia listener. New installs default to System; existing users keep their stored light/dark choice (legacy values remain valid preferences). Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Translations update (Donkie#825) * Translated using Weblate (Estonian) Currently translated at 17.3% (50 of 288 strings) Translated using Weblate (Persian) Currently translated at 96.5% (278 of 288 strings) Translated using Weblate (French) Currently translated at 98.9% (285 of 288 strings) Translated using Weblate (Greek) Currently translated at 72.9% (210 of 288 strings) Translated using Weblate (Dutch) Currently translated at 99.3% (286 of 288 strings) Translated using Weblate (Hungarian) Currently translated at 72.2% (208 of 288 strings) Translated using Weblate (Danish) Currently translated at 85.4% (246 of 288 strings) Translated using Weblate (Thai) Currently translated at 98.6% (284 of 288 strings) Translated using Weblate (Japanese) Currently translated at 99.3% (286 of 288 strings) Co-authored-by: Anonymous <noreply@weblate.org> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/et/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fa/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/fr/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ja/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/nl/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/th/ Translation: Spoolman/Web Client * Translated using Weblate (Ukrainian) Currently translated at 93.0% (268 of 288 strings) Translated using Weblate (Greek) Currently translated at 72.9% (210 of 288 strings) Translated using Weblate (Hungarian) Currently translated at 72.2% (208 of 288 strings) Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/el/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/ Translation: Spoolman/Web Client * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 98.6% (284 of 288 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 99.3% (286 of 288 strings) Translated using Weblate (Hindi (Latin script)) Currently translated at 3.8% (11 of 288 strings) Translated using Weblate (Swedish) Currently translated at 94.7% (273 of 288 strings) Translated using Weblate (Ukrainian) Currently translated at 93.0% (268 of 288 strings) Co-authored-by: Anonymous <noreply@weblate.org> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hi_Latn/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sv/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/uk/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/ Translation: Spoolman/Web Client * Translated using Weblate (Danish) Currently translated at 87.1% (251 of 288 strings) Co-authored-by: srbjessen <srbjessen@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translation: Spoolman/Web Client * Translated using Weblate (Czech) Currently translated at 100.0% (288 of 288 strings) Co-authored-by: Miloslav Kos <kos.m@post.cz> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/cs/ Translation: Spoolman/Web Client * Translated using Weblate (Korean) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Korean) Currently translated at 78.1% (225 of 288 strings) Added translation using Weblate (Korean) Co-authored-by: 김태남 <imedia0@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/ko/ Translation: Spoolman/Web Client * Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (288 of 288 strings) Co-authored-by: Kayz C <kayzed.x@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/zh_Hant/ Translation: Spoolman/Web Client * Translated using Weblate (Slovenian) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Slovenian) Currently translated at 38.1% (110 of 288 strings) Translated using Weblate (Slovenian) Currently translated at 14.9% (43 of 288 strings) Added translation using Weblate (Slovenian) Co-authored-by: Jernej Pangerc <jernejp21@tuta.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sl/ Translation: Spoolman/Web Client * Translated using Weblate (Slovak) Currently translated at 10.4% (30 of 288 strings) Translated using Weblate (Slovak) Currently translated at 9.7% (28 of 288 strings) Added translation using Weblate (Slovak) Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/sk/ Translation: Spoolman/Web Client * Translated using Weblate (Hungarian) Currently translated at 94.0% (271 of 288 strings) Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/hu/ Translation: Spoolman/Web Client * Translated using Weblate (Danish) Currently translated at 100.0% (288 of 288 strings) Translated using Weblate (Danish) Currently translated at 92.0% (265 of 288 strings) Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/da/ Translation: Spoolman/Web Client * Translated using Weblate (Latvian) Currently translated at 4.5% (13 of 288 strings) Added translation using Weblate (Latvian) Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> Translate-URL: https://hosted.weblate.org/projects/spoolman/web-client/lv/ Translation: Spoolman/Web Client --------- Co-authored-by: Daniel Hultgren <daniel.cf.hultgren@gmail.com> Co-authored-by: srbjessen <srbjessen@gmail.com> Co-authored-by: Miloslav Kos <kos.m@post.cz> Co-authored-by: 김태남 <imedia0@gmail.com> Co-authored-by: Kayz C <kayzed.x@gmail.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> * feat(i18n): add Lithuanian and Turkish locales Rework check-i18n to measure real translation coverage against en (non-empty and differing from the reference) instead of file size, gating inclusion at 50%. Add lt and tr, both ~96% translated. Claude-Session: https://claude.ai/code/session_01BQjJiwEbu6bHbeDjPPDm3V * Bump version to 0.24.0 * Add filtering and sorting for custom fields (Donkie#773) * Add filtering and sorting for custom fields * Fix tests and make code postgres compatible * Fix bugs and expand test coverage for custom field filter/sort * trigger CI * fix: address extra field filter review feedback * fix: clean up custom field table state handling * docs: update extra field table view description * fix: narrow boolean custom field filter syntax * Expand test coverage for all custom field types/entities and fix range filter/sort - Add comprehensive tests covering all 9 field types (text, integer, float, boolean, single-choice, multi-choice, datetime, integer_range, float_range) for filter and sort on spool, filament, and vendor entities - Add invalid-filter 400 tests for float, integer_range, and float_range - Fix integer_range/float_range filter to use LIKE pattern matching against Python's deterministic json.dumps output instead of fragile .contains() - Add integer_range/float_range sort support via a @compiles helper (_JsonArrayFirstElement) that emits CAST(col AS JSON)->>0 on PostgreSQL and JSON_EXTRACT(col, '$[0]') on SQLite/MariaDB - Add logger and __all__ to extra_fields.py (aligns with PR Donkie#893) - All tests verified passing on postgres, sqlite, and mariadb * Add filter UI for all custom field types and fix range filters - Add text input filter dropdown for text fields (substring search) - Add range input filter dropdowns (min/max) for integer, float, integer_range, and float_range fields - Add datetime picker filter dropdown for datetime fields - Fix boolean "No" filter to use empty value so it correctly matches unset/false fields - Fix integer_range/float_range filter to use numeric comparisons instead of LIKE-based exact matching (stored_min >= filter_min, stored_max <= filter_max) - Add range filter support for integer/float fields (min:max format) - Add _JsonArraySecondElement cross-database helper for extracting second JSON array element * Range filter for integer/float fields and fix range_field filter semantics Backend: - Add _JsonArraySecondElement cross-database helper (mirrors _JsonArrayFirstElement) - integer_range/float_range filter now uses numeric comparisons: stored_min >= filter_min and stored_max <= filter_max (was LIKE exact match) - integer/float filter now supports min:max range format in addition to exact match: stored_value >= min and/or stored_value <= max Frontend: - integer and float fields now use a range filter dropdown (min/max inputs) instead of a single exact-value input Tests: - Fix integer_range/float_range spool and vendor tests broken by new >= semantics (filter values updated to discriminate between test entries) - Add range filter tests (min only, max only, both) for integer and float fields * Replace 12 copy-pasted numeric field tests with 4 parametrized tests Introduce a _create_entity helper and @pytest.mark.parametrize("entity_type", ["spool", "filament", "vendor"]) to run each numeric field type test against all three entity types from a single test function. - Removes 12 individual tests (integer/float/integer_range/float_range × 3 entities) - Adds 4 parametrized tests covering the same ground plus the previously missing min-only / max-only range filter cases for filament and vendor - Uses try/finally for cleanup so entities are always deleted even on assertion failure * Add datetime range filter and restore integer/float range filter support - Backend: add datetime range filter using '|' separator (ISO dates contain ':') - Backend: restore integer/float range filter support (lost during attribution rewrite) - Frontend: replace single DateTimePicker with From/To range pickers for datetime fields * Use local timezone in filter picker; add range filter tests The DateTimeRangeFilterDropdown was initializing pickers with dayjs.utc(), causing them to display and accept UTC times directly. The entry form shows local time and converts to UTC, so the filter was inconsistent: entering the same displayed time would produce a filter value 2h offset from what was stored (in UTC+2), making exact boundaries fail unexpectedly. Fix: initialize picker values with dayjs() (local mode) so the filter picker behaves the same as the entry form — shows local times and converts to UTC. Tests: replace 3 separate entity-specific datetime tests with one parametrized test covering spool/filament/vendor, and add range filter cases (start|, |end, start|end). * Replace all entity-specific tests with parametrized tests Replace 16 separate spool/filament/vendor tests for text, boolean, single-choice, multi-choice, and empty-filter with 5 parametrized tests that run against all three entity types. Each test is now defined once and executed 3 times, eliminating copy-paste and ensuring consistent coverage across all entity types. Parametrize invalid-filter-value 400 tests across all entity types The non-numeric-value error uses "Invalid integer/float range filter value" while the missing-colon error uses "Invalid range filter value". Both contain "range filter value", so use that as the assertion substring. * Align custom filter dropdowns with Ant Design choice filter style All three custom filter dropdowns (text, number range, datetime range) now match the built-in choice filter footer: a border-top separator, Reset as a link button on the left (disabled when no filter is active), and OK as a primary button on the right. * fix(fields): correct custom-field filter edge cases across databases - Float exact-match filters now compare numerically (cast to Float) instead of by JSON string, so float fields stored as integer JSON (e.g. "2") or non-canonical decimals (e.g. "2.50") match an equivalent filter. - integer_range/float_range filter and sort no longer return HTTP 500 on CockroachDB: the PostgreSQL '->>' JSON extraction is now also registered for the cockroachdb dialect, which previously fell back to the unsupported JSON_EXTRACT function. - Text exact-match and single-choice equality use ensure_ascii=False so non-ASCII values match how the frontend's JSON.stringify stores them. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * test(fields): expand custom-field filter/sort coverage Add tests covering filter composition (multiple extra fields, extra + built-in, filter one field / sort another), pagination total-count, unknown-field handling, text case-insensitivity/substring/non-ASCII, numeric multi-value OR, boolean empty semantics and token rejection, multi-choice substring collisions, empty filters on numeric fields, null-bounded ranges, and additional invalid-value 400 paths. Verified against sqlite, postgres, mariadb and cockroachdb. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * style(client): format column.tsx with prettier The custom-field filter dropdowns' Reset buttons were written as one-line onClick handlers that prettier wraps across multiple lines. Apply prettier so the CI style check passes. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * fix(fields): decode JSON scalars in-DB for text/choice/datetime custom-field filters Match text, single-choice and datetime custom-field filters against the database-decoded JSON scalar (new _JsonScalarText cross-dialect helper: #>> '{}' on postgres/cockroachdb, JSON_UNQUOTE(JSON_EXTRACT) on mysql, json_extract on sqlite) instead of reconstructing the exact JSON serialization the client wrote. This decouples matching from json.dumps/JSON.stringify encoding quirks (non-ASCII escaping, surrounding quotes). Also escape LIKE wildcards so '%' and '_' in text and multi-choice queries match literally rather than acting as wildcards. Verified against sqlite, postgres, mariadb and cockroachdb. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> * test(fields): cover LIKE-wildcard escaping in custom-field filters Add parametrized tests (spool/filament/vendor) asserting that '%' and '_' are matched literally in text and multi-choice filters, that a literal '/' (the internal LIKE escape char) matches literally, and that single-choice equality handles a '%' value exactly. Co-authored-by: Dieter Blomme <dieterblomme@gmail.com> --------- Co-authored-by: Donkie <daniel.cf.hultgren@gmail.com> Co-authored-by: akira69 <akira69@gmail.com> * chore(ui): rename Hide Columns button label to Columns --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Joshua Piccari <joshua.piccari@gmail.com> Co-authored-by: Veli-Matti Leppänen <vellu@velhot.net> Co-authored-by: Donkie <daniel.cf.hultgren@gmail.com> Co-authored-by: chruoss <chrigi.ruoss@gmail.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Marco Thiel <mfranke87@icloud.com> Co-authored-by: Weblate (bot) <noreply@weblate.org> Co-authored-by: srbjessen <srbjessen@gmail.com> Co-authored-by: Miloslav Kos <kos.m@post.cz> Co-authored-by: 김태남 <imedia0@gmail.com> Co-authored-by: Kayz C <kayzed.x@gmail.com> Co-authored-by: jernejp21 <jernejp21@tuta.com> Co-authored-by: Pavol Vrba <pvrba2000@gmail.com> Co-authored-by: Tamas Veres <tamas.veres@gmail.com> Co-authored-by: JonasBentin <jonasbentin19+github@gmail.com> Co-authored-by: Jurijs Kiricenko <jurijskiricenko@gmail.com> Co-authored-by: Donkie <2332094+Donkie@users.noreply.github.com> Co-authored-by: Dieter Blomme <dieterblomme@gmail.com>
1 parent 7aaaa01 commit 147bab5

76 files changed

Lines changed: 8039 additions & 2689 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,59 @@ jobs:
249249
up-flags: "--abort-on-container-exit"
250250
down-flags: "--volumes"
251251
#
252+
# Perform frontend (browser) integration tests against the real image
253+
#
254+
test-frontend:
255+
needs: [build-amd64]
256+
runs-on: ubuntu-latest
257+
steps:
258+
- name: Checkout
259+
uses: actions/checkout@v6
260+
261+
- name: Download built image
262+
uses: actions/download-artifact@v4.1.7
263+
with:
264+
name: spoolman-image
265+
path: /tmp/spoolman-image
266+
267+
- name: Load built image
268+
run: docker load --input /tmp/spoolman-image/spoolman.tar
269+
270+
- name: Install node
271+
uses: actions/setup-node@v4.0.3
272+
with:
273+
node-version: '20'
274+
275+
- name: Install Playwright
276+
run: |
277+
cd tests_frontend
278+
npm ci
279+
npx playwright install --with-deps chromium
280+
281+
- name: Start Spoolman
282+
run: docker compose -f tests_frontend/docker-compose.yml up -d --wait
283+
284+
- name: Run frontend integration tests
285+
run: |
286+
cd tests_frontend
287+
npx playwright test
288+
289+
- name: Dump Spoolman logs on failure
290+
if: failure()
291+
run: docker compose -f tests_frontend/docker-compose.yml logs spoolman
292+
293+
- name: Upload Playwright report
294+
if: ${{ !cancelled() }}
295+
uses: actions/upload-artifact@v4.4.0
296+
with:
297+
name: playwright-report
298+
path: tests_frontend/playwright-report
299+
retention-days: 7
300+
301+
- name: Stop Spoolman
302+
if: always()
303+
run: docker compose -f tests_frontend/docker-compose.yml down -v
304+
#
252305
# Build arm64 image
253306
# Don't run this for pull requests
254307
# Only push to buildcache and let release job push to registry
@@ -336,7 +389,7 @@ jobs:
336389
#
337390
publish-images:
338391
if: ${{ github.event_name != 'pull_request' }}
339-
needs: [test, build-amd64, build-arm64, build-armv7, style, build-client]
392+
needs: [test, test-frontend, build-amd64, build-arm64, build-armv7, style, build-client]
340393
runs-on: ubuntu-latest
341394
permissions:
342395
packages: write
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
name: Guard Translations
2+
3+
# Translations are managed exclusively through Weblate (https://hosted.weblate.org/projects/spoolman/).
4+
# Only the "en" source locale is edited by hand in this repo; every other language is written back
5+
# by the Weblate bot. This workflow fails any PR (except Weblate's own) that MODIFIES or DELETES an
6+
# existing non-English translation file, so manual edits don't get overwritten and lost on the next
7+
# Weblate sync.
8+
#
9+
# ADDING a brand-new language file is allowed: contributors bootstrapping a new language must add the
10+
# code entry in client/src/i18n.ts, and they may include the initial client/public/locales/<lang>/
11+
# file in the same PR. Only edits to files that ALREADY exist on the base branch are blocked.
12+
13+
on:
14+
pull_request:
15+
types:
16+
- opened
17+
- synchronize
18+
- reopened
19+
20+
# NOTE: no `paths:` filter on purpose. If this check is marked "Required" in branch
21+
# protection, a paths filter would leave PRs that don't touch locales stuck "pending"
22+
# forever (the check would never post a status). Instead we always run and pass fast
23+
# when nothing offending changed.
24+
25+
permissions:
26+
contents: read
27+
28+
jobs:
29+
guard-translations:
30+
runs-on: ubuntu-latest
31+
# Skip the guard for Weblate's own translation-sync PRs.
32+
if: github.event.pull_request.user.login != 'weblate'
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@v4
36+
with:
37+
fetch-depth: 0
38+
39+
- name: Check for manual non-English translation changes
40+
env:
41+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
42+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
43+
run: |
44+
set -euo pipefail
45+
46+
# Files changed in this PR, keyed by change type. --no-renames turns any rename into a
47+
# delete + add pair, so a "renamed" (i.e. rewritten) existing file still trips the delete
48+
# rule below and can't be smuggled through as a fresh add.
49+
#
50+
# --diff-filter=MD keeps only Modified and Deleted paths: edits to files that ALREADY
51+
# exist on the base branch. Added (A) files are intentionally excluded, so a contributor
52+
# may add a brand-new client/public/locales/<lang>/ file for a new language.
53+
offending="$(git diff --no-renames --diff-filter=MD --name-only "$BASE_SHA" "$HEAD_SHA" \
54+
| grep -E '^client/public/locales/[^/]+/' \
55+
| grep -vE '^client/public/locales/en/' \
56+
|| true)"
57+
58+
if [ -n "$offending" ]; then
59+
echo "::error title=Manual translation edits are not allowed::This PR edits existing non-English translation files. See the job summary for details."
60+
61+
{
62+
echo "## ❌ Manual translation edits detected"
63+
echo ""
64+
echo "This PR modifies or deletes one or more **existing non-English** translation files:"
65+
echo ""
66+
echo '```'
67+
echo "$offending"
68+
echo '```'
69+
echo ""
70+
echo "### Why this is blocked"
71+
echo ""
72+
echo "Every language except the English source (\`client/public/locales/en/\`) is managed"
73+
echo "**exclusively through Weblate**. Any manual edit to an existing non-English file here"
74+
echo "will be **silently overwritten and lost** the next time Weblate syncs — so we don't"
75+
echo "accept them."
76+
echo ""
77+
echo "### What to do instead"
78+
echo ""
79+
echo "- **To fix or improve an existing translation:** contribute it through Weblate at"
80+
echo " <https://hosted.weblate.org/projects/spoolman/>. It's free, requires no coding,"
81+
echo " and your change flows back into the repo automatically."
82+
echo "- **To add or change a user-facing string:** edit **only** the English source files under"
83+
echo " \`client/public/locales/en/\`. Weblate will pick up the new keys and expose them to"
84+
echo " translators. Revert your changes to every other language and push again."
85+
echo "- **To add a brand-new language:** that IS allowed here. Add the language entry to"
86+
echo " \`client/src/i18n.ts\` and, if you like, an initial \`client/public/locales/<lang>/\`"
87+
echo " file — new files are fine, only edits to existing ones are blocked."
88+
echo ""
89+
echo "> If you believe an existing file genuinely needs a manual change (e.g. a structural"
90+
echo "> fix), please explain it in a PR comment so a maintainer can review."
91+
} >> "$GITHUB_STEP_SUMMARY"
92+
93+
echo ""
94+
echo "Offending files:"
95+
echo "$offending"
96+
exit 1
97+
fi
98+
99+
echo "No manual edits to existing non-English translation files detected. ✅"
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
name: Auto-merge Weblate PRs
2+
3+
# Translations are managed exclusively through Weblate (https://hosted.weblate.org/projects/spoolman/),
4+
# which opens a pull request for every sync back to this repo (see guard-translations.yml). Those PRs
5+
# are machine-generated, always scoped to client/public/locales/, and gated by the full CI suite just
6+
# like any other PR. Rather than hand-merging each one, this workflow turns on GitHub's native
7+
# auto-merge for Weblate-authored PRs so they merge themselves once all required status checks pass.
8+
#
9+
# Why auto-merge instead of giving Weblate direct push access to master:
10+
# - Every sync still runs through style/tests/build before it can land — nothing merges unvalidated.
11+
# - No actor gains unrestricted, un-CI'd direct write to master. A compromised Weblate token can only
12+
# open a PR; auto-merge fires solely on green CI for Weblate-authored PRs.
13+
# - GitHub write/bypass can't be scoped to locale files, so direct write would grant all-file access.
14+
15+
on:
16+
pull_request_target:
17+
types:
18+
- opened
19+
- reopened
20+
- synchronize
21+
22+
# contents+pull-requests write let the built-in GITHUB_TOKEN enable auto-merge and perform the squash
23+
# merge. The token is not a ruleset bypass actor and doesn't need to be: with 0 required approvals it
24+
# satisfies the branch requirements once the required checks go green, exactly like a manual merge.
25+
permissions:
26+
contents: write
27+
pull-requests: write
28+
29+
jobs:
30+
auto-merge:
31+
runs-on: ubuntu-latest
32+
# Only ever act on Weblate's own translation-sync PRs. Human PRs are never touched.
33+
# pull_request_target runs in the base-repo context (so GITHUB_TOKEN has write access even for
34+
# branch PRs), and this job is safe under it because it never checks out or executes PR code —
35+
# it only calls the GitHub API to enable auto-merge.
36+
if: github.event.pull_request.user.login == 'weblate'
37+
steps:
38+
# Defence in depth: don't trust the author alone, trust the diff. Even a compromised Weblate
39+
# token can only ever get auto-merged if the PR is confined to locale JSON files. Anything
40+
# else (a code file, a workflow, a non-.json file, a file outside client/public/locales/)
41+
# fails this step, auto-merge is never enabled, and the PR waits for a human. The file list is
42+
# read from the API — this step never checks out or runs PR code, so pull_request_target is safe.
43+
- name: Verify PR only changes locale JSON files
44+
env:
45+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
REPO: ${{ github.repository }}
47+
PR_NUMBER: ${{ github.event.pull_request.number }}
48+
run: |
49+
set -euo pipefail
50+
51+
files="$(gh api --paginate "repos/${REPO}/pulls/${PR_NUMBER}/files" --jq '.[].filename')"
52+
53+
if [ -z "$files" ]; then
54+
echo "::error title=Empty diff::Weblate PR reports no changed files; refusing to auto-merge."
55+
exit 1
56+
fi
57+
58+
# Every changed path must be a .json file under client/public/locales/<lang>/.
59+
offending="$(printf '%s\n' "$files" | grep -vE '^client/public/locales/[^/]+/.+\.json$' || true)"
60+
61+
if [ -n "$offending" ]; then
62+
echo "::error title=Out-of-scope Weblate PR::PR touches files outside client/public/locales/**/*.json; not auto-merging."
63+
{
64+
echo "## ❌ Auto-merge blocked: out-of-scope changes"
65+
echo ""
66+
echo "This Weblate PR changes files that are **not** locale JSON under \`client/public/locales/\`:"
67+
echo ""
68+
echo '```'
69+
echo "$offending"
70+
echo '```'
71+
echo ""
72+
echo "Weblate sync PRs are only auto-merged when every changed file is a translation JSON"
73+
echo "file. This PR isn't, so it has been left for a maintainer to review and merge by hand."
74+
} >> "$GITHUB_STEP_SUMMARY"
75+
exit 1
76+
fi
77+
78+
echo "All $(printf '%s\n' "$files" | wc -l) changed file(s) are locale JSON. Safe to auto-merge."
79+
80+
- name: Enable auto-merge
81+
env:
82+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83+
PR_URL: ${{ github.event.pull_request.html_url }}
84+
# Squash is the only merge method the repo and the master ruleset allow.
85+
run: gh pr merge --auto --squash "$PR_URL"

.gitignore

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ share/python-wheels/
2626
.installed.cfg
2727
*.egg
2828
MANIFEST
29-
requirements.txt
29+
# requirements.txt is committed (generated from uv on release) for bare-metal /
30+
# Moonraker update_manager consumers. See spoolman/bump.py.
3031

3132
# PyInstaller
3233
# Usually these files are written by a python script from a template
@@ -204,4 +205,5 @@ $RECYCLE.BIN/
204205

205206
data/
206207
.pdm-python
207-
.history
208+
.history
209+
.claude

Dockerfile

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
2424
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
2525
uv sync --locked --no-install-project
2626

27-
# Copy and install app
28-
COPY --chown=app:app migrations /home/app/spoolman/migrations
29-
COPY --chown=app:app spoolman /home/app/spoolman/spoolman
30-
COPY --chown=app:app alembic.ini README.md uv.lock pyproject.toml /home/app/spoolman/
27+
# Copy and install app. No --chown here: the "app" user only exists in the
28+
# runner stage, and Podman (unlike Docker/BuildKit) refuses to resolve it. Final
29+
# ownership is set when these files are copied into the runner stage below.
30+
COPY migrations /home/app/spoolman/migrations
31+
COPY spoolman /home/app/spoolman/spoolman
32+
COPY alembic.ini README.md uv.lock pyproject.toml /home/app/spoolman/
3133
RUN --mount=type=cache,target=/root/.cache/uv \
3234
uv sync --locked
3335

@@ -44,9 +46,7 @@ RUN apt-get update && apt-get install -y \
4446
&& rm -rf /var/lib/apt/lists/*
4547

4648
# Add local user so we don't run as root
47-
RUN groupmod -g 1000 users \
48-
&& useradd -u 1000 -U app \
49-
&& usermod -G users app \
49+
RUN useradd -u 1000 -U app \
5050
&& mkdir -p /home/app/.local/share/spoolman \
5151
&& chown -R app:app /home/app/.local/share/spoolman
5252

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ Spoolman is a self-hosted web service designed to help you efficiently manage yo
3232
* [OctoPrint](https://github.com/mdziekon/octoprint-spoolman)
3333
* [OctoEverywhere](https://octoeverywhere.com/spoolman?source=github_spoolman)
3434
* [Home Assistant](https://github.com/Disane87/spoolman-homeassistant)
35+
* [MCP Server](https://github.com/Disane87/spoolman-mcp) - Manage your filament inventory through AI assistants like Claude using the Model Context Protocol
3536

3637
**Web client preview:**
3738
![image](https://github.com/Donkie/Spoolman/assets/2332094/33928d5e-440f-4445-aca9-456c4370ad0d)

client/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
.env.development.local
1919
.env.test.local
2020
.env.production.local
21+
# Generated at build time (CI and tests_frontend/run.py) — never committed
22+
.env.production
2123

2224
npm-debug.log*
2325
yarn-debug.log*

0 commit comments

Comments
 (0)