Third and final piece of #1268, alongside the recursive-search +
README-panel commit that landed earlier in 0.2.5b1. Folders express
hierarchy (one home per file); tags are orthogonal labels — "toy",
"kid-safe", "petg-only" — and a single file can carry as many as the
user wants. Reporter wanted to find "every toy regardless of which
folder it lives in"; folders alone can't do that without forcing the
file into one bucket.
Design decisions locked with maziggy before code:
- file-only (folders already express hierarchy)
- multi-tag filter = AND
- tag filter IGNORES the selected folder (cross-cutting by design)
- bulk-tagging from multi-select toolbar in v1
- no auto-tags from 3MF metadata (user-authored only)
- label-only chips, no color/icon
Backend
- LibraryTag (id, name, name_key UNIQUE = LOWER(TRIM(name)))
in backend/app/models/library.py. Case-insensitive UNIQUE
collapses "Toys"/"toys"/"TOYS " into one row, so the route
returns 409 instead of silently fragmenting the catalog.
- LibraryFileTag(file_id, tag_id) association, composite PK,
ON DELETE CASCADE both directions. Deleting a tag drops every
chip; files survive. Deleting a file drops its tag links; the
catalog row survives.
- Both tables auto-create via Base.metadata.create_all — no
explicit run_migrations step needed for new tables.
- New router at backend/app/api/routes/library_tags.py with:
GET /library/tags (list + per-tag file_count)
POST /library/tags (create, 409 on case-insensitive dup)
PATCH /library/tags/{id} (rename, 409 on collision, self-rename OK)
DELETE /library/tags/{id} (cascade)
POST /library/tags/bulk-assign (add | remove | replace)
- Bulk-assign add is idempotent; replace with empty tag_ids clears
the file's tag set. Per-file ownership enforced — *_OWN callers
can only modify their own files; unknown file_ids quietly
skipped (matches library_trash bulk shape).
- list_files gains tag_ids: list[int] query param. AND semantics
via JOIN + GROUP BY + HAVING COUNT(DISTINCT) — portable across
SQLite and Postgres. When tag_ids is non-empty, folder_id /
project_id / include_root / recursive are all bypassed so the
result is cross-cutting.
- FileListResponse gains tags: list[{id, name}] via
selectinload(LibraryFile.tags) — N+1-free chip render.
- Permissions reuse existing constants: LIBRARY_UPDATE_ALL for
catalog mutations (global catalog, ownership-aware update isn't
meaningful), LIBRARY_UPDATE_ALL/OWN pair for bulk-assign,
LIBRARY_READ_ALL/OWN for list — file_count projection narrows
for *_OWN callers so chip counts match what they actually see.
Frontend
- LibraryTagsModal (catalog CRUD) opens from the toolbar's new
Tags button. max-w-4xl so multi-language subtitles don't wrap.
Delete-with-warning when file_count > 0 ("removes the chip from
all of them; files themselves are untouched").
- BulkTagsPickerModal opens from the multi-select toolbar (new
Tag button between Move and Delete). Add/Remove radio,
checkbox list, inline "create new tag" disabled on dup.
Apply disabled until at least one tag is selected. The replace
action is exposed in the API but deliberately NOT in this UI —
arbitrary multi-file replace is destructive and confusing.
- FileManagerPage integration:
* selectedTagIds state, sorted into the useQuery key so the
cache hits are stable regardless of toggle order
* filter rail above the file list lists EVERY catalog tag as
a togglable chip — inactive outlined, active filled green
with an X. Clear all when 1+ active. Bar hidden entirely
when catalog is empty.
* useEffect prunes selectedTagIds when a tag is deleted from
the catalog so the filter never strands on a phantom id
* dedicated Tags column in list view at minmax(0,200px)
between Prints and Actions
* grid view chips render below the metadata block
* chip clicks stop propagation so they don't toggle file
selection
- libraryTagsQueryKey extracted to frontend/src/utils/
libraryTagsQuery.ts so component files export only components
(Vite react-refresh rule).
- LibraryFileListItem.tags is OPTIONAL even though the backend
always emits an empty array — legacy msw mocks in pre-existing
tests construct partial file shapes without the field. Without
the ? the FileCard renderer crashed on .length and broke 49
unrelated tests across FileManagerPage + FileManagerExternalFolder.
Read sites use file.tags ?? [].
Reporter (@zumik3-del, seconded by @unLieb) asked for three File Manager
improvements: recursive search, tags, and a markdown preview side panel.
This commit ships the two scoped ones; tags is held back gated on the
"give the issue a thumbs up" interest check Martin posted on the issue
because it's a much larger surface (M2M schema, CRUD endpoints, tag UI +
filter + autocomplete + i18n for the management surface) and isn't the
right call without a real demand signal.
1) Recursive search inside the selected folder.
Until now, selecting "Toys" and typing "robot" only found files
directly in Toys/ — anything under Toys/Cars/Race/ stayed invisible.
The page's client-side filter ran over a server-narrowed listing
(/library/files?folder_id=X is strict equality on folder_id), so the
client filter couldn't see what the listing never loaded.
list_files (backend/app/api/routes/library.py:1729+) gains a
recursive=true query param. When combined with folder_id, the route
walks library_folders.parent_id via a recursive CTE rooted at the
requested folder and returns every descendant folder's files in one
query. Recursive CTEs work on both SQLite >=3.8.3 (2014, well below
Bambuddy's runtime floor) and Postgres without dialect branching.
Default off so the existing folder-browsing call sites (Project /
Archive detail, the FE's no-search case) keep their narrow scope.
FE opts in only when both a folder is selected AND searchQuery is
non-empty (FileManagerPage.tsx — derived as searchExpandsSubfolders,
threaded through the useQuery key so the cache invalidates on
toggle). Small "Including subfolders" caption renders under the
search input when active so the user understands why a file from two
levels deep showed up.
2) Per-folder markdown description panel.
New endpoint GET /library/folders/{folder_id}/readme returns the
first .md file in the folder as {filename, content, truncated}.
Selection prefers README.md / readme.md / description.md
(case-insensitive via func.lower(filename) LIKE '%.md' + an
in-Python stem-preference sort), falls back to the
alphabetically-first *.md otherwise. 404 when no markdown is present
so the FE can hide the side panel — non-users pay no UI cost.
Bytes are clipped at 512 KiB (_README_BYTES_CAP) with a truncated
flag so the panel can warn the reader. UTF-8 decode uses
errors="replace" so one bad byte never blanks the panel.
New FolderReadmePanel.tsx fetches on folder-select and renders via
react-markdown@9 + remark-gfm@4 (tables, strikethrough, task lists).
Collapsible (default expanded), max-height 24rem with internal
scroll. react-markdown 9 doesn't render raw HTML by default — no
dompurify needed. Links open in a new tab with rel=noopener
noreferrer. Tailwind has no typography plugin in this project so
per-element components map h1/h2/h3/p/ul/ol/code/blockquote/table
to explicit utility classes that match the rest of the app.
Scope and permissions.
Both endpoints reuse the existing LIBRARY_READ_ALL / LIBRARY_READ_OWN
ownership-aware pair, so a viewer-tier user with read_own only sees
their own files in recursive listings and can only fetch the README of
folders containing their own files. No new permission, no DB migration.
The recursive CTE is a single SQL query — no N+1, no per-folder
round-trip, scales to deeply-nested model libraries.
Reporter has a lot of nested cad / slicer directories and wanted
"folders that just got a new 3MF" surfaced without scrolling the
alphabet. Tree was always alphabetical; LibraryFolder.updated_at
only bumps on rename / move, not on file-add inside the folder.
Backend exposes latest_activity_at = max(folder.updated_at,
max(immediate-child file.updated_at)) on FolderResponse +
FolderTreeItem. The /folders tree route picks up a sibling
func.max(updated_at) group-by alongside the existing file-count
subquery; the by-project / by-archive / single-folder routes
collapse count + max into one trip. Recursion across subfolders
is intentionally not computed - bubbles immediate parent only,
keeps the query a single GROUP BY rather than a recursive CTE.
Frontend adds a folder-sidebar sort dropdown (By name / By recent
activity) plus an asc / desc arrow, persisted in localStorage.
sortedFolders memo applies the comparator recursively so order is
consistent at every depth. Empty folders fall back to name within
the activity bucket so they never elbow a recently-used folder to
a random position. Both the desktop sidebar and the mobile selector
consume the sorted list so order is identical across breakpoints.
External folders: LibraryFile rows are created for scanned external
files too, so the aggregate works on them - but the timestamp
reflects last scan, not filesystem mtime. Documented in the wiki.
Same change also fixes File Manager list-view column alignment:
header and body were sibling grids with min-content as the trailing
column, computed independently. Header empty trailing div resolved
to 0; body action strip to ~220px. Different trailing widths gave
the 1fr Name column different remaining space, shifting every fixed
column to its right. Replaced min-content with fixed 220px in both
auth-on / auth-off grid templates.
Bambuddy's project_file MQTT payload hardcoded "nozzle_offset_cali": 2 (skip),
giving users on H2D / H2D Pro / H2C / X2D no way to control the same toggle
BambuStudio exposes. Critical for diamond-nozzle setups that must keep the
calibration off.
start_print() now takes a nozzle_offset_cali kwarg; the value is encoded as
1 (run) or 2 (skip) and gated on is_dual_nozzle so single-nozzle machines
always send 2 even if a stale flag arrives. The kwarg threads through
printer_manager, both background_dispatch sites, and print_scheduler so
every dispatch path respects the per-item setting.
print_queue gains a nozzle_offset_cali column (DEFAULT TRUE, is_sqlite()
branch for Postgres BOOLEAN). Settings default key default_nozzle_offset_cali
defaults to TRUE to match BambuStudio. Schemas updated across print_queue,
library FilePrintRequest, archive ReprintRequest, settings.
PrintModal renders the new toggle only when the selected printer is dual-
nozzle (printer-mode: nozzle_count===2; model-mode: DUAL_NOZZLE_MODELS).
SettingsPage default-print-options row + QueuePage bulk-edit tri-state both
hide unless any registered printer is dual-nozzle. Labels reuse the existing
settings.default* keys so the only new i18n strings are
settings.defaultNozzleOffsetCali / Desc and queue.bulkEdit.nozzleOffsetCali
- real translations in all 11 locales.
The "Print" button on a printer card (and drag-drop-onto-card) used
FileUploadModal to persist the file as a LibraryFile, then dispatched
through POST /library/files/{id}/print. The LibraryFile row + disk file
were left behind after every one-off print, polluting File Manager with
entries the user never asked to save.
FilePrintRequest.cleanup_library_after_dispatch (default False) opts
into post-dispatch cleanup. When set, _run_print_library_file stages
db.delete(lib_file) in the same transaction as archive_print so a
mid-flight FTP / start_print failure rolls both back cleanly, commits
together, then unlinks the library disk file + thumbnail after commit
succeeds. External library files (is_external=True) are never touched.
Only the Printers-page Direct-Print PrintModal sets the flag. Every
other api.printLibraryFile caller (File Manager Print, Project Detail
Print) leaves it unset — their entries are there by user intent.
Also moves formatPrintName out of PrintersPage.tsx into a new
utils/printName.ts module — fa1c46d9 (#881) exported it inline so its
test could import it, tripping react-refresh/only-export-components.
* feat: print files directly from project view (closes#930)
Show printable files from linked library folders directly in the project
detail page, with Print Now and Add to Queue buttons per file. Removes
the detour through the File Manager for common reprint workflows.
Host directories (NAS, USB, network shares) can now be mounted
into the File Manager without copying files. Files are indexed
into the database on scan but read directly from their original
location. Supports read-only mode, hidden file filtering, and
automatic thumbnail extraction for 3MF/STL/gcode.
- POST /library/folders/external — create with path validation
- POST /library/folders/{id}/scan — discover/sync files
- Block uploads, moves, and deletes for read-only external folders
- Never delete actual files from external paths (DB-only removal)
- Purple folder icon + info bar with rescan button in UI
- i18n for all 7 languages
- 19 backend + 11 frontend tests
- Added query to fetch library file details in PrintModal
- Updated backend FileResponse schema to include metadata fields (print_name, print_time_seconds, filament_used_grams, sliced_for_model)
- Updated backend get_file endpoint to extract and return metadata fields
- Updated frontend LibraryFile interface to include metadata fields
- Now slicedForModel is properly extracted from both archives and library files
Co-authored-by: cadtoolbox <12723486+cadtoolbox@users.noreply.github.com>
Track and display who performs key actions in Bambuddy:
- Archives: who uploaded each archive file
- Library: who uploaded each file in File Manager
- Queue: who added each print job to the queue
- Printers: who started the current print (reprint tracking)
Backend changes:
- Add created_by_id column to print_archives, library_files, print_queue tables
- Add database migrations for new columns (auto-run on startup)
- Update archive, library, and queue routes to capture current user
- Add current-print-user endpoint for printer reprint tracking
- Track reprint user in PrinterManager in-memory state
- Fix file uploads not sending auth headers (FormData requires explicit headers)
Frontend changes:
- Display username on archive cards, library files, queue items
- Show "Started by" on printer cards during active prints
- Add auth headers to all 12 FormData upload functions
- Update TypeScript types for user tracking fields
Tests:
- Add unit tests for PrinterManager user tracking methods (7 tests)
- Add integration tests for current-print-user endpoint (3 tests)
- Add integration tests for library file user tracking (3 tests)
Works when authentication is enabled; gracefully hidden when disabled.
Closes#206
- Implemented batch STL thumbnail generation API endpoint.
- Added Pydantic schemas for batch thumbnail requests and responses.
- Created service for generating thumbnails from STL files using trimesh and matplotlib.
- Updated file upload and ZIP extraction endpoints to include thumbnail generation option.
- Enhanced frontend to support STL thumbnail generation during file uploads and ZIP extractions.
- Added integration and unit tests for the new thumbnail generation features.
- Updated requirements to include necessary libraries for STL processing.
- Add trimesh and matplotlib dependencies for software-based 3D rendering
- Create stl_thumbnail service with generate_stl_thumbnail() function
- Handle mesh simplification for large files (>100k vertices)
- Auto-generate thumbnails during STL file upload and ZIP extraction
- Add POST /library/files/{id}/regenerate-thumbnail endpoint
- Add POST /library/generate-stl-thumbnails batch endpoint
- Add "Generate Thumbnails" button to file manager toolbar
- Add "Regenerate Thumbnail" option to file context menu
- Add unit and integration tests for new functionality
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Features:
- ZIP file support in File Manager (Issue #121): Upload ZIP files to
automatically extract contents with option to preserve folder structure
- Delete operation loading indicator: Shows progress during folder/file
deletion operations
Fixed:
- Print time stats using actual elapsed time instead of slicer estimates
(Issue #137)
- Skip objects modal overflow with scrollable list for many objects
(Issue #134)
Removed:
- Duplicate badge feature from File Manager (per user request)
Tests:
- Added ZIP extraction backend tests (5 tests)
- Added ConfirmModal loading state frontend tests (5 tests)
- New RenameModal component with validation
- Backend endpoint supports filename updates (rejects path separators)
- Context menu "Rename" option in grid and list views
- Inline rename button in list view actions
- Add Print button to multi-selection toolbar:
- Appears when exactly one sliced file is selected
- Opens full PrintModal with plate selection and options
- Add to Queue button now uses Clock icon for clarity
- Improve mobile/PWA accessibility:
- Three-dot menu button always visible on mobile (md:opacity-0)
- Selection checkbox always visible on touch devices
- Better experience for PWA users on tablets and phones
Closes#94
- Print from File Manager with plate selection, AMS mapping, and print options. Closes#94
- Add to queue without creating archive (reduces clutter)
- Queue displays library file name, thumbnail, and print time
Fixes:
Queue items from File Manager now reference library files directly instead of
creating archives upfront. Archives are created automatically when prints start.