Commit graph

82 commits

Author SHA1 Message Date
dlawler489
b902ea91d6 Merge phase 4 fixes: tag wrapping, model page buttons, search refresh after AI tagging
All checks were successful
build-and-push / docker (push) Successful in 28s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 15:05:21 +10:00
dlawler489
4458963aa4 Fix tags splitting across lines and squeezed model buttons; refresh search after AI tagging
The model page's tag list wasn't a wrapping row, so the last tag could break in
half, and four action buttons crushed onto two lines each. AI runs also never
woke the background worker, so new AI tags and descriptions weren't searchable
until something else changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 14:39:41 +10:00
dlawler489
c7c39f99fa Merge phase 4: AI tags and descriptions, search by meaning, automatic tags
All checks were successful
build-and-push / docker (push) Successful in 1m9s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:54:03 +10:00
dlawler489
2796fab38c Docker: fail the build if the search model can't be fetched
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:54:03 +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
78c225f232 Merge phase 3: slicer hand-off and drag-out
All checks were successful
build-and-push / docker (push) Successful in 2m59s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:36:26 +10:00
dlawler489
01844b4941 Merge phase 2: formats and viewer (archives, plates, filament colours, server thumbnails)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:36:26 +10:00
dlawler489
f401eb4ca4 Merge phase 1: library core (duplicates, trash, notes, search and filters)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:36:26 +10:00
dlawler489
be1facdec3 Drag out from file tabs, plate chips and a Drag out handle; only offer dragging in Chromium browsers
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 10:26:48 +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
fbf1b706bf Fix model view for archives with long paths; preview inside archive-only models
- Long file names inside archives (deep folder paths) widened the details
  column until the viewer was crushed to a sliver and content ran off the
  modal. Both columns now keep their share (minmax(0, …)), long file names
  wrap, and viewer tabs truncate.
- Archive entries show the file name first with its folder dimmed after.
- A model whose only file is an archive opens the most useful file inside
  it (a slicer project, then other 3D files, then pictures/PDFs) instead of
  "No 3D preview for .none files".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 08:01:04 +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
fcb418f119 Show 3MF projects plate by plate in their filament colours
- 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>
2026-09-15 07:49:58 +10:00
dlawler489
d4b7e29348 Browse archives, preview companion files, and view FBX/STEP/3DM/OBJ+MTL
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>
2026-09-15 07:36:47 +10:00
dlawler489
502eefdec2 Stream-parse 3MF files so Bambu projects don't exhaust memory
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>
2026-09-15 07:02:00 +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
e141d800c2 Add SVG favicon (hexagon mark)
All checks were successful
build-and-push / docker (push) Successful in 29s
Brand hexagon in amber on the dark graphite, linked as the icon +
apple-touch-icon, plus a theme-color meta.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 13:35:57 +10:00
dlawler489
d3410e19ec Add tag autocomplete to the upload/edit tag inputs
All checks were successful
build-and-push / docker (push) Successful in 27s
Suggests existing library tags matching the token being typed (after the
last comma), with click or arrow-key/Enter selection; excludes tags
already entered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:43:23 +10:00
dlawler489
a2e08ecd03 License rules are the source of truth for sellability
All checks were successful
build-and-push / docker (push) Successful in 28s
- Changing a license policy applies to all models with that license, and
  opening Settings reconciles old imports to current rules (store.reconcileModelsToRules)
- Editing a model's license re-resolves its sellability from the rule
- Removed the per-model commercial-use override (set by license now)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 11:33:38 +10:00
dlawler489
2768a71c63 Add per-license commercial-use rules (settings + auto-register)
- license_rules table (name -> commercial); licenses auto-register on
  import with a best-effort seed, overridable in Settings (applies the
  choice to every model with that license)
- resolveCommercial() replaces ad-hoc inferCommercial across import/refresh
- GET/PUT /api/licenses; "License types" section in Settings
- Fix CC short codes: BY / BY-SA / BY-ND now classed sellable (MakerWorld
  stores e.g. "BY-SA"); NC variants stay non-commercial

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 10:20:28 +10:00
dlawler489
5af88eb5ce Add email/password auth + SMTP password resets
All checks were successful
build-and-push / docker (push) Successful in 29s
- users + password_resets tables; bcrypt hashing; JWT session in an
  httpOnly cookie (auth_secret auto-generated in settings)
