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:
Sucukdeluxe
2026-06-15 00:49:19 +02:00
parent 3a37c5a537
commit de154ab783
7 changed files with 296 additions and 94 deletions
+12 -4
View File
@@ -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 {
+2
View File
@@ -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
View File
@@ -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 {