Commit Graph
45 Commits
Author SHA1 Message Date
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 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 c8be881e03 fix(sort): keep never-used spools and filaments at the bottom
Sorting the library by "last used" led with everything that had never been
used, on PostgreSQL and CockroachDB. Those two default to NULLS FIRST on
DESC, while SQLite and MySQL treat NULL as the lowest value and put it last;
`last_used` is NULL until a spool is first used, so the default library
ordering came out inverted for half the supported databases (#984, #985).
Grouped views showed it worst: a group's date is the max over its spools, so
a filament holding nothing but unused spools aggregates to NULL and floated
to the top, above filaments used minutes ago.

order_by_clauses() now orders on an explicit "is it null" flag ahead of the
field itself, so an absent value sorts last in both directions everywhere.
It is written as `expr IS NULL` rather than SQLAlchemy's nullslast(), which
renders a literal NULLS LAST that MySQL and MariaDB reject. Applied to every
sort site -- spools, groups, filaments, vendors, extra fields -- so ordering
no longer depends on which database is underneath.

Verified against all four: 438 integration tests pass on SQLite, PostgreSQL,
MariaDB and CockroachDB, and both new regression tests fail on PostgreSQL
without the fix.

Claude-Session: https://claude.ai/code/session_01EaLK21WpQYDE1DkKwZHK3P
2026-07-31 14:38:50 +02:00
Donkie aa447dd05b Let any spool field value be renamed everywhere at once
Renaming a dashboard card only worked for Location, which has a
server-side rename that moves every spool in one statement. A custom
field had no equivalent, so the card name was read-only unless the group
was empty -- renaming it from the client would have meant patching each
spool one at a time, and only the ones the board had actually paged in.

Generalise the location rename to the spool's other string fields:

    PATCH /spool/field/{field}  {"value": "Shelf A", "new_value": "Shelf B"}
      -> {"spools_updated": 6}

where field is `location` or `extra.<key>`. Archived spools are included
-- the value is the value, and skipping them would leave half the spools
behind. Renaming onto a value already in use merges the two, though the
dashboard still refuses that so it can't happen by accident.

The old value travels in the body rather than the path, unlike
/location/{name}. Field keys are ^[a-z0-9_]+$ and so are path-safe, but
values are not: a location like "Rack A/B" needs an encoded slash, which
proxies routinely mangle. No reason to carry that flaw into the general
form.

Only fields the spool owns can be renamed. material/vendor/filament are
refused because they belong to another entity, and rewriting them "for
these spools" would change filaments that other spools share -- the same
rule that decides what the dashboard can group by, so grouping and
renaming now share one validator.

Like rename_location, no spool event is broadcast per row: the change is
one statement over what may be hundreds of spools, and that fan-out is
what the websocket layer avoids elsewhere.

Verified on all four databases, and in the browser on a 40-spool group
of which the board had loaded 30 -- all 40 moved, and the card renamed in
place without losing its position.
2026-07-30 09:08:58 +02:00
Donkie 21651216af Turn the locations page into a group-by-anything dashboard
The locations board could only ever answer one question: where is this
spool. The same card-and-drag interaction works for any field a spool
carries, so make the field a view mode and keep the rest.

Location is still the default view and behaves exactly as before. The
other views come from the user's own spool fields: any single-value text
or choice field now appears in a Group-by menu. A choice field renders a
card per choice, including the empty ones, which turns a "Status" field
into a kanban board.

Which field a card belongs to is decided by one small interface
(src/lib/dashboard/fields.ts), so the page never special-cases a field.
A field only qualifies if the SPOOL OWNS IT, because a drag moves one
spool and must mean exactly one change. That rules out material and
vendor, where writing one spool's card would silently rewrite every other
spool of the same filament, and lot number, which is stamped on by the
manufacturer and would be recording something untrue. Those stay library
filters. It also rules out numbers, dates, booleans and multi-choice,
whose values don't name a card you can drag into.

Backing changes:

- /spool/group grows group_by=extra.<key> for a spool's custom fields.
  The field table is aliased so it stays a distinct FROM element -- the
  extra-field filters reference the un-aliased table inside correlated
  subqueries, and without the alias those would correlate to this join
  instead of standing alone. Grouping is rejected for field types whose
  value isn't a single repeatable string. group_by is no longer a
  Literal, so an unsupported value is a 400 rather than a 422.

- The saved layout is per field: dashboard_groups and
  dashboard_spoolorders replace the location-only pair. The old
  `locations` / `locations_spoolorders` are read once as a seed for the
  location view and never written back, so an upgrade keeps the shelves
  it had and the old client keeps working.

- The view mode lives in the URL (?by=) so a board can be bookmarked and
  shared, and is mirrored to localStorage so a bare /dashboard resumes
  the last one looked at. The URL wins when it says something; a stale
  ?by= naming a deleted field falls back to location.

Verified against SQLite, PostgreSQL, MariaDB and CockroachDB -- Postgres
caught a real difference here, it sorts NULLs last where SQLite sorts
them first, so the new group assertions are keyed rather than ordered.
2026-07-30 08:41:36 +02:00
Donkie b369cf049e Clamp grouped remaining weight at 0 for over-used spools
Spool.from_db reports remaining_weight as max(initial - used, 0), but
find_groups' remaining_expr omitted the clamp. A spool used past empty
therefore contributed a negative number to total_remaining_weight, so a
group's total no longer matched the sum of the spools it contains — two
500 g spools, one with 200 g used and one with 800 g, reported 0 g instead
of 300 g.

Mirror the clamp with CASE rather than func.greatest, which is not portable
across all four supported databases. Covered by a new integration test.
2026-07-28 20:27:59 +02:00
Donkie d7486d5d51 Improved filtering/sorting. Allows filtering/sorting by extra fields. 2026-07-18 15:54:58 +02:00
Donkie c82e3911e1 WIP: add v2 client (Svelte) with spool grouping backend
Introduces client_v2, a new SvelteKit-based client, alongside the
backend it depends on: a /spool/group endpoint that returns
per-group aggregates (count, in-use count, remaining weight,
last-used) grouped by filament, vendor, material or location, so the
client can paginate whole groups instead of fetching every spool.

Not yet complete/wired up for release.
2026-07-12 19:21:34 +02:00
Donkie 70ea7ad414 Update python dependencies and ruff/black versions 2025-03-16 21:34:02 +01:00
Donkie c4693d3f34 Implemented used_length spool sort 2024-05-15 20:18:24 +02:00
Matt Gerega fdec032d19 Updated handling of higher measurements 2024-04-15 16:41:14 -04:00
Matt Gerega c6f9abeced Changed initial_weight to be net weight, not gross weight 2024-04-10 11:00:46 -04:00
Matt Gerega 23cfb42ba8 Updated based on PR requests 2024-04-08 17:38:18 -04:00
Matt Gerega 951592ab4c Add more integration tests for spool weights 2024-03-28 09:38:24 -04:00
Matt Gerega a6ae808389 Added additional integration tests for measure and add 2024-03-28 09:29:39 -04:00
Matt Gerega 19c414da4b Add integration tests for new measure endpoint 2024-03-27 15:44:54 -04:00
hcooper 1273b20224 Support dynamic ports 2024-01-23 11:16:57 -08:00
Donkie f8cc9f7eda Updated integration tests to not require exact match
This matches how we want clients to behave, they should ignore any extra fields that are sent in the response.
2024-01-23 18:58:58 +01:00
Donkie 7972f4d401 Update test_add.py 2024-01-23 18:58:58 +01:00
Donkie 4adee1327b Ruff fix 2024-01-23 18:58:57 +01:00
pdsccode 28a70d5190 Merged spool price testing into existing test methods 2024-01-07 14:24:14 +01:00
pdsccode 05dfb611cf Revert "Fixed test error that asserts 404 on create"
This reverts commit f13c61b9e9.
2024-01-07 14:17:59 +01:00
pdsccode f13c61b9e9 Fixed test error that asserts 404 on create 2024-01-07 13:44:04 +01:00
pdsccode 44b20fb360 Fixed code formatting 2024-01-07 13:25:05 +01:00
pdsccode a3be982632 Fixing test update spool issue 2024-01-06 23:04:53 +01:00
pdsccode 577ed1e680 Changed to show filament price if no spool price set and added tests 2024-01-06 21:42:09 +01:00
Donkie 9595aec675 Fixed registered field having wrong timezone
This happened with at least Postgres, which took it's server's timezone. It's complicated. Let's just set it from our side instead where we have some control.
2023-10-02 21:36:50 +02:00
Donkie 205574fa2c Fixed timezone being returned incorrectly
Happened if you've set TZ to non-UTC.

Apparently astimezone and replace doesn't do the same thing even if tzinfo is None.
2023-10-02 21:36:50 +02:00
Donkie 597dca7c28 Fixed combining empty and non-empty filter
Previously it would then act as a wildcard filter
2023-09-20 21:02:49 +02:00
Donkie b795680226 Added missing timezone in responses
Fixes #114
2023-09-20 20:13:02 +02:00
Donkie 6f94c17050 Renamed vendor.x to filament.vendor.x in spool API 2023-09-16 11:13:30 +02:00
Donkie 6ee3b8df70 Added support for multiple int where's 2023-09-16 11:13:30 +02:00
Donkie 841cc8b6df Added sort by remaining weight 2023-09-16 11:13:30 +02:00
Donkie a4d14c5b52 Added X-Total-Count test asserts
Fixed issue with incorrect total count for vendors
2023-09-16 11:13:30 +02:00
Donkie 0f58ddfd88 Modernized vendor tests 2023-09-16 11:13:30 +02:00
Donkie 978773583a Full testing done for filaments
Also added tests for having multiple sorts
2023-09-16 11:13:30 +02:00
Donkie acc6ab56a5 Fixed tests for non-sqlite dbs 2023-09-16 11:13:30 +02:00
Donkie aa28e51c28 Added offset and limit tests for spools 2023-09-16 11:13:30 +02:00
Donkie 13994c486f Added module-level fixtures in tests 2023-09-16 11:13:30 +02:00
Donkie e436aa4975 Added tests for spool find sorting 2023-09-16 11:13:30 +02:00
Donkie 9a7375d781 Added test cases for finding empty spool fields 2023-09-16 11:13:30 +02:00
Donkie 826fde6967 API find nested filters now specified using period
Previously you used underscore, like vendor_name. This made fields like filament_article_number have ambiguous underscores.
2023-09-16 11:13:30 +02:00
Donkie 8e9ca8e305 Fixed ruff in CI 2023-09-04 20:36:02 +02:00
Donkie e6b30da55d Split spool test_find into individual tests 2023-08-21 22:16:52 +02:00
Donkie c820112e11 Split up integration tests into individual files 2023-08-21 22:16:52 +02:00