Commit Graph

7 Commits

Author SHA1 Message Date
cproudlock
f09d14d28c Refuse a measurement that makes the label stop existing
Some checks failed
CI / backend (push) Failing after 7m11s
CI / naming (push) Failing after 7m10s
CI / frontend (push) Failing after 7m18s
CI / migrations-mysql (push) Failing after 7m5s
Reported as a barcode rendering 176 by 0. A bar height of zero is what does it:
the number went straight into a CSS length, height: 0in is perfectly valid, and
an image sized height: 100% of a zero-height box is zero pixels tall. The label
comes out full width with nothing on it, which reads as a rendering failure
rather than as a field doing exactly what it was told.

A cleared field was the same bug wearing a different hat. An emptied number
input holds '', not 0, so the page emitted `NaNin`; the browser dropped the
declaration and the box fell back to auto height. That one merely looked wrong
instead of vanishing, which is arguably worse for finding it.

Measurements now go through labelVars(), which will not emit NaN, an empty
length, or a zero for anything whose zero means the box stops existing. Padding,
the quiet zone and the gap still take zero, because there zero is a real answer,
and offsets stay signed. It is a separate module because this is exactly the
kind of arithmetic that needs tests rather than a preview: every case in
labelVars.spec.js is a value a number input can actually hold.

