Files
bambuddy/backend/app/services/print_log.py
T
maziggy 856b849ffa fix(stats): per-event aggregation so reprints add to Quick Stats instead of overwriting (#1378)
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.
2026-05-16 11:34:40 +02:00

65 lines
1.9 KiB
Python

"""Service for writing independent print log entries.
Log entries are written to a separate table and never touch archives or queue items.
"""
import logging
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.models.print_log import PrintLogEntry
logger = logging.getLogger(__name__)
async def write_log_entry(
db: AsyncSession,
*,
status: str,
archive_id: int | None = None,
print_name: str | None = None,
printer_name: str | None = None,
printer_id: int | None = None,
started_at: datetime | None = None,
completed_at: datetime | None = None,
filament_type: str | None = None,
filament_color: str | None = None,
filament_used_grams: float | None = None,
cost: float | None = None,
energy_kwh: float | None = None,
energy_cost: float | None = None,
failure_reason: str | None = None,
thumbnail_path: str | None = None,
created_by_id: int | None = None,
created_by_username: str | None = None,
) -> PrintLogEntry:
"""Write a print log entry."""
duration = None
if started_at and completed_at:
duration = int((completed_at - started_at).total_seconds())
entry = PrintLogEntry(
archive_id=archive_id,
print_name=print_name,
printer_name=printer_name,
printer_id=printer_id,
status=status,
started_at=started_at,
completed_at=completed_at,
duration_seconds=duration,
filament_type=filament_type,
filament_color=filament_color,
filament_used_grams=filament_used_grams,
cost=cost,
energy_kwh=energy_kwh,
energy_cost=energy_cost,
failure_reason=failure_reason,
thumbnail_path=thumbnail_path,
created_by_id=created_by_id,
created_by_username=created_by_username,
)
db.add(entry)
await db.flush()
return entry