UI Phase 2: Einstellungen-Tab neu strukturiert (Untergruppen + Erklaertexte) + konfigurierbarer Verlauf (Obergrenze + Zeitlimit)
Settings-Rework (User-Wunsch, mit Multi-Agent-Design-Runde fuer die Taxonomie: 3 Lenses -> Synthese, gegen das echte settingsDraft-Inventar auf Vollstaendigkeit geprueft, 51 Einstellungen je genau einmal platziert). UI (App.tsx + styles.css): - 5 der 6 Bereiche (Allgemein/Entpacken/Geschwindigkeit/Bereinigung/Updates) in klare Untergruppen mit Unter-Ueberschriften (.settings-subhead) gegliedert, je Bereich ein Intro, und unter JEDER Einstellung ein knapper Erklaertext (.setting-hint, <= ~90 Zeichen, echte Umlaute). Reihenfolge nach Aufgaben-Logik (z.B. Allgemein: Speicherort / Download-Verhalten / Verlauf / Oberflaeche / Discord; Entpacken: Ziel & Ablauf / Deutsche Tonspur / Ablageform / Leistung / Passwoerter). Einige Labels praezisiert (z.B. "Codeberg Repo" -> "Update-Quelle", "Light Mode" -> "Heller Modus"). Account-Bereich bleibt fuer Phase 3 unangetastet. Verlauf-Retention konfigurierbar (vorher: hart 500, kein Zeitlimit): - Neue Settings historyMaxEntries (Standard 500) + historyMaxAgeDays (Standard 0=aus) in types/constants/normalizeSettings (geclamped 50..100000 bzw. 0..3650) + App- Default-Snapshot. Zwei neue Zahlenfelder im Bereich Allgemein -> Verlauf, direkt unter "Verlauf speichern"; ausgegraut wenn nicht "Dauerhaft". - storage.ts: pruneHistoryEntries(entries, limits) wendet Alters- (completedAt < jetzt - Tage) und Anzahl-Grenze an; load/save/addHistoryEntry + die *ForRetention- Wrapper nehmen optionale Limits. app-controller reicht die Limits aus den Settings durch (historyLimits()) und schneidet den Verlauf bei Aenderung der Grenzen aktiv neu (damit "aelter als X Tage" wirklich von der Platte fliegt). 2 neue storage-Tests (Anzahl-Cap, Alters-Pruning). 810 Tests gruen, tsc=6 Baseline, self-check + build ok.
This commit is contained in:
@@ -36,7 +36,7 @@ import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
|
||||
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
|
||||
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||
import { MegaWebFallback } from "./mega-web-fallback";
|
||||
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
||||
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||
@@ -120,7 +120,7 @@ export class AppController {
|
||||
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
||||
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
||||
onHistoryEntry: (entry: HistoryEntry) => {
|
||||
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry);
|
||||
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry, this.historyLimits());
|
||||
}
|
||||
});
|
||||
this.manager.on("state", (snapshot: UiSnapshot) => {
|
||||
@@ -338,9 +338,13 @@ export class AppController {
|
||||
|
||||
this.overlayLiveUsageCounters(nextSettings);
|
||||
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
|
||||
const historyLimitsChanged = previousSettings.historyMaxEntries !== nextSettings.historyMaxEntries
|
||||
|| previousSettings.historyMaxAgeDays !== nextSettings.historyMaxAgeDays;
|
||||
this.settings = nextSettings;
|
||||
if (retentionChanged) {
|
||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||
} else if (historyLimitsChanged && this.settings.historyRetentionMode !== "never") {
|
||||
saveHistory(this.storagePaths, loadHistory(this.storagePaths), this.historyLimits());
|
||||
}
|
||||
saveSettings(this.storagePaths, this.settings);
|
||||
this.manager.setSettings(this.settings);
|
||||
@@ -644,7 +648,7 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
appVersion: APP_VERSION,
|
||||
exportedAt: new Date().toISOString(),
|
||||
session: this.manager.getSession(),
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode)
|
||||
history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits())
|
||||
});
|
||||
this.audit("INFO", "Backup exportiert", {
|
||||
kind: payloadObj.kind,
|
||||
@@ -803,8 +807,12 @@ public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
logger.info("App beendet");
|
||||
}
|
||||
|
||||
private historyLimits(): { maxEntries: number; maxAgeDays: number } {
|
||||
return { maxEntries: this.settings.historyMaxEntries, maxAgeDays: this.settings.historyMaxAgeDays };
|
||||
}
|
||||
|
||||
public getHistory(): HistoryEntry[] {
|
||||
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits());
|
||||
}
|
||||
|
||||
public clearHistory(): void {
|
||||
|
||||
@@ -101,6 +101,8 @@ export function defaultSettings(): AppSettings {
|
||||
theme: "dark" as const,
|
||||
collapseNewPackages: true,
|
||||
historyRetentionMode: "permanent",
|
||||
historyMaxEntries: 500,
|
||||
historyMaxAgeDays: 0,
|
||||
accountListShowDetailedDebridLinkKeys: false,
|
||||
autoSortPackagesByProgress: true,
|
||||
autoSkipExtracted: false,
|
||||
|
||||
+32
-13
@@ -451,6 +451,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
historyRetentionMode: VALID_HISTORY_RETENTION_MODES.has(settings.historyRetentionMode)
|
||||
? settings.historyRetentionMode
|
||||
: defaults.historyRetentionMode,
|
||||
historyMaxEntries: clampNumber(settings.historyMaxEntries, defaults.historyMaxEntries, 50, 100000),
|
||||
historyMaxAgeDays: clampNumber(settings.historyMaxAgeDays, defaults.historyMaxAgeDays, 0, 3650),
|
||||
accountListShowDetailedDebridLinkKeys: settings.accountListShowDetailedDebridLinkKeys !== undefined
|
||||
? Boolean(settings.accountListShowDetailedDebridLinkKeys)
|
||||
: defaults.accountListShowDetailedDebridLinkKeys,
|
||||
@@ -1129,6 +1131,23 @@ export async function saveSessionAsync(paths: StoragePaths, session: SessionStat
|
||||
}
|
||||
|
||||
const MAX_HISTORY_ENTRIES = 500;
|
||||
const HISTORY_HARD_CAP = 100000;
|
||||
|
||||
export interface HistoryLimits {
|
||||
maxEntries: number;
|
||||
maxAgeDays: number;
|
||||
}
|
||||
|
||||
function pruneHistoryEntries(entries: HistoryEntry[], limits?: HistoryLimits, now = Date.now()): HistoryEntry[] {
|
||||
const maxEntries = limits && limits.maxEntries > 0 ? Math.min(limits.maxEntries, HISTORY_HARD_CAP) : MAX_HISTORY_ENTRIES;
|
||||
const maxAgeDays = limits && limits.maxAgeDays > 0 ? limits.maxAgeDays : 0;
|
||||
let result = entries;
|
||||
if (maxAgeDays > 0) {
|
||||
const cutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
||||
result = result.filter((entry) => entry.completedAt >= cutoff);
|
||||
}
|
||||
return result.length > maxEntries ? result.slice(0, maxEntries) : result;
|
||||
}
|
||||
|
||||
export function normalizeHistoryEntry(raw: unknown, index: number): HistoryEntry | null {
|
||||
const entry = asRecord(raw);
|
||||
@@ -1153,7 +1172,7 @@ export function normalizeHistoryEntry(raw: unknown, index: number): HistoryEntry
|
||||
};
|
||||
}
|
||||
|
||||
export function loadHistory(paths: StoragePaths): HistoryEntry[] {
|
||||
export function loadHistory(paths: StoragePaths, limits?: HistoryLimits): HistoryEntry[] {
|
||||
ensureBaseDir(paths.baseDir);
|
||||
if (!fs.existsSync(paths.historyFile)) {
|
||||
return [];
|
||||
@@ -1164,19 +1183,19 @@ export function loadHistory(paths: StoragePaths): HistoryEntry[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
|
||||
const entries: HistoryEntry[] = [];
|
||||
for (let i = 0; i < raw.length && entries.length < MAX_HISTORY_ENTRIES; i++) {
|
||||
for (let i = 0; i < raw.length && entries.length < HISTORY_HARD_CAP; i++) {
|
||||
const normalized = normalizeHistoryEntry(raw[i], i);
|
||||
if (normalized) entries.push(normalized);
|
||||
}
|
||||
return entries;
|
||||
return pruneHistoryEntries(entries, limits);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function saveHistory(paths: StoragePaths, entries: HistoryEntry[]): void {
|
||||
export function saveHistory(paths: StoragePaths, entries: HistoryEntry[], limits?: HistoryLimits): void {
|
||||
ensureBaseDir(paths.baseDir);
|
||||
const trimmed = entries.slice(0, MAX_HISTORY_ENTRIES);
|
||||
const trimmed = pruneHistoryEntries(entries, limits);
|
||||
const payload = JSON.stringify(trimmed, safeJsonReplacer, 2);
|
||||
const tempPath = `${paths.historyFile}.tmp`;
|
||||
try {
|
||||
@@ -1188,22 +1207,22 @@ export function saveHistory(paths: StoragePaths, entries: HistoryEntry[]): void
|
||||
}
|
||||
}
|
||||
|
||||
export function addHistoryEntry(paths: StoragePaths, entry: HistoryEntry): HistoryEntry[] {
|
||||
const existing = loadHistory(paths);
|
||||
const updated = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
|
||||
saveHistory(paths, updated);
|
||||
export function addHistoryEntry(paths: StoragePaths, entry: HistoryEntry, limits?: HistoryLimits): HistoryEntry[] {
|
||||
const existing = loadHistory(paths, limits);
|
||||
const updated = pruneHistoryEntries([entry, ...existing], limits);
|
||||
saveHistory(paths, updated, limits);
|
||||
return updated;
|
||||
}
|
||||
|
||||
export function loadHistoryForRetention(paths: StoragePaths, retentionMode: HistoryRetentionMode): HistoryEntry[] {
|
||||
return retentionMode === "never" ? [] : loadHistory(paths);
|
||||
export function loadHistoryForRetention(paths: StoragePaths, retentionMode: HistoryRetentionMode, limits?: HistoryLimits): HistoryEntry[] {
|
||||
return retentionMode === "never" ? [] : loadHistory(paths, limits);
|
||||
}
|
||||
|
||||
export function addHistoryEntryForRetention(paths: StoragePaths, retentionMode: HistoryRetentionMode, entry: HistoryEntry): HistoryEntry[] {
|
||||
export function addHistoryEntryForRetention(paths: StoragePaths, retentionMode: HistoryRetentionMode, entry: HistoryEntry, limits?: HistoryLimits): HistoryEntry[] {
|
||||
if (retentionMode === "never") {
|
||||
return [];
|
||||
}
|
||||
return addHistoryEntry(paths, entry);
|
||||
return addHistoryEntry(paths, entry, limits);
|
||||
}
|
||||
|
||||
export function resetHistoryForRetention(paths: StoragePaths, retentionMode: HistoryRetentionMode): void {
|
||||
|
||||
Reference in New Issue
Block a user