Commit Graph
172 Commits
Author SHA1 Message Date
Donkie 61e2c0c27c client_v2: keep the frontend suite in step with the filter menu
Turning filter values into checkboxes that leave the menu open broke the
`addFilter` helper in two ways, and with it the grouping test that uses it
to pin the list down to its own spools.

An explicit `role="menuitemcheckbox"` replaces a button's implicit role, so
the helper's `getByRole("button")` no longer matched a value row at all;
and having picked one, the helper handed back a toolbar with the menu still
covering it, which the next call could not open through. It now asks for
the role the row actually has, and closes the menu itself, restoring the
postcondition every caller was already relying on.

The row's accessible name needed a real space rather than the margin that
had been standing in for one: without it a filament read as one
run-together word, "Galaxy BlackPLA".
2026-08-18 17:29:07 +02:00
Donkie 5624a0a605 client_v2: pick several filter values without reopening the menu
Narrowing a library to three filaments meant opening the filter menu,
stepping into Filament, picking one, and doing all of it again twice
more. The menu closed on every pick, and nothing in it said which values
were already on — the chips that say so sit in the toolbar, behind the
menu that covers them.

A value row now carries a checkbox and its state, the same way the "Show
archived" row already does, and picking one leaves the menu open. The two
go together: once the values are marked, the list reads as a multi-select,
and closing after a single pick is the surprising half. Escape and a click
outside still close it, and picking a filter property or a date range still
does too, since neither is a set.

Nothing has to be preserved by hand for this to work. Each toggle is a
query-only navigation that keeps the component mounted, and it already
asked to keep focus and not to scroll, so the search box stays focused and
the list stays where it was.

The Ctrl-modifier the issue suggested is deliberately not implemented: a
checkbox list that stays open needs no hint text to explain itself.

Closes #1090
Closes #1089
2026-08-17 21:00:17 +02:00
Donkie 6e289d76ca client_v2: show colors and material in the filament filter
The filament filter listed nothing but a label, which is the manufacturer
and the name. That is not enough to pick from: the colour is the thing
you actually recognise a filament by, and two filaments may legitimately
carry the same name in different materials, leaving the menu offering two
entries that read identically.

Each option now carries whatever it has beyond its label, so the menu
draws a swatch and the material where they exist and stays a plain list
of names where they don't (locations, lot numbers). The material sits
with the label rather than off at the row's right edge, since it is there
to qualify the name it follows. None of it costs a request: the option
list is built from whole filaments already fetched.

The search box narrows on the material too. It is on screen, so a query
that visibly matches a row has to keep it — and since terms match in any
order, "prusament petg" now finds one filament out of two same-named
ones.

