A tag can now be linked to a filament type, not just to one spool of it. The
tag table already carried filament_id and target_type for exactly this, so
there is no migration.
API, all additive:
POST /filament/{id}/tag link; 409 if anything else holds the UID
DELETE /filament/{id}/tag/{uid} unlink
GET /filament?tag=<uid> exact lookup on the normalized UID
Filament.tags also inside every spool's embedded filament
TagScan.matched_filament_id always present, like matched_spool_id
TagConflictMessage.filament_id names a filament holder on a 409
One UID still identifies one thing across both kinds: a spool's tag cannot be
linked to a filament or the reverse, and neither ?tag= lookup answers with the
other kind. The scan debounce key now includes the kind, since spool 7 and
filament 7 are different answers.
SpoolTag and SpoolTagParameters are renamed Tag and TagLinkParameters while
they are unreleased, so the schema does not describe filament tags as spool
tags forever.
client_v2: the filament inspector gets the Tags section, the link dialog names
and moves a tag held by either kind, and scanning a filament's tag opens that
filament when auto-navigate is on.
Tag having two delete-orphan parents was checked on SQLAlchemy 2.0.51: new tags
flush, unlinking deletes the row, and deleting either parent cascades.
Proven on sqlite, postgres, mariadb and cockroachdb: 558 integration tests
each. Unit tests, svelte-check and the Playwright tag specs pass.
The reader-to-Spoolman contract is one ordinary HTTP POST, which means a
complete conforming reader fits in a hundred lines of stdlib and needs no NFC
hardware at all. That is worth having as a tool: pairing, auto-navigate and
filling the link dialog are otherwise only reachable by someone who owns an
ESP32 and has already flashed it.
Interactive by default, because the thing you want while clicking around the
UI is to tap the same tag again, or a new one, without retyping a command.
Blank input re-taps the last UID -- which is also how to watch the server's
de-duplication behave, since it answers every repeat while broadcasting only
the first, exactly as it does for a real reader re-reading a tag that is
sitting on it.
Writes nothing and deletes nothing, unlike its neighbours in this directory:
a scan is an event, not a record.