release: harden provider rotation and download recovery
Apply account and provider changes to active conversions without restarting, isolate API and Web state, and abort the exact fallback attempt when settings change. Bound resume recovery, make disk reservations abortable, preserve cleanup totals and history, stabilize compact UI state, and canonicalize RapidGator host aliases. Expand bounded support diagnostics while redacting account identities, local paths, package names, and file names from current and rotated logs. Add regression coverage for rotation, live settings, HTTP 416 recovery, disk waits, cleanup, context menus, history failures, and support bundle privacy.
This commit is contained in:
+31
-6
@@ -1,12 +1,14 @@
|
||||
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { getMegaDebridAccountStatusId } from "../shared/mega-debrid-accounts";
|
||||
import type {
|
||||
AccountCreateCommand,
|
||||
AllDebridHostInfo,
|
||||
AppTheme,
|
||||
BandwidthScheduleEntry,
|
||||
DebugSetupCheckResult,
|
||||
BandwidthScheduleEntry,
|
||||
DebugSetupCheckResult,
|
||||
DebridAccountStatus,
|
||||
DebridFallbackProvider,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
@@ -878,6 +880,24 @@ export function getSnapshotRenderDelay(_itemCount: number, _running: boolean, _a
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function resolveAccountStatus(
|
||||
statuses: Readonly<Record<string, DebridAccountStatus>>,
|
||||
accountId: string | null,
|
||||
kind: AccountKind
|
||||
): DebridAccountStatus | undefined {
|
||||
if (!accountId) {
|
||||
return undefined;
|
||||
}
|
||||
const mode = kind === "megadebrid-api"
|
||||
? "api"
|
||||
: kind === "megadebrid-web"
|
||||
? "web"
|
||||
: null;
|
||||
return mode
|
||||
? statuses[getMegaDebridAccountStatusId(accountId, mode)] ?? statuses[accountId]
|
||||
: statuses[accountId];
|
||||
}
|
||||
|
||||
export interface ResetUiActionGate {
|
||||
busy: boolean;
|
||||
}
|
||||
@@ -2513,7 +2533,9 @@ export function App(): ReactElement {
|
||||
), [configuredAccountServices]);
|
||||
const accountEditOption = accountEditDialog ? findAccountOption(accountEditDialog.target.kind) : null;
|
||||
const accountEditRow = accountEditDialog ? accountRows.find((row) => row.rowKey === accountEditDialog.target.rowKey) ?? null : null;
|
||||
const accountEditStatus = accountEditRow?.accountId ? snapshot.settings.debridAccountStatuses?.[accountEditRow.accountId] ?? null : null;
|
||||
const accountEditStatus = accountEditRow
|
||||
? resolveAccountStatus(snapshot.settings.debridAccountStatuses, accountEditRow.accountId, accountEditRow.entry.kind) ?? null
|
||||
: null;
|
||||
const accountEditQuickAction = accountEditOption ? getAccountQuickActionMeta(accountEditOption.kind) : null;
|
||||
const accountDialogOption = accountDialog?.kind ? findAccountOption(accountDialog.kind) : null;
|
||||
const accountDialogSelectableOptions = useMemo(() => {
|
||||
@@ -3041,7 +3063,10 @@ export function App(): ReactElement {
|
||||
const removeAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
void (async () => {
|
||||
const username = resolveAccountUsername(row.username, row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId]?.email : undefined);
|
||||
const username = resolveAccountUsername(
|
||||
row.username,
|
||||
resolveAccountStatus(snapshot.settings.debridAccountStatuses, row.accountId, row.entry.kind)?.email
|
||||
);
|
||||
const confirmed = await askConfirmPrompt({
|
||||
title: `${row.hosterLabel} entfernen`,
|
||||
message: `Soll ${row.hosterLabel}${username !== "—" ? ` (${username})` : ""} wirklich entfernt werden?`,
|
||||
@@ -4987,7 +5012,7 @@ export function App(): ReactElement {
|
||||
? accountRowBindings.get(accountContextMenu.rowId) ?? null
|
||||
: null;
|
||||
const accountSources = useMemo<AccountRowSource[]>(() => accountRows.map((row) => {
|
||||
const checkedStatus = row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId] : undefined;
|
||||
const checkedStatus = resolveAccountStatus(snapshot.settings.debridAccountStatuses, row.accountId, row.entry.kind);
|
||||
const state: AccountRowSource["status"]["state"] = row.disabled
|
||||
? "disabled"
|
||||
: !checkedStatus
|
||||
|
||||
@@ -35,7 +35,7 @@ export type ContextMenuKeyboardAction =
|
||||
|
||||
export type ContextMenuSubmenuKeyboardAction = "open" | "close";
|
||||
|
||||
export function clampContextMenuPosition(
|
||||
export function clampContextMenuPosition(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
@@ -46,8 +46,30 @@ export function clampContextMenuPosition(
|
||||
return {
|
||||
x: Math.max(0, Math.min(x, Math.max(0, viewportWidth - width))),
|
||||
y: Math.max(0, Math.min(y, Math.max(0, viewportHeight - height)))
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function observeContextMenuPosition(
|
||||
menu: Pick<HTMLElement, "getBoundingClientRect">,
|
||||
anchor: () => { x: number; y: number },
|
||||
onPosition: (position: { x: number; y: number }) => void
|
||||
): () => void {
|
||||
const reposition = (): void => {
|
||||
const rect = menu.getBoundingClientRect();
|
||||
const point = anchor();
|
||||
onPosition(clampContextMenuPosition(
|
||||
point.x,
|
||||
point.y,
|
||||
rect.width,
|
||||
rect.height,
|
||||
window.innerWidth,
|
||||
window.innerHeight
|
||||
));
|
||||
};
|
||||
reposition();
|
||||
window.addEventListener("resize", reposition);
|
||||
return () => window.removeEventListener("resize", reposition);
|
||||
}
|
||||
|
||||
export function getContextSubmenuPosition(
|
||||
trigger: { left: number; right: number; top: number },
|
||||
@@ -223,13 +245,14 @@ export const ContextMenu = forwardRef<HTMLDivElement, ContextMenuProps>(function
|
||||
if (!previousFocusRef.current) {
|
||||
previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||
}
|
||||
const rect = menuRef.current.getBoundingClientRect();
|
||||
const next = clampContextMenuPosition(x, y, rect.width, rect.height, window.innerWidth, window.innerHeight);
|
||||
setPosition((current) => current.x === next.x && current.y === next.y && current.sourceX === x && current.sourceY === y && current.ready
|
||||
? current
|
||||
: { ...next, sourceX: x, sourceY: y, ready: true });
|
||||
getTopLevelMenuItems(menuRef.current)[0]?.focus();
|
||||
}, [open, x, y]);
|
||||
const stopObserving = observeContextMenuPosition(menuRef.current, () => ({ x, y }), (next) => {
|
||||
setPosition((current) => current.x === next.x && current.y === next.y && current.sourceX === x && current.sourceY === y && current.ready
|
||||
? current
|
||||
: { ...next, sourceX: x, sourceY: y, ready: true });
|
||||
});
|
||||
getTopLevelMenuItems(menuRef.current)[0]?.focus();
|
||||
return stopObserving;
|
||||
}, [open, x, y]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
|
||||
@@ -147,6 +147,8 @@ export function compactDownloadStatus(value: string): string {
|
||||
}
|
||||
if (/^Entpack-Fehler\b/i.test(status)) return "Entpack-Fehler";
|
||||
if (/^Extraction error\b/i.test(status)) return "Extraction error";
|
||||
if (/^Entpacken\s*-\s*(?:Error|Fehler)\b/i.test(status)) return "Entpack-Fehler";
|
||||
if (/^Extracting\s*-\s*(?:Error|Fehler)\b/i.test(status)) return "Extraction error";
|
||||
const extractionPending = status.match(/^(Entpacken|Extracting)\s*-\s*(Ausstehend|Pending|Warten auf Parts|Waiting for parts)/i);
|
||||
if (extractionPending) return `${extractionPending[1]} - ${extractionPending[2]}`;
|
||||
const extracting = status.match(/Entpacken\s+(\d+)%/i);
|
||||
@@ -168,6 +170,8 @@ export function compactDownloadStatus(value: string): string {
|
||||
if (percentage) return `${finalizing[1]} - ${progress(Number(percentage[1]))}%`;
|
||||
return finalizing[1];
|
||||
}
|
||||
if (/^Fehler(?:\s*:|$)/i.test(status)) return "Fehler";
|
||||
if (/^Error(?:\s*:|$)/i.test(status)) return "Error";
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { DebridProvider, HistoryEntry } from "../../../shared/types";
|
||||
import { normalizeHosterHostname } from "../../../shared/hoster";
|
||||
|
||||
export type HistoryFilter = "all" | "today" | "week" | "older" | "completed" | "deleted" | "failed";
|
||||
export type HistoryViewStatus = HistoryEntry["status"] | "failed";
|
||||
@@ -168,11 +169,12 @@ export function deriveHistoryHoster(urls: string[] | undefined): string {
|
||||
continue;
|
||||
}
|
||||
const hostname = url.hostname.toLocaleLowerCase("de-DE");
|
||||
if (!hostname || seen.has(hostname)) {
|
||||
const hoster = normalizeHosterHostname(hostname) === "rapidgator" ? "rapidgator.net" : hostname;
|
||||
if (!hostname || seen.has(hoster)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(hostname);
|
||||
hostnames.push(hostname);
|
||||
seen.add(hoster);
|
||||
hostnames.push(hoster);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user