A single plate dispatched from a multi-plate 3MF could log the entire file's
filament against that one plate. When the AMS tracker measured nothing, a
completed run's PrintLogEntry.filament_used_grams fell back to
PrintArchive.filament_used_grams -- the sum over every plate (correct for the
archive card / project rollup, #1593) -- ignoring the archive's plate_id. So
each printed plate of a 22-plate file logged the full ~12 kg; cost inherited
the same whole-file value.
Forward: when the archive has a plate_id and its 3MF is on disk, the completed-
run fallback uses that plate's own slicer estimate (extract_plate_metadata_from_3mf)
and scales cost by the plate's share of the whole. Tracker-measured runs and
single-plate archives are unchanged.
Backfill: a startup migration repairs rows already written -- completed entries
whose stored grams exactly equal the archive's whole-file value, with a plate_id
and an on-disk 3MF, get recomputed to plate-scoped grams + cost. The exact-match
guard never touches tracker-measured or partial rows; idempotent, data-only,
identical on SQLite and Postgres, and logs the correction.
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.
Statistics now aggregate over PrintLogEntry (one row per print event,
the same table backing the global Print Log) rather than PrintArchive
(one row per file). A reprint creates a new PrintLogEntry instead of
overwriting the source archive's runtime fields, so:
- a 100 g successful print + a 10 g failed reprint correctly sums to
110 g / 2 prints / 1 successful / 1 failed in Quick Stats and the
Prometheus /metrics endpoint (previously the failed reprint silently
replaced the source archive's data; totals dropped from 100 g to 10 g)
- the archive's card cost/energy_kwh are preserved on reprints (only
the first run writes them); per-run actuals live on PrintLogEntry
- failed/cancelled/stopped reprints record partial-aware filament: sum
of tracked spool deltas when inventory is set up, else estimate
scaled to progress%, else None — prevents the full slicer estimate
from inflating totals on a print that stopped at 10 % progress
PrintLogEntry gains six columns: archive_id (nullable FK, ON DELETE
SET NULL so log entries survive archive deletion preserving #1343
soft-delete-vs-stats decoupling), cost, energy_kwh, energy_cost,
failure_reason, created_by_id. Idempotent SQLite + Postgres migrations.
New per-archive surface:
- archive list response carries run_count / last_run_at /
total_filament_actual_grams / successful_run_count / failed_run_count
via a single batch JOIN, no N+1
- new GET /archives/{id}/runs endpoint returns every PrintLogEntry for
the archive (ARCHIVES_READ permission, newest-first ordering)
- archive cards render an orange "N prints" badge for archives with
more than one run; clicking the badge opens a dedicated PrintLogModal
with date/status/duration/filament/cost columns plus failure_reason
under failed runs. Also reachable via the context menu's new "Print
Log" entry (works for single-run archives too), and embedded at the
top of the Edit Archive modal for context.
The purge_stats=true delete path now hard-deletes linked PrintLogEntry
rows up front so the archive's contribution truly leaves the totals;
without it, ON DELETE SET NULL would orphan the runs and leave them
counting toward stats.