Compare commits
No commits in common. "af73934b0f5c6cf541a1dcbd9f08daf3e10b5695" and "db6e7d81a4376d0f6d99c0d7fa98fcfe33c33511" have entirely different histories.
af73934b0f
...
db6e7d81a4
12
README.md
12
README.md
@ -183,8 +183,6 @@ Runtime files are stored in Electron's `userData` directory, including:
|
|||||||
- `rd_downloader.log`
|
- `rd_downloader.log`
|
||||||
- `audit.log`
|
- `audit.log`
|
||||||
- `debug_ai_manifest.json`
|
- `debug_ai_manifest.json`
|
||||||
- `trace.log`
|
|
||||||
- `trace_config.json`
|
|
||||||
- `session-logs/session_*.txt`
|
- `session-logs/session_*.txt`
|
||||||
- `package-logs/package_*.txt`
|
- `package-logs/package_*.txt`
|
||||||
- `item-logs/item_*.txt`
|
- `item-logs/item_*.txt`
|
||||||
@ -204,8 +202,6 @@ Enable it by creating these files in the same runtime folder that contains `rd_d
|
|||||||
|
|
||||||
After startup, the app also writes `debug_ai_manifest.json` into the same runtime folder. This file is meant for support tooling and AI agents: it lists all available endpoints, the auth method, the related runtime files, and the one remaining external value the assistant may still need from you for remote access: the server IP or DNS name.
|
After startup, the app also writes `debug_ai_manifest.json` into the same runtime folder. This file is meant for support tooling and AI agents: it lists all available endpoints, the auth method, the related runtime files, and the one remaining external value the assistant may still need from you for remote access: the server IP or DNS name.
|
||||||
|
|
||||||
If you want extra support detail during a flaky or hard-to-reproduce issue, the app also maintains a `trace.log` plus `trace_config.json`. You can enable or disable the support trace from the app menu or remotely via the debug API.
|
|
||||||
|
|
||||||
Available endpoints after restart:
|
Available endpoints after restart:
|
||||||
|
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
@ -222,12 +218,9 @@ Available endpoints after restart:
|
|||||||
- `GET /log?lines=100&grep=keyword`
|
- `GET /log?lines=100&grep=keyword`
|
||||||
- `GET /logs/main?lines=100&grep=keyword`
|
- `GET /logs/main?lines=100&grep=keyword`
|
||||||
- `GET /logs/audit?lines=100&grep=keyword`
|
- `GET /logs/audit?lines=100&grep=keyword`
|
||||||
- `GET /logs/trace?lines=100&grep=keyword`
|
|
||||||
- `GET /logs/session?lines=100&grep=keyword`
|
- `GET /logs/session?lines=100&grep=keyword`
|
||||||
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
- `GET /logs/package?package=Release&lines=100&grep=keyword`
|
||||||
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
- `GET /logs/item?item=episode.part2.rar&lines=100&grep=keyword`
|
||||||
- `GET /trace/config?enable=1¬e=support`
|
|
||||||
- `GET /support/bundle`
|
|
||||||
- `GET /diagnostics?package=Release&lines=150`
|
- `GET /diagnostics?package=Release&lines=150`
|
||||||
|
|
||||||
Authentication works with either:
|
Authentication works with either:
|
||||||
@ -244,15 +237,12 @@ Invoke-RestMethod "http://SERVER:9868/accounts?token=YOUR_TOKEN"
|
|||||||
Invoke-RestMethod "http://SERVER:9868/stats?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/stats?token=YOUR_TOKEN"
|
||||||
Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
|
Invoke-RestMethod "http://SERVER:9868/history?token=YOUR_TOKEN&limit=20"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/audit?token=YOUR_TOKEN&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/trace?token=YOUR_TOKEN&lines=200"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1¬e=support"
|
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/package?token=YOUR_TOKEN&package=Release&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
Invoke-RestMethod "http://SERVER:9868/logs/item?token=YOUR_TOKEN&item=episode.part2.rar&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
||||||
Invoke-WebRequest "http://SERVER:9868/support/bundle?token=YOUR_TOKEN" -OutFile ".\\rd-support-bundle.zip"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, trace data, package/session/item logs, host-side Windows crash hints, and even a full ZIP support bundle can be inspected remotely.
|
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, package/session/item logs, and host-side Windows crash hints can be inspected remotely.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.71",
|
"version": "1.7.70",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -34,13 +34,10 @@ import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session
|
|||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
import { MegaWebFallback } from "./mega-web-fallback";
|
||||||
import { addHistoryEntry, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, saveHistory, saveSession, saveSettings } from "./storage";
|
import { addHistoryEntry, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistory, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, saveHistory, saveSession, saveSettings } from "./storage";
|
||||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||||
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
|
||||||
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
|
||||||
import type { SupportTraceConfig } from "../shared/types";
|
|
||||||
|
|
||||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||||
@ -80,7 +77,6 @@ export class AppController {
|
|||||||
initPackageLogs(this.storagePaths.baseDir);
|
initPackageLogs(this.storagePaths.baseDir);
|
||||||
initItemLogs(this.storagePaths.baseDir);
|
initItemLogs(this.storagePaths.baseDir);
|
||||||
initAuditLog(this.storagePaths.baseDir);
|
initAuditLog(this.storagePaths.baseDir);
|
||||||
initTraceLog(this.storagePaths.baseDir);
|
|
||||||
this.settings = loadSettings(this.storagePaths);
|
this.settings = loadSettings(this.storagePaths);
|
||||||
const session = loadSession(this.storagePaths);
|
const session = loadSession(this.storagePaths);
|
||||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||||
@ -184,29 +180,8 @@ export class AppController {
|
|||||||
return getAuditLogPath();
|
return getAuditLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTraceLogPath(): string | null {
|
|
||||||
return getTraceLogPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTraceConfig(): SupportTraceConfig {
|
|
||||||
return getTraceConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public rotateDebugToken(): { path: string; token: string } {
|
|
||||||
const rotated = rotateDebugToken(this.storagePaths.baseDir);
|
|
||||||
this.audit("WARN", "Debug-Token rotiert", { path: rotated.path });
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
||||||
logAuditEvent(level, message, fields);
|
logAuditEvent(level, message, fields);
|
||||||
logTraceEvent(level, "audit", message, fields);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
|
||||||
const next = setTraceEnabled(enabled, note);
|
|
||||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
|
||||||
return next;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
||||||
@ -498,18 +473,6 @@ export class AppController {
|
|||||||
return encryptBackup(payload);
|
return encryptBackup(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
|
||||||
this.audit("INFO", "Support-Bundle exportiert");
|
|
||||||
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
|
||||||
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
|
||||||
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir),
|
|
||||||
defaultFileName: getSupportBundleDefaultFileName()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
try {
|
try {
|
||||||
@ -606,7 +569,6 @@ export class AppController {
|
|||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
this.audit("INFO", "App beendet");
|
this.audit("INFO", "App beendet");
|
||||||
shutdownTraceLog();
|
|
||||||
shutdownAuditLog();
|
shutdownAuditLog();
|
||||||
logger.info("App beendet");
|
logger.info("App beendet");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import http from "node:http";
|
import http from "node:http";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import crypto from "node:crypto";
|
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { getAuditLogPath } from "./audit-log";
|
import { getAuditLogPath } from "./audit-log";
|
||||||
import { logger, getLogFilePath } from "./logger";
|
import { logger, getLogFilePath } from "./logger";
|
||||||
@ -10,8 +9,6 @@ import { getSessionLogPath } from "./session-log";
|
|||||||
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
||||||
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
||||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, updateTraceConfig } from "./trace-log";
|
|
||||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||||
import type { DownloadManager } from "./download-manager";
|
import type { DownloadManager } from "./download-manager";
|
||||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||||
@ -35,11 +32,9 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
|||||||
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
|
{ method: "GET", path: "/log", queryExample: "lines=100&grep=keyword", description: "Legacy alias for the main application log tail." },
|
||||||
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
|
{ method: "GET", path: "/logs/main", queryExample: "lines=100&grep=keyword", description: "Reads the main application log tail." },
|
||||||
{ method: "GET", path: "/logs/audit", queryExample: "lines=100&grep=keyword", description: "Reads the audit log for support-relevant UI and admin actions." },
|
{ method: "GET", path: "/logs/audit", queryExample: "lines=100&grep=keyword", description: "Reads the audit log for support-relevant UI and admin actions." },
|
||||||
{ method: "GET", path: "/logs/trace", queryExample: "lines=100&grep=keyword", description: "Reads the optional support trace log." },
|
|
||||||
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
{ method: "GET", path: "/logs/session", queryExample: "lines=100&grep=keyword", description: "Reads the session log tail." },
|
||||||
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
{ method: "GET", path: "/logs/package", queryExample: "package=Release&lines=100&grep=keyword", description: "Reads the package log for a specific package name or id." },
|
||||||
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
{ method: "GET", path: "/logs/item", queryExample: "item=episode.part2.rar&lines=100&grep=keyword", description: "Reads the item log for a specific file name or item id." },
|
||||||
{ method: "GET", path: "/trace/config", queryExample: "enable=1¬e=support", description: "Reads or updates the support trace configuration." },
|
|
||||||
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
{ method: "GET", path: "/settings", description: "Returns a redacted settings snapshot without raw secrets." },
|
||||||
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
{ method: "GET", path: "/accounts", description: "Returns a redacted account/provider configuration summary." },
|
||||||
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
{ method: "GET", path: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
||||||
@ -48,7 +43,6 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
|||||||
{ method: "GET", path: "/packages", queryExample: "package=Release&includeItems=1", description: "Lists packages and optional per-item detail." },
|
{ method: "GET", path: "/packages", queryExample: "package=Release&includeItems=1", description: "Lists packages and optional per-item detail." },
|
||||||
{ method: "GET", path: "/items", queryExample: "status=downloading&package=Release", description: "Lists items and supports status/package filters." },
|
{ method: "GET", path: "/items", queryExample: "status=downloading&package=Release", description: "Lists items and supports status/package filters." },
|
||||||
{ method: "GET", path: "/session", queryExample: "package=Release", description: "Returns session-wide or package-scoped item state." },
|
{ method: "GET", path: "/session", queryExample: "package=Release", description: "Returns session-wide or package-scoped item state." },
|
||||||
{ method: "GET", path: "/support/bundle", description: "Downloads a ZIP support bundle with logs, diagnostics, and redacted state." },
|
|
||||||
{ method: "GET", path: "/diagnostics", queryExample: "package=Release&lines=150", description: "Returns a combined support snapshot with logs, status, settings, accounts, stats, history, and host diagnostics." }
|
{ method: "GET", path: "/diagnostics", queryExample: "package=Release&lines=150", description: "Returns a combined support snapshot with logs, status, settings, accounts, stats, history, and host diagnostics." }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -75,10 +69,6 @@ function getAiManifestPath(baseDir: string = runtimeBaseDir): string {
|
|||||||
return path.join(baseDir, AI_MANIFEST_FILE);
|
return path.join(baseDir, AI_MANIFEST_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDebugTokenPath(baseDir: string = runtimeBaseDir): string {
|
|
||||||
return path.join(baseDir, "debug_token.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadToken(baseDir: string): string {
|
function loadToken(baseDir: string): string {
|
||||||
const tokenPath = path.join(baseDir, "debug_token.txt");
|
const tokenPath = path.join(baseDir, "debug_token.txt");
|
||||||
try {
|
try {
|
||||||
@ -142,23 +132,6 @@ function jsonResponse(res: http.ServerResponse, status: number, data: unknown):
|
|||||||
res.end(body);
|
res.end(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
function binaryResponse(
|
|
||||||
res: http.ServerResponse,
|
|
||||||
status: number,
|
|
||||||
body: Buffer,
|
|
||||||
contentType: string,
|
|
||||||
fileName?: string
|
|
||||||
): void {
|
|
||||||
res.writeHead(status, {
|
|
||||||
"Content-Type": contentType,
|
|
||||||
"Content-Length": String(body.length),
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
...(fileName ? { "Content-Disposition": `attachment; filename="${fileName}"` } : {})
|
|
||||||
});
|
|
||||||
res.end(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeLinesParam(rawValue: string | null, fallback: number): number {
|
function normalizeLinesParam(rawValue: string | null, fallback: number): number {
|
||||||
const parsed = Number(rawValue || String(fallback));
|
const parsed = Number(rawValue || String(fallback));
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
@ -188,31 +161,6 @@ function filterLines(lines: string[], grep: string): string[] {
|
|||||||
return lines.filter((line) => line.toLowerCase().includes(pattern));
|
return lines.filter((line) => line.toLowerCase().includes(pattern));
|
||||||
}
|
}
|
||||||
|
|
||||||
function toBooleanQuery(value: string | null): boolean | null {
|
|
||||||
if (value === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (/^(1|true|yes|on)$/i.test(value)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (/^(0|false|no|off)$/i.test(value)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sanitizeRequestUrlForTrace(rawUrl: string): string {
|
|
||||||
try {
|
|
||||||
const url = new URL(rawUrl || "/", "http://localhost");
|
|
||||||
if (url.searchParams.has("token")) {
|
|
||||||
url.searchParams.set("token", "***");
|
|
||||||
}
|
|
||||||
return `${url.pathname}${url.search}`;
|
|
||||||
} catch {
|
|
||||||
return String(rawUrl || "/");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatEndpointSummary(endpoint: DebugEndpointDescriptor): string {
|
function formatEndpointSummary(endpoint: DebugEndpointDescriptor): string {
|
||||||
return `${endpoint.method} ${endpoint.path}${endpoint.queryExample ? `?${endpoint.queryExample}` : ""}`;
|
return `${endpoint.method} ${endpoint.path}${endpoint.queryExample ? `?${endpoint.queryExample}` : ""}`;
|
||||||
}
|
}
|
||||||
@ -236,8 +184,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
||||||
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
||||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
||||||
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, or /history."
|
||||||
"If a full handoff is needed, download /support/bundle as a ZIP."
|
|
||||||
],
|
],
|
||||||
auth: {
|
auth: {
|
||||||
required: true,
|
required: true,
|
||||||
@ -253,8 +200,6 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
tokenFile: path.join(baseDir, "debug_token.txt"),
|
tokenFile: path.join(baseDir, "debug_token.txt"),
|
||||||
mainLogFile: getLogFilePath(),
|
mainLogFile: getLogFilePath(),
|
||||||
auditLogFile: getAuditLogPath(),
|
auditLogFile: getAuditLogPath(),
|
||||||
traceLogFile: getTraceLogPath(),
|
|
||||||
traceConfigFile: getTraceConfigPath(),
|
|
||||||
sessionLogFile: getSessionLogPath(),
|
sessionLogFile: getSessionLogPath(),
|
||||||
packageLogDir: path.join(baseDir, "package-logs"),
|
packageLogDir: path.join(baseDir, "package-logs"),
|
||||||
itemLogDir: path.join(baseDir, "item-logs"),
|
itemLogDir: path.join(baseDir, "item-logs"),
|
||||||
@ -288,19 +233,6 @@ function writeAiManifest(baseDir: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rotateDebugToken(baseDir: string = runtimeBaseDir): { path: string; token: string } {
|
|
||||||
const token = crypto.randomBytes(24).toString("hex");
|
|
||||||
const tokenPath = getDebugTokenPath(baseDir);
|
|
||||||
fs.writeFileSync(tokenPath, `${token}\n`, "utf8");
|
|
||||||
if (baseDir === runtimeBaseDir) {
|
|
||||||
authToken = token;
|
|
||||||
writeAiManifest(baseDir);
|
|
||||||
}
|
|
||||||
logger.info(`Debug-Server Token rotiert: ${tokenPath}`);
|
|
||||||
logTraceEvent("INFO", "support", "Debug-Token rotiert", { tokenPath });
|
|
||||||
return { path: tokenPath, token };
|
|
||||||
}
|
|
||||||
|
|
||||||
function summarizeItem(item: DownloadItem): Record<string, unknown> {
|
function summarizeItem(item: DownloadItem): Record<string, unknown> {
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@ -416,16 +348,6 @@ function buildStatusPayload(snapshot: UiSnapshot): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
|
||||||
const url = new URL(req.url || "/", "http://localhost");
|
|
||||||
const pathname = url.pathname;
|
|
||||||
const traceConfig = getTraceConfig();
|
|
||||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
|
||||||
logTraceEvent("INFO", "debug-http", "Request", {
|
|
||||||
method: req.method || "GET",
|
|
||||||
url: sanitizeRequestUrlForTrace(req.url || "/")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === "OPTIONS") {
|
if (req.method === "OPTIONS") {
|
||||||
res.writeHead(204, {
|
res.writeHead(204, {
|
||||||
"Access-Control-Allow-Origin": "*",
|
"Access-Control-Allow-Origin": "*",
|
||||||
@ -437,16 +359,13 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!checkAuth(req)) {
|
if (!checkAuth(req)) {
|
||||||
if (traceConfig.enabled && traceConfig.logDebugRequests) {
|
|
||||||
logTraceEvent("WARN", "debug-http", "Unauthorized request", {
|
|
||||||
method: req.method || "GET",
|
|
||||||
url: sanitizeRequestUrlForTrace(req.url || "/")
|
|
||||||
});
|
|
||||||
}
|
|
||||||
jsonResponse(res, 401, { error: "Unauthorized" });
|
jsonResponse(res, 401, { error: "Unauthorized" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = new URL(req.url || "/", "http://localhost");
|
||||||
|
const pathname = url.pathname;
|
||||||
|
|
||||||
if (pathname === "/health") {
|
if (pathname === "/health") {
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
status: "ok",
|
status: "ok",
|
||||||
@ -466,15 +385,11 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
port: bindPort
|
port: bindPort
|
||||||
},
|
},
|
||||||
supportFiles: {
|
supportFiles: {
|
||||||
aiManifest: getAiManifestPath(),
|
aiManifest: getAiManifestPath()
|
||||||
traceConfig: getTraceConfigPath(),
|
|
||||||
traceLog: getTraceLogPath()
|
|
||||||
},
|
},
|
||||||
logPaths: {
|
logPaths: {
|
||||||
main: getLogFilePath(),
|
main: getLogFilePath(),
|
||||||
audit: getAuditLogPath(),
|
session: getSessionLogPath()
|
||||||
session: getSessionLogPath(),
|
|
||||||
trace: getTraceLogPath()
|
|
||||||
},
|
},
|
||||||
endpoints: getEndpointSummaries()
|
endpoints: getEndpointSummaries()
|
||||||
});
|
});
|
||||||
@ -507,21 +422,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/logs/trace") {
|
|
||||||
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
|
||||||
const grep = url.searchParams.get("grep") || "";
|
|
||||||
const logPath = getTraceLogPath();
|
|
||||||
const lines = filterLines(readLogTailFromFile(logPath, count), grep);
|
|
||||||
jsonResponse(res, 200, {
|
|
||||||
path: logPath,
|
|
||||||
configPath: getTraceConfigPath(),
|
|
||||||
config: getTraceConfig(),
|
|
||||||
lines,
|
|
||||||
count: lines.length
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/logs/session") {
|
if (pathname === "/logs/session") {
|
||||||
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
const count = normalizeLinesParam(url.searchParams.get("lines"), 100);
|
||||||
const grep = url.searchParams.get("grep") || "";
|
const grep = url.searchParams.get("grep") || "";
|
||||||
@ -535,39 +435,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/trace/config") {
|
|
||||||
const patch: Record<string, unknown> = {};
|
|
||||||
const enabled = toBooleanQuery(url.searchParams.get("enable"));
|
|
||||||
const includeMainLog = toBooleanQuery(url.searchParams.get("includeMainLog"));
|
|
||||||
const includeAudit = toBooleanQuery(url.searchParams.get("includeAudit"));
|
|
||||||
const logDebugRequests = toBooleanQuery(url.searchParams.get("logDebugRequests"));
|
|
||||||
if (enabled !== null) {
|
|
||||||
patch.enabled = enabled;
|
|
||||||
}
|
|
||||||
if (includeMainLog !== null) {
|
|
||||||
patch.includeMainLog = includeMainLog;
|
|
||||||
}
|
|
||||||
if (includeAudit !== null) {
|
|
||||||
patch.includeAudit = includeAudit;
|
|
||||||
}
|
|
||||||
if (logDebugRequests !== null) {
|
|
||||||
patch.logDebugRequests = logDebugRequests;
|
|
||||||
}
|
|
||||||
const note = String(url.searchParams.get("note") || "").trim();
|
|
||||||
const config = Object.keys(patch).length > 0
|
|
||||||
? updateTraceConfig({ ...patch, ...(note ? { updatedAt: new Date().toISOString() } : {}) })
|
|
||||||
: getTraceConfig();
|
|
||||||
if (Object.keys(patch).length > 0) {
|
|
||||||
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note });
|
|
||||||
}
|
|
||||||
jsonResponse(res, 200, {
|
|
||||||
path: getTraceConfigPath(),
|
|
||||||
logPath: getTraceLogPath(),
|
|
||||||
config
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/logs/package") {
|
if (pathname === "/logs/package") {
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
jsonResponse(res, 503, { error: "Manager not initialized" });
|
jsonResponse(res, 503, { error: "Manager not initialized" });
|
||||||
@ -759,21 +626,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/support/bundle") {
|
|
||||||
if (!manager) {
|
|
||||||
jsonResponse(res, 503, { error: "Manager not initialized" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fileName = getSupportBundleDefaultFileName();
|
|
||||||
const body = buildSupportBundle(manager, runtimeBaseDir);
|
|
||||||
logTraceEvent("INFO", "support", "Support-Bundle über Debug-Server heruntergeladen", {
|
|
||||||
fileName,
|
|
||||||
sizeBytes: body.length
|
|
||||||
});
|
|
||||||
binaryResponse(res, 200, body, "application/zip", fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/diagnostics") {
|
if (pathname === "/diagnostics") {
|
||||||
if (!manager) {
|
if (!manager) {
|
||||||
jsonResponse(res, 503, { error: "Manager not initialized" });
|
jsonResponse(res, 503, { error: "Manager not initialized" });
|
||||||
@ -821,11 +673,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
path: getAuditLogPath(),
|
path: getAuditLogPath(),
|
||||||
lines: filterLines(readLogTailFromFile(getAuditLogPath(), lineCount), grep)
|
lines: filterLines(readLogTailFromFile(getAuditLogPath(), lineCount), grep)
|
||||||
},
|
},
|
||||||
trace: {
|
|
||||||
path: getTraceLogPath(),
|
|
||||||
config: getTraceConfig(),
|
|
||||||
lines: filterLines(readLogTailFromFile(getTraceLogPath(), lineCount), grep)
|
|
||||||
},
|
|
||||||
session: {
|
session: {
|
||||||
path: sessionLogPath,
|
path: sessionLogPath,
|
||||||
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
lines: filterLines(readLogTailFromFile(sessionLogPath, lineCount), grep)
|
||||||
|
|||||||
@ -9,8 +9,7 @@ const LOG_MAX_FILE_BYTES = 10 * 1024 * 1024;
|
|||||||
const rotateCheckAtByFile = new Map<string, number>();
|
const rotateCheckAtByFile = new Map<string, number>();
|
||||||
|
|
||||||
type LogListener = (line: string) => void;
|
type LogListener = (line: string) => void;
|
||||||
const logListeners = new Set<LogListener>();
|
let logListener: LogListener | null = null;
|
||||||
let legacyLogListener: LogListener | null = null;
|
|
||||||
|
|
||||||
let pendingLines: string[] = [];
|
let pendingLines: string[] = [];
|
||||||
let pendingChars = 0;
|
let pendingChars = 0;
|
||||||
@ -19,24 +18,7 @@ let flushInFlight = false;
|
|||||||
let exitHookAttached = false;
|
let exitHookAttached = false;
|
||||||
|
|
||||||
export function setLogListener(listener: LogListener | null): void {
|
export function setLogListener(listener: LogListener | null): void {
|
||||||
if (legacyLogListener) {
|
logListener = listener;
|
||||||
logListeners.delete(legacyLogListener);
|
|
||||||
}
|
|
||||||
legacyLogListener = listener;
|
|
||||||
if (listener) {
|
|
||||||
logListeners.add(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function addLogListener(listener: LogListener): void {
|
|
||||||
logListeners.add(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeLogListener(listener: LogListener): void {
|
|
||||||
logListeners.delete(listener);
|
|
||||||
if (legacyLogListener === listener) {
|
|
||||||
legacyLogListener = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function configureLogger(baseDir: string): void {
|
export function configureLogger(baseDir: string): void {
|
||||||
@ -213,8 +195,8 @@ function write(level: "INFO" | "WARN" | "ERROR", message: string): void {
|
|||||||
pendingLines.push(line);
|
pendingLines.push(line);
|
||||||
pendingChars += line.length;
|
pendingChars += line.length;
|
||||||
|
|
||||||
for (const listener of logListeners) {
|
if (logListener) {
|
||||||
try { listener(line); } catch { /* ignore */ }
|
try { logListener(line); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
while (pendingChars > LOG_BUFFER_LIMIT_CHARS && pendingLines.length > 1) {
|
||||||
|
|||||||
@ -490,32 +490,11 @@ function registerIpcHandlers(): void {
|
|||||||
return { saved: true };
|
return { saved: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE, async () => {
|
|
||||||
const exported = controller.exportSupportBundle();
|
|
||||||
const options = {
|
|
||||||
defaultPath: exported.defaultFileName,
|
|
||||||
filters: [{ name: "Support Bundle", extensions: ["zip"] }]
|
|
||||||
};
|
|
||||||
const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options);
|
|
||||||
if (result.canceled || !result.filePath) {
|
|
||||||
return { saved: false };
|
|
||||||
}
|
|
||||||
await fs.promises.writeFile(result.filePath, exported.buffer);
|
|
||||||
return { saved: true, filePath: result.filePath };
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_LOG, async () => {
|
||||||
const logPath = getLogFilePath();
|
const logPath = getLogFilePath();
|
||||||
await shell.openPath(logPath);
|
await shell.openPath(logPath);
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_AUDIT_LOG, async () => {
|
|
||||||
const logPath = controller.getAuditLogPath();
|
|
||||||
if (logPath) {
|
|
||||||
await shell.openPath(logPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_SESSION_LOG, async () => {
|
||||||
const logPath = controller.getSessionLogPath();
|
const logPath = controller.getSessionLogPath();
|
||||||
if (logPath) {
|
if (logPath) {
|
||||||
@ -523,13 +502,6 @@ function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_TRACE_LOG, async () => {
|
|
||||||
const logPath = controller.getTraceLogPath();
|
|
||||||
if (logPath) {
|
|
||||||
await shell.openPath(logPath);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_PACKAGE_LOG, async (_event: IpcMainInvokeEvent, packageId: string) => {
|
||||||
validateString(packageId, "packageId");
|
validateString(packageId, "packageId");
|
||||||
const logPath = controller.getPackageLogPath(packageId);
|
const logPath = controller.getPackageLogPath(packageId);
|
||||||
@ -538,23 +510,6 @@ function registerIpcHandlers(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string) => {
|
|
||||||
if (typeof enabled !== "boolean") {
|
|
||||||
throw new Error("enabled muss ein Boolean sein");
|
|
||||||
}
|
|
||||||
if (note !== undefined) {
|
|
||||||
validateString(note, "note");
|
|
||||||
}
|
|
||||||
return controller.setTraceEnabled(enabled, note);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
|
||||||
const rotated = controller.rotateDebugToken();
|
|
||||||
return { path: rotated.path };
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
|
ipcMain.handle(IPC_CHANNELS.OPEN_ITEM_LOG, async (_event: IpcMainInvokeEvent, itemId: string) => {
|
||||||
validateString(itemId, "itemId");
|
validateString(itemId, "itemId");
|
||||||
const logPath = controller.getItemLogPath(itemId);
|
const logPath = controller.getItemLogPath(itemId);
|
||||||
|
|||||||
@ -1,138 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { APP_VERSION } from "./constants";
|
|
||||||
import { getAuditLogPath } from "./audit-log";
|
|
||||||
import { getLogFilePath } from "./logger";
|
|
||||||
import { getPackageLogPath } from "./package-log";
|
|
||||||
import { getSessionLogPath } from "./session-log";
|
|
||||||
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
|
||||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath } from "./trace-log";
|
|
||||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
|
||||||
import type { DownloadManager } from "./download-manager";
|
|
||||||
|
|
||||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
|
||||||
|
|
||||||
function safeReadJson(filePath: string): unknown {
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addJson(zip: AdmZip, zipPath: string, value: unknown): void {
|
|
||||||
zip.addFile(zipPath, Buffer.from(`${JSON.stringify(value, null, 2)}\n`, "utf8"));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addFileIfExists(zip: AdmZip, sourcePath: string | null, zipPath: string): void {
|
|
||||||
if (!sourcePath || !fs.existsSync(sourcePath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
zip.addLocalFile(sourcePath, path.posix.dirname(zipPath), path.posix.basename(zipPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDirectoryIfExists(zip: AdmZip, dirPath: string, zipRoot: string): void {
|
|
||||||
if (!fs.existsSync(dirPath)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dirPath, entry.name);
|
|
||||||
const zipPath = path.posix.join(zipRoot, entry.name);
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
addDirectoryIfExists(zip, fullPath, zipPath);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
zip.addLocalFile(fullPath, path.posix.dirname(zipPath), path.posix.basename(zipPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTimestampForFileName(date: Date): string {
|
|
||||||
const y = date.getFullYear();
|
|
||||||
const mo = String(date.getMonth() + 1).padStart(2, "0");
|
|
||||||
const d = String(date.getDate()).padStart(2, "0");
|
|
||||||
const h = String(date.getHours()).padStart(2, "0");
|
|
||||||
const mi = String(date.getMinutes()).padStart(2, "0");
|
|
||||||
const s = String(date.getSeconds()).padStart(2, "0");
|
|
||||||
return `${y}-${mo}-${d}_${h}-${mi}-${s}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSupportBundleDefaultFileName(): string {
|
|
||||||
return `rd-support-bundle-${formatTimestampForFileName(new Date())}.zip`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildSupportBundle(manager: DownloadManager, baseDir: string): Buffer {
|
|
||||||
const zip = new AdmZip();
|
|
||||||
const storagePaths = createStoragePaths(baseDir);
|
|
||||||
const settings = loadSettings(storagePaths);
|
|
||||||
const history = loadHistory(storagePaths);
|
|
||||||
const snapshot = manager.getSnapshot();
|
|
||||||
const packageIds = Object.keys(snapshot.session.packages);
|
|
||||||
const itemIds = Object.keys(snapshot.session.items);
|
|
||||||
|
|
||||||
addJson(zip, "overview/meta.json", {
|
|
||||||
appVersion: APP_VERSION,
|
|
||||||
generatedAt: new Date().toISOString(),
|
|
||||||
runtimeBaseDir: baseDir,
|
|
||||||
packageCount: packageIds.length,
|
|
||||||
itemCount: itemIds.length
|
|
||||||
});
|
|
||||||
addJson(zip, "overview/status.json", snapshot.session);
|
|
||||||
addJson(zip, "overview/settings.json", buildRedactedSettingsPayload(settings));
|
|
||||||
addJson(zip, "overview/accounts.json", buildAccountSummary(settings));
|
|
||||||
addJson(zip, "overview/stats.json", {
|
|
||||||
...buildStatsPayload(snapshot),
|
|
||||||
allTime: {
|
|
||||||
totalDownloadedAllTime: settings.totalDownloadedAllTime,
|
|
||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addJson(zip, "overview/history.json", {
|
|
||||||
total: history.length,
|
|
||||||
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
|
||||||
});
|
|
||||||
addJson(zip, "overview/packages.json", {
|
|
||||||
count: packageIds.length,
|
|
||||||
packages: packageIds.map((packageId) => snapshot.session.packages[packageId]).filter(Boolean)
|
|
||||||
});
|
|
||||||
addJson(zip, "overview/items.json", {
|
|
||||||
count: itemIds.length,
|
|
||||||
items: itemIds.map((itemId) => snapshot.session.items[itemId]).filter(Boolean)
|
|
||||||
});
|
|
||||||
addJson(zip, "overview/host-diagnostics.json", getWindowsHostDiagnostics());
|
|
||||||
addJson(zip, "overview/trace-config.json", getTraceConfig());
|
|
||||||
|
|
||||||
addFileIfExists(zip, path.join(baseDir, AI_MANIFEST_FILE), `runtime/${AI_MANIFEST_FILE}`);
|
|
||||||
addFileIfExists(zip, path.join(baseDir, "debug_host.txt"), "runtime/debug_host.txt");
|
|
||||||
addFileIfExists(zip, path.join(baseDir, "debug_port.txt"), "runtime/debug_port.txt");
|
|
||||||
addFileIfExists(zip, storagePaths.configFile, "runtime/rd_downloader_config.json");
|
|
||||||
addFileIfExists(zip, storagePaths.sessionFile, "runtime/rd_session_state.json");
|
|
||||||
addFileIfExists(zip, storagePaths.historyFile, "runtime/rd_history.json");
|
|
||||||
addFileIfExists(zip, getTraceConfigPath(), "runtime/trace_config.json");
|
|
||||||
|
|
||||||
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
|
||||||
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
|
||||||
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
|
||||||
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
|
||||||
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
|
||||||
|
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "item-logs"), "logs/item-logs");
|
|
||||||
|
|
||||||
for (const packageId of packageIds) {
|
|
||||||
addFileIfExists(zip, manager.getPackageLogPath(packageId) || getPackageLogPath(packageId), `logs/live/package-${packageId}.txt`);
|
|
||||||
}
|
|
||||||
for (const itemId of itemIds) {
|
|
||||||
addFileIfExists(zip, manager.getItemLogPath(itemId), `logs/live/item-${itemId}.txt`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const aiManifest = safeReadJson(path.join(baseDir, AI_MANIFEST_FILE));
|
|
||||||
if (aiManifest) {
|
|
||||||
addJson(zip, "overview/ai-manifest.json", aiManifest);
|
|
||||||
}
|
|
||||||
|
|
||||||
return zip.toBuffer();
|
|
||||||
}
|
|
||||||
@ -1,217 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { addLogListener, removeLogListener } from "./logger";
|
|
||||||
import type { SupportTraceConfig } from "../shared/types";
|
|
||||||
|
|
||||||
type TraceLevel = "INFO" | "WARN" | "ERROR";
|
|
||||||
|
|
||||||
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
|
||||||
const TRACE_CONFIG_FILE = "trace_config.json";
|
|
||||||
|
|
||||||
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
|
||||||
enabled: false,
|
|
||||||
includeMainLog: true,
|
|
||||||
includeAudit: true,
|
|
||||||
logDebugRequests: true,
|
|
||||||
updatedAt: new Date(0).toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
let traceLogPath: string | null = null;
|
|
||||||
let traceConfigPath: string | null = null;
|
|
||||||
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
let pendingLines: string[] = [];
|
|
||||||
let flushTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
|
||||||
if (value === undefined || value === null) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value.replace(/\r?\n/g, "\\n");
|
|
||||||
}
|
|
||||||
if (typeof value === "number" || typeof value === "boolean") {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value).replace(/\r?\n/g, "\\n");
|
|
||||||
} catch {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatFields(fields?: Record<string, unknown>): string {
|
|
||||||
if (!fields) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
const parts = Object.entries(fields)
|
|
||||||
.filter(([, value]) => value !== undefined && value !== null && sanitizeFieldValue(value) !== "")
|
|
||||||
.map(([key, value]) => `${key}=${sanitizeFieldValue(value)}`);
|
|
||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function flushPending(): void {
|
|
||||||
if (!traceLogPath || pendingLines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const chunk = pendingLines.join("");
|
|
||||||
pendingLines = [];
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(traceLogPath, chunk, "utf8");
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scheduleFlush(): void {
|
|
||||||
if (flushTimer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
flushTimer = setTimeout(() => {
|
|
||||||
flushTimer = null;
|
|
||||||
flushPending();
|
|
||||||
}, TRACE_LOG_FLUSH_INTERVAL_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendTraceLine(line: string): void {
|
|
||||||
if (!traceLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
pendingLines.push(line);
|
|
||||||
scheduleFlush();
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTraceConfig(raw: unknown): SupportTraceConfig {
|
|
||||||
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
||||||
return { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
}
|
|
||||||
const value = raw as Partial<SupportTraceConfig>;
|
|
||||||
return {
|
|
||||||
enabled: Boolean(value.enabled),
|
|
||||||
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
|
||||||
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
|
||||||
logDebugRequests: value.logDebugRequests === undefined ? DEFAULT_TRACE_CONFIG.logDebugRequests : Boolean(value.logDebugRequests),
|
|
||||||
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
|
||||||
? value.updatedAt
|
|
||||||
: DEFAULT_TRACE_CONFIG.updatedAt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTraceConfig(): SupportTraceConfig {
|
|
||||||
if (!traceConfigPath) {
|
|
||||||
return { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(fs.readFileSync(traceConfigPath, "utf8")) as unknown;
|
|
||||||
return normalizeTraceConfig(parsed);
|
|
||||||
} catch {
|
|
||||||
return { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function persistTraceConfig(): void {
|
|
||||||
if (!traceConfigPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(traceConfigPath, `${JSON.stringify(traceConfig, null, 2)}\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainLogListener = (line: string): void => {
|
|
||||||
if (!traceConfig.enabled || !traceConfig.includeMainLog) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appendTraceLine(line);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function initTraceLog(baseDir: string): void {
|
|
||||||
traceLogPath = path.join(baseDir, "trace.log");
|
|
||||||
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
|
||||||
try {
|
|
||||||
fs.mkdirSync(baseDir, { recursive: true });
|
|
||||||
if (!fs.existsSync(traceLogPath)) {
|
|
||||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
traceConfig = loadTraceConfig();
|
|
||||||
persistTraceConfig();
|
|
||||||
fs.appendFileSync(traceLogPath, `=== Trace-Log Start: ${new Date().toISOString()} ===\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
traceLogPath = null;
|
|
||||||
traceConfigPath = null;
|
|
||||||
traceConfig = { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addLogListener(mainLogListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTraceLogPath(): string | null {
|
|
||||||
if (!traceLogPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fs.existsSync(traceLogPath) ? traceLogPath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTraceConfigPath(): string | null {
|
|
||||||
if (!traceConfigPath) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return fs.existsSync(traceConfigPath) ? traceConfigPath : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTraceConfig(): SupportTraceConfig {
|
|
||||||
return { ...traceConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateTraceConfig(patch: Partial<SupportTraceConfig>): SupportTraceConfig {
|
|
||||||
traceConfig = normalizeTraceConfig({
|
|
||||||
...traceConfig,
|
|
||||||
...patch,
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
});
|
|
||||||
persistTraceConfig();
|
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
|
||||||
return getTraceConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
|
||||||
const next = updateTraceConfig({ enabled });
|
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note })}\n`);
|
|
||||||
return next;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logTraceEvent(
|
|
||||||
level: TraceLevel,
|
|
||||||
category: string,
|
|
||||||
message: string,
|
|
||||||
fields?: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
if (!traceConfig.enabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (category === "audit" && !traceConfig.includeAudit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appendTraceLine(`${new Date().toISOString()} [${level}] [${category}] ${message}${formatFields(fields)}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function shutdownTraceLog(): void {
|
|
||||||
removeLogListener(mainLogListener);
|
|
||||||
if (!traceLogPath) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (flushTimer) {
|
|
||||||
clearTimeout(flushTimer);
|
|
||||||
flushTimer = null;
|
|
||||||
}
|
|
||||||
flushPending();
|
|
||||||
try {
|
|
||||||
fs.appendFileSync(traceLogPath, `=== Trace-Log Ende: ${new Date().toISOString()} ===\n`, "utf8");
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
traceLogPath = null;
|
|
||||||
traceConfigPath = null;
|
|
||||||
traceConfig = { ...DEFAULT_TRACE_CONFIG };
|
|
||||||
}
|
|
||||||
@ -56,16 +56,10 @@ const api: ElectronApi = {
|
|||||||
quit: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.QUIT),
|
quit: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.QUIT),
|
||||||
exportBackup: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_BACKUP),
|
exportBackup: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_BACKUP),
|
||||||
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
|
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
|
||||||
exportSupportBundle: (): Promise<{ saved: boolean; filePath?: string }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_SUPPORT_BUNDLE),
|
|
||||||
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
|
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
|
||||||
openAuditLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_AUDIT_LOG),
|
|
||||||
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
|
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
|
||||||
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
|
|
||||||
openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId),
|
openPackageLog: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_PACKAGE_LOG, packageId),
|
||||||
openItemLog: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ITEM_LOG, itemId),
|
openItemLog: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ITEM_LOG, itemId),
|
||||||
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
|
||||||
setTraceEnabled: (enabled: boolean, note?: string) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note),
|
|
||||||
rotateDebugToken: (): Promise<{ path: string }> => ipcRenderer.invoke(IPC_CHANNELS.ROTATE_DEBUG_TOKEN),
|
|
||||||
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
||||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||||
|
|||||||
@ -1284,7 +1284,6 @@ export function App(): ReactElement {
|
|||||||
const actionBusyRef = useRef(false);
|
const actionBusyRef = useRef(false);
|
||||||
const actionUnlockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const actionUnlockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const mountedRef = useRef(true);
|
const mountedRef = useRef(true);
|
||||||
const [supportTraceEnabled, setSupportTraceEnabled] = useState(false);
|
|
||||||
const dragOverRef = useRef(false);
|
const dragOverRef = useRef(false);
|
||||||
const dragDepthRef = useRef(0);
|
const dragDepthRef = useRef(0);
|
||||||
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
const [openMenu, setOpenMenu] = useState<string | null>(null);
|
||||||
@ -1559,11 +1558,6 @@ export function App(): ReactElement {
|
|||||||
let unsubClipboard: (() => void) | null = null;
|
let unsubClipboard: (() => void) | null = null;
|
||||||
let unsubUpdateInstallProgress: (() => void) | null = null;
|
let unsubUpdateInstallProgress: (() => void) | null = null;
|
||||||
void window.rd.getVersion().then((v) => { if (mountedRef.current) { setAppVersion(v); } }).catch(() => undefined);
|
void window.rd.getVersion().then((v) => { if (mountedRef.current) { setAppVersion(v); } }).catch(() => undefined);
|
||||||
void window.rd.getTraceConfig().then((config) => {
|
|
||||||
if (mountedRef.current) {
|
|
||||||
setSupportTraceEnabled(config.enabled);
|
|
||||||
}
|
|
||||||
}).catch(() => undefined);
|
|
||||||
void window.rd.getSnapshot().then((state) => {
|
void window.rd.getSnapshot().then((state) => {
|
||||||
if (!mountedRef.current) {
|
if (!mountedRef.current) {
|
||||||
return;
|
return;
|
||||||
@ -3397,49 +3391,6 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onExportSupportBundle = async (): Promise<void> => {
|
|
||||||
closeMenus();
|
|
||||||
await performQuickAction(async () => {
|
|
||||||
const result = await window.rd.exportSupportBundle();
|
|
||||||
if (result.saved) {
|
|
||||||
showToast("Support-Bundle exportiert", 2600);
|
|
||||||
}
|
|
||||||
}, (error) => {
|
|
||||||
showToast(`Support-Bundle fehlgeschlagen: ${String(error)}`, 2800);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onToggleSupportTrace = async (): Promise<void> => {
|
|
||||||
closeMenus();
|
|
||||||
const nextEnabled = !supportTraceEnabled;
|
|
||||||
await performQuickAction(async () => {
|
|
||||||
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle");
|
|
||||||
setSupportTraceEnabled(result.enabled);
|
|
||||||
showToast(result.enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", 2400);
|
|
||||||
}, (error) => {
|
|
||||||
showToast(`Support-Trace fehlgeschlagen: ${String(error)}`, 2800);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRotateDebugToken = async (): Promise<void> => {
|
|
||||||
closeMenus();
|
|
||||||
const confirmed = await askConfirmPrompt({
|
|
||||||
title: "Debug-Token rotieren",
|
|
||||||
message: "Das aktuelle Debug-Token wird ersetzt. Externe Debug-Links mit altem Token funktionieren danach nicht mehr.",
|
|
||||||
confirmLabel: "Token rotieren",
|
|
||||||
danger: true
|
|
||||||
});
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await performQuickAction(async () => {
|
|
||||||
const result = await window.rd.rotateDebugToken();
|
|
||||||
showToast(`Debug-Token rotiert: ${result.path}`, 4200);
|
|
||||||
}, (error) => {
|
|
||||||
showToast(`Token-Rotation fehlgeschlagen: ${String(error)}`, 3000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onMenuRestart = (): void => {
|
const onMenuRestart = (): void => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
void window.rd.restart();
|
void window.rd.restart();
|
||||||
@ -3751,26 +3702,9 @@ export function App(): ReactElement {
|
|||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openLog().catch(() => {}); }}>
|
||||||
<span>Log öffnen</span>
|
<span>Log öffnen</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openAuditLog().catch(() => {}); }}>
|
|
||||||
<span>Audit-Log öffnen</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openSessionLog().catch(() => {}); }}>
|
||||||
<span>Session-Log öffnen</span>
|
<span>Session-Log öffnen</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void window.rd.openTraceLog().catch(() => {}); }}>
|
|
||||||
<span>Trace-Log öffnen</span>
|
|
||||||
</button>
|
|
||||||
<div className="menu-separator" />
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onExportSupportBundle(); }}>
|
|
||||||
<span>Support-Bundle exportieren</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
|
||||||
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
|
||||||
<span>Debug-Token rotieren</span>
|
|
||||||
</button>
|
|
||||||
<div className="menu-separator" />
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
<button className="menu-dropdown-item" onClick={() => { closeMenus(); void onCheckUpdates(); }}>
|
||||||
<span>Suche Aktualisierungen</span>
|
<span>Suche Aktualisierungen</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -36,16 +36,10 @@ export const IPC_CHANNELS = {
|
|||||||
QUIT: "app:quit",
|
QUIT: "app:quit",
|
||||||
EXPORT_BACKUP: "app:export-backup",
|
EXPORT_BACKUP: "app:export-backup",
|
||||||
IMPORT_BACKUP: "app:import-backup",
|
IMPORT_BACKUP: "app:import-backup",
|
||||||
EXPORT_SUPPORT_BUNDLE: "app:export-support-bundle",
|
|
||||||
OPEN_LOG: "app:open-log",
|
OPEN_LOG: "app:open-log",
|
||||||
OPEN_AUDIT_LOG: "app:open-audit-log",
|
|
||||||
OPEN_SESSION_LOG: "app:open-session-log",
|
OPEN_SESSION_LOG: "app:open-session-log",
|
||||||
OPEN_TRACE_LOG: "app:open-trace-log",
|
|
||||||
OPEN_PACKAGE_LOG: "app:open-package-log",
|
OPEN_PACKAGE_LOG: "app:open-package-log",
|
||||||
OPEN_ITEM_LOG: "app:open-item-log",
|
OPEN_ITEM_LOG: "app:open-item-log",
|
||||||
GET_TRACE_CONFIG: "app:get-trace-config",
|
|
||||||
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
|
||||||
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
|
||||||
OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login",
|
OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login",
|
||||||
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
|
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
|
||||||
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
|
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import type {
|
|||||||
SessionStats,
|
SessionStats,
|
||||||
StartConflictEntry,
|
StartConflictEntry,
|
||||||
StartConflictResolutionResult,
|
StartConflictResolutionResult,
|
||||||
SupportTraceConfig,
|
|
||||||
UiSnapshot,
|
UiSnapshot,
|
||||||
UpdateCheckResult,
|
UpdateCheckResult,
|
||||||
UpdateInstallProgress,
|
UpdateInstallProgress,
|
||||||
@ -52,16 +51,10 @@ export interface ElectronApi {
|
|||||||
quit: () => Promise<void>;
|
quit: () => Promise<void>;
|
||||||
exportBackup: () => Promise<{ saved: boolean }>;
|
exportBackup: () => Promise<{ saved: boolean }>;
|
||||||
importBackup: () => Promise<{ restored: boolean; message: string }>;
|
importBackup: () => Promise<{ restored: boolean; message: string }>;
|
||||||
exportSupportBundle: () => Promise<{ saved: boolean; filePath?: string }>;
|
|
||||||
openLog: () => Promise<void>;
|
openLog: () => Promise<void>;
|
||||||
openAuditLog: () => Promise<void>;
|
|
||||||
openSessionLog: () => Promise<void>;
|
openSessionLog: () => Promise<void>;
|
||||||
openTraceLog: () => Promise<void>;
|
|
||||||
openPackageLog: (packageId: string) => Promise<void>;
|
openPackageLog: (packageId: string) => Promise<void>;
|
||||||
openItemLog: (itemId: string) => Promise<void>;
|
openItemLog: (itemId: string) => Promise<void>;
|
||||||
getTraceConfig: () => Promise<SupportTraceConfig>;
|
|
||||||
setTraceEnabled: (enabled: boolean, note?: string) => Promise<SupportTraceConfig>;
|
|
||||||
rotateDebugToken: () => Promise<{ path: string }>;
|
|
||||||
openRealDebridLogin: () => Promise<void>;
|
openRealDebridLogin: () => Promise<void>;
|
||||||
openAllDebridLogin: () => Promise<void>;
|
openAllDebridLogin: () => Promise<void>;
|
||||||
importBestDebridCookies: () => Promise<number>;
|
importBestDebridCookies: () => Promise<number>;
|
||||||
|
|||||||
@ -337,14 +337,6 @@ export interface SessionStats {
|
|||||||
queuedDownloads: number;
|
queuedDownloads: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SupportTraceConfig {
|
|
||||||
enabled: boolean;
|
|
||||||
includeMainLog: boolean;
|
|
||||||
includeAudit: boolean;
|
|
||||||
logDebugRequests: boolean;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HistoryEntry {
|
export interface HistoryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import http from "node:http";
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { once } from "node:events";
|
import { once } from "node:events";
|
||||||
import AdmZip from "adm-zip";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("../src/main/windows-host-diagnostics", () => ({
|
vi.mock("../src/main/windows-host-diagnostics", () => ({
|
||||||
@ -44,11 +43,10 @@ import { defaultSettings } from "../src/main/constants";
|
|||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "../src/main/audit-log";
|
||||||
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
import { startDebugServer, stopDebugServer } from "../src/main/debug-server";
|
||||||
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
import { ensureItemLog, initItemLogs, shutdownItemLogs } from "../src/main/item-log";
|
||||||
import { configureLogger, getLogFilePath, logger } from "../src/main/logger";
|
import { configureLogger, getLogFilePath } from "../src/main/logger";
|
||||||
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
import { ensurePackageLog, initPackageLogs, shutdownPackageLogs } from "../src/main/package-log";
|
||||||
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
||||||
import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage";
|
import { createStoragePaths, saveHistory, saveSettings } from "../src/main/storage";
|
||||||
import { getTraceConfigPath, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "../src/main/trace-log";
|
|
||||||
import { getDebridLinkApiKeyIds } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyIds } from "../src/shared/debrid-link-keys";
|
||||||
import type { DownloadManager } from "../src/main/download-manager";
|
import type { DownloadManager } from "../src/main/download-manager";
|
||||||
import type { UiSnapshot } from "../src/shared/types";
|
import type { UiSnapshot } from "../src/shared/types";
|
||||||
@ -235,17 +233,12 @@ async function createFixture() {
|
|||||||
}
|
}
|
||||||
logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" });
|
logAuditEvent("INFO", "AUDIT-LINE", { scope: "settings" });
|
||||||
|
|
||||||
initTraceLog(baseDir);
|
|
||||||
setTraceEnabled(true, "test-fixture");
|
|
||||||
logTraceEvent("INFO", "support", "TRACE-EVENT", { scope: "fixture" });
|
|
||||||
|
|
||||||
initSessionLog(baseDir);
|
initSessionLog(baseDir);
|
||||||
const sessionLogPath = getSessionLogPath();
|
const sessionLogPath = getSessionLogPath();
|
||||||
if (!sessionLogPath) {
|
if (!sessionLogPath) {
|
||||||
throw new Error("session log path missing");
|
throw new Error("session log path missing");
|
||||||
}
|
}
|
||||||
fs.appendFileSync(sessionLogPath, "2026-03-09T00:00:01.000Z [INFO] SESSION-LINE\n", "utf8");
|
fs.appendFileSync(sessionLogPath, "2026-03-09T00:00:01.000Z [INFO] SESSION-LINE\n", "utf8");
|
||||||
logger.info("TRACE-MAIN-LINE");
|
|
||||||
|
|
||||||
initPackageLogs(baseDir);
|
initPackageLogs(baseDir);
|
||||||
initItemLogs(baseDir);
|
initItemLogs(baseDir);
|
||||||
@ -280,7 +273,6 @@ async function createFixture() {
|
|||||||
startDebugServer(manager, baseDir);
|
startDebugServer(manager, baseDir);
|
||||||
const baseUrl = `http://127.0.0.1:${port}`;
|
const baseUrl = `http://127.0.0.1:${port}`;
|
||||||
await waitForReady(`${baseUrl}/health?token=${token}`);
|
await waitForReady(`${baseUrl}/health?token=${token}`);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@ -294,7 +286,6 @@ afterEach(() => {
|
|||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
shutdownTraceLog();
|
|
||||||
shutdownAuditLog();
|
shutdownAuditLog();
|
||||||
while (tempDirs.length > 0) {
|
while (tempDirs.length > 0) {
|
||||||
const dir = tempDirs.pop();
|
const dir = tempDirs.pop();
|
||||||
@ -324,7 +315,6 @@ describe("debug-server", () => {
|
|||||||
expect(payload.selectedPackage?.name).toBe("server-package");
|
expect(payload.selectedPackage?.name).toBe("server-package");
|
||||||
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
|
expect((payload.logs?.main?.lines || []).join("\n")).toContain("MAIN-LINE");
|
||||||
expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE");
|
expect((payload.logs?.audit?.lines || []).join("\n")).toContain("AUDIT-LINE");
|
||||||
expect((payload.logs?.trace?.lines || []).join("\n")).toContain("TRACE-EVENT");
|
|
||||||
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
|
expect((payload.logs?.session?.lines || []).join("\n")).toContain("SESSION-LINE");
|
||||||
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
expect((payload.logs?.package?.lines || []).join("\n")).toContain("PACKAGE-LINE");
|
||||||
expect(payload.accounts?.realDebrid?.configured).toBe(true);
|
expect(payload.accounts?.realDebrid?.configured).toBe(true);
|
||||||
@ -349,8 +339,6 @@ describe("debug-server", () => {
|
|||||||
expect(metaResponse.ok).toBe(true);
|
expect(metaResponse.ok).toBe(true);
|
||||||
const metaPayload = await metaResponse.json() as Record<string, any>;
|
const metaPayload = await metaResponse.json() as Record<string, any>;
|
||||||
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
||||||
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
|
||||||
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("serves package details and package log by package query", async () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
@ -398,17 +386,6 @@ describe("debug-server", () => {
|
|||||||
const auditPayload = await auditResponse.json() as Record<string, any>;
|
const auditPayload = await auditResponse.json() as Record<string, any>;
|
||||||
expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE");
|
expect((auditPayload.lines || []).join("\n")).toContain("AUDIT-LINE");
|
||||||
|
|
||||||
const traceResponse = await fetch(`${fixture.baseUrl}/logs/trace?token=${fixture.token}&lines=50`);
|
|
||||||
expect(traceResponse.ok).toBe(true);
|
|
||||||
const tracePayload = await traceResponse.json() as Record<string, any>;
|
|
||||||
expect((tracePayload.lines || []).join("\n")).toContain("TRACE-EVENT");
|
|
||||||
expect((tracePayload.lines || []).join("\n")).toContain("TRACE-MAIN-LINE");
|
|
||||||
|
|
||||||
const traceConfigResponse = await fetch(`${fixture.baseUrl}/trace/config?token=${fixture.token}&enable=0¬e=test`);
|
|
||||||
expect(traceConfigResponse.ok).toBe(true);
|
|
||||||
const traceConfigPayload = await traceConfigResponse.json() as Record<string, any>;
|
|
||||||
expect(traceConfigPayload.config?.enabled).toBe(false);
|
|
||||||
|
|
||||||
const settingsResponse = await fetch(`${fixture.baseUrl}/settings?token=${fixture.token}`);
|
const settingsResponse = await fetch(`${fixture.baseUrl}/settings?token=${fixture.token}`);
|
||||||
expect(settingsResponse.ok).toBe(true);
|
expect(settingsResponse.ok).toBe(true);
|
||||||
const settingsPayload = await settingsResponse.json() as Record<string, any>;
|
const settingsPayload = await settingsResponse.json() as Record<string, any>;
|
||||||
@ -438,24 +415,6 @@ describe("debug-server", () => {
|
|||||||
expect(historyPayload.entries?.[0]?.urlCount).toBe(1);
|
expect(historyPayload.entries?.[0]?.urlCount).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("downloads a support bundle zip", async () => {
|
|
||||||
const fixture = await createFixture();
|
|
||||||
const response = await fetch(`${fixture.baseUrl}/support/bundle?token=${fixture.token}`);
|
|
||||||
expect(response.ok).toBe(true);
|
|
||||||
expect(response.headers.get("content-type")).toContain("application/zip");
|
|
||||||
|
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
|
||||||
const zip = new AdmZip(buffer);
|
|
||||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
|
||||||
expect(entries).toContain("overview/settings.json");
|
|
||||||
expect(entries).toContain("overview/accounts.json");
|
|
||||||
expect(entries).toContain("overview/trace-config.json");
|
|
||||||
expect(entries).toContain("logs/audit.log");
|
|
||||||
expect(entries).toContain("logs/trace.log");
|
|
||||||
expect(entries).toContain("runtime/debug_ai_manifest.json");
|
|
||||||
expect(entries).not.toContain("runtime/debug_token.txt");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rejects unauthenticated requests", async () => {
|
it("rejects unauthenticated requests", async () => {
|
||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
const response = await fetch(`${fixture.baseUrl}/status`);
|
const response = await fetch(`${fixture.baseUrl}/status`);
|
||||||
|
|||||||
@ -1,53 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
|
||||||
import { configureLogger, logger } from "../src/main/logger";
|
|
||||||
import { getSessionLogPath, initSessionLog, shutdownSessionLog } from "../src/main/session-log";
|
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "../src/main/trace-log";
|
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
shutdownSessionLog();
|
|
||||||
shutdownTraceLog();
|
|
||||||
for (const dir of tempDirs.splice(0)) {
|
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("trace-log", () => {
|
|
||||||
it("captures main log lines and explicit trace events when enabled", async () => {
|
|
||||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-tlog-"));
|
|
||||||
tempDirs.push(baseDir);
|
|
||||||
|
|
||||||
configureLogger(baseDir);
|
|
||||||
initTraceLog(baseDir);
|
|
||||||
initSessionLog(baseDir);
|
|
||||||
setTraceEnabled(true, "test");
|
|
||||||
|
|
||||||
logger.info("TRACE-MAIN-CAPTURE");
|
|
||||||
logTraceEvent("INFO", "audit", "TRACE-AUDIT-CAPTURE", { source: "test" });
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
|
||||||
|
|
||||||
const traceLogPath = getTraceLogPath();
|
|
||||||
const sessionLogPath = getSessionLogPath();
|
|
||||||
const traceConfigPath = getTraceConfigPath();
|
|
||||||
expect(traceLogPath).not.toBeNull();
|
|
||||||
expect(sessionLogPath).not.toBeNull();
|
|
||||||
expect(traceConfigPath).not.toBeNull();
|
|
||||||
|
|
||||||
const traceContent = fs.readFileSync(traceLogPath!, "utf8");
|
|
||||||
expect(traceContent).toContain("Trace-Log Start");
|
|
||||||
expect(traceContent).toContain("TRACE-MAIN-CAPTURE");
|
|
||||||
expect(traceContent).toContain("TRACE-AUDIT-CAPTURE");
|
|
||||||
|
|
||||||
const sessionContent = fs.readFileSync(sessionLogPath!, "utf8");
|
|
||||||
expect(sessionContent).toContain("TRACE-MAIN-CAPTURE");
|
|
||||||
|
|
||||||
const traceConfig = getTraceConfig();
|
|
||||||
expect(traceConfig.enabled).toBe(true);
|
|
||||||
expect(JSON.parse(fs.readFileSync(traceConfigPath!, "utf8")).enabled).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in New Issue
Block a user