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>
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>
A Bambu Studio or OrcaSlicer 3MF names the filaments its plates use, but
reading that means shelling out to Python, so it can't be done per page load.
A background pass — in the same loop as thumbnails and fingerprints — reads it
once per model into models.filaments, which turns "what can I print now" into
a comparison of two lists.
Adds a Filament page for the spools on hand: material, colour, brand and grams
left, edited in place. Matching is on material only; a project's exact colours
would make almost nothing printable, and a different colour still prints.
Measured on the dev library: 10 models read with no errors, the two Bambu
projects resolving to four PLA colours each, and no plate images written
because the reader skips --images and queues behind anything interactive.
Not done yet: nothing in the library view uses this — no "printable now" badge
or filter — and the Filament page itself has not been seen rendered, because
the local preview browser has no session.
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>
The Red panda kept failing with "The AI reply wasn't the JSON asked for", and
nothing showed what came back. Now:
- the prompt includes an example object with just the requested fields
(DeepSeek's JSON mode can otherwise send back an empty reply)
- JSON is found inside code fences, notes, or braces after it
- a bad reply is asked again up to twice: with more room when it was cut off,
and the last time without JSON mode; max_tokens starts at 1500
- the error says whether the reply was empty or cut off and quotes its start
- tokens spent on failed attempts count towards the run's cost
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Descriptions sent to the AI are cut to 800 characters. When the cut landed
inside an emoji (stored as two UTF-16 halves), the request carried half of it,
which DeepSeek rejects: "unexpected end of hex escape". Text is now cut before
the emoji, anything already broken in the library is replaced before sending,
and the tags, names, collection ideas and descriptions read back are cut the
same safe way.
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>
MakerWorld answers an expired or missing auth token with {"error": "Please log
in to download models."} and no download link, which was skipped without a
word, so imports and "Download files from source" just ended with no files.
Now the reason comes back: missing settings, a refused token (probably expired),
no printable files on the page, or a failed download. The import screen shows
it, and the Settings hint explains where the token comes from.
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>
- threemf_stream reads Bambu Studio / OrcaSlicer project metadata: plates
(name, objects, preview images), filaments, and each part's type and
extruder. Placements now record which build item and part they belong to.
- convert_3mf.py leaves out negative/modifier/support parts, colours each
part by its filament (a part's extruder 0 falls back to its object's), and
takes --plate N. Geometry is still stored once per (part, colour).
- New threemf_info.py lists plates and filaments without loading geometry
and extracts plate preview images; served via /api/{files,entries}/:id/project.
- /glb?plate=N builds and caches a per-plate preview. Cached GLB names carry
a converter version, so previews made before colours are rebuilt.
- Model detail: plate chips (with the slicer's plate images) under the
viewer — multi-plate projects open on plate 1 — plus "All plates", and a
Filaments section with colour swatches.
- Fingerprints leave out negative/modifier parts, so a project matches the
model it prints.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Archives
- ZIP, 7Z, RAR and TAR/.tar.gz uploads are indexed in the background into
archive_entries (7-Zip compiled to WASM in a worker thread; node-tar for
tar). macOS/Windows clutter and paths that could escape the extraction
folder are skipped.
- Models inside archives are extracted once per archive to hash and
geometry-fingerprint them, so duplicates are found inside packs too.
Pictures, PDFs and readmes are listed but never matched.
- New routes list an archive's entries and serve/convert a single entry,
extracted on first use and cached under extracted/.
Viewer
- Model detail lists what's inside each archive with view/download links;
opening an entry adds a viewer tab.
- New 3D formats: FBX (with sibling textures), OBJ with its MTL and
textures, STEP/STP via occt-import-js, and Rhino 3DM via rhino3dm — the
WASM readers are served from node_modules, loaded on first use.
- Pictures, PDFs and text files (jpg/png/gif/webp/bmp/svg, pdf, txt/md)
can be uploaded and preview in the viewer; a model without an embedded
3MF preview uses its first picture as the card thumbnail.
- Inline file responses send nosniff, and SVGs are sandboxed.
- GLB conversions write to a temp file and rename, so a half-written
preview is never served; a later tab can't be overdrawn by a slow load.
- Better 3MF card image: the project thumbnail or plate 1 render, not a
slicer pick map.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
trimesh's 3MF loader re-reads a whole production-extension part file for
every component that points into it. A Bambu Studio project with 884
components across four ~50 MB part files grew past 3 GB within seconds
and failed both the preview conversion and the geometry fingerprint.
- Add tools/threemf_stream.py: parses each part once with lxml iterparse,
resolves each component to the single object it names, and returns each
mesh once plus its placements. The same project now converts in ~8 s at
~380 MB peak (fingerprinting ~190 MB).
- convert_3mf.py and fingerprint.py use it; the GLB stores each object once
and instances repeats. Stats are computed per placement (mirrored
placements keep a positive volume), without concatenating meshes.
- Run sidecar scripts through one queue (lib/python.js) so a preview and
background fingerprinting never load big meshes at the same time;
previews go first, and 3MFs are fingerprinted one per run.
- Surface the converter's JSON error / timeout instead of "Command failed".
- Tests with a Bambu-style component fixture (shared part, mirrored
placement, decoy object) and STL/3MF fingerprint parity.
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>