mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
e0982e708f
The panels follow the reworked reclaim state: Check now starts a check in the background and the panel polls until it is done, a category with an unusually large share of unused files offers Confirm, Freed comes from the last run that deleted, a stopped check says why and no longer reads as nothing found, the switch waits for its answer and a poll that was in flight cannot flip it back, and refusals name their reason in the operator's language. Strings updated in all 28 languages.
114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
import {
|
|
STORAGE_RECLAIM_CATEGORIES,
|
|
type StorageReclaimCategoryReport,
|
|
type StorageReclaimReport,
|
|
type StorageReclaimState,
|
|
} from '@trek/shared';
|
|
|
|
/** A number of files and their size, as the report counts them. */
|
|
export interface ReclaimCount {
|
|
objects: number;
|
|
bytes: number;
|
|
}
|
|
|
|
/** What one category of the last check has to say, when it has anything to say. */
|
|
interface ReclaimCategoryLine {
|
|
category: (typeof STORAGE_RECLAIM_CATEGORIES)[number];
|
|
/** Unused for a week or more: what the nightly run deletes once deletion is on. */
|
|
freeable: ReclaimCount;
|
|
/** Unused for less than a week: these get their week first. */
|
|
waiting: ReclaimCount;
|
|
/**
|
|
* An unusually large share of the category looks unused: nothing in it is
|
|
* deleted until an admin confirms that this is expected (a cleaned-up
|
|
* backlog, say, rather than a second installation in the same bucket).
|
|
*/
|
|
confirmationRequired: boolean;
|
|
/** Why the category was left alone, or stopped part way; null when it was checked through. */
|
|
skipped: Exclude<StorageReclaimCategoryReport['skipped'], 'not_reclaimed'> | null;
|
|
}
|
|
|
|
/** The admin's view of the reclaim pass: the switch and what the last check found. */
|
|
export interface ReclaimView {
|
|
deleteEnabled: boolean;
|
|
/** A check, nightly or on demand, is running now. */
|
|
running: boolean;
|
|
/** When the last check finished, ms; null before the first one. */
|
|
checkedAt: number | null;
|
|
/** Why the last check stopped before the end; null when it ran through. */
|
|
stopped: StorageReclaimReport['stopped'];
|
|
freeable: ReclaimCount;
|
|
waiting: ReclaimCount;
|
|
/** What the last run with deletion on removed: a check on demand only reports, so it never resets this. */
|
|
freed: ReclaimCount;
|
|
/** When that run finished, ms; null when nothing ever ran with deletion on. */
|
|
freedAt: number | null;
|
|
/** Only the categories with something to free, something waiting, something to confirm, or a reason they were skipped. */
|
|
lines: ReclaimCategoryLine[];
|
|
}
|
|
|
|
const NONE: ReclaimCount = { objects: 0, bytes: 0 };
|
|
|
|
function add(a: ReclaimCount, b: ReclaimCount): ReclaimCount {
|
|
return { objects: a.objects + b.objects, bytes: a.bytes + b.bytes };
|
|
}
|
|
|
|
function minus(a: ReclaimCount, b: ReclaimCount): ReclaimCount {
|
|
return { objects: Math.max(0, a.objects - b.objects), bytes: Math.max(0, a.bytes - b.bytes) };
|
|
}
|
|
|
|
function freedBy(run: StorageReclaimReport | null): ReclaimCount {
|
|
if (!run) return NONE;
|
|
return STORAGE_RECLAIM_CATEGORIES.reduce(
|
|
(sum, category) => add(sum, run.categories[category]?.deleted ?? NONE),
|
|
NONE
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The reclaim state as the storage panels show it, desktop and phone alike.
|
|
* `null` (a server from before the reclaim pass, or a state not loaded) reads
|
|
* as deletion off, not running and never checked.
|
|
*/
|
|
export function reclaimView(state: StorageReclaimState | null): ReclaimView {
|
|
const run = state?.lastRun ?? null;
|
|
const lines: ReclaimCategoryLine[] = [];
|
|
let freeable = NONE;
|
|
let waiting = NONE;
|
|
for (const category of STORAGE_RECLAIM_CATEGORIES) {
|
|
const report = run?.categories[category];
|
|
if (!report || report.skipped === 'not_reclaimed') continue;
|
|
// What was deleted on this run no longer waits to be freed.
|
|
const due = minus(report.eligible, report.deleted);
|
|
const young = minus(report.unreferenced, report.eligible);
|
|
// A category the pass stopped in, or that waits for a confirmation, still
|
|
// counts what it found; one it never judged counts nothing.
|
|
const judged = report.skipped === null || report.skipped === 'interrupted' || report.skipped === 'delete_failed';
|
|
if (judged) {
|
|
freeable = add(freeable, due);
|
|
waiting = add(waiting, young);
|
|
}
|
|
const counts = judged ? { freeable: due, waiting: young } : { freeable: NONE, waiting: NONE };
|
|
if (
|
|
counts.freeable.objects > 0 ||
|
|
counts.waiting.objects > 0 ||
|
|
report.confirmationRequired ||
|
|
report.skipped !== null
|
|
) {
|
|
lines.push({ category, ...counts, confirmationRequired: report.confirmationRequired, skipped: report.skipped });
|
|
}
|
|
}
|
|
const deleteRun = state?.lastDeleteRun ?? null;
|
|
return {
|
|
deleteEnabled: state?.deleteEnabled ?? false,
|
|
running: state?.running ?? false,
|
|
checkedAt: run?.finishedAt ?? null,
|
|
stopped: run?.stopped ?? null,
|
|
freeable,
|
|
waiting,
|
|
freed: freedBy(deleteRun),
|
|
freedAt: deleteRun?.finishedAt ?? null,
|
|
lines,
|
|
};
|
|
}
|