- /api gated behind auth (except /api/config and /api/auth/*)
- First-run creates the account (no open registration after that);
  login, logout, forgot/reset-password flows
- SMTP settings (host/port/secure/user/pass/from) in Settings menu;
  nodemailer sends reset links
- Auth screen (sign in / create account / forgot / reset) gates the SPA;
  Sign out button; COOKIE_SECURE=true in compose for HTTPS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:33:11 +10:00
dlawler489
b06767bd08 compose: match working Traefik labels (tls=true, certresolver=le)
All checks were successful
build-and-push / docker (push) Successful in 25s
The 404 was a router that wouldn't load: cert resolver is named `le`
(not `letsencrypt`) and the explicit `tls=true` label was missing.
Mirror the working paperless service.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:55:01 +10:00
dlawler489
5e02caf013 compose: default Traefik network to proxy
All checks were successful
build-and-push / docker (push) Successful in 9s
That's the actual network Traefik runs on; was defaulting to a
non-existent `traefik` network, causing "network ... could not be found".
Still overridable via TRAEFIK_NETWORK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:51:06 +10:00
dlawler489
5dfe809eb2 compose: Traefik label-based auto-discovery
All checks were successful
build-and-push / docker (push) Successful in 26s
Add Traefik labels so the Docker provider auto-discovers the app.
Includes traefik.docker.network (the app is on two networks) which is
the usual fix for routing not resolving. Host/entrypoint/certresolver
overridable via PRINTHIVE_HOST / TRAEFIK_ENTRYPOINT / TRAEFIK_CERTRESOLVER.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:47:06 +10:00
dlawler489
8ad25aff98 compose: route via shared Traefik network instead of host port
Drop the published host port (it clashed on the deploy host) and attach
the app to the external Traefik network so Traefik routes to it directly.
Stable container_name printhive-app for the file provider to target;
network name configurable via TRAEFIK_NETWORK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:45:02 +10:00
dlawler489
34ced61b51 compose: make host port configurable (APP_PORT)
Host port 3000 can clash with other services on the deploy host; allow
overriding via APP_PORT (default 3000). Only needed for Traefik to reach
the app — can be dropped entirely if Traefik shares the Docker network.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:43:05 +10:00
dlawler489
5fb3939bcd Add first-run setup wizard for import sources
All checks were successful
build-and-push / docker (push) Successful in 11s
On a fresh install (no integration settings yet), prompt once for the
Thingiverse/MakerWorld/Printables tokens and FlareSolverr URL, with a
"skip for now" option. /api/config exposes setupComplete (true if any
setting is configured or setup was dismissed); POST /api/setup/complete
records dismissal. DB password stays a deploy-time env var by necessity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:35:18 +10:00
dlawler489
5d69acd718 compose: pull image for deploy instead of building
Dockhand pulls the CI-built image, so drop `build: .` (no build context
on the deploy host) and add pull_policy: always.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:29:54 +10:00
dlawler489
3a6a2143ef CI: push image using a Forgejo access token
All checks were successful
build-and-push / docker (push) Successful in 15s
The auto GITHUB_TOKEN lacks package-write scope (push failed with
unauthorized: reqPackageAccess). Use a REGISTRY_TOKEN secret (Forgejo
token with write:package) and log in as the repo owner instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:48:57 +10:00
dlawler489
e5e1a2bfc1 Add Forgejo Actions CI to build & push the image
Some checks failed
build-and-push / docker (push) Failing after 53s
On push to main (and v* tags), builds the Docker image and pushes it to
the Forgejo container registry at git.plexultra.com/dlawler489/printhive
(:latest and :<short-sha>). Uses the auto-provided Actions token with
packages:write; plain docker CLI so it only needs the checkout action.
Compose image reference updated to the Forgejo registry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 17:00:49 +10:00
dlawler489
aabc0dd870 Add Docker packaging (Dockerfile, compose, sidecar deps)
- Dockerfile: node:22-bookworm-slim + Python venv with trimesh for the
  3MF->GLB sidecar (PYTHON_BIN env points the converter at it)
- docker-compose.yml: app + postgres 16 with healthcheck/depends_on,
  named volumes for pgdata/uploads/thumbnails/glb, token env passthrough
- .dockerignore, tools/requirements.txt
- converter.js honours PYTHON_BIN (defaults to local venv)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:51:06 +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