Account-Rotation: Login/Premium-Badges + Live-Rotations-Panel + "Alle pruefen"
- Pro Mega-Debrid-Account UND Debrid-Link-Key im Bearbeiten-Dialog: Badge mit Login-Gueltigkeit + Premium-Restlaufzeit (connectUser vip_end / account/infos premiumLeft) - "Alle pruefen"-Button oben rechts; prueft alle Accounts (Concurrency-Cap 4), Ergebnis persistiert (debridAccountStatuses), ueberlebt Neustart - Rotations-Verlauf-Panel: zeigt live welcher Account/Key versucht wurde + warum gewechselt (Ring-Buffer -> Snapshot -> UI), statt nur "Link-Umwandlung erneut" - Bug A: Mega-Debrid Per-Account-Verbrauch wurde nie erfasst (Heute/Insgesamt immer 0) - Bug B: isProviderConfigured erkannte reine megaCredentials-Multi-Config nicht - Neu: account-check.ts (standalone), CHECK_DEBRID_ACCOUNTS IPC, 13 Tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
748c07a531
commit
3977184fd4
+108
-4
@@ -1,6 +1,6 @@
|
||||
import { CSSProperties, DragEvent, KeyboardEvent as ReactKeyboardEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { parseMegaDebridAccounts, serializeMegaDebridAccounts, maskMegaDebridLogin } from "../shared/mega-debrid-accounts";
|
||||
import { getMegaDebridAccountId, parseMegaDebridAccounts, serializeMegaDebridAccounts, maskMegaDebridLogin } from "../shared/mega-debrid-accounts";
|
||||
import type {
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
@@ -866,6 +866,7 @@ const emptySnapshot = (): UiSnapshot => ({
|
||||
megaDebridAccountDailyLimitBytes: {},
|
||||
megaDebridAccountDailyUsageBytes: {},
|
||||
megaDebridAccountTotalUsageBytes: {},
|
||||
debridAccountStatuses: {},
|
||||
providerDailyUsageDay: getProviderUsageDayKey(),
|
||||
scheduledStartEpochMs: 0
|
||||
},
|
||||
@@ -1121,6 +1122,37 @@ function formatDebridLinkCountQuota(info: DebridLinkHostLimitInfo | null | undef
|
||||
return info.note || "Nicht verfügbar";
|
||||
}
|
||||
|
||||
function formatCheckedAgo(checkedAt: number): string {
|
||||
const deltaMs = Date.now() - checkedAt;
|
||||
if (!Number.isFinite(deltaMs) || deltaMs < 0) return "gerade eben";
|
||||
const min = Math.floor(deltaMs / 60000);
|
||||
if (min < 1) return "gerade eben";
|
||||
if (min < 60) return `vor ${min} Min`;
|
||||
const hours = Math.floor(min / 60);
|
||||
if (hours < 24) return `vor ${hours} Std`;
|
||||
const days = Math.floor(hours / 24);
|
||||
return `vor ${days} Tag${days === 1 ? "" : "en"}`;
|
||||
}
|
||||
|
||||
function rotationEventText(ev: { event: string; cooldownSec?: number; next?: string }): string {
|
||||
switch (ev.event) {
|
||||
case "OK": return "erfolgreich";
|
||||
case "FAILED": {
|
||||
const cd = ev.cooldownSec ? `, Cooldown ${ev.cooldownSec}s` : "";
|
||||
const nx = ev.next && ev.next !== "ENDE" ? ` → ${ev.next}` : "";
|
||||
return `fehlgeschlagen${cd}${nx}`;
|
||||
}
|
||||
case "FATAL": return "abgebrochen (fataler Fehler)";
|
||||
case "SKIP_COOLDOWN": return "übersprungen (Cooldown aktiv)";
|
||||
case "SKIP_DISABLED": return "übersprungen (deaktiviert)";
|
||||
case "SKIP_DAILY_LIMIT": return "übersprungen (Tageslimit erreicht)";
|
||||
case "SKIP_HOST_COOLDOWN": return "übersprungen (Host-Cooldown)";
|
||||
case "PROVIDER_WIDE": return "Provider-weiter Fehler, restliche Keys übersprungen";
|
||||
case "TRANSPORT_CASCADE": return "Netzwerk-Kaskade, restliche Keys übersprungen";
|
||||
default: return ev.event;
|
||||
}
|
||||
}
|
||||
|
||||
function getDebridLinkKeyStatusDisplay(
|
||||
key: DebridLinkAccountKeyEntry,
|
||||
info: DebridLinkHostLimitInfo | null | undefined
|
||||
@@ -1569,6 +1601,7 @@ export function App(): ReactElement {
|
||||
const [downloadsSortDescending, setDownloadsSortDescending] = useState(false);
|
||||
const [showAllPackages, setShowAllPackages] = useState(false);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
const [accountCheckBusy, setAccountCheckBusy] = useState(false);
|
||||
const actionBusyRef = useRef(false);
|
||||
const actionUnlockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const mountedRef = useRef(true);
|
||||
@@ -1893,6 +1926,12 @@ export function App(): ReactElement {
|
||||
unsubscribe = window.rd.onStateUpdate((wireState) => {
|
||||
// Merge delta payloads into the master snapshot. Full payloads replace
|
||||
// the master entirely (initial sync + periodic 30s resync).
|
||||
// NOTE: `settings` and `rotationEvents` are NOT delta-filtered — every emit
|
||||
// (full or delta) carries the complete `settings` object and recent
|
||||
// rotationEvents. The account-validity badges read
|
||||
// `snapshot.settings.debridAccountStatuses` and the rotation panel reads
|
||||
// `snapshot.rotationEvents`; if `settings` is ever delta-optimized, both
|
||||
// must keep flowing on every emit or those views go stale.
|
||||
let merged: UiSnapshot;
|
||||
const master = masterSnapshotRef.current;
|
||||
if (wireState.payloadKind === "delta" && master) {
|
||||
@@ -2597,6 +2636,24 @@ export function App(): ReactElement {
|
||||
}
|
||||
};
|
||||
|
||||
const checkAllAccounts = useCallback(async (): Promise<void> => {
|
||||
setAccountCheckBusy(true);
|
||||
try {
|
||||
const statuses = await window.rd.checkDebridAccounts();
|
||||
if (!statuses || statuses.length === 0) {
|
||||
showToast("Keine Mega-Debrid-/Debrid-Link-Accounts zum Prüfen konfiguriert.", 3200);
|
||||
} else {
|
||||
const valid = statuses.filter((st) => st.valid).length;
|
||||
const premium = statuses.filter((st) => st.isPremium).length;
|
||||
showToast(`Account-Check: ${valid}/${statuses.length} Login gültig, ${premium} mit Premium.`, 3600);
|
||||
}
|
||||
} catch (error) {
|
||||
showToast(`Account-Check fehlgeschlagen: ${String(error)}`, 3600);
|
||||
} finally {
|
||||
setAccountCheckBusy(false);
|
||||
}
|
||||
}, [showToast]);
|
||||
|
||||
const openCreateAccountDialog = (): void => {
|
||||
setAccountDialogSearch("");
|
||||
setAccountDialog(createAccountDialogState("create", null, settingsDraft));
|
||||
@@ -4868,9 +4925,14 @@ export function App(): ReactElement {
|
||||
<h3>Accounts</h3>
|
||||
<div className="hint">Accounts werden als Liste verwaltet. Neue Einträge kommen über den Dialog oben rechts dazu.</div>
|
||||
</div>
|
||||
<button className="btn accent" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
|
||||
Account hinzufügen
|
||||
</button>
|
||||
<div className="account-board-header-actions">
|
||||
<button className="btn" disabled={actionBusy || accountCheckBusy} onClick={() => { void checkAllAccounts(); }} title="Prüft Login-Gültigkeit und Premium-Restlaufzeit aller Mega-Debrid-/Debrid-Link-Accounts">
|
||||
{accountCheckBusy ? "Prüfe Accounts…" : "Alle prüfen"}
|
||||
</button>
|
||||
<button className="btn accent" disabled={actionBusy || availableAccountOptions.length === 0} onClick={openCreateAccountDialog}>
|
||||
Account hinzufügen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-board-summary">
|
||||
@@ -5035,6 +5097,31 @@ export function App(): ReactElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="settings-section card">
|
||||
<div className="account-board-header">
|
||||
<div>
|
||||
<h3>Rotations-Verlauf</h3>
|
||||
<div className="hint">Zeigt, welcher Account/Key zuletzt für die Link-Umwandlung versucht wurde und warum gewechselt wurde.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rotation-panel">
|
||||
{(!snapshot?.rotationEvents || snapshot.rotationEvents.length === 0) ? (
|
||||
<div className="rotation-empty">Noch keine Rotations-Ereignisse. Sobald ein Account/Key bei der Link-Umwandlung fehlschlägt oder gewechselt wird, erscheint es hier.</div>
|
||||
) : (
|
||||
snapshot.rotationEvents.map((ev) => (
|
||||
<div key={ev.id} className={`rotation-event ${ev.level}`}>
|
||||
<span className="rotation-time">{new Date(ev.at).toLocaleTimeString()}</span>
|
||||
<span className="rotation-body">
|
||||
<strong>{ev.provider} · {ev.accountLabel}</strong>{" "}
|
||||
{rotationEventText(ev)}
|
||||
{ev.reason ? <span className="rotation-reason"> ({ev.reason})</span> : null}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="settings-section card">
|
||||
<h3>Hoster-Reihenfolge</h3>
|
||||
<div className="hint">
|
||||
@@ -5600,6 +5687,15 @@ export function App(): ReactElement {
|
||||
<div className="account-dl-key-meta">
|
||||
<strong>Account {index + 1}</strong>
|
||||
<span>{maskMegaDebridLogin(account.login)}</span>
|
||||
{(() => {
|
||||
const st = snapshot?.settings?.debridAccountStatuses?.[getMegaDebridAccountId(account.login)];
|
||||
if (!st) return <span className="account-validity-badge unknown" title="Noch nicht geprüft – auf „Alle prüfen“ klicken">Noch nicht geprüft</span>;
|
||||
const checkedAgo = formatCheckedAgo(st.checkedAt);
|
||||
const tip = `${st.message}${st.email ? ` · ${st.email}` : ""} · geprüft ${checkedAgo}`;
|
||||
if (!st.valid) return <span className="account-validity-badge invalid" title={tip}>Login ungültig</span>;
|
||||
if (!st.isPremium) return <span className="account-validity-badge free" title={tip}>Login OK · kein Premium</span>;
|
||||
return <span className="account-validity-badge ok" title={tip}>{st.message}</span>;
|
||||
})()}
|
||||
</div>
|
||||
<button
|
||||
className="btn danger"
|
||||
@@ -5672,6 +5768,14 @@ export function App(): ReactElement {
|
||||
<div className="account-dl-key-meta">
|
||||
<strong>{key.label}</strong>
|
||||
<span>{key.masked}</span>
|
||||
{(() => {
|
||||
const st = snapshot?.settings?.debridAccountStatuses?.[key.id];
|
||||
if (!st) return <span className="account-validity-badge unknown" title="Noch nicht geprüft – auf „Alle prüfen“ klicken">Noch nicht geprüft</span>;
|
||||
const tip = `${st.message}${st.email ? ` · ${st.email}` : ""} · geprüft ${formatCheckedAgo(st.checkedAt)}`;
|
||||
if (!st.valid) return <span className="account-validity-badge invalid" title={tip}>Key ungültig</span>;
|
||||
if (!st.isPremium) return <span className="account-validity-badge free" title={tip}>Key OK · kein Premium</span>;
|
||||
return <span className="account-validity-badge ok" title={tip}>{st.message}</span>;
|
||||
})()}
|
||||
</div>
|
||||
<input
|
||||
inputMode="decimal"
|
||||
|
||||
Reference in New Issue
Block a user