25 Commits
Author SHA1 Message Date
Donkie 332b3e0fe4 Revert "Give phase 2 somewhere to record a tag's instance identity"
This reverts 9aea271.

The column bought one thing, that phase 2 would need no migration, and
cost something worse: it commits the schema to a shape nobody has run a
decoder against. Width, and above all the choice not to make it unique,
were read off a design document rather than off working code, and
goeland86's codec in #880 computes an effective_instance_uuid that may
not mean the same thing. Getting that wrong leaves us altering a
populated column on four databases, which is the exact operation the
"settle the shape before release" rule exists to avoid.

That rule earns its keep for target_type, target_value and filament_id,
where retrofitting a discriminator or a nullability change is painful.
Appending one nullable column later is not painful, so phase 2 can add
it when there is something to write into it.
2026-08-20 19:15:35 +02:00
Donkie 9aea271ee5 Give phase 2 somewhere to record a tag's instance identity
OpenPrintTag derives an instance_uuid from the brand uuid plus the tag's
hardware UID, and phase 2 needs it stored so that rescanning a tag updates
the spool it already created instead of making a second one.

Added now, into the migration that has shipped to nobody, because the
table is the expensive half to change once it is populated on four
databases. Nothing writes the column yet, which is the same position
filament_id is already in.

Indexed for that lookup and deliberately not unique. uid already enforces
that one physical tag identifies one thing, and copying a tag's payload
onto a second sticker can legitimately put one instance identity on two
rows pointing at the same spool, which a unique constraint would reject.
2026-08-19 22:47:37 +02:00
Donkie d43552bd26 Generalise the tag table beyond spools before it ships
A tag maps a UID to "the thing to bring up", and only some of those things are
spools -- or rows at all. Locations are the case that settles the shape: a
location in Spoolman is a String(64) on Spool, not a table, so a tag meaning
"show me Shelf A" can never be a foreign key.

Getting this wrong is asymmetrically expensive. Renaming the table, dropping
spool_id's NOT NULL and adding a discriminator all mean altering a populated
table on four databases; adding an endpoint or a target kind later is additive
and free. The migration has shipped to nobody -- it is not on master and
v0.26.1 predates it -- so the schema is settled now and the unshipped revision
is rewritten in place rather than chased with a second one.

spool_tag becomes tag:

  uid           unchanged, still globally unique -- one physical tag, one thing
  target_type   which kind of target is in force, rather than leaving readers
                to infer it from whichever column is non-null
  spool_id      nullable now; targets that ARE rows keep real foreign keys, so
  filament_id   the database still cascades deletes and refuses orphans
  target_value  targets that are not rows; a new kind of this sort costs no
                migration at all, only a new target_type string

Exactly-one-target is enforced in database/tag.py rather than by a CHECK: no
migration in this tree uses one, and MySQL below 8.0.16 parses and silently
ignores them, so a constraint here would be real on three databases out of
four. Single write path beats a constraint that is only sometimes a constraint
-- the same argument, in the same module, as normalize_uid.

filament_id gets a column and a foreign key but no ORM relationship: nothing
writes filament tags, and a selectin collection on Filament would add a query
to every filament listing for rows that cannot exist. ON DELETE CASCADE holds
integrity until the feature that needs the relationship arrives.

No API surface changes. POST /spool/{id}/tag, ?tag=, the spool response's tags
and matched_spool_id are all exactly as they were; the reads are just explicit
now about wanting a spool rather than assuming the tag they found is one.
TagConflictError's spool_id becomes optional for the same reason, which the
client already handled -- it only offers "move it here" when there is an id.

Proven on sqlite, postgres, mariadb and cockroachdb: 526 integration tests each.
2026-08-15 09:35:13 +02:00
Donkie de533bc74d NFC phase 1 backend: tag identity, lookup, and the scan relay
Implements the backend half of design/nfc-phase-1-tag-identity.md. Format
agnostic: it keys only on a tag's hardware UID, so it works with blank NTAG
stickers, Prusa NFC-V tags, and the Bambu/Creality/Qidi MIFARE tags whose
contents Spoolman can never decrypt.

Answers the question the whole external ecosystem has been working around:
which spool is this physical tag? Every project so far invented its own way to
smuggle a Spoolman id into the tag contents, which fails the moment the tag is
a vendor tag you cannot write to.

Data model: new spool_tag table, additive only, with a unique index on uid.
Many tags per spool, since copying a vendor tag onto a blank sticker leaves one
spool with two UIDs. UIDs are normalized to uppercase hex with separators
stripped, in the database layer rather than a Pydantic validator, so the unique
constraint holds for callers that never arrive over HTTP.