The stylesheet now carries fallbacks on its var() reads as well, but they are a
net, not the fix: a custom property set to garbage is substituted and then
dropped at computed value time, and the fallback does not apply - it only covers
a property that is absent entirely.
2026-08-21 12:09:26 -04:00
cproudlock
e9fcd1e598 Stop a caption from eating the barcode, and put the labels beside the settings
Some checks failed
CI / naming (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / migrations-mysql (push) Has been cancelled
CI / backend (push) Has been cancelled
Reported as the CODE128 not appearing in the preview. It was appearing, in 44
of the 45 preset and layout combinations swept - as a 36 pixel stub. The code
box and the caption were both allowed to grow, so a caption of any length took
half the label and then kept taking, and on 1.00 x 0.50in stock a few words of
caption left a barcode too narrow to scan. Present, useless, and indisinguishable
from a failed render. The caption now takes the room it needs and no more, and a
CODE128 holds at least 40% of the label, or all of it when stacked: past that
there are too few narrow bars left to read, and a caption that wraps or clips is
the better trade.

So that a real failure can never again look like this one, an empty code box now
says so in red in the preview. It is screen-only - the marker must never reach a
printed label.

The settings were a narrow column with the preview trailing below them, which
put the labels being adjusted off the bottom of the screen. Settings on the
left, labels on the right in a column that stays in view and scrolls on its own,
folding back to one column on a narrow window.

That scrolling column is a print hazard worth naming: an ancestor with its own
overflow clips a multi-page print to whatever happened to be on screen. Print
resets it, and the check is a six-page two-sided run coming out as six pages
with the label as the only clipping box in the chain.
2026-08-21 11:55:36 -04:00
cproudlock
1abb6430f5 Put a code on the back of a card, a picture on the front, and the page in the app
Some checks failed
CI / backend (push) Failing after 7m17s
CI / naming (push) Failing after 7m14s
CI / migrations-mysql (push) Has been cancelled
CI / frontend (push) Has been cancelled
The back side landed last commit carrying text by default, and a back laid out
as text-only renders no code box at all. So a two-sided run came out with a
blank reverse, which reads as the code having failed rather than as a setting
doing exactly what it said. The back now defaults to carrying a code, takes its
own content per row, and chooses its own symbology - a QR on the front that
opens the record, a CODE128 on the back that the bench scanner already reads.
Text-only is refused while the back carries a code, and the contradiction is
reconciled on load, so a saved setting from the broken combination repairs
itself rather than printing another blank box of cards.

A picture can go on a label: above the code, beside it, or behind everything as
a watermark, on either side or both. The file is read in the browser and never
sent anywhere, which is what makes it usable at an air-gapped site, and it is
deliberately not saved between visits - a data URL runs to megabytes and would
take every sizing value with it when the quota gave out. The watermark says
plainly that it costs the scanner contrast.

The generator was a full-screen route with no navigation and a narrow column of
controls against the left edge. That shape exists so a print cannot put the
sidebar on label stock, which is a real hazard and the reason core's label pages
are built that way - but it is a hazard the print stylesheet can answer, and
style.css now hides the shell at print time. A tool someone spends ten minutes
in gets the navigation and the full width; the stock still gets nothing but
labels, confirmed against a print PDF rather than a preview.

Two smaller things found while checking the above. A test print paired each page
with a rendered code by loop position, while the codes are keyed to position in
the whole run, so a two-sided test card could carry the wrong code; the index
now travels with the page. And the back's dotted preview outline outspecified
the print reset, drawing a dashed rule down the edge of every second card.
2026-08-21 10:59:28 -04:00
cproudlock
39ede87d55 Give a label its own gap, its own position, and a back side
Some checks failed
CI / backend (push) Failing after 7m10s
CI / frontend (push) Has been cancelled
CI / migrations-mysql (push) Has been cancelled
CI / naming (push) Has been cancelled
Three things the code generator could not do, each of which had someone
working around it with a control that meant something else.

The gap between a code and its caption was the quiet zone. That is the
clear space a scanner needs around the code, so closing up a caption meant
shaving the thing that makes the code readable, and the only way to get a
tight label to scan was to push the caption further away than it looked
like it should be. Quiet zone now means the code's own margin and nothing
else; gap means the distance to the caption, and adds to the quiet zone
rather than eating into it. Each stock preset carries the gap that suits it.

Position had one pair of controls and they moved the label box on the
stock. That is the fix for a printer whose origin is off, and it is not the
fix for a code sitting in the wrong place on a label - but it was the only
lever there, so it got used for both. The label div is now the media and
holds still; everything printed sits in a content wrapper that Align,
Vertical and Content X/Y move within it. The old pair kept its behaviour
and is labelled Media X/Y.

A badge card has two sides and the generator only ever emitted one page per
card. The back can now carry the same code again, a fixed block of text,
its own per-row content, or nothing. Page order is a choice because the two
kinds of printer want opposite things: interleaved for a duplex card
printer, which takes pages two at a time, and grouped for a single-sided
one whose stack gets flipped and reloaded. A row that leaves its back
column empty still gets a blank back page - a page count that varies per
row shifts every later back onto the wrong front, which prints a box of
perfectly readable cards that are all wrong.

CSV parsing and page building moved to plugins/tools/frontend/labelPages.js
(committed in 875fde9), where the column contract and both page orders are
covered by tests rather than by looking at the preview.
2026-08-21 09:08:37 -04:00
cproudlock
875fde9f48 Fix what the last round of device fixes broke, and two dips it missed
Some checks failed
CI / backend (push) Failing after 7m15s
CI / naming (push) Failing after 7m14s
CI / migrations-mysql (push) Has been cancelled
CI / frontend (push) Has been cancelled
A review of d60ed60 and 8b9b936 found five things. Three were introduced by
those commits.

REFUSING A NAMED DEVICE MADE A MARKER PC CLAIM ITS OPERATION. _sync_partmarker
returned [] both for "not a marker PC" and for "a marker PC that linked
nothing", and the caller reads [] as the first - so a bay whose asset-id.txt
named something unresolvable fell through to the ordinary machine link, and for
a marker PC the machine number IS the operation. It took an active link to a
record that can only have one holder while several markers share it, and the
warning said "not linked". The previous behaviour minted a twin; this traded
that for a contested operation. None now means "not a marker PC" and is the only
answer that lets the machine link run; the response normalises it away so the
API shape is unchanged.

A DORMANT CHALLENGER WAS PROMOTED BY DELETING A FILE. Recording a challenger
dormant leaves a row that the next cycle finds as `reuse` and reactivated with
no incumbent check - so a second PC took a live device by its enrollment file
becoming unreadable. Both reuse branches re-check incumbency now, which is what
_sync_machine_link always did.

AN INCUMBENT UNDER ANOTHER COLLECTOR LABEL WAS INVISIBLE. Incumbency was queried
on our own label, but on a CMM the instrument IS the reported bay, so the
incumbent's link is the machine sync's row. A second PC naming that instrument
found no incumbent and linked actively: two live holders of one instrument, each
invisible to the other. Incumbency now counts any collector-owned label. A row
made BY HAND carries none of them and is still excluded - a person's link is not
the collector's to archive.

THE NETWORK FORM LOCKED OUT THE ROWS IT NEEDED TO FIX. Asset number is disabled
while editing, correctly, but the payload is built in script so the blank was
still sent - and the new server-side guard rejects it. A device with no asset
number could not be saved at all, and the field could not be typed into. It now
unlocks only for a record that loaded without one, with a hint saying why.

"2 in 4.0d" WAS THE LABEL LYING. Replacements are counted across the whole
history window; basisdays is only how long the current cartridge has been in.
Joining them with "in" claimed two changes inside four days - the exact shape
reported as unbelievable, except here the data was right. Now "2, this one 4.0d".

Two toner dips the same review found:

A MULTI-POLL OUTAGE STILL MINTED A PHANTOM SWAP. Only single readings were
dropped, so 90, 0, 0, 90 survived and 0 -> 90 scored as a change. Dips of any
length are handled now. One bad sample stays a candidate whatever the polling
cadence, because a reading is an instant; several consecutive low ones only
count as one outage when they are close together, since days at zero is a real
empty period. That time bound also separates an outage from a swap, ordinary
consumption, and a second swap, which have the same shape in levels alone.

THE DIP FILTER ATE REAL SWAPS OF NEARLY-FULL CARTRIDGES. Recovery was tested
with an absolute difference, so 95 then 5 then 100 read as a recovery because
100 and 95 are close, and the swap evidence was deleted. Toner only falls: a
recovery comes back at or BELOW where it left, a new cartridge comes back
higher.

The review also proved by reverting each feature that the previous tests did not
pin the median burn rate or the near-full rule - both passed with the bug
restored. Verified by the same method that all four toner behaviours now fail
when reverted, and the burst assertion is tight enough to tell 0.2 from 0.88.
2026-08-21 08:49:38 -04:00
cproudlock
c648bdf560 labels: one module knows how to draw a code, seven views stop guessing
Three core pages and four plugin pages each imported qrcode and jsbarcode
directly, and each carried its own answer to the same questions: what margin,
what width, which error correction, how big a module must be before a scanner
can read it. The answers had already drifted - margin 0 in one place and 2 in
another, width 150 against 160 - and on a label that is the difference between
a sticker that scans and one that does not.

frontend/src/utils/codes.js owns it now: the label-stock presets, the quiet-zone
and margin defaults, CODE128 with no printed value, and the printer-resolution
arithmetic that only the Tech Tools generator had. A view passes what is
specific to its own label and nothing else - MachineBadge still asks for CODE39,
because the badge readers predate the shop-floor scanners and decode nothing
else, and that is exactly the kind of thing a call site should say out loud.

views/print/qrLogo.js is folded in rather than left as a second half-shared
helper that only some of the pages reached into.

The check script now fails a build that imports either library outside that
module. Without it this re-forks within a month: the next label page starts by
copying the nearest existing one, which is how it happened the first time.

Tests cover the part no amount of looking at a screen verifies - a QR that
looks fine at 96 dpi on a monitor can be unreadable at 203 dpi on half-inch
stock.
2026-08-14 14:03:49 -04:00
cproudlock
f66763e6df tools: a Tech Tools section, starting with codes on label stock
A place for the small utilities a technician reaches for at a bench. The
plugin owns no API and no tables: every tool runs entirely in the browser,
so an air-gapped site gets them for free and a bad network cannot break
them. Adding the next tool is a view, a route, and one entry in tools.js.

First tool is a barcode/QR generator. Content is typed text, a URL, or a
CSV (content,label,copies - quoted fields and an optional header both
handled), so a batch of a few hundred is one paste. Label stock is
adjustable in inches with five presets, and the code renders to an SVG
data URI rather than a PNG: a bitmap gets downscaled to label size and
smears the module edges a scanner reads, where SVG rasterizes at the
printer's resolution with hard edges.

It also carries the dot-grid rule that is easy to get wrong by eye. A
thermal head cannot render a fraction of a dot, so a code sized off the
grid gets uneven modules; pick a DPI and the page says what the current
size lands on and what to use instead. The quiet zone is blank label
rather than white baked into the code, so it can be tuned - and it applies
to CODE128 too, which needs clear space at each end and was letting bars
run into the caption.

Tech Tools is the first bundled plugin that owns no schema, which two
guards did not model: it belongs in the universal installer profile, and
upgrade-all reports it 'no-migrations' where every plugin was assumed to
report 'ok'. The migration test now asserts that status explicitly for
schema-less plugins, so a table-owning plugin whose chain went missing
still fails.
2026-08-12 11:43:48 -04:00