Harden support logging and debug setup
This commit is contained in:
parent
af73934b0f
commit
fc4fafa0d6
12
README.md
12
README.md
@ -189,6 +189,8 @@ Runtime files are stored in Electron's `userData` directory, including:
|
||||
- `package-logs/package_*.txt`
|
||||
- `item-logs/item_*.txt`
|
||||
|
||||
`audit.log` and `trace.log` are rotated automatically. The current file is kept plus one `.old` backup, and outdated backups are purged automatically.
|
||||
|
||||
### Remote debug server
|
||||
|
||||
For headless or server-style troubleshooting, the app can expose a small authenticated HTTP debug API with live status and log tails.
|
||||
@ -204,12 +206,15 @@ 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.
|
||||
|
||||
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.
|
||||
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. By default, the support trace now auto-disables again after 2 hours so it does not stay enabled forever by accident.
|
||||
|
||||
The app menu under `Hilfe` also includes a `Debug-Setup prüfen` action. It verifies the current host/port/token/AI-manifest/trace setup locally and shows the exact local and remote URLs that support tooling can use.
|
||||
|
||||
Available endpoints after restart:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /meta`
|
||||
- `GET /debug/setup`
|
||||
- `GET /host/diagnostics`
|
||||
- `GET /status`
|
||||
- `GET /settings`
|
||||
@ -226,7 +231,7 @@ Available endpoints after restart:
|
||||
- `GET /logs/session?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 /trace/config?enable=1¬e=support`
|
||||
- `GET /trace/config?enable=1¬e=support&durationMinutes=120`
|
||||
- `GET /support/bundle`
|
||||
- `GET /diagnostics?package=Release&lines=150`
|
||||
|
||||
@ -243,9 +248,10 @@ Invoke-RestMethod "http://SERVER:9868/settings?token=YOUR_TOKEN"
|
||||
Invoke-RestMethod "http://SERVER:9868/accounts?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/debug/setup?token=YOUR_TOKEN"
|
||||
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/trace/config?token=YOUR_TOKEN&enable=1¬e=support&durationMinutes=120"
|
||||
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/host/diagnostics?token=YOUR_TOKEN"
|
||||
|
||||
@ -37,10 +37,11 @@ import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } fro
|
||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||
import { getDebugSetupCheck } from "./debug-setup";
|
||||
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";
|
||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||
|
||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||
@ -198,13 +199,17 @@ export class AppController {
|
||||
return rotated;
|
||||
}
|
||||
|
||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||
}
|
||||
|
||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
||||
logAuditEvent(level, message, fields);
|
||||
logTraceEvent(level, "audit", message, fields);
|
||||
}
|
||||
|
||||
public setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
||||
const next = setTraceEnabled(enabled, note);
|
||||
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
||||
const next = setTraceEnabled(enabled, note, durationMs);
|
||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
||||
return next;
|
||||
}
|
||||
|
||||
@ -3,6 +3,9 @@ import path from "node:path";
|
||||
|
||||
type AuditLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
const AUDIT_LOG_MAX_FILE_BYTES = Number(process.env.RD_AUDIT_LOG_MAX_BYTES || 10 * 1024 * 1024);
|
||||
const AUDIT_LOG_RETENTION_DAYS = Number(process.env.RD_AUDIT_LOG_RETENTION_DAYS || 30);
|
||||
|
||||
let auditLogPath: string | null = null;
|
||||
|
||||
function sanitizeFieldValue(value: unknown): string {
|
||||
@ -32,10 +35,46 @@ function formatFields(fields?: Record<string, unknown>): string {
|
||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
||||
}
|
||||
|
||||
function rotateIfNeeded(filePath: string): void {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size < AUDIT_LOG_MAX_FILE_BYTES) {
|
||||
return;
|
||||
}
|
||||
const backup = `${filePath}.old`;
|
||||
try {
|
||||
fs.rmSync(backup, { force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
fs.renameSync(filePath, backup);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupOldBackup(filePath: string): void {
|
||||
const backup = `${filePath}.old`;
|
||||
try {
|
||||
const stat = fs.statSync(backup);
|
||||
const cutoff = Date.now() - AUDIT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
if (stat.mtimeMs < cutoff) {
|
||||
fs.rmSync(backup, { force: true });
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function initAuditLog(baseDir: string): void {
|
||||
auditLogPath = path.join(baseDir, "audit.log");
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
||||
cleanupOldBackup(auditLogPath);
|
||||
if (!fs.existsSync(auditLogPath)) {
|
||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||
}
|
||||
rotateIfNeeded(auditLogPath);
|
||||
if (!fs.existsSync(auditLogPath)) {
|
||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||
}
|
||||
@ -50,6 +89,10 @@ export function logAuditEvent(level: AuditLevel, message: string, fields?: Recor
|
||||
return;
|
||||
}
|
||||
try {
|
||||
rotateIfNeeded(auditLogPath);
|
||||
if (!fs.existsSync(auditLogPath)) {
|
||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||
}
|
||||
fs.appendFileSync(
|
||||
auditLogPath,
|
||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
||||
|
||||
@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import crypto from "node:crypto";
|
||||
import { APP_VERSION } from "./constants";
|
||||
import { getAuditLogPath } from "./audit-log";
|
||||
import { getDebugSetupCheck } from "./debug-setup";
|
||||
import { logger, getLogFilePath } from "./logger";
|
||||
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
||||
import { getSessionLogPath } from "./session-log";
|
||||
@ -11,7 +12,7 @@ import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
|
||||
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
|
||||
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
|
||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, updateTraceConfig } from "./trace-log";
|
||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
|
||||
import { getWindowsHostDiagnostics } from "./windows-host-diagnostics";
|
||||
import type { DownloadManager } from "./download-manager";
|
||||
import type { DownloadItem, PackageEntry, UiSnapshot } from "../shared/types";
|
||||
@ -31,6 +32,7 @@ type DebugEndpointDescriptor = {
|
||||
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||
{ method: "GET", path: "/health", description: "Basic health, uptime, and memory information." },
|
||||
{ method: "GET", path: "/meta", description: "Lists runtime metadata and all available endpoints." },
|
||||
{ method: "GET", path: "/debug/setup", description: "Checks whether the local debug setup is configured for support." },
|
||||
{ method: "GET", path: "/host/diagnostics", description: "Returns Windows host crash and dump diagnostics." },
|
||||
{ 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." },
|
||||
@ -39,7 +41,7 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||
{ 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/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: "/trace/config", queryExample: "enable=1¬e=support&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
||||
{ 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: "/stats", description: "Returns live session stats plus persisted all-time totals." },
|
||||
@ -236,6 +238,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
||||
"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.",
|
||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
||||
"Use /debug/setup to quickly verify whether token, host, manifest, and trace files are in a good support state.",
|
||||
"Use /diagnostics for an overview, then drill into /logs/item, /logs/package, /status, /packages, /items, /settings, /accounts, /stats, /history, or /logs/trace.",
|
||||
"If a full handoff is needed, download /support/bundle as a ZIP."
|
||||
],
|
||||
@ -270,6 +273,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
||||
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
||||
remoteHostHint
|
||||
},
|
||||
setupCheckEndpoint: "/debug/setup",
|
||||
askUserFor: [
|
||||
"Server IP or DNS name, if remote access is required and not already known."
|
||||
],
|
||||
@ -470,6 +474,9 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
traceConfig: getTraceConfigPath(),
|
||||
traceLog: getTraceLogPath()
|
||||
},
|
||||
supportChecks: {
|
||||
setup: "/debug/setup"
|
||||
},
|
||||
logPaths: {
|
||||
main: getLogFilePath(),
|
||||
audit: getAuditLogPath(),
|
||||
@ -481,6 +488,11 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/debug/setup") {
|
||||
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/host/diagnostics") {
|
||||
jsonResponse(res, 200, getWindowsHostDiagnostics());
|
||||
return;
|
||||
@ -554,11 +566,21 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
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();
|
||||
const durationMinutesRaw = Number(url.searchParams.get("durationMinutes") || "120");
|
||||
const durationMinutes = Number.isFinite(durationMinutesRaw) && durationMinutesRaw > 0
|
||||
? Math.min(Math.floor(durationMinutesRaw), 24 * 60)
|
||||
: 120;
|
||||
let config = getTraceConfig();
|
||||
if (enabled !== null) {
|
||||
config = setTraceEnabled(enabled, note, durationMinutes * 60 * 1000);
|
||||
}
|
||||
const configPatch = { ...patch };
|
||||
delete configPatch.enabled;
|
||||
if (Object.keys(configPatch).length > 0) {
|
||||
config = updateTraceConfig(configPatch);
|
||||
}
|
||||
if (Object.keys(patch).length > 0) {
|
||||
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note });
|
||||
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note, durationMinutes });
|
||||
}
|
||||
jsonResponse(res, 200, {
|
||||
path: getTraceConfigPath(),
|
||||
@ -797,7 +819,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
debugServer: {
|
||||
host: bindHost,
|
||||
port: bindPort
|
||||
}
|
||||
},
|
||||
setup: getDebugSetupCheck(runtimeBaseDir)
|
||||
},
|
||||
status: buildStatusPayload(snapshot),
|
||||
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
||||
|
||||
133
src/main/debug-setup.ts
Normal file
133
src/main/debug-setup.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||
|
||||
const DEFAULT_PORT = 9868;
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
||||
|
||||
function readToken(baseDir: string): string {
|
||||
try {
|
||||
return fs.readFileSync(path.join(baseDir, "debug_token.txt"), "utf8").trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function readPort(baseDir: string): number {
|
||||
try {
|
||||
const raw = Number(fs.readFileSync(path.join(baseDir, "debug_port.txt"), "utf8").trim());
|
||||
if (Number.isFinite(raw) && raw >= 1024 && raw <= 65535) {
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return DEFAULT_PORT;
|
||||
}
|
||||
|
||||
function readHost(baseDir: string): string {
|
||||
try {
|
||||
const raw = fs.readFileSync(path.join(baseDir, "debug_host.txt"), "utf8").trim();
|
||||
if (!raw) {
|
||||
return DEFAULT_HOST;
|
||||
}
|
||||
if (/^(localhost|0\.0\.0\.0|127\.0\.0\.1|::1)$/i.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
if (/^[a-z0-9.-]+$/i.test(raw)) {
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return DEFAULT_HOST;
|
||||
}
|
||||
|
||||
function readTraceConfig(baseDir: string): SupportTraceConfig {
|
||||
const fallback: SupportTraceConfig = {
|
||||
enabled: false,
|
||||
includeMainLog: true,
|
||||
includeAudit: true,
|
||||
logDebugRequests: true,
|
||||
autoDisableAt: null,
|
||||
updatedAt: new Date(0).toISOString()
|
||||
};
|
||||
try {
|
||||
const filePath = path.join(baseDir, "trace_config.json");
|
||||
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as Partial<SupportTraceConfig>;
|
||||
return {
|
||||
enabled: Boolean(parsed.enabled),
|
||||
includeMainLog: parsed.includeMainLog === undefined ? true : Boolean(parsed.includeMainLog),
|
||||
includeAudit: parsed.includeAudit === undefined ? true : Boolean(parsed.includeAudit),
|
||||
logDebugRequests: parsed.logDebugRequests === undefined ? true : Boolean(parsed.logDebugRequests),
|
||||
autoDisableAt: typeof parsed.autoDisableAt === "string" && parsed.autoDisableAt.trim() ? parsed.autoDisableAt : null,
|
||||
updatedAt: typeof parsed.updatedAt === "string" && parsed.updatedAt.trim() ? parsed.updatedAt : fallback.updatedAt
|
||||
};
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
const host = readHost(baseDir);
|
||||
const port = readPort(baseDir);
|
||||
const token = readToken(baseDir);
|
||||
const tokenPath = path.join(baseDir, "debug_token.txt");
|
||||
const aiManifestPath = path.join(baseDir, AI_MANIFEST_FILE);
|
||||
const traceConfigPath = path.join(baseDir, "trace_config.json");
|
||||
const traceLogPath = path.join(baseDir, "trace.log");
|
||||
const traceConfig = readTraceConfig(baseDir);
|
||||
const localOnly = /^(127\.0\.0\.1|localhost|::1)$/i.test(host);
|
||||
const warnings: string[] = [];
|
||||
const notes: string[] = [];
|
||||
|
||||
if (!token) {
|
||||
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
||||
}
|
||||
if (localOnly) {
|
||||
warnings.push("Der Debug-Server ist aktuell nur lokal erreichbar. Für Remote-Support debug_host.txt auf 0.0.0.0 setzen.");
|
||||
} else {
|
||||
notes.push("Der Debug-Server ist für Remote-Zugriff konfiguriert. Firewall oder Provider-Regeln müssen separat offen sein.");
|
||||
}
|
||||
if (!fs.existsSync(aiManifestPath)) {
|
||||
warnings.push("debug_ai_manifest.json fehlt. App einmal neu starten, damit die KI-Support-Datei neu geschrieben wird.");
|
||||
}
|
||||
if (!fs.existsSync(traceConfigPath)) {
|
||||
warnings.push("trace_config.json fehlt. Trace-Funktionen sind lokal noch nicht initialisiert.");
|
||||
}
|
||||
if (traceConfig.enabled && !traceConfig.autoDisableAt) {
|
||||
warnings.push("Support-Trace ist aktiv ohne automatische Abschaltzeit. Einmal neu aktivieren, damit die 2-Stunden-Begrenzung gesetzt wird.");
|
||||
}
|
||||
if (traceConfig.enabled && traceConfig.autoDisableAt) {
|
||||
notes.push(`Support-Trace aktiv bis ${traceConfig.autoDisableAt}.`);
|
||||
}
|
||||
notes.push("Die App kann Netzwerk-Firewalls oder Provider-Sicherheitsgruppen nicht direkt prüfen.");
|
||||
|
||||
return {
|
||||
enabled: Boolean(token),
|
||||
host,
|
||||
port,
|
||||
localOnly,
|
||||
tokenConfigured: Boolean(token),
|
||||
tokenPath,
|
||||
aiManifestPath,
|
||||
aiManifestPresent: fs.existsSync(aiManifestPath),
|
||||
traceConfigPath: fs.existsSync(traceConfigPath) ? traceConfigPath : null,
|
||||
traceLogPath: fs.existsSync(traceLogPath) ? traceLogPath : null,
|
||||
traceEnabled: traceConfig.enabled,
|
||||
traceAutoDisableAt: traceConfig.autoDisableAt,
|
||||
warnings,
|
||||
notes,
|
||||
localUrls: {
|
||||
health: `http://127.0.0.1:${port}/health?token=${token || "<TOKEN>"}`,
|
||||
meta: `http://127.0.0.1:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||
diagnostics: `http://127.0.0.1:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||
},
|
||||
remoteUrlTemplates: {
|
||||
health: `http://<SERVER_IP_OR_DNS>:${port}/health?token=${token || "<TOKEN>"}`,
|
||||
meta: `http://<SERVER_IP_OR_DNS>:${port}/meta?token=${token || "<TOKEN>"}`,
|
||||
diagnostics: `http://<SERVER_IP_OR_DNS>:${port}/diagnostics?token=${token || "<TOKEN>"}`
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -538,16 +538,21 @@ function registerIpcHandlers(): void {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK, async () => controller.getDebugSetupCheck());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string) => {
|
||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
|
||||
if (typeof enabled !== "boolean") {
|
||||
throw new Error("enabled muss ein Boolean sein");
|
||||
}
|
||||
if (note !== undefined) {
|
||||
validateString(note, "note");
|
||||
}
|
||||
return controller.setTraceEnabled(enabled, note);
|
||||
if (durationMinutes !== undefined && (!Number.isFinite(durationMinutes) || durationMinutes <= 0)) {
|
||||
throw new Error("durationMinutes muss eine positive Zahl sein");
|
||||
}
|
||||
return controller.setTraceEnabled(enabled, note, durationMinutes ? durationMinutes * 60 * 1000 : undefined);
|
||||
});
|
||||
|
||||
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
||||
|
||||
@ -3,6 +3,7 @@ import path from "node:path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { APP_VERSION } from "./constants";
|
||||
import { getAuditLogPath } from "./audit-log";
|
||||
import { getDebugSetupCheck } from "./debug-setup";
|
||||
import { getLogFilePath } from "./logger";
|
||||
import { getPackageLogPath } from "./package-log";
|
||||
import { getSessionLogPath } from "./session-log";
|
||||
@ -89,6 +90,7 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
||||
}
|
||||
});
|
||||
addJson(zip, "overview/debug-setup.json", getDebugSetupCheck(baseDir));
|
||||
addJson(zip, "overview/history.json", {
|
||||
total: history.length,
|
||||
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
||||
@ -115,8 +117,10 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
||||
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
||||
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
||||
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
||||
addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
|
||||
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
||||
addFileIfExists(zip, getTraceLogPath(), "logs/trace.log");
|
||||
addFileIfExists(zip, getTraceLogPath() ? `${getTraceLogPath()}.old` : null, "logs/trace.log.old");
|
||||
|
||||
addDirectoryIfExists(zip, path.join(baseDir, "session-logs"), "logs/session-logs");
|
||||
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
||||
|
||||
@ -7,12 +7,16 @@ type TraceLevel = "INFO" | "WARN" | "ERROR";
|
||||
|
||||
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
||||
const TRACE_CONFIG_FILE = "trace_config.json";
|
||||
const TRACE_LOG_MAX_FILE_BYTES = Number(process.env.RD_TRACE_LOG_MAX_BYTES || 10 * 1024 * 1024);
|
||||
const TRACE_LOG_RETENTION_DAYS = Number(process.env.RD_TRACE_LOG_RETENTION_DAYS || 30);
|
||||
const TRACE_DEFAULT_AUTO_DISABLE_MS = Number(process.env.RD_TRACE_AUTO_DISABLE_MS || 2 * 60 * 60 * 1000);
|
||||
|
||||
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
||||
enabled: false,
|
||||
includeMainLog: true,
|
||||
includeAudit: true,
|
||||
logDebugRequests: true,
|
||||
autoDisableAt: null,
|
||||
updatedAt: new Date(0).toISOString()
|
||||
};
|
||||
|
||||
@ -21,6 +25,7 @@ let traceConfigPath: string | null = null;
|
||||
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||
let pendingLines: string[] = [];
|
||||
let flushTimer: NodeJS.Timeout | null = null;
|
||||
let autoDisableTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
function sanitizeFieldValue(value: unknown): string {
|
||||
if (value === undefined || value === null) {
|
||||
@ -62,6 +67,37 @@ function flushPending(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function rotateIfNeeded(filePath: string): void {
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
if (stat.size < TRACE_LOG_MAX_FILE_BYTES) {
|
||||
return;
|
||||
}
|
||||
const backup = `${filePath}.old`;
|
||||
try {
|
||||
fs.rmSync(backup, { force: true });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
fs.renameSync(filePath, backup);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupOldBackup(filePath: string): void {
|
||||
const backup = `${filePath}.old`;
|
||||
try {
|
||||
const stat = fs.statSync(backup);
|
||||
const cutoff = Date.now() - TRACE_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||
if (stat.mtimeMs < cutoff) {
|
||||
fs.rmSync(backup, { force: true });
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleFlush(): void {
|
||||
if (flushTimer) {
|
||||
return;
|
||||
@ -76,6 +112,14 @@ function appendTraceLine(line: string): void {
|
||||
if (!traceLogPath) {
|
||||
return;
|
||||
}
|
||||
rotateIfNeeded(traceLogPath);
|
||||
if (!fs.existsSync(traceLogPath)) {
|
||||
try {
|
||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
pendingLines.push(line);
|
||||
scheduleFlush();
|
||||
}
|
||||
@ -90,6 +134,9 @@ function normalizeTraceConfig(raw: unknown): SupportTraceConfig {
|
||||
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),
|
||||
autoDisableAt: typeof value.autoDisableAt === "string" && value.autoDisableAt.trim()
|
||||
? value.autoDisableAt
|
||||
: null,
|
||||
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
||||
? value.updatedAt
|
||||
: DEFAULT_TRACE_CONFIG.updatedAt
|
||||
@ -126,11 +173,58 @@ const mainLogListener = (line: string): void => {
|
||||
appendTraceLine(line);
|
||||
};
|
||||
|
||||
function clearAutoDisableTimer(): void {
|
||||
if (autoDisableTimer) {
|
||||
clearTimeout(autoDisableTimer);
|
||||
autoDisableTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function disableTraceDueToExpiry(): void {
|
||||
clearAutoDisableTimer();
|
||||
if (!traceConfig.enabled) {
|
||||
return;
|
||||
}
|
||||
traceConfig = normalizeTraceConfig({
|
||||
...traceConfig,
|
||||
enabled: false,
|
||||
autoDisableAt: null,
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
persistTraceConfig();
|
||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace automatisch deaktiviert | reason=expired\n`);
|
||||
}
|
||||
|
||||
function scheduleAutoDisable(): void {
|
||||
clearAutoDisableTimer();
|
||||
if (!traceConfig.enabled || !traceConfig.autoDisableAt) {
|
||||
return;
|
||||
}
|
||||
const until = Date.parse(traceConfig.autoDisableAt);
|
||||
if (!Number.isFinite(until)) {
|
||||
return;
|
||||
}
|
||||
const remainingMs = until - Date.now();
|
||||
if (remainingMs <= 0) {
|
||||
disableTraceDueToExpiry();
|
||||
return;
|
||||
}
|
||||
autoDisableTimer = setTimeout(() => {
|
||||
autoDisableTimer = null;
|
||||
disableTraceDueToExpiry();
|
||||
}, Math.min(remainingMs, 2_147_483_647));
|
||||
}
|
||||
|
||||
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 });
|
||||
cleanupOldBackup(traceLogPath);
|
||||
if (!fs.existsSync(traceLogPath)) {
|
||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||
}
|
||||
rotateIfNeeded(traceLogPath);
|
||||
if (!fs.existsSync(traceLogPath)) {
|
||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||
}
|
||||
@ -144,6 +238,7 @@ export function initTraceLog(baseDir: string): void {
|
||||
return;
|
||||
}
|
||||
addLogListener(mainLogListener);
|
||||
scheduleAutoDisable();
|
||||
}
|
||||
|
||||
export function getTraceLogPath(): string | null {
|
||||
@ -171,13 +266,17 @@ export function updateTraceConfig(patch: Partial<SupportTraceConfig>): SupportTr
|
||||
updatedAt: new Date().toISOString()
|
||||
});
|
||||
persistTraceConfig();
|
||||
scheduleAutoDisable();
|
||||
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`);
|
||||
export function setTraceEnabled(enabled: boolean, note = "", durationMs: number = TRACE_DEFAULT_AUTO_DISABLE_MS): SupportTraceConfig {
|
||||
const autoDisableAt = enabled && durationMs > 0
|
||||
? new Date(Date.now() + durationMs).toISOString()
|
||||
: null;
|
||||
const next = updateTraceConfig({ enabled, autoDisableAt });
|
||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note, autoDisableAt })}\n`);
|
||||
return next;
|
||||
}
|
||||
|
||||
@ -198,6 +297,7 @@ export function logTraceEvent(
|
||||
|
||||
export function shutdownTraceLog(): void {
|
||||
removeLogListener(mainLogListener);
|
||||
clearAutoDisableTimer();
|
||||
if (!traceLogPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -63,8 +63,9 @@ const api: ElectronApi = {
|
||||
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_LOG),
|
||||
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),
|
||||
getDebugSetupCheck: () => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK),
|
||||
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
||||
setTraceEnabled: (enabled: boolean, note?: string) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note),
|
||||
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note, durationMinutes),
|
||||
rotateDebugToken: (): Promise<{ path: string }> => ipcRenderer.invoke(IPC_CHANNELS.ROTATE_DEBUG_TOKEN),
|
||||
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
AppSettings,
|
||||
AppTheme,
|
||||
BandwidthScheduleEntry,
|
||||
DebugSetupCheckResult,
|
||||
DebridFallbackProvider,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
@ -49,8 +50,10 @@ interface ConfirmPromptState {
|
||||
title: string;
|
||||
message: string;
|
||||
confirmLabel: string;
|
||||
cancelLabel?: string;
|
||||
danger?: boolean;
|
||||
details?: string;
|
||||
detailsLabel?: string;
|
||||
}
|
||||
|
||||
interface ContextMenuState {
|
||||
@ -137,6 +140,44 @@ interface ConfiguredAccountEntry {
|
||||
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
||||
}
|
||||
|
||||
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||
const lines: string[] = [
|
||||
`Debug-Server aktiv: ${setup.enabled ? "ja" : "nein"}`,
|
||||
`Host: ${setup.host}`,
|
||||
`Port: ${setup.port}`,
|
||||
`Token-Datei: ${setup.tokenPath}`,
|
||||
`KI-Manifest: ${setup.aiManifestPresent ? "vorhanden" : "fehlt"} (${setup.aiManifestPath})`,
|
||||
`Trace aktiv: ${setup.traceEnabled ? "ja" : "nein"}`,
|
||||
`Trace-Auto-Ende: ${setup.traceAutoDisableAt || "nicht gesetzt"}`,
|
||||
"",
|
||||
"Lokale URLs:",
|
||||
setup.localUrls.health,
|
||||
setup.localUrls.meta,
|
||||
setup.localUrls.diagnostics,
|
||||
"",
|
||||
"Remote-Vorlagen:",
|
||||
setup.remoteUrlTemplates.health,
|
||||
setup.remoteUrlTemplates.meta,
|
||||
setup.remoteUrlTemplates.diagnostics
|
||||
];
|
||||
|
||||
if (setup.warnings.length > 0) {
|
||||
lines.push("", "Warnungen:");
|
||||
for (const warning of setup.warnings) {
|
||||
lines.push(`- ${warning}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (setup.notes.length > 0) {
|
||||
lines.push("", "Hinweise:");
|
||||
for (const note of setup.notes) {
|
||||
lines.push(`- ${note}`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
const ACCOUNT_OPTIONS: AccountOption[] = [
|
||||
{
|
||||
kind: "realdebrid-api",
|
||||
@ -3413,14 +3454,34 @@ export function App(): ReactElement {
|
||||
closeMenus();
|
||||
const nextEnabled = !supportTraceEnabled;
|
||||
await performQuickAction(async () => {
|
||||
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle");
|
||||
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle", 120);
|
||||
setSupportTraceEnabled(result.enabled);
|
||||
showToast(result.enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", 2400);
|
||||
showToast(result.enabled ? "Support-Trace für 2 Stunden aktiviert" : "Support-Trace deaktiviert", 2600);
|
||||
}, (error) => {
|
||||
showToast(`Support-Trace fehlgeschlagen: ${String(error)}`, 2800);
|
||||
});
|
||||
};
|
||||
|
||||
const onRunDebugSetupCheck = async (): Promise<void> => {
|
||||
closeMenus();
|
||||
try {
|
||||
const setup = await window.rd.getDebugSetupCheck();
|
||||
const warningText = setup.warnings.length > 0 ? `Warnungen: ${setup.warnings.length}` : "Keine akuten Warnungen";
|
||||
const reachabilityText = setup.localOnly ? "Nur lokal gebunden" : "Remote-fähig konfiguriert";
|
||||
const details = buildDebugSetupDetails(setup);
|
||||
await askConfirmPrompt({
|
||||
title: "Debug-Setup prüfen",
|
||||
message: `${warningText}\n${reachabilityText}\nHost: ${setup.host}:${setup.port}`,
|
||||
confirmLabel: "Schließen",
|
||||
cancelLabel: "Schließen",
|
||||
details,
|
||||
detailsLabel: "Details anzeigen"
|
||||
});
|
||||
} catch (error) {
|
||||
showToast(`Debug-Setup-Check fehlgeschlagen: ${String(error)}`, 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const onRotateDebugToken = async (): Promise<void> => {
|
||||
closeMenus();
|
||||
const confirmed = await askConfirmPrompt({
|
||||
@ -3767,6 +3828,9 @@ export function App(): ReactElement {
|
||||
<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 onRunDebugSetupCheck(); }}>
|
||||
<span>Debug-Setup prüfen</span>
|
||||
</button>
|
||||
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
||||
<span>Debug-Token rotieren</span>
|
||||
</button>
|
||||
@ -4855,12 +4919,12 @@ export function App(): ReactElement {
|
||||
<p style={{ whiteSpace: "pre-line" }}>{confirmPrompt.message}</p>
|
||||
{confirmPrompt.details && (
|
||||
<details className="modal-details">
|
||||
<summary>Changelog anzeigen</summary>
|
||||
<summary>{confirmPrompt.detailsLabel || "Details anzeigen"}</summary>
|
||||
<pre>{confirmPrompt.details}</pre>
|
||||
</details>
|
||||
)}
|
||||
<div className="modal-actions">
|
||||
<button className="btn" onClick={() => closeConfirmPrompt(false)}>Abbrechen</button>
|
||||
<button className="btn" onClick={() => closeConfirmPrompt(false)}>{confirmPrompt.cancelLabel || "Abbrechen"}</button>
|
||||
<button
|
||||
className={confirmPrompt.danger ? "btn danger" : "btn"}
|
||||
onClick={() => closeConfirmPrompt(true)}
|
||||
|
||||
@ -43,6 +43,7 @@ export const IPC_CHANNELS = {
|
||||
OPEN_TRACE_LOG: "app:open-trace-log",
|
||||
OPEN_PACKAGE_LOG: "app:open-package-log",
|
||||
OPEN_ITEM_LOG: "app:open-item-log",
|
||||
GET_DEBUG_SETUP_CHECK: "app:get-debug-setup-check",
|
||||
GET_TRACE_CONFIG: "app:get-trace-config",
|
||||
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
||||
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
||||
|
||||
@ -2,6 +2,7 @@ import type {
|
||||
AddLinksPayload,
|
||||
AllDebridHostInfo,
|
||||
AppSettings,
|
||||
DebugSetupCheckResult,
|
||||
DebridLinkHostLimitInfo,
|
||||
DebridProvider,
|
||||
DuplicatePolicy,
|
||||
@ -59,8 +60,9 @@ export interface ElectronApi {
|
||||
openTraceLog: () => Promise<void>;
|
||||
openPackageLog: (packageId: string) => Promise<void>;
|
||||
openItemLog: (itemId: string) => Promise<void>;
|
||||
getDebugSetupCheck: () => Promise<DebugSetupCheckResult>;
|
||||
getTraceConfig: () => Promise<SupportTraceConfig>;
|
||||
setTraceEnabled: (enabled: boolean, note?: string) => Promise<SupportTraceConfig>;
|
||||
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => Promise<SupportTraceConfig>;
|
||||
rotateDebugToken: () => Promise<{ path: string }>;
|
||||
openRealDebridLogin: () => Promise<void>;
|
||||
openAllDebridLogin: () => Promise<void>;
|
||||
|
||||
@ -342,9 +342,37 @@ export interface SupportTraceConfig {
|
||||
includeMainLog: boolean;
|
||||
includeAudit: boolean;
|
||||
logDebugRequests: boolean;
|
||||
autoDisableAt: string | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface DebugSetupCheckResult {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
localOnly: boolean;
|
||||
tokenConfigured: boolean;
|
||||
tokenPath: string;
|
||||
aiManifestPath: string;
|
||||
aiManifestPresent: boolean;
|
||||
traceConfigPath: string | null;
|
||||
traceLogPath: string | null;
|
||||
traceEnabled: boolean;
|
||||
traceAutoDisableAt: string | null;
|
||||
warnings: string[];
|
||||
notes: string[];
|
||||
localUrls: {
|
||||
health: string;
|
||||
meta: string;
|
||||
diagnostics: string;
|
||||
};
|
||||
remoteUrlTemplates: {
|
||||
health: string;
|
||||
meta: string;
|
||||
diagnostics: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HistoryEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
|
||||
@ -29,4 +29,20 @@ describe("audit-log", () => {
|
||||
expect(content).toContain("Settings changed");
|
||||
expect(content).toContain("changedKeys");
|
||||
});
|
||||
|
||||
it("rotates oversized audit logs on startup", () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-alog-rotate-"));
|
||||
tempDirs.push(baseDir);
|
||||
|
||||
const oversizedPath = path.join(baseDir, "audit.log");
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
fs.writeFileSync(oversizedPath, "x".repeat(10 * 1024 * 1024 + 256), "utf8");
|
||||
|
||||
initAuditLog(baseDir);
|
||||
|
||||
expect(fs.existsSync(oversizedPath)).toBe(true);
|
||||
expect(fs.existsSync(`${oversizedPath}.old`)).toBe(true);
|
||||
const content = fs.readFileSync(oversizedPath, "utf8");
|
||||
expect(content).toContain("Audit-Log Start");
|
||||
});
|
||||
});
|
||||
|
||||
@ -341,6 +341,7 @@ describe("debug-server", () => {
|
||||
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
||||
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
||||
expect(manifest.quickstart?.[1]).toContain("server IP");
|
||||
expect(manifest.setupCheckEndpoint).toBe("/debug/setup");
|
||||
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
||||
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
||||
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
||||
@ -351,6 +352,24 @@ describe("debug-server", () => {
|
||||
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
||||
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
||||
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
||||
expect(metaPayload.supportChecks?.setup).toBe("/debug/setup");
|
||||
});
|
||||
|
||||
it("serves a debug setup check with trace expiry details", async () => {
|
||||
const fixture = await createFixture();
|
||||
const response = await fetch(`${fixture.baseUrl}/debug/setup?token=${fixture.token}`);
|
||||
expect(response.ok).toBe(true);
|
||||
const payload = await response.json() as Record<string, any>;
|
||||
|
||||
expect(payload.enabled).toBe(true);
|
||||
expect(payload.host).toBe("0.0.0.0");
|
||||
expect(payload.localOnly).toBe(false);
|
||||
expect(payload.tokenConfigured).toBe(true);
|
||||
expect(payload.aiManifestPresent).toBe(true);
|
||||
expect(payload.traceEnabled).toBe(true);
|
||||
expect(payload.traceAutoDisableAt).toBeTruthy();
|
||||
expect(payload.remoteUrlTemplates?.health).toContain("<SERVER_IP_OR_DNS>");
|
||||
expect(Array.isArray(payload.notes)).toBe(true);
|
||||
});
|
||||
|
||||
it("serves package details and package log by package query", async () => {
|
||||
@ -449,6 +468,7 @@ describe("debug-server", () => {
|
||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
||||
expect(entries).toContain("overview/settings.json");
|
||||
expect(entries).toContain("overview/accounts.json");
|
||||
expect(entries).toContain("overview/debug-setup.json");
|
||||
expect(entries).toContain("overview/trace-config.json");
|
||||
expect(entries).toContain("logs/audit.log");
|
||||
expect(entries).toContain("logs/trace.log");
|
||||
|
||||
@ -48,6 +48,43 @@ describe("trace-log", () => {
|
||||
|
||||
const traceConfig = getTraceConfig();
|
||||
expect(traceConfig.enabled).toBe(true);
|
||||
expect(traceConfig.autoDisableAt).toBeTruthy();
|
||||
expect(JSON.parse(fs.readFileSync(traceConfigPath!, "utf8")).enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("auto-disables support trace after the requested duration", async () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-tlog-expire-"));
|
||||
tempDirs.push(baseDir);
|
||||
|
||||
configureLogger(baseDir);
|
||||
initTraceLog(baseDir);
|
||||
setTraceEnabled(true, "expire-test", 50);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 350));
|
||||
|
||||
const traceConfig = getTraceConfig();
|
||||
expect(traceConfig.enabled).toBe(false);
|
||||
expect(traceConfig.autoDisableAt).toBeNull();
|
||||
|
||||
const traceLogPath = getTraceLogPath();
|
||||
expect(traceLogPath).not.toBeNull();
|
||||
const traceContent = fs.readFileSync(traceLogPath!, "utf8");
|
||||
expect(traceContent).toContain("Support-Trace automatisch deaktiviert");
|
||||
});
|
||||
|
||||
it("rotates oversized trace logs on startup", () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-tlog-rotate-"));
|
||||
tempDirs.push(baseDir);
|
||||
|
||||
const oversizedPath = path.join(baseDir, "trace.log");
|
||||
fs.mkdirSync(baseDir, { recursive: true });
|
||||
fs.writeFileSync(oversizedPath, "x".repeat(10 * 1024 * 1024 + 256), "utf8");
|
||||
|
||||
initTraceLog(baseDir);
|
||||
|
||||
expect(fs.existsSync(oversizedPath)).toBe(true);
|
||||
expect(fs.existsSync(`${oversizedPath}.old`)).toBe(true);
|
||||
const currentContent = fs.readFileSync(oversizedPath, "utf8");
|
||||
expect(currentContent).toContain("Trace-Log Start");
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user