API, all additive:
  GET    /spool?tag=<uid>          exact match on the normalized UID
  POST   /spool/{id}/tag           201, 409 with the conflicting spool id
  DELETE /spool/{id}/tag/{uid}     204
  POST   /tag/scan                 broadcast and return the match
  GET    /tag/reader               readers seen recently
  WS     /tag/scan[/{reader_id}]   the relay

Link and unlink emit the existing spool event, so clients need no new wiring.
Spool responses gain a tags array.

The relay decouples where the reader is from where the user is looking: an
agent POSTs a scan, a paired browser reacts. Scans are ephemeral, never
persisted, and go through a SECOND WebsocketManager instance rather than the
shared one. The shared tree propagates to subscribers at every level, and the
root /api/v1/ pool means "any change", so putting scans there would inject a
novel resource into every existing consumer's stream. An integration test pins
that they never reach the entity websockets.

Repeated identical (uid, reader_id) scans are debounced before the broadcast
only, since readers re-read a tag that is sitting still. The response always
carries the match, so a de-duplicated scan never looks to a device like a
failed lookup.

Migration creates the table and its indexes in one revision, following the
extra-fields tables rather than the doc's suggested split. The DDL/DML split
cockroachdb needs does not apply when there is no data migration, and the 4-DB
suite confirms it.

Verified: 506 integration tests on sqlite, postgres, mariadb and cockroachdb;
263 unit tests; ruff clean.

Frontend, docs, and the #783 action links are not included.
2026-08-13 18:56:35 +02:00
Sanjay Santhanam 7e4d2ee419 fix(api): repair filament rows already stored with a leading #
The validator fix only guards the write path. Instances that already hit #780
keep 500ing on GET /filament because the stored value exceeds the response
model's max_length, and the UI can't repair what it can't list.

Filament.from_db now strips the # and returns None when the stored value still
isn't a valid 6/8 character code, so a single bad row no longer takes down the
whole list. A migration chained from 415a8f855e14 rewrites existing rows one by
one through op.get_bind() rather than with SQL string functions, so it behaves
the same on sqlite, postgres, mysql and cockroachdb. downgrade() is a no-op.
2026-08-02 00:22:27 -07:00
Donkie 8c164ac55f Added backend support for multi-color filaments 2024-05-28 18:49:24 +02:00
Donkie 535fa40ad2 Updated most python dependencies
Primarily FastAPI and Pydantic to v2. Also ruff to latest.

Updated some code to support Pydantic v2
2024-05-23 19:42:27 +02:00
Donkie 1e4c73138c Added external ID fields to filaments and vendors 2024-05-12 21:58:00 +02:00
Matt Gerega 1ec16e64fb Updated calculation of default values during migration and added additional help 2024-04-15 10:44:03 -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 e68f88da2a Added empty spool weight as an option field for the vendor 2024-03-26 20:35:50 -04:00
Matt Gerega 83fd3073ec Fixed formatting 2024-03-26 14:02:17 -04:00
Matt Gerega 1d6830d769 Added initial_weight and empty_weight to spool 2024-03-26 13:55:44 -04:00
Donkie 77ac9d6a5c Added custom extra fields to backend
No integration tests yet, TBD
2024-01-23 18:58:57 +01:00
Donkie 5900f2248a Updated settings migration 2024-01-16 19:20:44 +01:00
Donkie 44da991d47 Added a new backend settings system
This can be used for saving runtime-configurable settings in the database.
2024-01-16 19:17:06 +01:00
pdsccode 44b20fb360 Fixed code formatting 2024-01-07 13:25:05 +01:00
pdsccode df45a47b99 Added db migration for spool prices 2023-12-30 08:44:54 +01:00
Donkie af77bf6c26 Updated migration to support sqlite 2023-08-12 22:33:20 +02:00
Donkie 4cb2542a41 Added support for color_hex alpha channel 2023-08-12 21:32:20 +02:00
Donkie f89cf8ecd4 Added archived field to spools
Resolves #13
2023-07-14 14:19:57 +02:00
Donkie 4e409f6bb6 Added filament color code field 2023-06-01 19:56:24 +02:00
Donkie 27731974fd Added pre-commit 2023-05-28 23:10:36 +02:00
Donkie 7c7ee9296c Added extruder/bed temp override settings
Closes #4
2023-05-28 21:52:51 +02:00
Donkie de1bf05922 Added Alembic for migrations 2023-05-27 23:14:06 +02:00