Commit graph

24 commits

Author SHA1 Message Date
dlawler489
08d9bdabb1 Note the anti-robot check in the README
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 15:45:09 +10:00
dlawler489
a91c7b4bf7 Show the chamber camera on the printer card
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>
2026-09-18 15:07:22 +10:00
dlawler489
13b5e550a3 Show live printer status over a standing read-only connection
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>
2026-09-17 21:30:13 +10:00
dlawler489
e98edaa9ed Add a Printers page for any number of printers, and show spools' RFID tags
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>
2026-09-17 21:00:43 +10:00
dlawler489
48f564c648 Add spools automatically from what the printer has loaded
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>
2026-09-17 20:39:30 +10:00
dlawler489
93130967af Connect to a Bambu Lab printer, read-only
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>
2026-09-17 20:15:15 +10:00
dlawler489
8f3e63585c Let a slot ask for any colour
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>
2026-09-17 13:19:47 +10:00
dlawler489
2635a1e9f0 Pick spools from the maker's filament list, and name slot colours
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>
2026-09-17 13:01:09 +10:00
dlawler489
7b018aefdb Match slots by colour, not just material
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>
2026-09-17 12:47:18 +10:00
dlawler489
8814e79669 Count slots, not materials: four colours need four spools
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>
2026-09-17 12:41:40 +10:00
dlawler489
8adac1018f Correct what the README says about MakerWorld's download limit
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>
2026-09-16 18:53:32 +10:00
dlawler489
81b6016d63 Watch collections as a metadata feed, not a downloader
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>
2026-09-16 13:07:47 +10:00
dlawler489
a65c9d047f Don't retry MakerWorld's anti-bot refusal
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>
2026-09-16 12:53:36 +10:00
dlawler489
a313e0e224 Retry MakerWorld's anti-bot check, and flag imports that got no files
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>
2026-09-16 12:34:04 +10:00
dlawler489
4c54f76ae3 Read whole collections, and never check one twice at once
- 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>
2026-09-16 09:58:09 +10:00
dlawler489
0df38c83c7 Watch a MakerWorld collection and import models added to it
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>
2026-09-16 08:53:35 +10:00
dlawler489
4a88d66e90 Ask for a fresh MakerWorld token when MakerWorld refuses it, then retry
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>
2026-09-15 18:33:10 +10:00
dlawler489
9d7a978f03 AI collection suggestions, pick-what-to-do runs, and a retry for bad replies
- 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>
2026-09-15 17:52:41 +10:00
dlawler489
2506f4dce9 Let AI runs give models with unclear names a clearer one, with undo
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>
2026-09-15 15:26:29 +10:00
dlawler489
0f49fe8c1a AI tags and descriptions, search by meaning, and automatic tags
- 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>
2026-09-15 11:12:58 +10:00
dlawler489
f3a69c82ec Open models in your slicer and drag files out
- 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>
2026-09-15 09:15:37 +10:00
dlawler489
ea2afeac0e Make thumbnails on the server in the background
- 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>
2026-09-15 07:54:26 +10:00
dlawler489
00a4b4ba07 Add duplicate finder, trash, notes, and library search/filters
- 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>
2026-09-14 23:06:09 +10:00
dlawler489
a37db15a3f Initial commit: Printhive 3D-print file library
Self-hosted Express + Postgres app for organizing STL/3MF/OBJ/etc. with:
- Model/file library, tags, collections, print settings
- URL import: Thingiverse (API), Printables (GraphQL), MakerWorld
  (FlareSolverr + token), cubee3d/other (metadata + manual files)
- In-browser 3D viewer; 3MF rendered via Python/trimesh GLB conversion
- Auto thumbnails, license/commercial-use tracking, in-app editing
- Settings (API tokens / FlareSolverr) stored in DB with .env fallback

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:15:17 +10:00