* feat: expand storage usage reporting with disk breakdown
* feat: add providers admin view
* feat(storage): add background reorganization and admin browser
Adds background-driven storage reorganization for moving model files
when the storage template changes, with an admin interface to preview
changes, check impact, monitor progress, and browse the storage tree
built from plate records rather than filesystem walks.
* feat: new auth admin settings page + OIDC support
* test: fix failing test
Work now merges into develop by pull request; main is the release branch, updated by pull requests from develop, and remains the only branch that publishes images and releases.
The test was timing-dependent: it checked for a 409 while the first run
is still going, but completion depended on when the mock fetch resolved.
Replace the instant mock with a promise the test controls, making the
assertion deterministic. Also save/restore the Thingiverse access token
to prevent test isolation issues.
Admins can now make members admins and delete members from the Users page.
The owner, the first admin, can also demote and delete other admins; nobody
can demote or delete the owner, or themselves. A migration adds the owner
flag and gives it to each existing instance's longest-standing admin, and
the first sign-up of a fresh instance takes an advisory lock so concurrent
sign-ups can't make two owners.
Deleting a user removes their models with every file, their tag mosaics and
storage folder, then the account. Roles are now read from the database on
each request, so a demotion or deletion takes effect at once. Both actions
are logged under the acting admin.
Each tag tile now loads one WebP built by the backend from its newest models'
pictures, instead of up to ten images laid out in the browser. Mosaics are
made on first request and cached under storage/tag-mosaics; their URL carries
a hash of the pictures in them, so tagging, untagging or a new render gives a
new URL with nothing to invalidate, and existing libraries need no backfill.
The summary joins pictures only for the requested page's tags, a single
tag's mosaic looks up just that tag's newest models, and the Tags page asks
for enough tags to fill the screen. Deleting a tag, or all of a user's
models, removes its mosaics.
Pins the dependencies stage in the frontend Dockerfile to $BUILDPLATFORM
so npm ci and vite build run natively on the build machine rather than
under QEMU emulation for arm64 builds. Only the nginx runtime stage is
built per target platform. Adds a 30-minute timeout to prevent hung
build steps from running until GitHub's 6-hour limit.
When the last AI model is deleted, categorization is now forced
off server-side, preventing it from silently restarting when a
new model is added. The frontend distinguishes between loading
and no connections, hiding the feature toggle instead of
disabling it, and removes the unused "Add a model first" message.
Provider prompts now identify category candidates by label/path instead
of numeric index, with duplicate-path and fuzzy-match handling. Add
reasoning_effort toggle with automatic fallback for providers that
don't support it.
Replace blocking single-print recategorize with background-queued flow
backed by notifications. Extend bulk-run scope to include folders and
manual categories alongside existing filters.
* fix(import): stop flagging models that mention robots as a MakerWorld CAPTCHA, and let a manual retry try again
isCaptchaChallenge looked for "captcha" or "robot" in every top-level string of a reply,
design titles and summaries included, so a model named "Robot ..." failed as a CAPTCHA on
every retry and started the 2h cooloff for every other import. Only a captcha key or the
reply's own error, message or msg text counts now.
The cooloff also failed a retry straight away after the user solved the puzzle in the
browser, which is what the error message tells them to do. Starting or retrying a queue job
by hand, a bulk start or retry, and POST /import/jobs/:id/retry now lift it. Automatic
collection syncs still respect it.
* feat(extension): offer "Add to the queue" when MakerWorld blocks an import
A CAPTCHA or the daily download limit won't clear by clicking Import again, so the error
panel lets an admin park the link in the queue with the same collection and profile scope.
* fix(import): let a queue job cut off by a restart be started or retried
A restart marks a running job ERROR with its links still PENDING or RUNNING. The queue page
only offered Start for PAUSED jobs and Retry for jobs with FAILED links, and the API agreed,
so such a job could never run again. The same held for a job whose runner crashed.
Start now takes an ERROR job with links waiting, and a retry reruns links an interrupted
run never reached even when none of them failed.
* fix(ai): leave room for a full answer and explain a cut-off one
max_tokens 300 cut answers off once the reason filled its 300-character
limit, and the run showed JSON.parse's position instead of the cause.
Raise it to 1024 and turn a parse failure into a readable error, naming
the token limit when the provider reports finish_reason length.
* feat(backend): add a shared logger with levels and greppable fields
One line per event with a level, a scope and key=value fields, or one
JSON object per line with LOG_FORMAT=json. LOG_LEVEL drops lines below
it. Errors log their message, status and code, plus the stack at error
level, and bearer tokens are redacted. Nothing outside it changes yet.
* feat(ai,import): log categorization and import runs through the logger
A bulk AI run kept only its last error in memory, so which models failed
and why was lost. Each provider answer it can't use now logs the model,
finish_reason, completion and reasoning tokens and the end of the answer;
each model's outcome and the run's start and totals are logged too.
Collection, Thingiverse, Printables, profile and zip imports dropped a
failed model without a trace; they now log it with its reason, plus the
job's start and totals. The links runner's console lines move to the
logger.
* fix(ai): read an answer wrapped in markdown or prose
Xiaomi MiMo ignores response_format and sent the object in bold,
`**{...}**`, which failed as not valid JSON although the answer was
complete. When the whole text doesn't parse, try the outermost braces.
MakerWorld can leave a design's defaultInstanceId pointing at a print profile that no longer
exists. pickMakerworldInstanceId (extension) and makerworldInstanceIdFromNextData (backend)
returned it unchecked, so the download-link request went to the dead profile and MakerWorld
answered 403 "The client does not have access rights to the content." Use the default only when
it is still one of the design's instances, otherwise the first, as selectMakerworldProfiles
already does.
Fixes#14
Claude-Session: https://claude.ai/code/session_01KGCb65Zhzwd3daF862ZE6R
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The automatic preview renderer on the model page sits in an invisible box whose
sx width/height of 1 resolve to 100%, so it covers the Description and Tags and
swallows clicks on their links. Let pointer events pass through it.
Fixes#12
Claude-Session: https://claude.ai/code/session_01KGCb65Zhzwd3daF862ZE6R
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
So the web app's Browse tab can list the user's collections there
without asking for a username. Thingiverse's comes from the page
header's own account link; Printables' from its API's `me`, asked with
the session token the way the site does. Sent only when it changes.
Every collection's Sync configuration now works before it's synced: a
Browse tab lists the user's own collections on MakerWorld (through
their cookie, private ones included), Printables and Thingiverse (by a
username asked once), and takes any collection's link. Each provider's
collections are drawn in its own colours and manner.
On Printables and Thingiverse the collection's current models are
imported by the sync's first check. MakerWorld rate-limits bulk
imports, so there only models added from now on come in. Settings can
be picked before turning sync on.
Add POST /collection/:id/items. Thingport Grab uses it to file a
MakerWorld collection's already-imported models instead of one request
per model, falling back on older instances.
A session token outlives a password change, so a stale saved password
only surfaced days later as failing imports. Check the connection by
signing in (at most every 10 minutes) at browser start, before showing
the in-page icon, and when the popup opens.
When it fails, the icon shows a "not connected" badge whose dialog says
whether the credentials were rejected or the instance didn't answer,
and opens the popup's edit form. The toolbar icon gets a badge too.
Concurrent calls with an expired token now share one sign-in.
The collection panel sent one /import/status request per model, all at
once. When any failed it counted the model as new, so a collection
already imported could offer to import almost everything again.
Add POST /import/status/batch and use it, falling back to paced
per-model checks on older instances. A failed check now shows an error
instead of guessing.
Raise the /api proxy timeout from 60s to 300s, the longest AI timeout the
settings allow. When a proxy answers with an HTML error page, show a short
message instead of the raw HTML.
The route replaced every provider error with a bare "AI provider failed",
so a stopped Ollama looked the same as a timeout or a bad answer. Pass on the
reason (the API key is already redacted) and log it.
A base URL with no path, like Ollama's http://host:11434, gets /v1 added.
URLs that already have a path are kept, so OpenRouter (/api/v1) and Gemini
(/v1beta/openai) keep working. Saved values are normalized when read.
Move weather endpoints to the server with caching and rate-limiting. Prioritize
3D viewer renders over background thumbnails. Query featured models from database
instead of loading all into memory.