3 Commits
Author SHA1 Message Date
maziggy e7045597bc fix(archives): bulk and auto purge now honour the soft / hard delete choice from #1343 (#1390 follow-up)
Reporter IndividualGhost1905 followed up after the #1378 / #1343
  backfill landed and pointed at the next inconsistency: the per-
  archive delete dialog has had a "Also remove this print from Quick
  Stats" checkbox since #1343, but the "Purge Old" button and the
  scheduled daily auto-purge sweeper both ignored that choice and
  hard-deleted unconditionally. From the user side this looked like
  "automatically deleted from statistics without any warning" — half-
  true, and the inconsistency was real either way.

  The actual current shape (before this fix):

    - POST /archives/purge -> archive_purge_service.purge_older_than
      -> ArchiveService.delete_archive (hard). Archive row dropped.
      Linked PrintLogEntry rows have ON DELETE SET NULL so they
      survive as orphans with archive_id=NULL. Quick Stats keeps the
      filament / cost / energy contribution because the log rows are
      still there, but the archive-list-iterating widgets (Filament
      Trends, By Material, Color Distribution, Printer Stats) lose
      the row, and Time Accuracy loses its join target. Visibly
      inconsistent.
    - Scheduled _maybe_run_auto_purge -> same code path, same effect.
    - Single-archive DELETE /archives/{id} -> already takes
      purge_stats=true|false (default false=soft) and routes either
      soft_delete_archive (keeps everything, flips deleted_at) or
      deletes PrintLogEntry rows first + hard-deletes archive.

  The fix threads the same purge_stats flag through every bulk surface
  with soft as the default, matching the single-archive default:

  Backend:
    - archive_purge_service.purge_older_than(..., purge_stats=False)
      -> per-row soft_delete_archive when False, per-row
      PrintLogEntry deletion + delete_archive when True. Each runs in
      its own session (same pattern the sweeper already used).
    - preview_purge gains the same kwarg so the eligible-count
      matches what an actual run would touch: soft mode excludes
      already-soft-deleted rows, hard mode counts them as eligible
      for promotion.
    - get_settings / set_settings now persist archive_auto_purge_stats
      (default False). _maybe_run_auto_purge reads it on every tick.
    - ArchivePurgeRequest / ArchivePurgeResponse / ArchivePurgeSettings
      schemas extended.
    - Route /archives/purge accepts the body flag, /purge/preview
      accepts the query param, /purge/settings GET + PUT echo the
      setting.

  Frontend:
    - "Purge old archives" modal: new "Also remove from statistics"
      checkbox under the preview, unchecked by default. Plumbed into
      the preview query key + the execute mutation.
    - Settings -> Archives auto-purge card: matching toggle next to
      the days slider, disabled when auto-purge itself is off.
    - api.previewArchivePurge / api.executeArchivePurge accept the
      flag; ArchivePurgeSettings type gains purge_stats.
    - All 8 locales (en, de, fr, it, ja, pt-BR, zh-CN, zh-TW) get
      new purgeStatsLabel / purgeStatsHint / purgeStatsDescription
      keys, plus rewritten effect / warning copy in archivePurge and
      archiveAutoPurge to reflect that the default no longer
      "permanently removes from the database" but instead hides the
      row + removes files while keeping Quick Stats intact. i18n
      parity check clean: 4814 keys across all 8 locales, no fallback.

  Behaviour change for existing users on auto-purge: the sweeper used
  to hard-delete by default and now soft-deletes by default. After
  the upgrade those installs start *preserving* more data in Quick
  Stats rather than losing it — safer direction of the two, but worth
  the explicit call-out. Anyone who wants the old behaviour ticks the
  new toggle once and it persists.
