114 Commits
Author SHA1 Message Date
Donkie 8656e04055 fix(deps): patch sponsors image decoder vulnerabilities
Update sharp from 0.35.3 to 0.35.5 and its matching prebuilt binaries, addressing GHSA-wq5f-xc86-pv6w and GHSA-rgj7-g3m4-5g8c (Dependabot alerts #273 and #256).

Verified npm ci reports zero vulnerabilities and sharp renders the existing sponsors SVG to an 800x570 PNG. No application dependencies or workflow configuration changed.
2026-10-11 15:08:01 +02:00
Donkie bb0bcc1b5f Run CI on bot PRs and merge Weblate PRs weekly
Dependabot and Weblate branches are excluded from push runs, and the
root jobs skipped same-repo pull_request runs, so their PRs got no
checks and stayed blocked. Let those PRs run the pull_request suite.

Weblate PRs are now merged by a Monday schedule instead of as soon as
each sync goes green, to cut down the merge commits on master.
2026-10-11 10:24:34 +02:00
Donkie 629b8228d8 chore: update sponsors image 2026-10-10 09:15:57 +00:00
Donkie 022285d64d Add Argos visual snapshots of client_v2 pages to CI
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.
2026-10-10 09:35:51 +02:00
Donkie 6b1e01ecd8 chore: update sponsors image 2026-10-04 09:11:19 +00:00
Donkie 7c4dcc7da7 chore: update sponsors image 2026-10-01 09:38:56 +00:00
Donkie dcb4618399 chore: update sponsors image 2026-09-30 09:11:16 +00:00
Donkie 7a1114f8fc chore: update sponsors image 2026-09-29 09:20:58 +00:00
Donkie aae2b4df56 ci: skip the duplicate pull_request run for same-repo branches
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.
2026-09-27 08:32:44 +02:00
Donkie e1ad9feb30 ci: build every branch and publish it to GHCR under the branch name
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.
2026-09-27 08:32:44 +02:00
Donkie a353983f85 Speed up armv7 image builds on layer-cache misses
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.
2026-09-25 16:04:21 +02:00
Donkie f55188e346 chore: update sponsors image 2026-09-10 07:42:55 +00:00
Donkie 348499f469 Tag issues opened by sponsors
The Supporter tier promises that issues a sponsor opens get an exclusive tag
so they surface first.

The sponsorship lookup is viewer-relative, so it runs on the PAT rather than
GITHUB_TOKEN, whose viewer is the Actions bot that nobody sponsors. Reading
privacyLevel needs that token's read:user scope.

Private sponsors are skipped. The label is public and applying it would
disclose a sponsorship they chose to keep hidden.
2026-09-02 17:59:16 +02:00
Donkie d34fb88b31 chore: update sponsors image 2026-09-02 15:48:39 +00:00
Donkie 798d8b1ded chore: update sponsors image 2026-09-01 20:32:50 +00:00
Donkie f8ffaa0837 Push the sponsors image with the PAT 2026-09-01 22:32:29 +02:00
Donkie 789b7fd88f Render a sponsors board in the README
A nightly workflow runs sponsorkit against the GitHub Sponsors API and commits
.github/media/sponsors.svg when it changes, so the board stays current without
any manual work.

Ko-fi has no API to query, so tips that arrive there are kept in a small
hand-maintained list in the config and fed in through a custom provider. A tip
counts toward a tier for 30 days and then drops to Past Sponsors on its own,
which is how sponsorkit treats real Ko-fi tips and how a one-time GitHub
sponsorship ages out. marko-p's $10 tip is the first entry.
2026-09-01 21:21:20 +02:00
Donkie 205de5f0ce Update FUNDING.yml 2026-08-23 17:19:51 +02:00
Donkie a4c113ba8e ci: merge Weblate PRs instead of squashing them
Squash merging replaces Weblate's commits with a new one whose hash, and
for multi-commit PRs whose patch-id, matches nothing Weblate knows. Its
clone then rebases those commits onto a master that already contains the
same strings and hits a content conflict, after which Weblate stops
syncing until it is reset by hand. PR #1004 squashed two Russian commits
and broke ru/common.json exactly this way.

A merge commit keeps each Weblate commit reachable at its original hash,
so the rebase drops them as already-upstream. Nothing required squash
here: allow_merge_commit is on, the master ruleset has no linear-history
rule, and human PRs already land as merge commits.
2026-08-05 18:36:16 +02:00
Donkie e175708b3e fix(ci): diff PRs from the merge base in guard-translations
The guard compared the base branch tip against the PR head directly, so
every commit that landed on master after a PR forked counted as one of
that PR every time it re-ran. Merging a Weblate sync therefore failed
unrelated PRs with client_v2/locales/sv/common.json in the offending
list, and the only way out was rebasing a PR that had never touched a
translation file.

Diffing BASE...HEAD resolves the merge base first, which is what "the
changes this PR makes" means, and it stays right however far master
moves on. Verified against PR 1001: the old form flags sv, the new form
passes, and a real hand-edit of sv is still caught.
2026-08-01 22:11:42 +02:00
DonkieandClaude Opus 4.8 4e1e3c50c9 Point the translation workflows at client_v2, close the legacy client
Weblate translates client_v2 from now on. The two translation workflows
still only knew about client/public/locales/, which meant a Weblate sync
for the new client would be rejected by the auto-merge scope check and
hand-edits to client_v2 translations would sail through unguarded.

- weblate-automerge: accept locale JSON under either client_v2/locales/
  or client/public/locales/. The legacy tree stays on the allowlist only
  so a sync opened before its component was locked isn't stranded; the
  Weblate-side lock, not this allowlist, is what ends legacy translation.

- guard-translations: guard both trees, with different rules. For
  client_v2, modifying or deleting an existing non-English file is
  blocked while adding a brand-new language stays allowed (bootstrapping
  one also needs entries in project.inlang/settings.json and
  src/lib/i18n/languages.ts, which only a human PR can add — the failure
  message now says so). For the legacy client every non-English change
  is blocked, adds included, and the message redirects translators to
  the client users actually get.

- README: 18 -> 26 languages, and say outright that translations come
  through Weblate rather than pull requests.

Verified: the guard script passes 9 synthetic PR cases (en edits and new
v2 languages allowed; v2 edits/deletes, legacy edits and legacy new
languages blocked; both sections render when a PR trips each rule), and
the auto-merge regex accepts both locale trees while still rejecting
code, workflows, non-JSON and path-prefix smuggling.

Also confirmed the format risk is a non-issue: if Weblate's i18next v4
writer emits nested JSON instead of the repo's flat dotted keys,
paraglide still compiles it — nested German round-tripped intact, and a
nested _one/_other pair compiled to a correct CLDR plural with other
locales still falling back to English.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-31 10:35:01 +02:00
Donkie ff7170f0f7 Build on Node 24, which is not end-of-life
Every workflow pinned node-version '20'. Node 20 reached end-of-life on
2026-04-30, so for three months CI has been building the artifacts that
go into released images on a runtime that no longer gets security fixes.
Vite 8 also floors at ^20.19, which setup-node's '20' happens to satisfy
today only because it resolves to the latest 20.x -- a floor we were
clearing by accident is not one worth keeping.

24 rather than 22: it is Active LTS until 2026-10-20 and supported
through 2028-04-30, so this pin outlives the 22 line by a year. Node 26
does not ship until October.

The legacy client declared engines "20.x", which would have made every
npm ci in that package print EBADENGINE against the new runner. It is
widened to the floor of the Vite 7 it already runs on, so it now
describes what the package actually supports instead of naming a single
line.

Verified locally on Node 24.13.0: lefthook ci green, both clients
install clean and build, client_v2's unit tests and the
tests_frontend_v2 Playwright suite pass. tests_frontend needs the built
image so it is left to CI; its only dependency declares node >=18.

The setup-node pin itself is untouched and still on v4.0.3, whose action
runtime is the other deprecated Node 20 in the logs -- GitHub already
force-runs it on 24. That is an action upgrade, and a separate change.
2026-07-31 09:00:16 +02:00
Donkie b00c1b875d Add dashboard and label-designer screenshots to the README
The library screenshot shows one of the client's four pages. The two
that are hardest to guess at from a feature list -- spools as draggable
cards grouped by location, and labels built from arbitrary spool fields
-- now sit under it in a two-column table, so they cost one screen's
height between them rather than one each.

They are captured at a narrower viewport than the library shot rather
than scaled down from it: at half width a 1440px page reduces to
unreadable, whereas 900px reflows the dashboard to two columns and the
designer to a tighter grid, so the text survives the downscale. Both are
cropped to a 3:2 aspect and have the nav bar removed -- it is already in
the shot above -- so the pair sits level.

Fixed along the way, since it would otherwise have been immortalised in
the dashboard screenshot: the card separators lost a space where they
met an {#if} boundary, which Svelte trims, rendering "Bambu Lab -Bambu
Green" and "1 kg- Last used 2 days ago". The optional parts are now
built as one expression, which has no block edge to trim.

demo_seed gains a note on why label designs aren't seeded: they're a
blob of a client-side schema, so writing one from Python would duplicate
it. The designer's own "Create your first label" is the honest route.
2026-07-31 08:37:47 +02:00
Donkie 0c03f8b404 Stop promoting a spool's comment to its library row title
Grouped by filament, a row had nothing of its own to put in the bold
title slot -- #id, fill, weight and location all have columns -- so it
borrowed the comment when one was set, and dropped the usage status to a
quiet second line behind it.

That assumed comments are short labels. The field is a free-form
textarea: people keep order URLs, multi-line notes and pasted receipts
there. Whatever is in it got the widest, loudest slot in the row, pushed
out "used N ago" -- the one thing every row can say -- and, since .name
is nowrap, ran any newlines together into one line before ellipsing
mid-word.

Rows now always show the usage status. The comment is unabridged in the
inspector, which is the only place it was ever readable in full.

The screenshot is regenerated against the new behaviour, and demo_seed's
comments go back to natural length -- they were written short only to
survive the row.
2026-07-30 22:09:06 +02:00
Donkie 67022e7c6d Regenerate the README screenshot from catalog-imported filaments
The old one was seeded with stress_seed.py, which builds a filament name
from one random pool and its color hex from another, so the picture
contradicted itself: "PETG Copper Bronze" had a blue swatch and "PVA
Sparkle Silver" a tan one. That is the first thing a visitor sees.

scripts/demo_seed.py imports every filament from SpoolmanDB instead,
issuing the same request the client's Add-spools flow does, so names,
colors, densities and temperatures come from one source and agree with
each other. Twenty filaments across eight real vendors are pinned by
external id, chosen for material spread and a varied swatch column; if
one leaves the catalog the script fails with the list rather than
quietly drifting.

The spools on top of them are still made up, because that half really is
per-user data -- partial weights, locations, lot numbers, prices,
first/last-used dates, a few archived and a few still sealed -- plus the
handful of extra fields a real user defines, so the inspector shows
content where it used to show its empty state.

Two details are shaped by how the list renders rather than by realism:
spools consume a whole number of grams, since a decimal wraps onto a
second line in the narrow weight column, and comments stay under ~20
characters, since the list shows a comment in place of "used N ago" and
truncated the longer ones mid-word.
2026-07-30 22:03:19 +02:00
Donkie 4ab99768ae Abort last workflow run for PRs when new push 2026-07-30 21:48:55 +02:00
Donkie 9bdf4d1a5e Install client_v2 deps in the style job
lefthook's ci hook grew a client_v2 group, but the style job only ran
npm ci in client/, so all three of its jobs died at exit 127 on missing
./node_modules/.bin binaries -- eslint, prettier and svelte-kit. The
hook is invoked from the repo root and the paths are hardcoded (for
Windows' sake), so nothing resolved them from client/'s tree. That has
been failing every run since the group was added, on a branch where
client_v2 is the client that ships, so its lint, format and type checks
were the ones not running.

client_v2/docs is gitignored scratch space that prettier still walked,
which fails the hook locally on files CI never sees; the ignore files
now agree.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:29:07 +02:00
Donkie 59672ecb30 Merge master into feature/v2-client
Brings the branch up to 0.25.0 and picks up cosign image signing, the
armv7/arm64 smoke tests, the armv7 greenlet/libstdc++ fix, the
PUID/PGID and Kubernetes SPOOLMAN_PORT entrypoint guards, and the
externaldb unwritable-path fix.

The only conflict was publish-images' needs list, where both sides had
added a job: test-frontend-v2 here, test-multiarch-smoke on master.
Resolved as the union, so nothing publishes until every test job has
passed. The rest of the graph is unchanged, and the new smoke tests
inherit client_v2 coverage for free -- build-arm64 and build-armv7
already download the client_v2 artifact, so booting those images to
/health now also proves the default client is present in them.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:02:14 +02:00
Donkie c781869d09 Refresh the README screenshot and note the legacy-client rollback
The screenshot was the 2023 React client -- a different application from
the one a new visitor now installs. The replacement is client_v2's
library with a spool's details open beside it, which is where anyone
using Spoolman spends their time, and it lives in the repo rather than
on user-content URLs so it can be regenerated with the client.

The feature list gains search/group/filter, which client_v2 makes a
first-class part of the library rather than per-column table filters.

Installation now mentions SPOOLMAN_LEGACY_CLIENT. Someone whose setup
the new client misbehaves on will look here before .env.example, and
the useful thing to know is that both clients ship in every release and
share the API, so falling back costs nothing.

Claude-Session: https://claude.ai/code/session_01UB5KocsTBm9GL5x3Uuup5F
2026-07-30 21:00:36 +02:00
Donkie 17f8b9fee1 client_v2: add Playwright frontend integration tests
Mirrors tests_frontend, but drives the Svelte client instead of the legacy
React one: same production image, same PostgreSQL-backed compose stack, same
"only the first navigation uses a URL" discipline.

- smoke.spec.ts clicks through every top-bar tab, asserting each route's title
  and the app shell, with no browser console errors; a second test proves the
  library reached the API rather than falling into its offline state.
- crud.spec.ts walks the first-use happy path. client_v2 folds the legacy
  client's three create forms into one "Add spools" modal, so a manufacturer,
  a filament and a spool are created by a single submit, then verified back in
  the library list and on the locations page.

Two client_v2-specific helpers earn their keep: the top bar renders its nav
twice (desktop + mobile, one hidden by CSS) so nav locators need narrowing to
the visible one, and NumberInput's accessible name picks up its stepper and
help buttons, so numeric fields are found via their form item instead of
getByLabel.

The suite publishes on host port 8001 so it can run alongside the legacy one.
CI gains a test-frontend-v2 job cloned from test-frontend, gating
publish-images alongside it.

Also fixes tests_frontend/run.py: it only built client/dist, but the Dockerfile
copies client_v2/build unconditionally, so the image could not be built from a
clean checkout.
2026-07-29 19:08:12 +02:00
Donkie eced411a51 ci: bump issue-manager to 0.8.1 and fix waiting delay
Bump tiangolo/issue-manager 0.5.1 -> 0.8.1. Correct the waiting label
delay from 60400s (~16.8h) to 604800s (7 days) to match its message.
2026-07-24 10:22:08 +02:00
Donkie a0987e063e Add smoke tests in CI for armv7/arm64 targets 2026-07-21 22:16:00 +02:00
Aleksei Sviridkin fc606ec8fc ci: sign the released images with cosign (#963)
Published images carry no signature, so there is no way for a user to verify
that the image they pull was built by this workflow from this repository.

Sign them keyless, using the Github OIDC token, so verification needs no key
material on either side. The manifest is signed by digest rather than by tag:
every tag produced for a release points at the same digest, and the same
digest is valid in both registries the build pushes to, so signing each tag
would only repeat the same signature.

Verify with:

    cosign verify ghcr.io/donkie/spoolman:<version> \
      --certificate-identity-regexp '^https://github\.com/Donkie/Spoolman/\.github/workflows/ci\.yml@refs/' \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com

Assisted-By: Claude <noreply@anthropic.com>

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-07-21 22:07:57 +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 2c638da22a 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
2026-07-07 10:11:28 +02:00
Donkie 48ad01b09b 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
2026-07-07 09:35:12 +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 c033a3ffe6 Fix trigger release ci 2026-01-23 21:09:39 +01:00
Donkie 1974d4b8d2 Version bump updates 2026-01-23 21:07:45 +01:00
Donkie 8a29ce063a Update ci.yml 2026-01-18 17:30:55 +01:00
Donkie fa6343c368 Fix artifact job 2026-01-18 17:11:54 +01:00
Donkie 1cd8c7a962 Added ci job to produce artifact package for all builds 2026-01-18 17:05:00 +01:00
Donkie b361aee956 CI fixes 2026-01-18 16:55:31 +01:00
Donkie 8af6013331 Add issue manager workflow for automated issue handling 2025-06-16 19:23:26 +02:00
Donkie 7b7d73224d Set github ci permissions 2025-06-08 12:23:37 +02:00
Donkie 11a74e2197 Use env for ci ghcr_image since vars is not accessible in PRs 2025-04-12 19:42:53 +02:00
Donkie ec5173067e Add push to dockerhub 2025-03-19 19:05:17 +01:00
Donkie 590b0df191 Fixed release zip missing dot-files 2024-09-23 19:26:29 +02:00
Donkie 909d8c38bd Test stage now only downloads the relevant images 2024-09-08 22:17:46 +02:00
Donkie 13a038cb3e Updated all action versions 2024-09-08 13:29:27 +02:00