Archives created from prints Bambuddy didn't archive (cloud / Handy /
SD-card prints) carry file_path="". The two source-upload routes
computed the destination as (base_dir / archive.file_path).parent /
"source", which collapsed to base_dir.parent / "source" for fallback
rows — sending the file to /app/source/ (outside the data volume,
orphaned on container restart) and raising 500 on the final
relative_to.
Centralise the destination math in _resolve_source_3mf_path. Normal
archives keep the <archive>/source/<filename> layout. Fallback
archives land at <base_dir>/archive/no_source/<id>/<filename>, which
stays inside the data volume and is addressable by every existing
read site. The helper also asserts the resolved directory is under
base_dir.resolve() so a corrupted row fails with a clear message
instead of writing outside the volume.
Both upload routes (upload_source_3mf and upload_source_3mf_by_name)
now route through the helper. Two regression tests in
TestUploadSourceThreeMF pin both branches.
PrintLogEntry.failure_reason is captured once at print-completion time
(main.py:3641) by copying archive.failure_reason — which is NULL while
the user hasn't classified the failure yet. The PATCH /archives/{id}
route then writes only to print_archives via a generic setattr loop,
so the log entry stays NULL and failure_analysis.py keeps grouping the
print as "Unknown". Same desync hits status — flipping it in the modal
never reached the entry either.
Mirror failure_reason and status from the PATCH payload to the latest
PrintLogEntry for that archive (highest id). Latest-only because
archive.failure_reason / status already reflect the latest run's outcome
(each reprint clears the archive value at main.py:2195 and rewrites it
at completion), so the Edit Archive modal is implicitly editing the
latest run — reprints of an archive that succeeded on the second attempt
keep the earlier failed run's original classification intact.
Scoped to those two fields only. cost / print_name / printer_id stay
unmirrored because per-run values legitimately diverge from archive
ones (partial-print cost on a failed run vs source archive's full-print
cost — see _compute_run_filament_grams at main.py:596).
Three independent root causes behind the divergences the reporter
flagged after the archived-spool fix shipped — fixed together.
(1) Filament Used vs Total Consumed.
_compute_run_filament_grams returned the slicer estimate for completed
prints even when inventory had measured the actual AMS weight delta.
That made Stats and Inventory two different sources of truth: Stats
showed slicer-estimate grams, Inventory showed AMS-tracked grams, and
the two never agreed. Reordered the helper so the tracked spool delta
(same source that drives weight_used behind Total Consumed) takes
priority for every status. Slicer estimate stays as the fallback when
no inventory was tracked; partial-progress scale stays as the fallback
for failed/cancelled with no tracker. The _run_cost block right next
to it was already tracker-first; only filament_used_grams was
inconsistent.
(2) Printer Stats By Time vs Quick Stats Print Time.
/archives/slim only set actual_time_seconds when status == "completed".
For failed/cancelled rows the frontend fell back to print_time_seconds
(the slicer's full-print estimate — wrong number for a print that
failed at 15%). Quick Stats already summed elapsed duration across
all statuses, so the two halves of the page disagreed by the
(estimate - actual-elapsed) gap on every non-completed event. Dropped
the completed-only gate; failed/cancelled now report measured elapsed.
(3) Success Rate %.
Was successful / (successful + failed), excluding cancelled / stopped
from the denominator. With "Total Prints: N" displayed right above
the gauge that produced confusing numbers — 4 successful, 0 failed,
48 cancelled showed 100% out of an apparent 52 prints. Switched to
successful / total_prints — matches the count the user reads from
the widget header.
After #1378 moved Quick Stats to print_log_entries, six widgets and
Failure Analysis still iterated the archive list. That made reprints
multiply event-based widgets while leaving archive-based ones unchanged,
and made hard-deleted archives drop from archive-based widgets while
their orphan events kept feeding Quick Stats.
Swap the data source in two places:
- GET /archives/slim now reads PrintLogEntry, LEFT JOINs the archive for
the sliced print_time_seconds estimate, prefers PrintLogEntry's own
duration_seconds as the measured-time field. StatsPage is the only
caller -- every widget realigns in one step.
- FailureAnalysisService swapped from PrintArchive to PrintLogEntry for
every aggregation. project_id filter still resolves through archives
but counts matching events.
Conftest archive_factory now syncs the synthesized event's created_at
with the archive's so backdated test data survives the change.
Archives → Print Log was 404-storming the thumbnail endpoint on every
render: PrintLogEntry.thumbnail_path is copied by value from the archive
at write-time, but the FK on archive_id is ON DELETE SET NULL (#1378) so
log entries survive archive deletion to preserve stats history - and the
cached path keeps pointing at a file that was removed when the archive's
directory was deleted. Same shape for failed prints whose extractor never
wrote the thumbnail.
Two-part fix:
1. Route self-heals: get_print_log_thumbnail NULLs thumbnail_path on the
entry and commits before returning 404 when the file is missing on
disk. The frontend's <img> tag is gated on entry.thumbnail_path being
truthy, so the next fetch of the log list skips the request entirely.
2. Eager clear on archive delete: new _null_print_log_thumbnail_paths()
helper called from both soft_delete_archive and delete_archive before
the on-disk files are removed. Avoids the one-time storm for future
deletes; covers both the manual delete route and the auto-purge
sweeper at archive_purge.py.
Regression tests cover soft delete, hard delete via ArchiveService, and
the route's lazy-NULL for failed-print orphans where the file was never
written.
Reported by @IndividualGhost1905: printing the same model ten times and
then deleting nine archive entries (to keep the file list tidy) silently
rewound the totals on the Statistics page — total prints, filament,
cost, and per-print energy all dropped back to whatever the surviving
row contributed, as if the other nine prints had never happened.
Root cause: every metric in get_archive_stats is recomputed live from
PrintArchive rows via COUNT / SUM, so removing a row removes its
contribution. Energy in the default "Total" mode already survived
deletion because it reads the smart-plug lifetime counters — that's
the architectural shape we now generalise to the rest.
Fix: soft delete with opt-in hard purge.
Backend:
- New nullable, indexed deleted_at column on print_archives, dialect-
conditional migration (DATETIME on SQLite, TIMESTAMP on PostgreSQL).
- ArchiveService.soft_delete_archive flips deleted_at and removes the
files from disk (still reclaims storage); the path-safety checks were
extracted into _resolve_archive_dir_for_delete so soft and hard delete
share the rules.
- DELETE /archives/{id} accepts ?purge_stats=true; default is soft.
- Listings filter deleted_at IS NULL: list_archives, search FTS + LIKE
fallback, GET /{id} (404 on soft-deleted), tag listing, duplicate
detection (so a 1-live + 9-soft-deleted group no longer marks the
survivor as a duplicate), and ArchiveComparisonService's "similar"
suggestions. GET /stats and GET /slim deliberately do NOT filter so
Quick Stats and the dashboard widgets keep counting deleted prints.
Frontend:
- ConfirmModal gained an optional children slot.
- ArchivesPage (both card and detail views) own a per-instance
deletePurgeStats boolean and render an opt-in checkbox in the delete
dialog; resets to off on every close so the destructive option is
never sticky.
- api.deleteArchive(id, purgeStats?) appends ?purge_stats=true only
when the box is ticked.
- One new i18n key archives.modal.deletePurgeStats added across all 8
locales (full German, English fallbacks elsewhere).
- Add lightweight GET /archives/slim endpoint with column-level SELECT
(13 fields vs 46), skipping duplicate detection for ~70-80% payload
reduction on the stats dashboard
- Add ArchiveSlim Pydantic schema and TypeScript type
- Switch StatsPage and FilamentTrends to use ArchiveSlim
- Fix critical bug in failure_analysis.py: use effective_days for week
count, separate non-date filters, build fresh week_filter per loop
- Fix busiestDay timezone bug: parse YYYY-MM-DD with split() + local
Date constructor instead of new Date() which creates UTC midnight
- Fix success streak ordering: sort by completed_at || created_at
instead of created_at alone
- Add 6 integration tests for /archives/slim endpoint
Implement centralized tag management for print archives:
- GET /archives/tags endpoint to list all tags with usage counts
- PUT /archives/tags/{name} endpoint to rename tags across archives
- DELETE /archives/tags/{name} endpoint to delete tags from archives
- TagManagementModal component with search, sort, rename, and delete
- Gear icon button next to tag filter dropdown on Archives page
- Fix tag autocompletion in EditArchiveModal using dedicated getTags API
Closes#183
Allow users to manually set external URLs for archives from Printables,
Thingiverse, or other sources. The Globe button now opens the external
link when set, falling back to auto-detected MakerWorld URL.
- Add external_url field to PrintArchive model with migration
- Add input field to archive edit modal
- Update Globe button: external_url > makerworld_url > disabled
- Include external_url in backup/restore
- Add API test for external_url update
- Update docs (changelog, wiki, website)
Closes#151
When reprinting a multi-plate 3MF file exported with "All sliced file",
the system now asks which plate to print and only maps filaments for
that specific plate.
Backend changes:
- Add GET /archives/{id}/plates endpoint to list available plates
with thumbnails, print times, and per-plate filament requirements
- Add GET /archives/{id}/plate-thumbnail/{index} for plate thumbnails
- Update GET /archives/{id}/filament-requirements to accept plate_id
query parameter for filtering filaments by plate
- Add plate_id field to ReprintRequest schema
- Update POST /archives/{id}/reprint to use plate_id from request body
instead of auto-detecting (maintains backward compatibility)
Frontend changes:
- Add getArchivePlates API method
- Update getArchiveFilamentRequirements to accept optional plateId
- Update reprintArchive to accept plate_id in options
- Add plate selection UI to ReprintModal when multi-plate 3MF detected
- Show plate thumbnails, names, and filament counts in selection grid
- Require plate selection before printing multi-plate files
- Filter filament requirements to show only selected plate's filaments
Tests:
- Add unit tests for multi-plate slice_info.config parsing
- Add unit tests for plate detection from gcode files
- Add integration tests for new plate endpoints
- Add f3d_path field to PrintArchive model and ArchiveResponse schema
- Add API endpoints for F3D upload/download/delete
- Add cyan badge on archive cards when F3D file is attached
- Add context menu options: Upload/Replace F3D, Download F3D, Remove F3D
- Include F3D files in backup/restore
- Add database migration for f3d_path column
- Add API tests for F3D endpoints
- Rebuild CHANGELOG from GitHub releases
Resolves#90
- New APIBrowser component with full OpenAPI schema integration
- Fetches and parses /openapi.json automatically
- Groups endpoints by API tags (printers, archives, settings, etc.)
- Expandable endpoint sections with color-coded method badges
- Path parameter, query parameter, and JSON body editors
- Auto-populates request body with schema examples
- Live API request execution with response display
- Response shows status code, timing, and formatted JSON
- Copy response button with clipboard fallback
- Search to filter endpoints across all categories
- Expand All / Collapse All buttons
- Link to Swagger UI (/docs)
- Two-column layout for API Keys tab
- Left: API key management + webhook documentation
- Right: API Browser with dedicated test key input
- Parameter validation
- Shows warning for missing required parameters
- Validates before sending requests to avoid 422 errors
- UX improvements
- "Use in API Browser" button on newly created keys
- Responsive layout (stacked on mobile, side-by-side on xl+)
Backend:
- pytest configuration with async support and coverage
- Unit tests for notification service (23 tests)
- Unit tests for smart plug manager (12 tests)
- Unit tests for archive service (16 tests)
- Integration tests for API endpoints
- Fix: notifications now send immediately (digest is summary only)
Frontend:
- Vitest configuration with jsdom and coverage
- MSW for API mocking
- Component tests for Toggle, Button, Card, ConfirmModal (77 tests)
- Test utilities with custom render wrapper
CI/CD:
- GitHub Actions workflow for automated testing
- Backend lint, unit tests, integration tests
- Frontend lint, type-check, unit tests, build