Two more problems from #1149.
Sheet mode set @page size to the paper's exact dimensions. A 62x34 page makes
the browser send it as landscape, and Chromium hides the orientation picker
when the page CSS sets a size. Label printer drivers disagree on which way
round a roll is: a Brother QL needed landscape, a Niimbot B1 needed portrait,
so any fixed choice prints rotated for someone. Sheet mode now uses size: auto
like v1 did, and the page is still laid out in mm. Label mode keeps its exact
size.
The v1 migration derived the label size from the raw margins. v1 users paired
-20mm margins with a 1.5mm printer margin to pull an oversized cell back onto a
62x34 page, which migrated as a 102x74 label. Margins are now clamped at
migration; the safe-zone alone gives the same content inset (max(safe, margin)
in both versions), and the label comes out at the paper size.
Two bugs in the sheet-mode safe-zone inset, both reported in #1149.
The label raster has a fixed aspect ratio, but labelImg sized its width and
height independently, so a safe-zone squashed the label (1.5mm top/bottom on a
34mm label is a 9% vertical squeeze). v1 padded a box of reflowing HTML, where
that was harmless; a raster has to scale uniformly, so the inset box now gets
object-fit: contain.
Negative page margins made it much worse. v1 accepted them and used them to size
the grid cells; in v2 the label size comes from the design and the margin only
positions an absolutely placed grid inside an overflow:hidden page, so a negative
margin can only clip. It also inflated overSafe (safe - margin), which turned a
1.5mm safe-zone into a 21.5mm inset: at margin -20 the label rendered 22x12mm in
the corner of a 62x34mm page, and sheetGrid invented a second row off the paper.
Margins are now clamped to zero on read, so existing presets carrying v1's
negative values are fixed without the user editing them, and the margin inputs
refuse new negative values.
It sat in the same quiet link style as the phone reader, which is an
alternative route rather than the thing to press. Same 600 as a UID gets
elsewhere in the tag UI, and no wrapping between verb and object.
The dialog waits for a fresh tap, which is right for a sticker in your hand and
awkward for a reader that already holds the tag. On a scale the spool is on the
pad to be weighed, so filling in the field means lifting it off and putting it
back; from the user's side that looks like the dialog not working.
With the last UID now in the registry, the dialog can simply say what the reader
it listens to last read, and offer it. Paired to one reader, that reader answers
or nothing does. Listening to all of them, the most recently seen one wins,
which is the one the person most likely just used. The offer disappears once the
field already holds that UID, so it never sits there as a no-op.
Nothing changes for the tap: it still fills the field the moment it arrives.
The Add tag dialog waits for a fresh tap. That is fine when the tag is a
sticker in your hand, and awkward when the reader is a scale: the spool is
sitting on the pad to be weighed, and filling in the field means lifting it
and putting it back. A reader also cannot help here, because it has no reason
to report a tag that has not moved.
The registry already tracks each reader by id, name and last_seen, all in
memory. Keeping the last UID beside them is enough for a client to offer what
is on the reader right now, with no back channel to the device, no protocol
change, and nothing to update on the agent side - every existing ESPHome
reader gets this for free.
One UID, not a history: this answers "what is on the reader", not "what has
been". It lives and dies with the registry entry, so it is gone on restart
like the rest of it, and no scan is written anywhere.
Both frontends and the README now point at github.com/sponsors/Donkie.
The GitHub-suggested iframe embed is not usable here: Spoolman is often
run on a LAN with no internet access, so it would render as an empty box,
and it cannot be translated or themed. The existing buttons keep their
shell and get an inlined Octicon heart plus a translated label instead.
Only the en locale source is touched; the new "sponsor" key goes out to
the other locales via Weblate.
Auto-navigate rewrote the URL to a bare `/?sel=spool:N`, which since the filter
menu landed means a tap throws away the grouping, sort and filter chips you had
set up -- and lands on the shipped default view rather than the remembered one.
openSearchResult is the navigation a picked search result already gets, and a
tag scan asks the same question: on the Library it merges the selection into
the current view, elsewhere it opens the Library on just that spool. The
inspector resolves a selection by id on its own, so the spool still opens when
the active filters exclude it from the list behind it.
Master split filter application in two: the filters a filament can answer on
its own (_apply_filament_filters) now serve both the spool query and the new
include_empty query, which selects FROM the filaments so that a filament with
no spools still produces a group.
The tag filter stays where it was, in _apply_spool_filters, and the comment
there now says why it cannot move: a tag is linked to a spool, so a filament
on its own can no more answer "which tag is on you" than it can answer "which
shelf are you on". Nothing routes a tag filter into the filament-driven query
-- it never calls this builder, and the group endpoint takes no `tag` -- so
there is no silently-dropped filter to guard against.
The root .gitattributes sets `* text=auto`, so a Windows checkout gets CRLF in
the working tree. Prettier defaults to `endOfLine: "lf"` and therefore reports
every text file as mis-formatted -- 163 files in client_v2 alone -- which makes
`npm run lint` and `lefthook run ci` unusable locally. The pre-commit hook is
worse: it runs `prettier --write .` with stage_fixed, so any commit made from
Windows rewrites and stages the whole client tree.
Set `endOfLine: "auto"` in both client configs. Git still normalizes to LF in
the index and in the repo thanks to `text=auto`, and CI runs on ubuntu-latest,
so LF in version control is unaffected; prettier just stops fighting the
checkout. This is Prettier's documented fix for this scenario.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Listing every filament you own no spools of turned out to be the wrong default.
Finding a spool to print with is the daily job and those rows cannot serve it,
and how many of them there are scales with the size of a user's catalogue -- a
no-op for one library and a tripling of another. Off by default, one click to
turn on, remembered afterwards: a missed feature beats a primary view that is
permanently worse until you find the switch.
The toggle sits beside Show Archived and shows a dismissible chip when on, and
is offered only while grouping by filament, since there is nowhere else to put
the empty groups.
Remembered through viewPrefs, which until now deliberately stored nothing that
hides rows -- a hidden filter restored days later reads as missing data. This
one only ever adds rows, so it cannot cause that confusion; the comment there
now says which side of the line it falls on and why. Entries written before the
flag existed keep working: a missing field means off rather than a corrupt entry
that would throw away the grouping the user picked.
The badge now reads "No spools" rather than "Out of stock". Spoolman knows what
you have recorded, not what is on your shelf.
The first cut of include_empty grew a second 177-line function next to
find_groups that repeated its filters, ordering, pagination and hydration, so
every future change to grouping had to be made twice or silently diverge.
Only the FROM ever differed. Writing the aggregates so they hold for a group
with no spools in it -- COUNT over a column instead of COUNT(*), one explicit
zero for the one SUM -- removes the need for a second set, and with it the
COALESCE that CockroachDB rejected and the CAST that worked around it. What is
left is one query with a branch over how its rows are selected, and one shared
tail.
Spool-level filters are now refused alongside include_empty rather than quietly
producing a contradiction (every filament as empty, or no empty groups at all).
That also leaves the archived rule as the only spool-level condition the
filament-driven query has to carry, in its JOIN.
Group ordering now breaks ties on the grouped column. Every ordering can tie,
and empty groups tie on all four aggregates, so paging could otherwise repeat or
drop a group between pages.
Grouping by filament aggregates over spools, so a filament you own no spools of
could never produce a group and simply vanished from the library. A filtered
view then looked fully stocked when it was the opposite, and the thing you
needed to re-order was the one thing not on screen (#1092).
GET /api/v1/spool/group takes include_empty, which inverts the query: spools are
aggregated per filament in a subquery that the filament list is LEFT JOINed
against, so filaments with no matching spools survive with their aggregates
folded to zero. Which side of that join a filter goes is the whole subtlety, and
the docstring spells it out - spool-scoped conditions have to stay in the
subquery or they delete the very rows the flag exists to keep.
The client asks for it while grouping by filament and every active filter is one
a filament can answer on its own. "Which of these are on Shelf A" has no answer
for a filament that is nowhere, and including them there would bury the shelf
under the whole catalogue. Such a group renders as an out-of-stock row: no
disclosure, no fetch, and a marker in place of the zeroes it would otherwise
report.
The group header also picks up the truncation tooltip added in the previous
commit. Its title sits under a link stretched across the whole row, so the
pointer never lands on the text itself and the tooltip has to hang off the row.
Text cut off with an ellipsis gave no way to read the value behind it, and that
value — a filament name, a location, a manufacturer — is usually the whole
reason the row is on screen (#1093).
A shared action measures the element and gives it a title only while the text
really is cut off, so rows that fit stay quiet instead of popping a redundant
tooltip under every pass of the pointer. It re-measures on resize and on text
changes alike: the list pane is resizable, and a live update can lengthen a name
without the column moving at all.
Applied to the spool rows, the unused-spool rows, the search results and the
dashboard chips.
The sort chip showed the direction but opened the menu wherever you clicked it,
so reversing an order meant reopening the dropdown and finding the field that
was already selected (#1091).
Split it into two controls sharing one frame: the field label still opens the
menu, the arrow only flips the direction. A divider and a hover of its own mark
the arrow as separate, and both halves stretch to fill the chip so the 44px
mobile tap target survives the split.
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".
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#1090Closes#1089
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#1087Closes#1088
Nothing in this section sets a scanner up -- that is hardware, and the link
above it goes to the wiki for it. Every control here answers one question:
when a tag is scanned, does this screen jump to its spool, and which reader is
it listening to. "Tag scanner" as a heading promised the former and delivered
the latter.
So the section is "Scanned tags", the reader row talks about which scans open
spools here rather than about listening in the abstract, and "Listen to all"
becomes "Use any reader" -- the same thing said in terms of the outcome rather
than of the plumbing. The auto-navigate description drops its first sentence,
which only repeated the label it sits under, and spends the room on the limits
instead.
The e2e assertions that named this copy now match on the clause that
distinguishes the two states rather than on whole sentences, since prose gets
reworded and the assertion is about which state is showing.