Brings in the A4 (millimetre) report designer alongside the existing
matrix editor, migrates the frontend from react-scripts to Vite/Vitest,
and fixes four defects found while using the A4 band tools.
Layout — layout.js now implements the standard banded-report model:
everything prints where it was drawn, and a band growing per data row is
the only thing that moves anything. Previously `paginate` re-flowed every
band from the top margin, so a band placed halfway down the page
previewed at the top, and `header` was treated as a page header and
hoisted above the statics drawn before it. A header is a COLUMN header:
it prints in place and repeats only on a continuation page. Statics now
take part too — one drawn below a band keeps its drawn gap as that band
grows, and follows onto the next page rather than vanishing when pushed
past the bottom margin. The displacement is keyed on the band's design
bottom, not on an instance's laid-out y: every row of a band shares one
design position, so keying on the instance measured the gap from row 1
and let the static overlap rows 2..n.
Band children — a child's x is relative to its band, as its y already
was. layout() emitted it raw, so a field printed measured from the paper
edge instead of the band's left margin. Instances now carry bandX.
Band placement — a band's x comes from the page margins, so creation
takes only the click's Y (the factory's x was being overwritten by the
spread order) and dragging is locked to the vertical axis. Bands render
as a strip, with top/bottom rules only, matching the matrix editor.
fromApiReport reflows on load so reports saved with a stray x straighten
themselves. Matrix bands are constrained to vertical too.
Drag lifecycle — useElementDrag re-subscribed its document listeners on
every render during a drag, because the effect depended on an unmemoized
onDrag. Each re-subscribe removed the mouseup listener before adding the
next, measured as add/remove/add/remove/add across two mousemoves, and a
release landing in that gap was lost: the element kept following the
cursor until the next click. Snapping made it routine, since a held
guide stops x/y changing and leaves the guide overlay's render sitting
between the move and the release. onDrag and dragData now go through
refs so the effect depends on [isDragging] alone.
Note: the matrix editor has no test coverage, so its band constraint is
verified by reading only. The A4 suite is 201 passing tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The setting no longer exists: it was removed along with the bridge's unused
Django client. The surrounding section already states the bridge is
independent of Django, so this line was the one place in the file implying
otherwise.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The bridge reads the COM port and streams SSE to the frontend; it never
talks to Django. backend_client.py (a Django API client) and config.py
(which held BACKEND_URL / API_ENDPOINT) were imported by nothing - app.py
calls load_dotenv() and reads os.getenv directly, and the PyInstaller spec
builds from app.py alone.
Their presence implied a coupling that does not exist, and the stale
BACKEND_URL=8000 in .env read as a misconfiguration rather than an inert
value.
Also drops the matching backend settings from .env.example, the now
unnecessary 'requests' hidden import from the spec, and the two files from
the SETUP.md tree. requests and urllib3 remain in requirements.txt - no
bridge module uses them now, but pruning dependencies is a build concern
left for a separate change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md described the backend on 8000 and the frontend on 3000, but
frontend/.env has overridden these to 8002 and 5172 (matching .env.example
and the CORS defaults in settings.py). Following the documented ports leads
to checking servers that are not there.
Also records the CORS_ALLOWED_ORIGINS override and the real default origin
list, and notes that the frontend port comes from frontend/.env. The serial
bridge's BACKEND_URL is left at 8000 because that is what serial_bridge/.env
actually contains - it points at a port Django no longer listens on, which
is a config issue to fix separately, not a documentation error.
Adds .claude/screenshots/ and .playwright-mcp/ to .gitignore so local
verification output stays out of the repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A selector is a nomenclature whose value decides which other nomenclatures
appear beneath it: picking "Ship" reveals the ship fields, "Warehouse"
reveals the warehouse ones. Previously every vehicle nomenclature rendered
at once, so the operator had to know which were relevant and a vehicle
could silently carry both a ship and a warehouse.
Visibility is one rule - a nomenclature is hidden if some selector option
reveals it and that selector's value is not the revealing option - kept in
nomenclatures/visibility.py and mirrored in services/nomenclatureVisibility.js.
A nomenclature no selector references always shows, so installations without
selectors are unchanged.
The serializer, not the UI, enforces the invariant: a value for a hidden
nomenclature is rejected, as is a selector value that matches no option.
The frontend clearing values on switch and pruning hidden keys before save
is convenience on top of that.
Selector values join lookups in the quick-fill combination projection, so
applying a saved combination sets the source and its revealed fields in one
click rather than collapsing "Ship + cargo" and "Warehouse + cargo" into
one indistinguishable row.
Configuration rejects nesting (a selector revealing another selector) and
double-reveal (one nomenclature revealed by two selectors, leaving its
visibility ambiguous). The admin UI filters both out of the reveal list as
well, so an invalid configuration cannot be built in the first place.
Note: introducing a selector over existing data hides values already
entered - vehicles predating the selector have no selector key, so anything
it reveals becomes invisible until a source is chosen. The values remain
stored. Worth a data check before configuring the first selector.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Plans record the decisions behind each feature and, for the protocol and
licensing work, what remains unvalidated against real hardware.
~WRL0003.tmp was a Word autosave artifact committed by accident.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main.jsx now parses readings through the selected protocol rather than
an inline regex, shows one Measure button instead of separate tare and
gross, and colours the live weight green when the scale reports stable
and red while it moves. A reading with no stability marker counts as
unstable, so an unrecognised format fails safe.
While only one weighing exists it appears as "First weighing" rather
than filling in the Tare card, because a lighter second reading can
still move it to gross and a value visibly jumping between cards would
read as a bug.
Header gains admin links to the scale monitor and licence screens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Admin screen shows the machine fingerprint to send for a licence key,
installs a pasted key, and lists connected stations. A persistent banner
warns all users during the grace period.
The browser sends the station heartbeat rather than the bridge, because
the bridge deliberately has no dependency on Django; it already talks to
both. Two tabs on one station share a slot, and a station reconnecting
at the limit reclaims its own slot so a brief drop cannot lock it out
behind its own expiring registration.
license_tool.py is the only thing that can sign a licence - the
application can only verify. The private key it generates is gitignored
and must never ship: if it leaks every licence becomes forgeable, and it
cannot be rotated without reissuing to every customer, since the public
key is compiled into shipped binaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bridge becomes pure transport: it opens the port, writes a poll
string it is given at runtime, and streams raw lines. Protocol
definitions live in Django and parsing happens in the browser, so a new
scale type never means a new binary on the station PCs. The poll loop
stays in the bridge because a browser cannot be the heartbeat - hidden
tabs get throttled and a closed tab would stop the scale.
Every payload now carries a hex field. Toledo Continuous sends STX and
bit-packed status, which UTF-8 decoding silently corrupts. Fixed-column
protocols need the opposite care: a space can mean balanced, so the
trailing whitespace must survive.
Parsers for the simulator, fixed-column, Toledo Continuous and SICS
return an explicit error rather than a guess on a frame that does not
match, since a silently wrong weight on a weighbridge is worse than no
weight. Toledo and SICS are written from documented structure and have
not been validated against real hardware.
The admin monitor shows the raw stream as text and hex, sends one-off
probe commands, and builds fixed-column protocols against captured
lines. Spaces render as middots because in some protocols a space is
the stability flag.
The test writer now simulates a weighing cycle - idle, ramp, balanced,
drop - alternating empty and loaded trucks, quantised to the 20kg and
50kg divisions real truck scales use.
Bridge logging rotates and per-reading lines drop to DEBUG; the old
config had grown two log files totalling 368MB.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Shows previously-used combinations of lookup nomenclature values above
the Additional Data fields in both the new-vehicle and edit forms.
Clicking a row merges the combination into the current values, so
free-text fields the operator already typed are preserved.
NomenclatureContext now fans extra_combination events out to
subscribers; without that they hit the unknown-object warning and were
dropped, so deletes would not have reached other open tabs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four related backend features. They are committed together because they
interleave in api/views.py and api/serializers.py - one hunk holds the
combinations ViewSet, the licensing helpers and LicenseView - and
splitting by hunk would produce commits that do not import.
ExtraCombination records previously-used combinations of lookup
nomenclature values so an operator can refill the Additional Data fields
in one click. Deduplicated by a canonical order-independent hash, since
JSONField equality compares serialized text on SQLite and so depends on
key order.
Weighing replaces set_tare/set_gross with a single measure action that
assigns by weight - lighter becomes tare, heavier gross - because trucks
arrive both loaded and empty. A second, lighter reading moves the first
to gross carrying its own timestamp, user and manual fields, so
provenance follows the value rather than the slot. Manual entries record
who, when and why, gated on the previously unenforced can_measure and
can_manually_measure flags.
ScaleProtocol stores how to read each indicator, so a new scale type is
a configuration change rather than a new binary on every station PC.
Licensing is offline and node-locked: the server verifies an Ed25519
signature against a fingerprint of four hardware components, tolerating
one change, and counts concurrently connected stations. Sites are
air-gapped, so there is no activation server. A 14-day grace period
keeps an unlicensed or newly-invalid install working, because a licence
check must never stop a weighbridge without warning.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nomenclatures can be reordered by dragging in the left-hand list, which
PATCHes sort_order on the rows that moved. PATCH rather than PUT, since a
full update recreates the nested fields collection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Backend now runs on 8002 and the frontend dev server on 5172, with the
proxy, CORS origins and the run_*.bat launchers updated to match. The
launchers also activate the venv so they work from a clean shell.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>