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.
This commit is contained in:
maziggy
2026-05-17 10:54:28 +02:00
parent 0ddf0925b7
commit e7045597bc
18 changed files with 513 additions and 259 deletions
+15 -5
View File
@@ -3506,12 +3506,18 @@ export const api = {
request<void>(`/archives/${id}${purgeStats ? '?purge_stats=true' : ''}`, { method: 'DELETE' }),
// ========== Archive auto-purge (#1008 follow-up) ==========
previewArchivePurge: (olderThanDays: number) =>
request<ArchivePurgePreview>(`/archives/purge/preview?older_than_days=${olderThanDays}`),
executeArchivePurge: (olderThanDays: number) =>
request<{ deleted: number }>('/archives/purge', {
previewArchivePurge: (olderThanDays: number, purgeStats: boolean = false) =>
request<ArchivePurgePreview>(
`/archives/purge/preview?older_than_days=${olderThanDays}&purge_stats=${purgeStats}`,
),
// #1390: purgeStats=false (default) soft-deletes each old archive — Quick Stats
// preserved, files removed from disk, row hidden via deleted_at. true matches
// the single-archive delete's `?purge_stats=true` semantics (hard-deletes the
// linked PrintLogEntry rows so the contribution drops from /stats too).
executeArchivePurge: (olderThanDays: number, purgeStats: boolean = false) =>
request<{ deleted: number; purge_stats: boolean }>('/archives/purge', {
method: 'POST',
body: JSON.stringify({ older_than_days: olderThanDays }),
body: JSON.stringify({ older_than_days: olderThanDays, purge_stats: purgeStats }),
}),
getArchivePurgeSettings: () =>
request<ArchivePurgeSettings>('/archives/purge/settings'),
@@ -5950,6 +5956,10 @@ export interface ArchivePurgePreview {
export interface ArchivePurgeSettings {
enabled: boolean;
days: number;
// #1390: when true, bulk-deletes the linked PrintLogEntry rows so the
// contribution drops from Quick Stats too. Default false — soft-delete,
// Quick Stats preserved.
purge_stats: boolean;
}
export interface LibraryFileUploadResponse {