Closes #1087
Closes #1088
2026-08-17 20:59:29 +02:00
Donkie 9423acbc9e client_v2: create a filament while changing what a spool holds
The change-filament dialog could only pick a filament that already existed,
in the catalog or in SpoolmanDB. But the only way to create one anywhere in
client_v2 is the add-spools flow, which always mints at least one spool — so
refilling a shelf slot with a roll nobody had catalogued meant adding a
throwaway spool and deleting it again, handing out exactly the new spool id
the feature exists to avoid (issue #1010).

The dialog now offers the same third path the add-spools flow does. Picking
it swaps the search for the new-filament form and applies as one action:
the filament (and its manufacturer) are created, then the spool is repointed
at it. The spool keeps its number, location, dates and usage history.

The form itself moves to NewFilamentCards, shared with AddSpoolModal so both
dialogs keep the per-record card layout from #1038, with its rules and its
mapping onto the API in lib/filament/draft.ts. The one difference is the
weight/spool weight/price trio: the add-spools flow writes those to the spool
it creates as well, and so keeps showing them in its spool block, while here
they belong to the filament alone.
2026-08-13 18:11:56 +02:00
Donkie aac4a26cde Merge master into client-v2-form-required-fields
Master reorganized the spool half of the add form into headline / fill /
where / money groups and added the roll-size presets. Kept that layout and
re-applied the required-field wiring on top of it: the data-field hooks, the
per-field touch handlers and the err() calls that decide when an error shows.

FIELD_ORDER follows the new reading order too, so Fill level now comes before
Spool Weight when the form picks which bad field to jump to.
2026-08-12 22:20:39 +02:00
Donkie 2d08061c45 client_v2: require the material when creating a filament
Leaving Material blank sent you to Density instead. Density is only prefilled
for a material Spoolman recognises, so a blank material surfaced as "Density
is required" — an error on the field you didn't fill in because of the field
you did miss, and inside the collapsed Advanced block at that.

Material is now required on this form, so the error lands on the field that
actually needs attention and Advanced stays shut. Typing a material we don't
have specs for still asks for a density afterwards, which is the right order
to be asked in.

Client-side only: the API still accepts a filament with no material, so
existing ones are unaffected.
2026-08-12 20:25:00 +02:00
Donkie aec9b276b2 client_v2: point at the first bad field instead of listing them all
The summary panel above the add button was more machinery than the problem
needed, and it spent five translation strings restating what the inline
field errors already say. Drop it.

Pressing Add still reveals every outstanding error and still opens the
collapsed Advanced block when the problem is hiding in there; it just takes
you to the first bad field and lets the error under it do the talking. The
issue count on the Advanced toggle and the "* required" legend go too.

The change now adds no new translation strings at all.
2026-08-12 20:12:36 +02:00
Donkie 0931b03a80 client_v2: say what is missing instead of disabling the add button
An incomplete add-spool form was answered with a dead Add button, leaving
"which field haven't I filled in?" to be found by eye. Worst case the answer
was invisible: density is required but only prefilled for known materials,
and it lives inside the collapsed Advanced block, so the form could look
complete and still refuse.

Add stays clickable. Pressing it on an incomplete form reveals every
outstanding error at once, lists them above the button as rows that jump to
their field, opens the section hiding one, and puts the caret in the first.
The list stays as a checklist and shrinks as fields are fixed. Per-field
errors now appear once you leave a field rather than immediately, so a
freshly opened form is quiet.

Make the asterisk mean one thing: every required field carries it and
nothing else does. Count was missing one. A "* required" legend says what it
means, and the controls carry aria-required/aria-invalid so it is not
conveyed by a glyph alone.

The extra-fields editor in settings follows the same convention: asterisks
on key, name and choices, and a save error now outlines and focuses the
field it came from.
2026-08-12 19:53:11 +02:00
Donkie 740b1ad808 Merge pull request #1070 from Donkie/client-v2-vendor-extra-fields
client_v2: edit the manufacturer's extra fields when the add flow creates one
2026-08-12 19:51:38 +02:00
Donkie 379354159a Merge pull request #1068 from Donkie/client-v2-searchable-menus
client_v2: make the long toolbar menus searchable
2026-08-11 22:48:02 +02:00
Donkie e9b06d91c7 Merge pull request #1067 from Donkie/worktree-issue-1059-transparency
client_v2: bring back translucent filament colors
2026-08-11 22:43:53 +02:00
Donkie fb723f0af8 client_v2: stop the alpha slider tiling colour under its border
A `background` shorthand resets background-origin to the padding box, so
the slider's gradient was sized to the padding box but painted out under
its 1px border — and backgrounds repeat, so the strip under the border
showed the far edge of the next tile: a hairline of full-opacity colour
down the transparent end. It surfaced wherever the border doesn't cover
that strip exactly, i.e. Firefox at fractional display scaling.

Anchor every layer to the border box, and keep the colour gradient from
repeating at all.
2026-08-11 22:35:15 +02:00
Donkie 84caec0f81 client_v2: drop the More details disclosure from the add-spool form
Lot number, usage dates and comment are back in the open. The lot number
moves next to Location, since it identifies the spool rather than pricing it.
2026-08-11 22:23:27 +02:00
Donkie 250aa7c308 client_v2: reorganize the add-spool form around what you know at a glance
The spool fields were one grid of interchangeable boxes, so the roll-size
shortcuts had nowhere to go but a second row under the weight field. Order
them by how easily they are answered instead: count and weight share the top
line, with the sizes beside the weight rather than under it; fill level and
location follow; spool weight and price drop to their own row as the numbers
that come off a product page; and lot number, usage dates and comment fold
into a 'More details' disclosure like the filament half above already does.
Net result is a shorter form than before the shortcuts existed.
2026-08-11 22:13:13 +02:00
Donkie 5deb6f8ca6 client_v2: quick-pick buttons for common roll sizes
The weight field in the add-spool modal now offers 250 g / 500 g / 750 g /
1 kg / 2 kg / 3 kg as one-click shortcuts, with the one matching the current
value marked. Closes #1051.
2026-08-10 20:44:39 +02:00
Donkie 948c95866d client_v2: bring back translucent filament colors
The legacy client could set a filament's alpha channel, and the API and
database have stored an 8-digit #RRGGBBAA code ever since the color_hex
alpha migration, but client_v2 had no way to edit it (#1059).

Each row of the colour editor gets an opacity slider and a percentage
field, both backed by the hex code's alpha byte, so a single- or
multi-colour filament can be marked translucent. Full opacity drops the
alpha channel again, keeping plain colours 6-digit for the API and for
ecosystem consumers, and the native RGB picker no longer wipes an alpha
that is already set.

Swatches layer a translucent colour over a checkerboard so it reads as
transparent rather than as a different, lighter colour.
2026-08-10 19:38:16 +02:00
Donkie 0cc77dccb3 client_v2: make the long toolbar menus searchable
The Library's sort and filter menus grow with the install. Every custom
extra field adds a sort field and a filter property, and a filter's
values are the whole library: every filament, every location, every lot
number. Past a point the only way through them was to scroll.

Each list now carries a search box once it is long enough to earn one,
so the group menu's five fixed entries stay as they were. Matching is a
plain substring test — these lists are on screen while you type, so a
fuzzy match that surfaced "Location" for "lot" would read as a bug —
widened only by folding diacritics and by letting whitespace-separated
terms match in any order, since a filament reads "<maker> <name>".

Escape clears the query, then closes the menu; Enter takes the first
entry still listed. The box takes focus as the menu opens, but only
where the pointer is fine: on a touch device that same focus would
raise the keyboard over the list it filters.

Closes #1045
2026-08-10 19:26:07 +02:00
Donkie 16feb404ba client_v2: edit the manufacturer's extra fields when the add flow creates one
The add-spool form can create a manufacturer, but offered no way to fill its
custom fields, unlike the filament and spool it creates alongside. Show them
in the manufacturer card, and send them with the POST that creates it.

The inputs only appear while the typed name is a new one: matching an
existing manufacturer links that record, and must never rewrite its values.

Fixes #1055
2026-08-10 19:14:34 +02:00
Donkie 7dc17f12da Merge pull request #1054 from Donkie/client-v2-date-filters
client_v2: filter the library by date
2026-08-09 21:56:27 +02:00
Donkie 2c84acfd24 Filter dates with the grammar the extra fields already use
The two commits before this one invented `<field>_after`, `<field>_before`
and `<field>_unset` for the built-in timestamps. That was a third way to
ask a question this API already answers: extra-field filters have taken
`<start>|<end>` for datetimes and `<min>:<max>` for numbers since v0.26.0,
with an empty value meaning "unset", the same as it means for `location`
or `lot_nr`.

Two grammars for one question is bad on its own, but the split was worse
than cosmetic. Someone who learned `extra.opened_on=2025-01-01T00:00:00Z|`
and tried `last_used=2025-01-01T00:00:00Z|` got every spool back, because
an unknown query param is silently ignored.

So the eight parameters become three, named after their columns and
taking the shipped grammar:

    /spool?last_used=2026-08-01T00:00:00Z|      since
    /spool?last_used=|2026-01-01T00:00:00Z      until
    /spool?last_used=<start>|<end>              between
    /spool?last_used=                           never used

Commas OR several of those, and quotes are accepted and ignored, so every
filter on the endpoint is spelled the same way. `_unset` disappears: the
empty value already means unset everywhere else here. No new convention
enters the API, and the next filterable column costs one parameter rather
than three.

Two consequences worth naming. Malformed input is now a 400 with a message
instead of FastAPI's automatic 422, matching the other unparseable filters
on this endpoint. And "has a timestamp" (the old `_unset=false`) has no
spelling any more, because the shipped grammar has none; nothing offered
it in the UI.

The client is unchanged apart from how it builds the request. The chip
value in the app URL keeps its own grammar, since it also carries relative
offsets and "never", neither of which the API takes.
2026-08-09 15:18:46 +02:00
Donkie 43bf140675 client_v2: separate manufacturer, filament and spool in the add flow
Step 2 of the add-spool modal creates up to three records at once, but laid
them out as one flat run of fields, so there was no way to tell which record
a field would land on. The vendor combobox in particular sat among the
filament fields while quietly creating a second entity.

Lay the step out as one labelled block per entity: the manufacturer and the
new filament each get their own card, and the spool fields get a heading of
their own instead of just following on. Weight, spool weight and price are
written to both the new filament and the spool, so say so rather than let
the headings imply otherwise.

Closes #1038
2026-08-09 11:19:40 +02:00
Donkie b6d220a3ef Filter the library by spools that have never been used
A date range cannot express "no date at all": a NULL fails every
comparison, so no bound, however far back it reaches, selects the spools
that have never been used. That state needed a filter of its own.

Backend: /spool and /spool/group take first_used_unset / last_used_unset.
True matches only the spools missing that timestamp, false only those
that have one. Registered gets no such param — the server sets it at
creation and a spool never lacks one.

Client: the chip value `never` sits alongside the range grammar, and
Last Used / First Used offer it under their presets. It is labelled
"Never", deliberately not "unused": the list already calls a spool unused
when no weight has come off it, which is a different claim — a spool can
be unused and still carry a last_used date, or have used weight and no
date at all.
2026-08-09 11:02:26 +02:00
Donkie 179ea3b4d0 Filter the library by date range
The library could filter by every discrete property a spool has but by
none of its timestamps, so "what have I used this week" and "what has
been sitting untouched for a year" were questions the UI could not ask.

Backend: /spool and /spool/group take first_used / last_used /
registered `_after` and `_before` bounds, each optional, inclusive, and
resolved against the UTC-naive form the columns store (an offset-aware
value is converted; a naive one is read as UTC). A spool with no
timestamp matches neither end.

Client: the Filter menu gains the three timestamps, each opening presets
(last 24 hours ... older than 1 year) plus a custom range whose empty
end means open. A range lives in the URL as a chip value, `<from>..<to>`,
where an end is a calendar day or an offset back from now. Relative ends
stay relative all the way into the URL and are resolved at request time,
so a bookmarked "last 24 hours" keeps meaning the last 24 hours instead
of drifting into "since the day I made it".
2026-08-09 09:53:42 +02:00
Donkie 7f19ff0952 Merge pull request #1046 from Donkie/client-v2-numeric-inputs
client_v2: accept decimal commas in every numeric field
2026-08-08 20:10:03 +02:00
Donkie e09d63b443 client_v2: accept decimal commas in every numeric field
Most of the world writes 1,75 rather than 1.75, and `<input type="number">`
only accepts the separator the browser locale dictates: everything else it
reports as an empty value. A user typing a comma therefore watched a weight
or a diameter silently come out blank or, once the separator was dropped,
a hundred times too large. The same input type also accepts "e", "+" and "-"
anywhere in the text, since they are legal exponent syntax, and then reports
the whole value as empty too, so "1e" read back as nothing at all.

Numeric fields are now text inputs with `inputmode="decimal"`, guarded by a
shared `numericInput` action that filters at the keystroke: both separators
go in, letters cannot, and a pasted "12,5 g" is cleaned rather than refused.
Reading is centralised in `parseDecimal`, which takes either separator and
returns null instead of NaN for text that is not a number, so an unset field
stays distinguishable from a real 0. What the parent, and the API, receive is
always canonical.

The typed separator stays on screen: each field keeps a draft of the text and
gives it up only when the value moves somewhere that text no longer means, so
storing 1.5 does not rewrite a typed "1,5" between keystrokes. Arrow-key
stepping, which was native to the old input type, is now the component's own.

Covers the spool/filament/vendor inspectors, the add-spool modal (whose price
box was a bare text input accepting anything at all), extra fields of every
numeric type, the extra-field editor's order box and the low-stock threshold.
2026-08-08 19:42:39 +02:00
Donkie dd13f6ea34 Merge pull request #1042 from Donkie/client-v2-label-export-formats
client_v2: pluggable label export formats, add AML
2026-08-08 10:12:11 +02:00
Donkie 93f49449a0 client_v2: restore the remembered view by navigating, not by parsing
Folding the remembered grouping into parseLibraryState left the address
bar saying nothing while the list was grouped, and that gap broke the
group control: picking the default grouping serialises back to the very
same bare URL, and an unchanged URL means SvelteKit reuses its cached
load, so the choice never took effect. The load was also no longer a
pure function of the URL, which is what that per-URL cache assumes -
preloading the nav tab cached a redirect built from the old preference
and replayed it.

The page now navigates on arrival to the URL that spells the remembered
view out, replacing the history entry. The load goes back to reading the
URL alone, the view stays linkable, and every distinct view has a
distinct URL again. Only on arrival: a goto is a choice the user just
made and a popstate is Back, and overruling either would be wrong.
2026-08-08 10:01:10 +02:00
Donkie 538c78e7b3 client_v2: remember the library's grouping and sorting
Group and sort live in the URL, so they survive a reload of a library
link but not a trip to Settings and back: every other route links to a
bare `/`, which handed back the shipped view and made a deliberately
chosen grouping something the user re-picked several times a day.

A URL that mentions neither grouping nor sorting now means "however I
had it last", restored from localStorage; one that mentions either still
wins outright, so a shared link shows its recipient what its sender saw.
Only the layout is remembered - filters, search and archived-visibility
hide spools, and one silently restored days later reads as missing data.

Fixes #1036
2026-08-08 10:00:33 +02:00
Donkie bef99d5891 client_v2: stamp exported PNGs with their real resolution
Label designs are authored in millimeters and rasterized at the layout's
DPI, but canvas.toDataURL() writes no pHYs chunk, so every reader falls back
to 96 dpi. A 50x25mm label exported at 300 dpi opened as a ~156x78mm image,
and printing it "at 100%" produced a label three times too big.

Stamp pHYs with the layout's density on the way out, replacing any existing
chunk and inserting after IHDR so it precedes IDAT as the spec requires.
Anything that doesn't parse cleanly as a chunk stream is passed through
untouched — a wrong DPI beats a corrupted download.

Verified independently with Pillow: the stamped files are valid PNGs and
read back as exactly 300 and 203 dpi.

AML is unaffected; it carries the label size in mm in its own header.
2026-08-08 09:36:28 +02:00
Donkie 2f1a2a2b2d client_v2: make label export pluggable and add .aml
The label designer's "image" mode could only ever write PNG. Move the
encoding step behind a small format registry ($lib/labels/export): the
renderer produces one PNG raster per label, and the selected format turns
that into file bytes. Adding a format is now one module plus a registry
entry, with no changes to the render, download or zip path.

Add AML as the second format. It is the label-document format the companion
apps of most cheap thermal label printers read (Phomemo/Aimo, Niimbot and
friends) — XML carrying the physical label size in mm, wrapping our raster
as a single full-bleed base64 image. That size is the part a bare PNG cannot
express, so the app scales to the loaded media instead of guessing at pixels.

AML has no published spec, so the output is modelled field-for-field on real
.aml files written by those apps; aml.test.ts pins the document, page and
image field sets against a Phomemo PM-241-BT template.

The chosen format is saved per design (PrintLayout.exportFormat) and resolved
leniently, so a design naming a format this build lacks falls back to PNG.
2026-08-08 09:23:30 +02:00
Donkie 16e34da538 client_v2: make the library list column resizable
The list was a fixed 470px, which truncated longer filament names with no
way to see the rest. The divider between the list and the inspector is now
a draggable, focusable separator, and the width is remembered per browser.

Fixes #1034
2026-08-08 08:33:43 +02:00
Donkie dd39a11c7e Show every unassigned spool when grouping by an extra field
Fixes #1019. A dashboard card grouped by a custom spool field counted more
spools than it listed: the group aggregate summed them, the card's own query
returned a subset.

Grouping and filtering disagreed about what "no value" means. group_by keys on
the decoded value, so a spool holding the JSON empty string a cleared text box
writes became its own group, separate from the null one; the client renders
both as "unassigned" and adds the counts. The card then fetches its spools with
`extra.<key>=`, whose empty branch matched only NULL and JSON null - never the
empty string. Built-in string columns have matched both NULL and '' since
forever (add_where_clause_str_opt), which is why grouping by location was fine.

- The empty extra-field filter matches a stored empty string too.
- Value-keyed group axes (extra fields, location, material) fold a blank into
  NULL, so "unassigned" is one group rather than two the client can only draw
  as one.
- Emptying a text extra field clears it instead of storing "", so no new spool
  lands in the state above.
- The dashboard clears an extra field with a real JSON null. It sent the string
  'null', which the API rejects as not-a-string, so dragging a spool onto the
  unassigned card 400'd on every extra-field board.
2026-08-05 19:42:59 +02:00
Donkie b2f5088081 client_v2: let a filament's manufacturer be changed
The manufacturer was decided once, in the add-spool modal, and there was no
way back: a filament created under the wrong brand — or under none — stayed
there, and since the library groups by manufacturer that mis-files every
spool of it. The companion to the spool's change-filament (#1010), one level
up the chain.

Adds a "Change" action to the MANUFACTURER section of the filament
inspector, opening a dialog with the three destinations the API allows: one
of your manufacturers, a new one named on the spot (as the add-spool form
already allows), or none at all. There is no SpoolmanDB side to this one —
the external catalog carries filaments and materials, not manufacturers — so
the picker fetches the vendor list whole and filters it in the browser.

The dialog spells out the one consequence that isn't obvious. A filament
with no empty spool weight of its own takes its manufacturer's, and that is
the figure `PUT /spool/{id}/measure` subtracts from a reading off the scale,
so the note says what it becomes — or that it is about to have none. A
filament that has its own keeps it and gets no note: unlike the spool's full
weight, that field is editable right there in the panel.

Clearing the manufacturer travels as an explicit `vendor_id: null`, which is
what the API reads as "no manufacturer"; dropping the key instead would have
made that choice a silent no-op.
2026-08-05 18:14:28 +02:00
Donkie 7aa864e457 Merge pull request #1016 from Donkie/worktree-fix-1012-unused-grouping
client_v2: file unused spools under their own filament
2026-08-03 20:35:11 +02:00
Donkie 032b3f05c2 client_v2: file unused spools under their own filament
A collapsed "×N unused" row speaks for its whole pile with one filament's
name, swatch, weight and location, all read off the first spool. That only
holds within a filament group. Under Manufacturer (or Material/Location) a
group spans many filaments, so the whole unused pile was filed under
whichever filament happened to sort first: five different SUNLU woods
listed as "Wood Cherry ×5 unused" (#1012).

Bucket the pile by filament before collapsing it, keeping the backend's
order via first appearance. Buckets of one render inline as before.

Fixes #1012
2026-08-03 20:27:33 +02:00
Donkie 4ed1fbfc67 client_v2: let a label print the spool's own tare weight
The v1 client offered the spool's empty-spool weight and its filament's as two
separate label tokens. v2 kept only the filament's, so `{spool_weight}` in a
migrated v1 design had no equivalent to map to and resolved to "?" — and a
design rebuilt in v2 could only print the filament's figure, which is not
necessarily the one that spool is measured against.

`spool.spoolWeight` falls back to the filament the way `spool.price` already
does, so a `spool.*` token prints what actually applies to that spool.
2026-08-03 20:09:42 +02:00
Donkie 155e8f3737 client_v2: make the override marks footnotes rather than badges
Accent-coloured pills next to four fields read as warnings about something
being wrong, when all they report is that a spool holds a value of its own.
Keep the accent colour, which is what makes them findable, and drop the border,
weight and uppercasing that gave them the urgency.

Both ends now render identically. The winning and shadowed levels were
distinguished by colour, but the wording already says which is which
("Overrides filament" vs "Overridden by this spool"), so the variant carried
nothing the label didn't.
2026-08-03 19:59:45 +02:00
Donkie b885108a3d client_v2: let a spool override the values it inherits from its filament
A spool can hold its own full weight, empty-spool (tare) weight and price,
falling back to its filament's where it has none. The API has had all three
since #339, and the v1 client exposed them, but client_v2 never mapped
spool_weight at all and had no field for it or for the full weight.

The result was silent wrong arithmetic (#1013): the API gives every spool it
creates a copy of the filament's tare weight, so correcting the *filament*
later looked like it had worked — the spool panel showed the new figure — while
"adjust by measured weight" went on subtracting the old one. A reporter's empty
261 g spool came back with 129 g still on it, exactly the difference.

Both ends of a disagreement are now marked. The spool's field says what it
shadows, the filament's row says what applies instead and is struck through,
and the same treatment covers the manufacturer's empty-spool weight, which
seeds a filament and is then left behind. Only where the values actually
differ: merely having one of its own is already visible in the field, and
marking every spool would teach everyone to stop reading the marks.

Also fixes a 500 from PUT /spool/{id}/measure when nothing in the chain has a
tare weight — reachable in one click now that the field can be cleared.
2026-08-03 18:06:05 +02:00
Donkie 1d5b8c1d3d Merge pull request #1014 from Donkie/worktree-issue-1010-change-filament
client_v2: let a spool's filament be changed
2026-08-03 17:24:30 +02:00
Donkie f2b060f6ca client_v2: let a spool's filament be changed
The v1 client had a filament dropdown on the spool edit form; client_v2
dropped it, leaving no way to re-point a spool at a different filament
(#1010). People who organise their shelves by spool number need slot #37
to stay slot #37 when the roll in it is replaced, and deleting and
re-adding the spool hands them a new id.

Adds a "Change" action to the FILAMENT section of the spool inspector,
opening a dialog that searches the local catalog and SpoolmanDB (an
external pick is imported on apply, as in the add-spools flow). The spool
keeps its id, location, dates, lot, comment and used weight.

The full weight needs a decision the dialog makes explicit: a spool
created from a filament records that filament's weight as its own, so it
would otherwise keep the old figure forever and this client has no other
field for it. When the new filament's weight differs, the dialog offers
to take it (checked by default) and spells out the resulting remaining
weight. A spool with no weight of its own follows its filament anyway —
that case just says so.
2026-08-02 22:51:16 +02:00
Donkie ae12072cdd Merge remote-tracking branch 'origin/master' into worktree-client-v2-401-reload
# Conflicts:
#	client_v2/src/lib/api/search.ts
2026-08-02 14:13:41 +02:00
Donkie 2a03f9f64c client_v2: reload on 401 so a forward-auth session can recover
Behind a forward-auth proxy (Authelia, Authentik, oauth2-proxy) an idle
tab starts getting 401s once the session expires. The proxy can only
redirect a top-level navigation, so a page that is already loaded never
gets offered the login portal and simply fails every request until the
user works out that a manual reload fixes it.

Do that reload for them. It counts as a navigation, so it earns the
redirect and brings the user back to the page they were on. Rate limited
to once per 30s so a 401 that a reload cannot fix does not put the tab in
a loop, and skipped for writes so a half filled form is not lost to a
login redirect.

Both transports are covered. REST goes through http.ts. Websockets get an
HTTP probe instead, since a handshake refused by a proxy exposes no status
to the browser and is otherwise indistinguishable from a backend that is
down. search.ts moves onto the shared fetch helper so it is covered too.

Dormant without a proxy in front, since Spoolman never answers 401 itself.

Refs #923
2026-08-02 12:47:13 +02:00
Donkie 3beb2b4483 Merge pull request #1006 from Donkie/worktree-search-spool-pills
Offer a filament's spools in the search results
2026-08-02 10:32:08 +02:00
Donkie 0c3ce54da9 Merge pull request #1007 from Donkie/worktree-issue-994-reorder-choices
Allow reordering of extra field choices
2026-08-02 10:31:55 +02:00
Donkie 46beda2264 Offer a filament's spools in the search results
Searching a filament name only ever returned the filament: a spool matches
on its own text (comment, location, lot number, extra fields) or its exact
id, so a name that lives on the filament could not reach any of its spools.
The hit named the filament you own and then dead-ended.

Each filament result now carries its first few spools as pills, with an
overflow link to the filament for the rest. GET /search gains an opt-in
spools_per_filament parameter; at 0, the default, the response is byte for
byte what it was, so nothing else that reads the endpoint changes.

The spools are fetched in one query over the matched filaments and grouped
in Python rather than with a per-group window function, which keeps the
behaviour identical on all four supported databases, and yields the exact
total count for free. They obey allow_archived like the rest of the
response, and are ordered by id so "first" means the same thing everywhere.

Fixes #993
2026-08-02 08:40:41 +02:00
Donkie 18f114419f Let extra field choices be reordered by dragging
Choice values could only be appended, so the only way to get a value into
the right place was to remove everything after it and type it all back in.
Both clients now render each choice with a grip: drag it, or focus it and
press the left/right arrow keys, to move it. The backend already accepts a
reordered list — it only refuses removals.

Fixes #994
2026-08-02 08:35:18 +02:00
Donkie 3c1d8bfa6d client_v2: separate the search result's material tag from the match tag
The material chip and the "matched: <field>" chip sat side by side at the
trailing edge with identical styling, so two unrelated facts read as one kind
of thing. Move material next to the filament name (mirroring the group header)
and give it an outline variant, leaving the filled chip to mean "why this
result matched".
2026-08-02 08:23:10 +02:00
Donkie f8db2a0f09 Merge pull request #1001 from Donkie/fix/library-header-affordances
fix(library): make the group header's destinations findable
2026-08-02 08:15:07 +02:00
Donkie a5e4bc0bf3 fix(library): drop the group header arrow
The hover underline and colour already say the title is a link, and the
arrow was one more glyph on a row that has a chevron doing something
else. Touch users lose the at-rest cue, but the manufacturer link below
is visible without hover either way.
2026-08-01 21:55:42 +02:00
Donkie 13b7a9b62f Merge pull request #999 from Donkie/worktree-issue-992-detail-fields
Show all filament and manufacturer fields in the library detail views
2026-08-01 17:18:29 +02:00