feat(admin dashboard): clarify quota usage vs backend usage

This commit is contained in:
Edouard Vanbelle
2026-08-02 12:08:08 +02:00
parent dd1528de92
commit 4297d8139a
7 changed files with 421 additions and 51 deletions
@@ -1,8 +1,11 @@
//! First tenant of Part 2 (recoverable-run engine).
//!
//! Iterates `storage.drives` and reports each drive whose cached
//! `used_bytes` differs from `SUM(files.size) WHERE NOT is_trashed`
//! for that drive. **Read-only** — reports drift as findings but does
//! `used_bytes` differs from `SUM(files.size)` for that drive.
//! Includes trashed files — matches the hot-path delta (upload
//! writes never decrement on `move_to_trash`) and the sweep at
//! `storage_usage_service.rs::update_all_drives_storage_usage`.
//! **Read-only** — reports drift as findings but does
//! NOT fix it. The existing `storage_reconcile` job (Part 1) is what
//! corrects the counter; this check surfaces WHEN drift happens so
//! operators can trace it back to root cause (missed delta call,
@@ -167,7 +170,6 @@ impl RecoverableJobHandler for DrivesConsistencyCheck {
SELECT SUM(size)::bigint
FROM storage.files
WHERE drive_id = d.id
AND NOT is_trashed
), 0) AS actual_bytes
FROM storage.drives d
LEFT JOIN storage.folders rf ON rf.id = d.root_folder_id
@@ -75,6 +75,15 @@ pub const STORAGE_MIGRATION_JOB_NAME: &str = "storage_migration";
/// projections read the same constant.
pub const TARGET_NAME_PARAM: &str = "target_name";
/// Companion to [`TARGET_NAME_PARAM`] — records the source entry
/// name (the active backend at Fresh-open time) so a run row read
/// months later self-describes the migration direction. Without
/// this, an operator inspecting a Completed row from an old
/// deployment could see "migrated to `s3_prod`" but had to
/// cross-reference `admin_settings` history to know what it came
/// from. Stamped once on Fresh open; Resume reads it back.
pub const SOURCE_NAME_PARAM: &str = "source_name";
/// Rows per batch. Copies are I/O-bound (source read + target write);
/// larger batches amortise fewer SQL round-trips but the checkpoint
/// / cancel-poll cadence lengthens. 100 balances the two — one
@@ -259,11 +268,56 @@ impl RecoverableJobHandler for StorageMigrationService {
// reference reads from this local. A hot-swap that fires
// mid-run (e.g., a second migration starting after this one
// completes) doesn't reshape our decisions from underneath.
let active_backend_name = self
.active_backend_name
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
//
// For Fresh runs we ALSO stamp this into `params.source_name`
// so an audit-log reader can self-describe the migration
// direction without cross-referencing `admin_settings`
// history. On Resume we read it back — the ORIGINAL source
// (from when the run was opened) is what's audit-worthy,
// not whatever the active backend happens to be at resume
// time.
let active_backend_name = if is_fresh {
let snap = self
.active_backend_name
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Err(e) = store.set_string_param(SOURCE_NAME_PARAM, &snap).await {
return RunOutcome::Failed {
message: format!("failed to persist source_name to params: {e}"),
};
}
snap
} else {
match store.get_string_param(SOURCE_NAME_PARAM).await {
Ok(Some(name)) => name,
Ok(None) => {
// Paused row predates K3.8's source-stamping.
// Fall back to current active name and log a
// note so the audit trail is at least
// approximately correct.
let fallback = self
.active_backend_name
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
tracing::warn!(
target: "oxicloud::migration",
event = "storage_migration.legacy_paused_row_source_defaulted",
run_id = %store.run_id(),
fallback_source = %fallback,
"resumed run has no source_name in params (pre-K3.8 row) — defaulting \
to current active backend for the audit line"
);
fallback
}
Err(e) => {
return RunOutcome::Failed {
message: format!("read {SOURCE_NAME_PARAM} from params: {e}"),
};
}
}
};
// First-line guard: target name equals the currently-active
// entry. Silent no-op if we let it through — the app would