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>
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>
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>
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>
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>
Reported from the library: a four-colour AMS bookmark showed as "printable now"
against a single spool of Jade White. The matching collapsed the project's four
slots into the set of materials it used — {PLA} — and then asked only whether a
PLA spool existed. A sixteen-colour model would have passed the same way.
Ignoring colour is right: printing a bookmark in red instead of white still
prints. Ignoring slot count is not: four slots need four spools loaded. Matching
now counts slots per material and reports the shortfall — "needs 3 more PLA",
and on the model page "Needs 4 × PLA. You have 1 of 4 PLA."
The tests carried the same mistake, asserting that one spool satisfied a
multi-slot project, so they are rewritten around the reported case.
Also documents the feature in the README, which had no entry for it at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It claimed an account may only download "a couple in a row". Measured today:
67 models downloaded in a row, spaced about 20 seconds apart, with no refusal
at all. The refusals happen when downloads are fired a few seconds apart, as a
watch check used to do — so it is a rate limit on bursts, not an allowance.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
MakerWorld allows an account only a couple of downloads in a row, so a check
that tried to fetch files left most models empty. A check now imports each
model's page — name, description, picture, licence — and marks it 'partial';
"Get files" pulls the files for the ones you actually want to print.
Verified against a public collection: 11 models imported with descriptions and
thumbnails, no files fetched, no MakerWorld token needed for the check.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured against the live site: FlareSolverr reports no challenge on that
endpoint and an unauthenticated call gets a normal reply in 3s, so this is
MakerWorld limiting an account to a couple of downloads in a row, not a
Cloudflare page. Two models retried three times each still came back empty —
the retry only made every failure a minute slower.
Keeps the classification, so a refusal is named for what it is and the model
is flagged as still without files.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A run of downloads gets challenged by Cloudflare partway through: the answer
comes back as an ordinary reply, so nothing retried it and the model landed
with only its metadata — looking, on the watch page, like a success.
resolveDownload now recognises a challenge and gives FlareSolverr's browser a
couple more goes; what still fails is recorded as 'partial' and shown with its
reason and a "Get files" button, which clears the flag once the files arrive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- A collection page lists only its first ~20 models. Checks now page through the
same load-more call the page uses (offset + a shared seed, 64 at a time, up to
~640 models), so a watch sees the whole collection.
- A private collection read without a valid token comes back as a placeholder
(id -1, no models), which was being treated as an empty collection. It now
reports the token problem instead.
- Adding a watch starts a check, and "Check now" could start a second one
alongside it, importing models twice. One check per collection at a time now,
and a check interrupted by a restart no longer looks like it's still running.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A watch reads the collection page through FlareSolverr carrying your token, so
private collections work, and imports the models it hasn't seen into a Printhive
collection of the same name (optionally AI-tagging them). Checks run on your
chosen interval, a few models at a time with pauses between, and each design is
remembered so nothing is imported twice. A refused token stops the check and the
Watched page offers to update it; models removed on MakerWorld stay here.
The import endpoint's work moved into importFromUrl() so watches reuse it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When a download fails because MakerWorld refused (or is missing) the auth
token, the error carries reason makerworld_token. The import and "Download
files from source" then open an "Update your MakerWorld token" prompt with
steps to copy the token cookie; saving it retries the download straight away
and says if MakerWorld still refuses it. A missing FlareSolverr URL is reported
separately, since a token alone won't help.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Runs can now do any mix of: tags & descriptions, clearer names, and
collection suggestions. A run without tags doesn't mark models as tagged.
- Collection ideas reuse your collections and earlier ideas where they fit and
wait on the AI page, grouped by name, until you create/add or dismiss them.
Dismissed ideas aren't suggested for that model again; spelling variants
group together under the capitalised spelling.
- A reply that isn't the JSON asked for is asked again once; both requests
count towards the run's tokens and cost.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A run option asks the AI for a clearer name only when the current one is messy
(file-name leftovers, typos, site boilerplate). The previous name is kept, the
run feed shows what was renamed, and the model page offers Undo. Giving a model
a name yourself clears the undo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- AI tagging through any OpenAI-compatible API: DeepSeek by default, or
OpenRouter, OpenAI, or a local server (Ollama, LM Studio). Settings → AI
with presets, prices and "Save & test connection"; the key never leaves
the server except to the provider.
- Runs over untagged models, all models, or one model from its page: live
progress, tokens and cost, stop/resume (retrying failures), an optional
spending limit, and paused rather than resumed after a restart. Adapts to
models without JSON mode or image input; stops on a rejected key or no
credit. Tags are only added; descriptions filled only where empty unless
asked.
- Search by meaning: local bge-small-en-v1.5 embeddings (transformers.js),
indexed in the background whenever a model's text changes; Words/Meaning
switch on the search box. Stored as REAL[] in Postgres, no pgvector needed.
- Automatic tags: strict whole-word rules (part types, mechanisms, materials,
techniques) over names, file names and archive contents, plus your own
word -> tag rules; on/off (optionally removing its tags) and re-apply.
- Tags remember where they came from (user / rule / ai); edits keep that,
rule and AI passes never touch your own tags, and the model page marks them.
- Docker: ships the embedding model and only Linux onnxruntime binaries.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Settings → Slicer: pick Bambu Studio, OrcaSlicer, PrusaSlicer, Cura, a
custom open-link template, or plain download (remembered per browser)
- Bambu Studio / OrcaSlicer launch via their URL schemes with a 10-minute
signed download link (/dl/<token>/<name>) that needs no session cookie
- PrusaSlicer and Cura fall back to a download (they can't open our links)
- 3MF projects report which app saved them; opening one in a different
slicer asks first
- "slicer" action on every file row and archive entry
- Drag model cards (file, or zip of all files) and file/entry rows out to
the desktop via Chromium's DownloadURL
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The background worker now also gives models without a thumbnail one:
an embedded 3MF/plate preview, else a picture uploaded with the model,
else a 3MF preview or picture inside one of its archives, else a render
of an STL/OBJ/3MF (loose or inside an archive).
- tools/render_thumbnail.py renders without a GPU: meshes are simplified
per distinct part (the 8M-face lion project renders in ~8 s / ~200 MB),
3MF projects show their first plate in filament colours, and only
camera-facing faces are painted (inside-out meshes and mirrored parts are
re-oriented first).
- models.thumbnail_status stops retries when there's nothing to use; adding
files clears it. The browser now only renders FBX/STEP/3DM thumbnails.
- The library checks back while fingerprints or thumbnails are pending and
refreshes when they land.
- Python sidecar needs pillow and fast-simplification; compose keeps the
archive extraction cache in a volume. README covers the phase 2 features.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Fingerprint every file in the background: SHA-256 for all formats plus a
trimesh geometry fingerprint for STL/OBJ/3MF, so renamed, re-exported or
converted copies are still matched. Backfills existing files on startup.
- Duplicates view groups models that share files (exact copies vs same
shape), recommends which to keep, merges tags/collections/notes into the
kept model and trashes the rest; groups can be dismissed.
- Deleting now moves models to a restorable trash; files are only removed
from disk on permanent delete or when the trash is emptied.
- Per-model notes with autosave.
- Search across names, notes, tags, files and designers; filter by format,
source and license; include/exclude tags with any/all matching; sort and
group by source or format. Preferences persist per browser.
- Add node:test unit tests for duplicate grouping and fingerprints.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>