diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 6b7b5b4..41df20d 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -45,7 +45,7 @@ import { runInstallWithResume } from "./update-install-flow"; import { rotateDebugToken, startDebugServer, stopDebugServer, restartDebugServer, getDebugServerRuntimeStatus, getActiveDebugToken, getDebugAllowlist, writeDebugServerConfig, clearDebugToken } from "./debug-server"; import { encodeConnectionCode, loadRemoteMeta, saveRemoteMeta } from "./connection-code"; import { encryptBackup, decryptBackup } from "./backup-crypto"; -import { buildBackupPayload, planBackupImport } from "./backup-payload"; +import { buildBackupPayload, planBackupImport, resolveMcpRemoteRestore, BackupMcpRemote } from "./backup-payload"; import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log"; import { initAccountRotationLog, shutdownAccountRotationLog } from "./account-rotation-log"; import { initConversionLog, shutdownConversionLog } from "./conversion-trace"; @@ -372,6 +372,20 @@ export class AppController { return this.getRemoteDiagnostics(); } + private restoreMcpRemoteFromBackup(section: unknown): void { + const restore = resolveMcpRemoteRestore(section); + if (!restore) { + return; + } + writeDebugServerConfig({ host: restore.host, port: restore.port, allowlist: restore.allowlist }); + void restartDebugServer().catch(() => {}); + this.audit("INFO", "Ferndiagnose-Einstellungen aus Backup wiederhergestellt", { + port: restore.port ?? null, + allowlistCount: restore.allowlist?.length ?? 0, + host: restore.host ?? "unveraendert" + }); + } + public getDebugSetupCheck(): DebugSetupCheckResult { return getDebugSetupCheck(this.storagePaths.baseDir); } @@ -724,13 +738,22 @@ public async checkDebridAccounts(): Promise { } public exportBackup(): Buffer { - const includeDownloads = Boolean(this.settings.backupIncludeDownloads); + let mcpRemote: BackupMcpRemote | undefined; + if (Boolean(this.settings.backupIncludeMcp)) { + const status = getDebugServerRuntimeStatus(); + mcpRemote = { + allowlist: getDebugAllowlist(), + port: status.port, + hostMode: status.host === "0.0.0.0" ? "network" : "local" + }; + } const payloadObj = buildBackupPayload({ settings: { ...this.settings }, appVersion: APP_VERSION, exportedAt: new Date().toISOString(), session: this.manager.getSession(), - history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()) + history: loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode, this.historyLimits()), + mcpRemote }); this.audit("INFO", "Backup exportiert", { kind: payloadObj.kind, @@ -804,6 +827,7 @@ public async checkDebridAccounts(): Promise { this.settings = restoredSettings; saveSettings(this.storagePaths, this.settings); this.manager.setSettings(this.settings, { suppressRetroactiveCleanup: true }); + this.restoreMcpRemoteFromBackup(parsed.mcpRemote); this.audit("INFO", "Backup importiert (nur Einstellungen)", { accountSummary: buildAccountSummary(this.settings) }); diff --git a/src/main/backup-payload.ts b/src/main/backup-payload.ts index 8a3ba24..3f1066b 100644 --- a/src/main/backup-payload.ts +++ b/src/main/backup-payload.ts @@ -2,6 +2,12 @@ import type { AppSettings, SessionState, HistoryEntry } from "../shared/types"; export type BackupKind = "full" | "settings-only"; +export interface BackupMcpRemote { + allowlist: string[]; + port: number; + hostMode: "local" | "network"; +} + export interface BackupPayload { version: 2; kind: BackupKind; @@ -10,6 +16,7 @@ export interface BackupPayload { settings: AppSettings; session?: SessionState; history?: HistoryEntry[]; + mcpRemote?: BackupMcpRemote; } export interface BuildBackupInput { @@ -19,6 +26,7 @@ export interface BuildBackupInput { /** Only bundled when includeDownloads is true. */ session: SessionState; history: HistoryEntry[]; + mcpRemote?: BackupMcpRemote; } /** @@ -40,9 +48,39 @@ export function buildBackupPayload(input: BuildBackupInput): BackupPayload { base.session = input.session; base.history = input.history; } + if (Boolean(input.settings.backupIncludeMcp) && input.mcpRemote) { + base.mcpRemote = input.mcpRemote; + } return base; } +export interface McpRemoteRestore { + host?: "127.0.0.1" | "0.0.0.0"; + port?: number; + allowlist?: string[]; +} + +export function resolveMcpRemoteRestore(section: unknown): McpRemoteRestore | null { + if (!section || typeof section !== "object") { + return null; + } + const s = section as { allowlist?: unknown; port?: unknown; hostMode?: unknown }; + const allowlist = Array.isArray(s.allowlist) + ? s.allowlist.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim()) + : undefined; + const port = (typeof s.port === "number" && Number.isInteger(s.port) && s.port >= 1024 && s.port <= 65535) ? s.port : undefined; + let host: "127.0.0.1" | "0.0.0.0" | undefined; + if (s.hostMode === "network") { + host = allowlist && allowlist.length > 0 ? "0.0.0.0" : "127.0.0.1"; + } else if (s.hostMode === "local") { + host = "127.0.0.1"; + } + if (host === undefined && port === undefined && allowlist === undefined) { + return null; + } + return { host, port, allowlist }; +} + export interface ImportPlan { valid: boolean; /** Restore the download list (session + history) and relaunch. */ diff --git a/src/main/constants.ts b/src/main/constants.ts index b0312db..f3f9b1d 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -109,6 +109,7 @@ export function defaultSettings(): AppSettings { hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, + backupIncludeMcp: false, notifyUrl: "", notifyMention: "", notifyOnPackageCompleted: false, diff --git a/src/main/storage.ts b/src/main/storage.ts index a1b016c..07ed598 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -461,6 +461,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings { hideExtractedItems: settings.hideExtractedItems !== undefined ? Boolean(settings.hideExtractedItems) : defaults.hideExtractedItems, confirmDeleteSelection: settings.confirmDeleteSelection !== undefined ? Boolean(settings.confirmDeleteSelection) : defaults.confirmDeleteSelection, backupIncludeDownloads: settings.backupIncludeDownloads !== undefined ? Boolean(settings.backupIncludeDownloads) : defaults.backupIncludeDownloads, + backupIncludeMcp: settings.backupIncludeMcp !== undefined ? Boolean(settings.backupIncludeMcp) : defaults.backupIncludeMcp, notifyUrl: asText(settings.notifyUrl) || defaults.notifyUrl, notifyMention: asText(settings.notifyMention) || defaults.notifyMention, notifyOnPackageCompleted: settings.notifyOnPackageCompleted !== undefined ? Boolean(settings.notifyOnPackageCompleted) : defaults.notifyOnPackageCompleted, diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7c95ab2..c9cf310 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -856,7 +856,7 @@ const emptySnapshot = (): UiSnapshot => ({ autoReconnect: false, reconnectWaitSeconds: 45, completedCleanupPolicy: "never", maxParallel: 4, maxParallelExtract: 2, extractCpuPriority: "high", retryLimit: 0, speedLimitEnabled: false, speedLimitKbps: 0, speedLimitMode: "global", updateRepo: "", autoUpdateCheck: true, clipboardWatch: false, minimizeToTray: false, - theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, + theme: "dark", collapseNewPackages: true, historyRetentionMode: "permanent", historyMaxEntries: 500, historyMaxAgeDays: 0, autoSortPackagesByProgress: true, autoSkipExtracted: false, hideExtractedItems: true, confirmDeleteSelection: true, backupIncludeDownloads: false, backupIncludeMcp: false, notifyUrl: "", notifyMention: "", notifyOnPackageCompleted: false, notifyOnPackageFailed: false, notifyOnRunFinished: false, accountListShowDetailedDebridLinkKeys: false, bandwidthSchedules: [], totalDownloadedAllTime: 0, totalCompletedFilesAllTime: 0, totalRuntimeAllTimeMs: 0, @@ -5442,6 +5442,8 @@ export function App(): ReactElement {
Sicherheitsabfrage vor dem Entfernen ausgewählter Einträge.
Sicherung enthält auch die Download-Liste; Standard: nur Einstellungen.
+ +
Allowlist, Port und Freigabemodus (lokal/Netzwerk) reisen mit. Verbindungs-Token und eigene Adresse bleiben pro Server – nach dem Import einmal „Aktivieren" drücken.