Prepare v2.0.74 sidepanel reliability release

Restore and persist the package-based link collector with progressive metadata, bounded high-volume updates, safe hydration, visible selection, and complete localization.

Harden download controls, snapshot ordering, history pagination, statistics recovery, settings saves, backup imports, notification persistence, and Windows storage races with regression coverage.
This commit is contained in:
Sucukdeluxe
2026-08-26 20:35:06 +02:00
parent bdcf9e3754
commit 2ea494cd5c
69 changed files with 7660 additions and 883 deletions
+70 -7
View File
@@ -2,6 +2,11 @@ export type CollectorAvailability = "online" | "offline" | "unknown";
export type CollectorLinkStatus = "ready" | "offline" | "unknown";
export type CollectorPackageNameSource = "explicit" | "inferred";
export const COLLECTOR_MAX_PACKAGES = 2_000;
export const COLLECTOR_MAX_LINKS = 20_000;
export const COLLECTOR_MAX_NAME_LENGTH = 1_024;
export const COLLECTOR_MAX_PERSISTENCE_BYTES = 64 * 1024 * 1024;
export interface CollectorLink {
id: string;
url: string;
@@ -52,6 +57,64 @@ export interface CollectorPersistenceState {
collapsedPackageIds: string[];
}
export type CollectorCapacityResult = {
ok: true;
packageCount: number;
linkCount: number;
} | {
ok: false;
packageCount: number;
linkCount: number;
message: string;
};
export type CollectorPersistenceSizeResult = {
ok: true;
byteCount: number;
} | {
ok: false;
byteCount: number;
message: string;
};
export function inspectCollectorCapacity(packages: readonly Pick<CollectorPackage, "links">[]): CollectorCapacityResult {
const packageCount = packages.length;
let linkCount = 0;
for (const pkg of packages) linkCount += pkg.links.length;
if (packageCount > COLLECTOR_MAX_PACKAGES) {
return {
ok: false,
packageCount,
linkCount,
message: `Der Linksammler kann höchstens ${COLLECTOR_MAX_PACKAGES.toLocaleString("de-DE")} Pakete enthalten.`
};
}
if (linkCount > COLLECTOR_MAX_LINKS) {
return {
ok: false,
packageCount,
linkCount,
message: `Der Linksammler kann höchstens ${COLLECTOR_MAX_LINKS.toLocaleString("de-DE")} Links enthalten.`
};
}
return { ok: true, packageCount, linkCount };
}
export function inspectCollectorPersistenceSize(
state: CollectorPersistenceState,
maximumBytes = COLLECTOR_MAX_PERSISTENCE_BYTES
): CollectorPersistenceSizeResult {
const byteCount = new TextEncoder().encode(JSON.stringify({
version: 1,
packages: state.packages,
collapsedPackageIds: state.collapsedPackageIds,
updatedAt: Number.MAX_SAFE_INTEGER
})).byteLength;
return byteCount <= maximumBytes
? { ok: true, byteCount }
: { ok: false, byteCount, message: "Der Linksammler ist zu groß, um gespeichert zu werden." };
}
function validAddedAt(value: unknown): value is number {
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
}
@@ -107,7 +170,7 @@ function validCollectorLink(value: unknown): value is CollectorLink {
&& raw.url.length <= 32767
&& /^https?:\/\/[^\s]+$/i.test(raw.url)
&& typeof raw.fileName === "string"
&& raw.fileName.length <= 1024
&& raw.fileName.length <= COLLECTOR_MAX_NAME_LENGTH
&& (raw.fileSizeBytes === null || (typeof raw.fileSizeBytes === "number" && Number.isSafeInteger(raw.fileSizeBytes) && raw.fileSizeBytes >= 0))
&& typeof raw.hoster === "string"
&& raw.hoster.length <= 255
@@ -125,7 +188,7 @@ function validCollectorPackage(value: unknown): value is CollectorPackage {
&& raw.id.length <= 160
&& typeof raw.name === "string"
&& raw.name.length > 0
&& raw.name.length <= 1024
&& raw.name.length <= COLLECTOR_MAX_NAME_LENGTH
&& (raw.nameSource === "explicit" || raw.nameSource === "inferred")
&& Array.isArray(raw.links)
&& raw.links.length > 0
@@ -144,9 +207,9 @@ export function validateCollectorEnrichmentRequest(value: unknown): CollectorEnr
|| raw.requestId.length > 160
|| !Array.isArray(raw.packages)
|| raw.packages.length === 0
|| raw.packages.length > 2_000
|| raw.packages.length > COLLECTOR_MAX_PACKAGES
|| raw.packages.some((entry) => !validCollectorPackage(entry))
|| raw.packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > 20_000) {
|| raw.packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > COLLECTOR_MAX_LINKS) {
throw new Error("Linksammler-Anreicherung ist ungültig");
}
return { requestId: raw.requestId, packages: structuredClone(raw.packages) as CollectorPackage[] };
@@ -161,11 +224,11 @@ export function validateCollectorPersistenceState(value: unknown): CollectorPers
const collapsedPackageIds = raw.collapsedPackageIds;
if (Object.keys(raw).some((key) => key !== "packages" && key !== "collapsedPackageIds")
|| !Array.isArray(packages)
|| packages.length > 2_000
|| packages.length > COLLECTOR_MAX_PACKAGES
|| packages.some((entry) => !validCollectorPackage(entry))
|| packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > 20_000
|| packages.reduce((sum, entry) => sum + (entry as CollectorPackage).links.length, 0) > COLLECTOR_MAX_LINKS
|| !Array.isArray(collapsedPackageIds)
|| collapsedPackageIds.length > 2_000
|| collapsedPackageIds.length > COLLECTOR_MAX_PACKAGES
|| collapsedPackageIds.some((entry) => typeof entry !== "string" || entry.length === 0 || entry.length > 160)) {
throw new Error("Linksammler-Speicherzustand ist ungültig");
}
+1
View File
@@ -19,6 +19,7 @@ export const IPC_CHANNELS = {
ENRICH_COLLECTOR_PACKAGES: "collector:enrich-packages",
COLLECTOR_ENRICHMENT_PROGRESS: "collector:enrichment-progress",
GET_COLLECTOR_STATE: "collector:get-state",
GET_COLLECTOR_STATE_SYNC: "collector:get-state-sync",
SAVE_COLLECTOR_STATE: "collector:save-state",
SAVE_COLLECTOR_STATE_SYNC: "collector:save-state-sync",
GET_START_CONFLICTS: "queue:get-start-conflicts",
+2 -1
View File
@@ -91,8 +91,9 @@ export interface ElectronApi {
enrichCollectorPackages: (request: CollectorEnrichmentRequest) => Promise<CollectorInspectionResult>;
onCollectorEnrichmentProgress: (callback: (progress: CollectorEnrichmentProgress) => void) => () => void;
getCollectorState: () => Promise<CollectorPersistenceState>;
getCollectorStateSync: () => CollectorPersistenceState | null;
saveCollectorState: (state: CollectorPersistenceState) => Promise<CollectorPersistenceState>;
saveCollectorStateSync: (state: CollectorPersistenceState) => void;
saveCollectorStateSync: (state: CollectorPersistenceState) => boolean;
getPathForDroppedFile: (file: File) => string;
getStartConflicts: () => Promise<StartConflictEntry[]>;
resolveStartConflict: (packageId: string, policy: DuplicatePolicy) => Promise<StartConflictResolutionResult>;
+11 -1
View File
@@ -28,6 +28,7 @@ export type DebridProvider =
| "linksnappy";
export type DebridFallbackProvider = DebridProvider | "none";
export type AppTheme = "dark" | "light";
export type ThemePreference = AppTheme | "system";
export type AppLanguage = "en" | "de";
export type PackagePriority = "high" | "normal" | "low";
export type ExtractCpuPriority = "high" | "middle" | "low";
@@ -87,6 +88,10 @@ export interface StatisticsRolling24Hours {
export interface StatisticsLedger {
version: 2;
startedAt: number;
minuteTrackingStartedAt?: number;
providerSeedSuppressedDay?: string;
providerSeedBaselineBytes?: Partial<Record<DebridProvider, number>>;
providerBytesOnlyDays?: string[];
days: StatisticsDayBucket[];
minutes: StatisticsMinuteBucket[];
}
@@ -202,6 +207,7 @@ export interface AppSettings extends DailyStartSettings {
clipboardWatch: boolean;
minimizeToTray: boolean;
theme: AppTheme;
themePreference: ThemePreference;
logStorageLocation: LogStorageLocation;
collapseNewPackages: boolean;
animatePackageDisclosure: boolean;
@@ -336,6 +342,7 @@ export interface RendererSettings extends DailyStartSettings {
clipboardWatch: boolean;
minimizeToTray: boolean;
theme: AppTheme;
themePreference: ThemePreference;
logStorageLocation: LogStorageLocation;
collapseNewPackages: boolean;
animatePackageDisclosure: boolean;
@@ -687,6 +694,8 @@ export interface UiSnapshot {
clipboardActive: boolean;
reconnectSeconds: number;
packageSpeedBps: Record<string, number>;
runRemainingBytes?: number;
runRemainingUnknownItems?: number;
diskWaitEvents?: Array<{
phase: "download" | "extract" | "remux";
ownerId: string;
@@ -698,8 +707,9 @@ export interface UiSnapshot {
deficitBytes: number;
retryAt: number;
}>;
snapshotRevision?: number;
payloadKind?: "full" | "delta";
removedItemIds?: string[];
removedItemIds?: string[];
removedPackageIds?: string[];
rotationEvents?: RotationEvent[];
accountRuntime?: AccountRuntimeEntry[];