mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
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.
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""Archive auto-purge endpoints (#1008 follow-up).
|
|
|
|
Admin-only (``ARCHIVES_PURGE``). Provides:
|
|
|
|
* ``GET /archives/purge/preview`` — live count for the admin slider
|
|
* ``POST /archives/purge`` — one-shot manual bulk delete
|
|
* ``GET/PUT /archives/purge/settings`` — auto-purge toggle + threshold
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from backend.app.core.auth import require_permission_if_auth_enabled
|
|
from backend.app.core.database import get_db
|
|
from backend.app.core.permissions import Permission
|
|
from backend.app.models.user import User
|
|
from backend.app.schemas.archive_purge import (
|
|
ArchivePurgePreviewResponse,
|
|
ArchivePurgeRequest,
|
|
ArchivePurgeResponse,
|
|
ArchivePurgeSettings,
|
|
)
|
|
from backend.app.services.archive_purge import (
|
|
MAX_AUTO_PURGE_DAYS,
|
|
MIN_AUTO_PURGE_DAYS,
|
|
archive_purge_service,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
router = APIRouter(prefix="/archives", tags=["archives-purge"])
|
|
|
|
|
|
@router.get("/purge/preview", response_model=ArchivePurgePreviewResponse)
|
|
async def preview_archive_purge(
|
|
older_than_days: int = Query(ge=1, le=3650),
|
|
purge_stats: bool = Query(
|
|
False,
|
|
description=(
|
|
"When False (default) the count reflects soft-delete mode — "
|
|
"already-soft-deleted rows are excluded so the number matches "
|
|
"what a fresh purge would actually touch. When True the count "
|
|
"includes already-soft-deleted rows (eligible for promotion to "
|
|
"hard-delete). #1390."
|
|
),
|
|
),
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
"""Count + size of archives eligible for purge. Read-only."""
|
|
result = await archive_purge_service.preview_purge(db, older_than_days=older_than_days, purge_stats=purge_stats)
|
|
return ArchivePurgePreviewResponse(**result)
|
|
|
|
|
|
@router.post("/purge", response_model=ArchivePurgeResponse)
|
|
async def execute_archive_purge(
|
|
body: ArchivePurgeRequest,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
"""Bulk-delete archives older than the threshold.
|
|
|
|
Soft-delete by default (Quick Stats preserved). Set ``purge_stats=true``
|
|
in the body to also drop the contribution from /stats — irreversible
|
|
in that mode, same as the single-archive route's ``?purge_stats=true``.
|
|
"""
|
|
deleted = await archive_purge_service.purge_older_than(
|
|
db,
|
|
older_than_days=body.older_than_days,
|
|
purge_stats=body.purge_stats,
|
|
)
|
|
return ArchivePurgeResponse(deleted=deleted, purge_stats=body.purge_stats)
|
|
|
|
|
|
@router.get("/purge/settings", response_model=ArchivePurgeSettings)
|
|
async def get_archive_purge_settings(
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
cfg = await archive_purge_service.get_settings(db)
|
|
return ArchivePurgeSettings(enabled=cfg["enabled"], days=cfg["days"], purge_stats=cfg["purge_stats"])
|
|
|
|
|
|
@router.put("/purge/settings", response_model=ArchivePurgeSettings)
|
|
async def update_archive_purge_settings(
|
|
body: ArchivePurgeSettings,
|
|
db: AsyncSession = Depends(get_db),
|
|
_: User | None = Depends(require_permission_if_auth_enabled(Permission.ARCHIVES_PURGE)),
|
|
):
|
|
if body.days < MIN_AUTO_PURGE_DAYS or body.days > MAX_AUTO_PURGE_DAYS:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"days must be between {MIN_AUTO_PURGE_DAYS} and {MAX_AUTO_PURGE_DAYS}",
|
|
)
|
|
saved = await archive_purge_service.set_settings(
|
|
db, enabled=body.enabled, days=body.days, purge_stats=body.purge_stats
|
|
)
|
|
return ArchivePurgeSettings(enabled=saved["enabled"], days=saved["days"], purge_stats=saved["purge_stats"])
|