2026-05-17 10:54:28 +02:00
maziggy 689bc04d7b ● feat(#1008): honour reprint dates in archive purge + clarify purge UX
Fix a silent correctness bug: archive purge used `created_at` which is
  pinned to the first print, so reprinting a two-year-old archive yesterday
  would still make it eligible for a 365-day purge. The preview and purge
  queries now age each archive by `COALESCE(completed_at, started_at,
  created_at)` — reprints refresh the clock.

  Also flesh out both purge modals (File Manager + Archives) with an
  explicit "What happens when you click Purge" effects list so users see
  upfront that library files go to Trash (reversible) while archives are
  hard-deleted (irreversible), plus what disk artefacts get removed.

  Backend:
  - services/archive_purge.py: `_last_activity_expr()` helper used by
    preview, purge, and sample query
  - tests/integration/test_archive_purge_api.py: new test covering the
    reprinted-archive case

  Frontend:
  - PurgeOldFilesModal / PurgeArchivesModal: new effects bullet list
  - i18n: reprint-aware ageLabel/description/warning and effects bullets
    across all 8 locales (en/de fully translated, rest English fallback)

  Docs:
  - wiki/features/archiving.md: "How old is measured" note + effects list
  - wiki/features/file-manager.md: "What happens when you click Purge"
    section + explicit age-rule breakdown
  - CHANGELOG: archive auto-purge entry rewritten to mention reprint
    semantics, `archives:purge` permission backfill, and updated test count
2026-04-23 17:38:41 +02:00
maziggy bf511c54cd feat(#1008): archive auto-purge + dedicated archives:purge permission
Adds an archive counterpart to the library trash sweeper shipped in the
  previous commit. Unlike the library flow, archives are hard-deleted —
  print history is a decaying timeline, so there is no trash intermediate;
  download or favourite anything you want to keep first.

  Backend
  - New ArchivePurgeService (backend/app/services/archive_purge.py) with
    its own 15-minute scheduler loop and a 24h throttle on actual purge
    runs. Delegates every delete to the existing safety-checked
    ArchiveService.delete_archive so the 3MF, thumbnail, timelapse, source
    3MF, F3D, and photo folder all get cleaned up together with the DB
    row. Per-row session via async_session() avoids commit-per-row churn
    on any caller-passed session.
  - New /archives/purge/{preview,settings} + POST /archives/purge routes
    gated on a dedicated archives:purge permission (not archives:delete_all)
    so admins can delegate bulk-delete to a role without granting
    per-archive delete on other users' rows.
  - seed_default_groups() now backfills both library:purge and
    archives:purge on the Administrators group for upgraded installs —
    the original library:purge was added after Administrators was first
    seeded so the "create if not exists" path skipped existing DBs and
    left admins without the permission.
  - 8 new integration tests (defaults, settings roundtrip, bound
    validation, preview, manual purge, auto-purge enabled path, 24h
    throttle, disabled skip).

  Frontend
  - Settings → Archives card gains an auto-purge toggle + age input (7d
    floor, 10y ceiling, 365d default), with a save-toast on every change.
    The bulk "Purge old" button lives on the Archives page header
    (rightmost, after Upload 3MF) to match the File Manager pattern —
    configuration in Settings, one-shot action on the page.
  - New PurgeArchivesModal mirrors PurgeOldFilesModal: live preview (count
    + total size freed + sample filenames) debounced at 300ms, amber
    "hard-delete, no undo" warning.
  - Admin-only UI gates on archives:purge via the standard hasPermission
    hook; Permission TS union updated.
  - i18n blocks across all 8 locales (en/de full, other 6 English
    fallback per project convention).

  Docs
  - CHANGELOG entry under 0.2.4b1 following the existing library-trash
    entry.
  - bambuddy-wiki archiving.md gains a new "Auto-Purge" section.
  - bambuddy-website features.html gets a matching bullet.

  Verification: python -m ruff check backend/app/ clean; 25 integration
  tests pass (8 archive_purge + 17 library_trash regression); npm run
  build clean.
2026-04-23 16:47:53 +02:00