Timelapse was on, the video never reached the archive, and Scan for Timelapse
found nothing afterwards. Across 247 support bundles this was the norm, not an
edge case: 457 automatic scans scheduled, 262 attached.
The scan looked four times over ~65s. The attempt that found the video was #1
272 times, then 17 / 13 / 13 — flat against the cutoff, not decaying, i.e.
files were still arriving when we stopped. What ran afterwards searched for the
print name inside the filename; Bambu only writes "video_<timestamp>", so it
fired 159 times and matched zero.
The manual Scan had no baseline at all and matched on filename timestamp, FTP
mtime, or "there is only one video" — all reading a clock a LAN-only printer
cannot sync. The reporter's P1S was six and a half days out.
- Poll for minutes instead of ~65s; drop the name-match fallback.
- Persist the print-start baseline on the archive, so the diff survives a
restart mid-print and the manual Scan runs the same comparison. With a
baseline present the clock-based strategies are skipped entirely — they can
only turn an honest "pick one" into a confident wrong answer.
- When several files are new (a previous print's video landing late), exclude
the ones already attached to another archive instead of ordering the
candidates. Ordering could only be done on the printer's clock.
- Delete the video from the printer once archived. Keeps /timelapse to
unclaimed files, which is what makes the diff unambiguous, and stops P1S
cards filling with AVIs.
- Gate that delete on a verified transfer: download_file now compares against
the size from the listing. An FTPS connection closing early does not always
raise, so a partial buffer was being attached as a complete video — which
would also have been the one case where deleting the source lost data.
Bounded twice on purpose: wall-clock deadline plus a derived round cap, since
the deadline stops bounding the loop as soon as the sleeps are shortened.
Per-round logging only speaks when the listing changed — 31 rounds of full
listings would bury the interesting line in the support bundle.
Migration adds print_archives.timelapse_baseline as JSON, spelled the same on
both dialects so a migrated database matches a fresh one.
-----------
fix(finish-photo): add the timelapse frame to the archive after the notification (#2704)
When a print records a timelapse, its last frame is the better finish photo:
the firmware stops recording with the toolhead parked and before the end
G-code drops the bed, where a live grab at that moment catches a lowered
plate. Bambuddy waited 60s for the video and then gave up, because the
print-complete notification blocks on that photo and holding a notification
for minutes is worse than sending it with the live grab.
P1-series printers write MJPEG AVI rather than H.264 MP4 and serve it slowly.
Measured over 261 attaches in the support bundles: P1S median 33s, p90 167s,
worst 546s, while every other model finished inside 26s. So the printers that
most needed the better framing were the ones that never got it.
Keep the notification on the same bound, and keep waiting off to the side.
_capture_finish_photo_from_timelapse now reports whether it ran out of time or
concluded — a video that landed and failed extraction is not worth retrying,
one that never arrived is. On the first, schedule a background task that waits
up to 15 minutes and inserts the extracted frame at the front of the archive's
photo list, where the gallery opens.
The live grab stays on disk: the notification already links to that exact
file, so removing it would leave a broken image in Discord or Telegram.
The length check proves we received what the listing said, not that the file
was finished. The first look happens ~5s after the print ends, while the
printer may still be writing, so a growing file can be listed short, served
short, and pass. Re-list after the download and only accept the video once its
size has stopped changing — a failed re-list counts as not settled, since
"could not check" must not mean "safe to delete".
Projects made of many distinct files that each need N prints (e.g. 13
plates x 10 sets = 130 prints) only had aggregate progress. Finding out
"how many times have I printed plate_7?" meant reading the Activity
Timeline line by line, unusable at 130 events.
Projects now take an optional Copies per File target. Every printable
file in the project's linked folders shows an X / N badge with a mini
progress bar (gray not started, amber in progress, green done), and the
progress card gains a Complete Sets bar - the minimum per-file count,
i.e. how many finished assemblies can be shipped right now. Without the
target, printable files show a plain printed-count badge.
Counting matches the aggregate project stats: completed runs only,
served by a new /projects/{id}/file-progress endpoint. Runs attribute
to a file via a new library_file_id stamp on queue-dispatched archives,
falling back to content hash and then filename for historical rows.
Also fixed: files queued from a project-linked File Manager folder now
inherit that project, so their prints count toward project statistics -
previously only prints started from the project page were attributed.
Test-harness fix along the way: the test suite's get_db override never
committed, unlike production get_db, so endpoints relying on the
request-scoped commit silently lost their writes in tests. The override
now mirrors production commit/rollback semantics.
A print queued from a specific plate of a multi-plate 3MF showed as Plate 1
in Print History after cancellation: the archive derives its plate from the
filename, but a whole multi-plate 3MF uploads under one name with no plate
suffix, so the parser defaulted to plate 1 and nothing copied the queue
item's plate_id onto the archive (which had no plate field).
Add a nullable print_archives.plate_id, copy it from the queue item at
dispatch (archive- and library-file paths), expose it in the archive API,
and render it in Print History. A startup backfill copies the plate onto
existing archives from their linked queue rows. Column add + backfill are
identical on SQLite and Postgres.
Also fix a related lifecycle bug: stopping a printing item while the printer
was offline left the linked archive stuck at "printing" (queue row
cancelled, but no MQTT completion ever arrives to reconcile the archive).
The offline-stop path now closes the archive out directly; the online path
still defers to the MQTT completion event.
- Introduced CostCenter and related models for managing print costs and budgets.
- Updated PrintArchive and PrintQueueItem models to include cost_center_id and estimated_cost.
- Implemented budget reservation logic in finance services to validate and manage print costs.
- Enhanced ArchiveService and BackgroundDispatchService to handle cost center information during print jobs.
- Added wallet transaction handling for print charges, including partial charges based on filament usage.
- Created finance billing and budget services to manage user wallets and budget reservations.
- Ensured user finance defaults are created upon user registration, including wallets and private cost centers.
- Updated print scheduler to validate budget before processing print jobs.
Bambu's end-gcode lowers the bed at gcode_state=FINISH. Bambuddy's
live-camera grab captured the bed already dropped, ruining the photo
framing. Source the photo from a brief Bambu timelapse instead —
firmware stops timelapse recording AFTER toolhead parks but BEFORE
bed-drop runs, so the last frame frames the finished print correctly.
When capture_finish_photo is on AND the user did not opt in to
timelapse for this print, force timelapse=True at dispatch + mark the
new PrintArchive.bambuddy_forced_timelapse column. After extraction
(success or failure), cleanup deletes the locally-attached file,
clears archive.timelapse_path, and walks the four scanner directories
(/timelapse, /timelapse/video, /record, /recording) trying FTP DELE
against the original filename. User-opted-in timelapses pass through
unchanged.
Resolver lives at services/background_dispatch.py::resolve_effective_timelapse
(module-level so the print queue can reuse it). Both dispatch paths
wired: background_dispatch.py (Print Now / Reprint) AND
print_scheduler.py:_start_print (the queue). Field testing caught the
scheduler gap on the first round — AST regression test now asserts
start_print(timelapse=...) references effective_timelapse, not the raw
item.timelapse, so a future refactor can't silently drop it.
Extractor: ffmpeg -i input.mp4 -update 1 -q:v 2 out.jpg. Decoded
frames overwrite the same output file, so the file left on disk is the
literal last frame regardless of duration. Bambu records one frame per
layer-change, so a 16-layer cube produces a 0.6 s timelapse — the
original -sseof -1.0 approach seeked before the start of the file and
returned frame 0 (empty bed). Decoding every frame is fine; Bambu
timelapses are short by construction even on hours-long prints.
Migration adds bambuddy_forced_timelapse branched on is_sqlite()
(DEFAULT 0 / DEFAULT FALSE — PG rejects DEFAULT 0 for BOOLEAN).
Verified live on postgres:16-alpine.
Photo-task wait_for budget extends 45s -> 75s when timelapse_was_active
so the notification carries the bed-up photo instead of falling back
to the live-cam grab on slow links.
Scope limit, documented in the camera wiki: prints started directly
on the printer touchscreen / Bambu Handy / Bambu Studio Send bypass
both dispatch paths, so the override doesn't fire there. Future
option: mid-print M981 S1 P20000 MQTT toggle in on_print_start.
Setting description rewritten in all 11 locales to drop the "only
works when timelapse enabled" caveat (Bambuddy now forces it) and
explain the kept-or-deleted behaviour.
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).
Show an OrcaSlicer-style bed icon in the archive card's printer-name row
indicating which build plate the print was sliced for (Cool /
Cool SuperTack / Engineering / High Temp / Textured PEI / Smooth PEI),
with the full plate name in the hover tooltip. Closes the gap where
users had to remember which plate matched a re-print or open the
source 3MF in a slicer just to read the bed setting.
Card row also unified: archives with a real Bambuddy-printer
association used to render "H2D-1 GCODE ..." while slicer-only uploads
rendered "Sliced for X1C GCODE ..." -- same line, two different shapes.
Drop the "Sliced for " prefix so both render as a uniform
"<name-or-model> [bed-icon] GCODE <hash>" row, scanning identically
regardless of provenance.
Backend: new bed_type column on print_archives (idempotent ALTER TABLE
migration; SQLite + Postgres safe). Populated from curr_bed_type in
Metadata/slice_info.config (per-plate, authoritative -- that's what
got sent to the printer for the exported plate) with a fallback to
project_settings.config for older 3MF shapes. Wired through both
archive_to_response() (the hand-rolled dict converter that bypasses
from_attributes -- easy to miss) and the /rescan endpoint, so old
archives can be re-parsed via the existing per-archive Rescan button.
Backfill script (scripts/backfill_archive_bed_type.py, --dry-run
supported) re-opens every NULL archive's 3MF on disk to populate the
column. Auto-loads .env from project root before importing backend
modules (config.py reads DATABASE_URL from os.environ at import time,
not from pydantic-settings at Settings() time) and prints the resolved
DB URL with credentials redacted, so operators can confirm they're
hitting the intended database -- Postgres or SQLite.
Frontend: 6 OrcaSlicer-style PNGs ship in frontend/public/img/bed/ --
under /img/ because that path is already statically mounted; a
toplevel /bed-icons/ tried first hit the SPA catch-all and returned
index.html as text/html. New utils/bedType.ts maps slicer strings
case-insensitively, covering both Bambu Studio and OrcaSlicer naming
variants for the same physical plate. Unmapped or NULL bed_type
simply omits the icon, so cards stay clean for pre-feature archives.
Second wave of #972 — reproducer on a 37.5 MB BambuStudio print to an A1
showed three stacking root causes when Bambuddy restarts mid-print.
1. Archive start_time lost on container restart. The name-based dedup
cancelled any "printing" archive older than 4h and recreated it with
started_at=now(), so a 13h print that saw a restart 10h in ended up
showing ~1.5h duration. Persist MQTT subtask_id on every archive and
match on that first, regardless of age — same id means same print,
resume in place. Also revives Stale-cancelled rows for users
upgrading mid-print.
2. 3MF FTP search tried non-existent paths for ~48 min. Order was
/cache → /model → /data → /data/Metadata → / with 11×30s retries
each; BambuStudio actually pushes to / on A1, so the real path was
tested last. Reorder to / first, and raise a new FileNotOnPrinterError
sentinel from download_to_file on 550 so with_ftp_retry short-circuits
via non_retry_exceptions. 425 / SSL EOF / connection resets still
retry as before.
3. Cover endpoint and archive flow downloaded the same 36 MB twice and
competed for the printer's single FTP socket, producing 425 errors
that fed cause-2's retry storm. Add an in-memory _threemf_path_cache
keyed on (printer_id, normalized filename); whichever flow fetches
first populates it, the other reuses the file read-only. Eviction
runs on on_print_complete and deletes the temp file.
Backend: 14 new tests across test_bambu_ftp.py and a new
test_subtask_archive_resume.py. Existing suite: 2737 pass. ruff clean,
frontend build clean.
The Statistics page reported "Gesamt" (All Time) kWh correctly but showed
zero for Today/Week/Month in total-consumption mode. Two bugs drove it:
1. The starting plug counter was kept in an in-memory dict
`_print_energy_start` that was lost on any backend restart mid-print, so
the per-print `energy_kwh` delta silently never got computed. The stats
endpoint's fallback path `SUM(PrintArchive.energy_kwh)` therefore summed
to zero for users running in total mode.
2. Total-consumption mode has no per-print delta by design — it includes
idle/preheat/standby — so the fallback to archive rows was the wrong
strategy even when the data existed.
Fix, in two parts:
- Persist `energy_start_kwh` on the archive row and read it back from a
fresh session at print end. Deletes `_print_energy_start` and its 5
call sites, replacing them with a single `_record_energy_start()` helper.
Per-print tracking is now restart-resilient regardless of tracking mode.
- Add hourly `smart_plug_energy_snapshots` table + `_snapshot_loop()` in
SmartPlugManager. Rewrote the `/archives/stats` energy branch as
`_sum_snapshot_deltas()` which computes per-plug
`max(0, last-in-range - baseline)` where baseline is the latest snapshot
at or before the range start, falling back to the earliest-ever snapshot
and signalling `energy_data_warming_up` when no pre-range baseline
exists (fresh upgrade). MQTT plugs are skipped from snapshots since they
only report "today" and have no lifetime counter.
Frontend: QuickStatsWidget renders an AlertTriangle next to Energy Used /
Energy Cost with a tooltip when `energy_data_warming_up` is true, so the
"low values right after upgrading" situation is explained in-product.
Fully localised across 7 UI languages.
Tests: new backend unit tests cover the snapshot delta arithmetic
(baseline/endpoint, counter reset clamp, multi-plug, warming-up fallback,
endpoint windowing), per-print restart resilience via expunge_all, and the
snapshot task lifecycle (start idempotent, stop cancels). Frontend tests
assert the warning icon appears only when the flag is set and only on the
energy tiles.
Docs: updated `CHANGELOG.md`, `README.md`, wiki `features/energy.md`,
wiki `features/statistics.md`, and website `features.html` with the new
behaviour and warming-up explanation.
Bambuddy can now use an external PostgreSQL database via the
DATABASE_URL environment variable. SQLite remains the default.
Dialect-aware helpers handle upserts, PRAGMAs, FTS (FTS5 vs
tsvector+GIN), backup/restore, and health checks. All migration
blocks use savepoints to prevent Postgres transaction poisoning.
Backups are always portable SQLite format regardless of backend.
Cross-database restore imports SQLite backups into PostgreSQL
with automatic boolean/datetime conversion, NOT NULL default
filling, and FK constraint handling.
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
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
- 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
- Track number of items per print job (default: 1)
- Project stats now show total items vs print jobs
- Progress bar counts items toward target, not just archives
- "Items Printed" field in archive edit modal
Backend:
- Added quantity field to PrintArchive model
- Database migration for quantity column
- Updated project stats to use SUM(quantity)
- Updated backup/restore to include quantity
- Updated CSV/Excel export with quantity field
Frontend:
- Added quantity input to EditArchiveModal
- Updated ProjectsPage to display total_items
- Updated ProjectDetailPage stats
- Fixed project delete not refreshing the list
### Projects / Print Grouping
- Create projects to group related prints (e.g., "Voron Build" with 50 parts)
- Track progress with target count and completion percentage
- Assign archives to projects via edit modal or context menu
- Project cards show archive thumbnails with clickable links
- Color-coded project badges on archive cards
- Filter and manage projects by status (active/completed/archived)
### Full-Text Search (FTS5)
- SQLite FTS5 virtual table for efficient searching
- Search across print_name, filename, tags, notes, designer, filament_type
- Automatic index sync with triggers for INSERT/UPDATE/DELETE
### Webhooks & API Keys
- API key authentication with granular permissions
- Permissions: can_read_status, can_manage_queue, can_control_printer
- Secure key generation with prefix display only after creation
- Settings page API Keys tab for key management
- Webhook endpoints for external integrations
### Failure Analysis
- Dashboard widget showing failure rate with color coding
- Correlate failures with conditions (filament type, printer, time)
- Top failure reasons breakdown
- Weekly trend visualization
### Archive Comparison
- Select 2-5 archives to compare side-by-side
- Highlight differences in print settings (yellow)
- Success/failure correlation insights
- Modal with close via button, X, Escape, or backdrop
### CSV/Excel Export
- Export archives and statistics with current filters
- Support for both CSV and Excel (.xlsx) formats
- openpyxl dependency added
## Bug Fixes
- Fixed context menu submenu not showing (removed overflow-hidden)
- Fixed project card thumbnails using correct API endpoint
- Fixed EditArchiveModal to invalidate projects query on save
- Fixed clipboard API fallback for HTTP contexts
- Fixed archive PATCH 500 error (FTS5 index rebuild)
- Fixed FastAPI trailing slash routing for projects endpoint
## UI Improvements
- Context menu submenu with hover/click support
- Project badge on archive cards with project color
- "Go to Project" context menu item for assigned archives
- Clickable project card thumbnails linking to archives
- Reset Layout button moved to Stats page header