withFiles was declared inside the try block but read by the wait after it,
so every check threw ReferenceError once it had imported a single model.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A stack manager that deploys from its own directory resolved ./data to an
empty folder, so Postgres initialised a blank database and the install
looked wiped. The compose defaults are absolute now, and survive a sync
that deletes an untracked .env.
The backup script asks the running container where /app/uploads comes from
rather than trusting a setting, refuses to mirror an empty folder, and
won't write a dump of an empty library over backups that have one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The backup script missed DATA_DIR when the line carried an export prefix,
quotes, padding or a CR, and silently fell back to ./data — dumping first
and only then discovering there was nothing to mirror. It now strips all
that, prints the paths it's using, and stops before writing anything if
the files folder isn't there.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A dated pg_dump plus an rsync mirror of the model files, with the stack
running. The dump is read back with pg_restore -l inside the container
before it replaces the day's file, so a bad run leaves older backups
alone. Older dumps are pruned after KEEP_DAYS.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DATA_DIR takes the files and PGDATA_DIR the database, so the database can
stay on a filesystem that supports ownership when the files live on a disk
that doesn't. The example .env points both at /Volumes/Data/printhive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The database and files now live under ./data (DATA_DIR to move them), so
they can be seen, backed up and restored without going through Docker.
tools/volumes-to-folders.sh copies an existing install's named volumes
across; it only copies, leaving the volumes in place.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The challenge was detected but treated as an ordinary download failure, so
the message didn't say that only you can clear it. Now it stops the run,
is remembered centrally, and watches skip downloads while it stands —
metadata still imports, and models stay partial for "Get files". The
Watched page shows a banner with a link to MakerWorld and an "I've solved
it" button; a successful download clears it too. Detection also covers a
challenge page returned where JSON was expected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Camera button on each card opens the printer's chamber view, served
through Printhive so the access code stays on the server. One connection
per printer however many people are watching, dropped as soon as the last
viewer closes it; the latest frame goes to a new viewer straight away.
Nothing is recorded.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
P1 and A1 series printers serve JPEG frames on port 6000 over TLS, behind
the same access code. This adds the connection, a frame reader, and
POST /api/printers/:id/camera/test, which waits for one picture and says
what arrived — enough to find out whether the camera answers before any
viewer is built.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The live connection reads tray_now (unit × 4 + slot, 254 external, 255
none) and works out the unit by id or, failing that, by position — so the
A2L's AMS lite numbered 16 is handled. Cards say 'Using' (or 'Loaded' when
not printing) with the filament's name and slot, mark the slot in the list,
and draw a tube in its colour from that spool to the toolhead, with a
moving flow while printing. The external spool is drawn when it's in use.
Debug reports include tray_now and tray_tar.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a model adds /model/<id> to the address (#collection/12/model/34),
so a refresh or bookmark reopens it and Back closes it. Moving from one
model to the next doesn't add Back steps, and closing takes back the step
opening added. A model that's gone is dropped from the address.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each page has an address (#filament, #printers, #collection/12), so a refresh
or bookmark returns to it and Back/Forward move between pages. The search
and filter bar is hidden on the Filament page, where it did nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A printer mid-print reports every second, and each report restarted the
five-second wait, so a printing printer's spools never synced. Only a change
in what's loaded restarts it now. Printers without a chamber sensor report
5 °C, which is no longer shown.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each printer now has one MQTT connection that stays open, merging the
partial reports printers send while printing. Cards poll every 3 seconds
(paused while the tab is hidden) and show state, file, progress with time
left and finish time, layer, nozzle/bed/chamber temperatures, speed
setting, and print error and HMS codes. A spool change syncs the Filament
page and the card's drawing within seconds; a full report is requested
every 10 minutes as a backstop, replacing the 10-minute probe loop.
"Test & sync" uses the open connection rather than making a second one
(?debug=1 still makes a short separate one). Connections are reconciled
when printers are added, edited or removed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each sync keeps what the printer reported, slot by slot (material, colour,
empty, tagged — nothing that identifies a spool). The card's drawing colours
in the first AMS unit's spools, dashes empty slots, and lists every slot and
the external holder underneath with its colour name. "Test & sync" redraws
the card with what it just read.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Printers move out of Settings into their own table and page: each has an
optional nickname (the model is shown when blank), a model from the Bambu
lineup with a line drawing that follows the selection, an AMS type
suggested from the model, and its address, serial and access code. Access
codes are never returned to the browser. The printer set up in Settings is
migrated automatically and its settings rows removed.
Background sync covers every printer. Grams left are filled in only from an
AMS that measures (AMS, AMS 2 Pro, AMS HT), never from an AMS lite.
Spools now show their RFID tag ID, product code and filament code, and say
which printer they're loaded in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every Bambu spool reports an RFID identity — a spool id and a tag id, both
unique — so a printer's report can now keep the Filament page in step: a new
tagged spool is created, named and coded from the bundled filament list; one
already known is moved to its current slot; one you added by hand before the
printer was connected is adopted rather than duplicated; and one the printer no
longer reports is marked as taken out. It runs on "Save & test connection" and
every ten minutes in the background, still only reading from the printer.
Untagged spools are listed, not stored: without a tag, one spool of a colour is
indistinguishable from the next, so guessing would mean duplicates.
The planning is a pure function, tested against the cases that would cause
duplicates. The database side was exercised separately against a real
Postgres: three successive reports created, moved and unloaded spools as
intended, and a deliberate duplicate was refused by the partial unique index.
The amount left isn't taken from the printer yet: an AMS lite only reports full
or empty, so it's left blank until printers can say which kind of AMS they have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tray reader kept material, colour, profile and amount but dropped the
spool's RFID fields, so the printer test couldn't have shown a filament's
serial even if the printer sent one. It now keeps tray_uuid (the spool),
tag_uid (the tag) and tray_id_name where the firmware provides it, and turns
the all-zero values sent for untagged spools into "none" rather than a fake id.
The printer test also takes ?debug=1, which returns one loaded tray exactly as
the printer sent it, to see which fields a given printer and firmware provide
before designing around them. It's filament data; no credentials are in it.
Also notes that an AMS lite's "remain" is full-or-empty rather than a
measurement, which the next change needs to account for per printer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings → Printer takes the printer's address, serial number and access code
(the code stored as a secret, like the other tokens). "Save & test connection"
connects to the printer's own MQTT broker on port 8883, subscribes to its
reports, asks once for a full report, and listens for a few seconds — then
says what it heard: its state, and the filament in each AMS slot or on the
external spool holder, with how much is left where the printer knows.
Nothing sent to the printer changes what it is doing; the only thing published
is the request for a full report. If the printer is in cloud mode and treats
even that as a write, the test says it received nothing rather than failing
silently. The access code never leaves the server.
Adds mqtt as a runtime dependency — server.js now loads it at startup, so the
lockfile has to travel with this change.
Not verified against a real printer yet: the printer's ports answer on the
network, but no connection has been made with its credentials.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found on the live library: setting slot 2 of a four-colour bookmark to "any
colour" made it take the one Jade White spool, leaving slot 4 — which asks for
#FFFFFF — with nothing, and the verdict stuck at "needs 3 colours" when it
should have improved to 2.
The assignment sorts candidate pairs by colour distance and scored a colourless
slot 0, a perfect match, so it always claimed a spool ahead of a slot that
needed that exact colour. Colourless slots now sort last and take whatever is
left over.
Fixed in both the server and the browser copy, which mirror each other, and
pinned by a test built from the reported case.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some prints don't care about the shade, and some change with the order. Each
slot in the editor now has an "any colour" box: ticking it greys out the swatch
and saves the slot without a colour, which the matcher fills from any spool of
that material. A slot added by hand starts this way, since adding one usually
means you care about the material rather than the shade.
An unparseable colour already became "any colour" rather than being refused;
that stays, and is now covered by tests along with the shapes the checkbox
actually sends — null, empty string, and no colour key at all.
Worth knowing when reading the printable count: ticking "any colour" makes a
model easier to satisfy, so the count now partly reflects your decisions rather
than only what the project files ask for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The slot list comes from the project file, which is sometimes wrong and
sometimes not what you intend to print. "Edit slots" turns it into fields:
material and colour per slot, with slots you can add or drop. Saved slots are
marked as yours and shown with a "your list" badge, and "Use the project's
list" throws the override away and re-reads the 3MF.
Edits have to outlive the background reader, which is the part that would fail
silently: the reader only picks up models with no status, and reread() now
skips 'edited' rather than nulling everything. Verified on a real model — the
edit survived a reread plus a full worker pass, and the revert restored the
project's own four slots exactly.
Input cleaning moved out of the route into lib/filaments.js so it can be
tested: a slot with no material is dropped, and a colour that isn't a proper
hex becomes "any colour of that material" rather than being rejected.
Not verified: none of this has been seen rendered — the local preview browser
has no session.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seventeen PLA filaments are #000000 and eleven are #FFFFFF, so slot 1 of a
bookmark came out as "Aero Black" — true, but not what anyone calls it. Naming
now prefers Basic, then Matte, before specialist types.
Also puts a space between the "none" badge and the nearest-spool hint, which
were running together as "nonenearest: Jade White".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adding a spool no longer means typing a hex code: choose "PLA Basic — Jade
White (10100)" and the material, colour name, swatch and filament code fill
themselves. Slot colours on the model page are named where they match, so a
slot reads "#586579 · Basic Blue Gray" instead of bare hex.
The filament code is stored per spool. It isn't decoration — the AMS reports
filament IDs, so this is what a later read-only printer connection will match a
loaded tray against.
db/bambu-filaments.json holds Bambu Lab's 325 filaments (material, type, colour
name, code, colours), included by kind permission of 3dfilamentprofiles.com,
whose terms otherwise reserve the data. The permission and source are recorded
in the file itself as well as the README, so nobody reuses it by accident.
Naming uses a much tighter threshold than matching (12 against 50): identifying
a colour and substituting for one are different questions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Counting slots fixed one half of the problem; this fixes the other. A slot is
now filled by a spool of the same material whose colour is close to what the
slot asks for, one spool per slot, best matches assigned first. So four PLA
slots in black, blue-grey, grey and white are not satisfied by four spools of
white — only the white slot is.
Closeness is redmean distance with a tolerance of 50, which treats genuinely
near shades as the same colour while keeping #000000 and #161616 (about 66
apart) distinct. Slot colours arrive as #RRGGBB and #RRGGBBAA — the alpha
channel is parsed off rather than making the colour unreadable.
The model page now lists every slot: its colour, which spool fills it, and the
nearest thing on the shelf when nothing does.
Two things this fixes in code I wrote an hour ago: renderDetailPrintable still
called a function the rewrite removed, so opening any model would have thrown;
and the README claimed colours weren't matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>