Two faults behind the same kind of print: one that arrives without a
retrievable 3MF, which on an H2S is any job started from the printer's
own internal library.
Such an archive has no file_path, and Path("").parent is Path("."), so
every site that derived the archive's folder from it landed on the data
directory itself. The finish-photo capture spotted that and wrote to
<archive_dir>/<id>/photos instead. Nothing else did. The photo was
written in one place and looked for in another: reads 404'd, deletes
dropped the name and left the file, and the notification attachment
never found the image. Hand-uploaded photos worked only because upload
and read agreed with each other rather than with the capture. Give the
question one owner in utils/archive_paths and have all four sites ask
it. Lookups check the old shared location too, so photos already
uploaded there stay reachable; uploads now go where captures go.
Separately, the remain%-delta fallback that stands in for a missing 3MF
can charge nothing for several reasons, and did so without a word. The
AMS reading is coarse and, on the reporter's printer, noisy: it rises
mid-print, swings five points over a job, sits at 100% through a
36-minute print on a fresh spool, and goes negative on a nearly empty
one -- which the start-of-print gate rejects, dropping the only slot
that was printing. Two of their prints went uncounted for two different
reasons and both read as "no spools updated", which is also what a print
with nothing to charge prints. Name the slot and the two readings in
each case, on the Spoolman path and on the internal-inventory path,
which has carried the same gates since #1119.
The Spoolman path also had no notion of which slots the print used, so a
spool swapped into an idle slot mid-print reads as consumption and is
billed to whoever that slot is assigned to -- the fault #1269 fixed for
the internal tracker, still open here, and likeliest on exactly the
prints this fallback serves, where nothing else narrows the field. Use
the same three pieces of evidence it does: the print's mapping, its
mid-print tray changes, and the tray it started on. The last needs
storing, because the internal tracker's row is deleted before this runs
and a screen-started print has no mapping to fall back on -- hence a new
nullable column, and no backfill, since a row from before it existed has
nothing to say. Where no evidence exists at all, every slot is still
considered.
Both paths also treated tray_now == 255 as naming a slot. It does not:
it is the field's initial value, the fallback for an unparseable
reading, and what it reports with nothing loaded. Mapped as a tray id it
becomes (255, 1), so as the only evidence it excluded every real slot
and charged nothing at all -- this issue's own bug, arriving by a new
route. On the internal path that is live today; on the Spoolman path it
would have shipped with the guard above. The external holder reports 254
when it is genuinely in use.
The arithmetic is untouched: at one percent per step this cannot resolve
a small print, and pretending otherwise would be worse than saying so.
An archive's filament_color was parsed verbatim from the print job's
3MF (filament_colour in project_settings.config) — the slicer's
filament-slot colour, which a user picks independently of the exact
hex they curate on the Bambuddy inventory spool. So a print from a
#000000 inventory spool showed #161616 (the slicer's near-black) in
the archive card and the Color Distribution graph, even though usage
tracking correctly decremented the right spool.
Once usage tracking has resolved the print's filament slots to
inventory spools, the spool colours are authoritative. _track_from_3mf
(built-in inventory) and report_usage (Spoolman mode) now overwrite
the archive's filament_color with the slot-ordered, de-duplicated
colours of the matched spools.
The rewrite is all-or-nothing: it only applies when every used slot
resolved to a spool carrying a colour, so a partially-mapped
multi-colour print keeps the 3MF colour rather than silently dropping
the unmatched slots.
Shipped for both inventory modes: built-in spools read Spool.rgba,
Spoolman spools read the spool's filament.color_hex (fetched via
get_spool for tag-less slot-assignment matches). New helpers
_spool_color_to_hex / _archive_colors_from_spools in usage_tracker.py,
reused by spoolman_tracking.py via _apply_spool_colors_to_archive.
Tests: 12 new in test_usage_tracker.py (hex normalisation, the
all-or-nothing rule across single/multi/partial/no-colour/AMS-fallback
cases, end-to-end rewrite), 4 in test_spoolman_tracking.py (Spoolman
rewrite + empty/partial/missing-archive no-ops). 70 tracking tests
green; backend ruff clean.
archives stop reporting near-zero cost (#1344)
Reporter @nicktags hit $0.01 on a 110.3g multi-color print with the
global default filament cost set to $10/kg. archive.py initial cost
calc was correct (~$1.10), then usage_tracker.on_print_complete
overwrote archive.cost with sum(r.cost for r in results) -- where
results only includes AMS trays mapped to a spool in Bambuddy's
inventory. On a multi-color print where 3 of 4 used trays had no
inventory spool, only the one tracked slot's tiny share (~1g) survived
and the archive recorded $0.01.
The overwrite logic dates to #505 (Feb 2026) and is correct for
fully-tracked single-color prints, but the multi-color slicer feature
in 0.2.4 (988c0055) made the partial-inventory state common -- users
slice + print multi-color from Bambuddy without first setting up an
inventory entry for every tray.
Cover the gap: any filament weight not represented in results gets
charged at the global default rate. For a fully-tracked print,
untracked grams = 0 and the top-up adds nothing, so the single-color
behavior is preserved. For a partial print, the missing slots are
priced at the user's documented default rate so the archive cost
reflects the whole print.
Three call sites updated to share the same logic:
- usage_tracker.py: live cost-update on print complete
- archives.py rescan_archive: per-archive manual recalc
- archives.py recalculate_all_costs: bulk recalc button
The AMS remain% delta path charged every tray with a delta, not just
trays involved in the print. Swapping a spool in an UNUSED slot mid-
print made the slot report remain=0 (fresh spool, no tag), versus a
print-start snapshot of 100%, so the originally-assigned spool got
charged the full 1000g.
Build print_used_keys from ams_mapping, tray_change_log, and
tray_now_at_start, and skip fallback for trays not in that set.
Legacy "scan every tray" behavior preserved when none of the three
signals are present.
When auto-archive was off, archive_id was None at print completion so
the entire 3MF tracking path was skipped. AMS remain% fallback also
failed on printers reporting remain=-1. Now searches library files and
previous archives by filename to locate the 3MF without an archive,
and captures the AMS slot-to-tray mapping at print start so it's
available at completion regardless of archive state.
When a spool ran empty during a print and the AMS auto-switched to a
backup, on_ams_change eagerly deleted the SpoolAssignment (fingerprint
mismatch). on_print_complete then queried the now-deleted assignment
and silently dropped usage.
Snapshot all spool assignments into PrintSession at print start. At
completion, prefer the snapshot over live DB queries in both the 3MF
primary path and the AMS remain% delta fallback. Fall back to live
query for backward compatibility with in-flight sessions started
before upgrade.
3MF slicer estimates are now the primary tracking source for ALL spools
(BL and non-BL), with AMS remain% delta as fallback. Per-layer G-code
analysis provides accurate partial usage for failed/cancelled prints.
Fix wrong-spool tracking bug: 3MF slot_id was mapped directly to AMS
tray position, but the printer remaps slicer slots at print time. Now
uses queue ams_mapping for queue prints and tray_now from printer state
for single-filament non-queue prints.
Add filament_grams, filament_details, and progress template variables
to print_complete, print_failed, and print_stopped notification events.
Webhook payloads include per-slot filament breakdown. Per-slot data
stored in archive extra_data during 3MF parsing.