Compare commits
No commits in common. "28452373bf3fcf19b704c11de5abe4db1f9e18fd" and "af73934b0f5c6cf541a1dcbd9f08daf3e10b5695" have entirely different histories.
28452373bf
...
af73934b0f
12
README.md
12
README.md
@ -189,8 +189,6 @@ Runtime files are stored in Electron's `userData` directory, including:
|
|||||||
- `package-logs/package_*.txt`
|
- `package-logs/package_*.txt`
|
||||||
- `item-logs/item_*.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
|
### 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.
|
For headless or server-style troubleshooting, the app can expose a small authenticated HTTP debug API with live status and log tails.
|
||||||
@ -206,15 +204,12 @@ 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. By default, the support trace now auto-disables again after 2 hours so it does not stay enabled forever by accident.
|
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.
|
||||||
|
|
||||||
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:
|
Available endpoints after restart:
|
||||||
|
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /meta`
|
- `GET /meta`
|
||||||
- `GET /debug/setup`
|
|
||||||
- `GET /host/diagnostics`
|
- `GET /host/diagnostics`
|
||||||
- `GET /status`
|
- `GET /status`
|
||||||
- `GET /settings`
|
- `GET /settings`
|
||||||
@ -231,7 +226,7 @@ Available endpoints after restart:
|
|||||||
- `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&durationMinutes=120`
|
- `GET /trace/config?enable=1¬e=support`
|
||||||
- `GET /support/bundle`
|
- `GET /support/bundle`
|
||||||
- `GET /diagnostics?package=Release&lines=150`
|
- `GET /diagnostics?package=Release&lines=150`
|
||||||
|
|
||||||
@ -248,10 +243,9 @@ Invoke-RestMethod "http://SERVER:9868/settings?token=YOUR_TOKEN"
|
|||||||
Invoke-RestMethod "http://SERVER:9868/accounts?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/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/debug/setup?token=YOUR_TOKEN"
|
|
||||||
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/logs/trace?token=YOUR_TOKEN&lines=200"
|
||||||
Invoke-RestMethod "http://SERVER:9868/trace/config?token=YOUR_TOKEN&enable=1¬e=support&durationMinutes=120"
|
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"
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.7.72",
|
"version": "1.7.71",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -37,11 +37,10 @@ import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } fro
|
|||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, 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 { getDebugSetupCheck } from "./debug-setup";
|
|
||||||
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
||||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
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);
|
||||||
@ -199,17 +198,13 @@ export class AppController {
|
|||||||
return rotated;
|
return rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
|
||||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
logTraceEvent(level, "audit", message, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
public setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
||||||
const next = setTraceEnabled(enabled, note, durationMs);
|
const next = setTraceEnabled(enabled, note);
|
||||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,6 @@ import path from "node:path";
|
|||||||
|
|
||||||
type AuditLevel = "INFO" | "WARN" | "ERROR";
|
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;
|
let auditLogPath: string | null = null;
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
function sanitizeFieldValue(value: unknown): string {
|
||||||
@ -35,46 +32,10 @@ function formatFields(fields?: Record<string, unknown>): string {
|
|||||||
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
|
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 {
|
export function initAuditLog(baseDir: string): void {
|
||||||
auditLogPath = path.join(baseDir, "audit.log");
|
auditLogPath = path.join(baseDir, "audit.log");
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
fs.mkdirSync(path.dirname(auditLogPath), { recursive: true });
|
||||||
cleanupOldBackup(auditLogPath);
|
|
||||||
if (!fs.existsSync(auditLogPath)) {
|
|
||||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
rotateIfNeeded(auditLogPath);
|
|
||||||
if (!fs.existsSync(auditLogPath)) {
|
if (!fs.existsSync(auditLogPath)) {
|
||||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
fs.writeFileSync(auditLogPath, "", "utf8");
|
||||||
}
|
}
|
||||||
@ -89,10 +50,6 @@ export function logAuditEvent(level: AuditLevel, message: string, fields?: Recor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
rotateIfNeeded(auditLogPath);
|
|
||||||
if (!fs.existsSync(auditLogPath)) {
|
|
||||||
fs.writeFileSync(auditLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
fs.appendFileSync(
|
fs.appendFileSync(
|
||||||
auditLogPath,
|
auditLogPath,
|
||||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import path from "node:path";
|
|||||||
import crypto from "node:crypto";
|
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 { getDebugSetupCheck } from "./debug-setup";
|
|
||||||
import { logger, getLogFilePath } from "./logger";
|
import { logger, getLogFilePath } from "./logger";
|
||||||
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
import { getItemLogPath as getPersistedItemLogPath } from "./item-log";
|
||||||
import { getSessionLogPath } from "./session-log";
|
import { getSessionLogPath } from "./session-log";
|
||||||
@ -12,7 +11,7 @@ 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 { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
|
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";
|
||||||
@ -32,7 +31,6 @@ type DebugEndpointDescriptor = {
|
|||||||
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
|
||||||
{ method: "GET", path: "/health", description: "Basic health, uptime, and memory information." },
|
{ 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: "/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: "/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: "/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." },
|
||||||
@ -41,7 +39,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/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&durationMinutes=120", description: "Reads or updates the support trace configuration." },
|
{ 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." },
|
||||||
@ -238,7 +236,6 @@ 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 /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.",
|
"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."
|
"If a full handoff is needed, download /support/bundle as a ZIP."
|
||||||
],
|
],
|
||||||
@ -273,7 +270,6 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
remoteBaseUrlTemplate: `http://<SERVER_IP_OR_DNS>:${bindPort}`,
|
||||||
remoteHostHint
|
remoteHostHint
|
||||||
},
|
},
|
||||||
setupCheckEndpoint: "/debug/setup",
|
|
||||||
askUserFor: [
|
askUserFor: [
|
||||||
"Server IP or DNS name, if remote access is required and not already known."
|
"Server IP or DNS name, if remote access is required and not already known."
|
||||||
],
|
],
|
||||||
@ -474,9 +470,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
traceConfig: getTraceConfigPath(),
|
traceConfig: getTraceConfigPath(),
|
||||||
traceLog: getTraceLogPath()
|
traceLog: getTraceLogPath()
|
||||||
},
|
},
|
||||||
supportChecks: {
|
|
||||||
setup: "/debug/setup"
|
|
||||||
},
|
|
||||||
logPaths: {
|
logPaths: {
|
||||||
main: getLogFilePath(),
|
main: getLogFilePath(),
|
||||||
audit: getAuditLogPath(),
|
audit: getAuditLogPath(),
|
||||||
@ -488,11 +481,6 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/debug/setup") {
|
|
||||||
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pathname === "/host/diagnostics") {
|
if (pathname === "/host/diagnostics") {
|
||||||
jsonResponse(res, 200, getWindowsHostDiagnostics());
|
jsonResponse(res, 200, getWindowsHostDiagnostics());
|
||||||
return;
|
return;
|
||||||
@ -566,21 +554,11 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
patch.logDebugRequests = logDebugRequests;
|
patch.logDebugRequests = logDebugRequests;
|
||||||
}
|
}
|
||||||
const note = String(url.searchParams.get("note") || "").trim();
|
const note = String(url.searchParams.get("note") || "").trim();
|
||||||
const durationMinutesRaw = Number(url.searchParams.get("durationMinutes") || "120");
|
const config = Object.keys(patch).length > 0
|
||||||
const durationMinutes = Number.isFinite(durationMinutesRaw) && durationMinutesRaw > 0
|
? updateTraceConfig({ ...patch, ...(note ? { updatedAt: new Date().toISOString() } : {}) })
|
||||||
? Math.min(Math.floor(durationMinutesRaw), 24 * 60)
|
: getTraceConfig();
|
||||||
: 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) {
|
if (Object.keys(patch).length > 0) {
|
||||||
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note, durationMinutes });
|
logTraceEvent("INFO", "support", "Trace-Konfiguration über Debug-Server geändert", { ...patch, note });
|
||||||
}
|
}
|
||||||
jsonResponse(res, 200, {
|
jsonResponse(res, 200, {
|
||||||
path: getTraceConfigPath(),
|
path: getTraceConfigPath(),
|
||||||
@ -819,8 +797,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
debugServer: {
|
debugServer: {
|
||||||
host: bindHost,
|
host: bindHost,
|
||||||
port: bindPort
|
port: bindPort
|
||||||
},
|
}
|
||||||
setup: getDebugSetupCheck(runtimeBaseDir)
|
|
||||||
},
|
},
|
||||||
status: buildStatusPayload(snapshot),
|
status: buildStatusPayload(snapshot),
|
||||||
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
settings: buildRedactedSettingsPayload(readSupportSettings()),
|
||||||
|
|||||||
@ -1,133 +0,0 @@
|
|||||||
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,21 +538,16 @@ 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.GET_TRACE_CONFIG, async () => controller.getTraceConfig());
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string, durationMinutes?: number) => {
|
ipcMain.handle(IPC_CHANNELS.SET_TRACE_ENABLED, async (_event: IpcMainInvokeEvent, enabled: boolean, note?: string) => {
|
||||||
if (typeof enabled !== "boolean") {
|
if (typeof enabled !== "boolean") {
|
||||||
throw new Error("enabled muss ein Boolean sein");
|
throw new Error("enabled muss ein Boolean sein");
|
||||||
}
|
}
|
||||||
if (note !== undefined) {
|
if (note !== undefined) {
|
||||||
validateString(note, "note");
|
validateString(note, "note");
|
||||||
}
|
}
|
||||||
if (durationMinutes !== undefined && (!Number.isFinite(durationMinutes) || durationMinutes <= 0)) {
|
return controller.setTraceEnabled(enabled, note);
|
||||||
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 () => {
|
ipcMain.handle(IPC_CHANNELS.ROTATE_DEBUG_TOKEN, async () => {
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import path from "node:path";
|
|||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { APP_VERSION } from "./constants";
|
import { APP_VERSION } from "./constants";
|
||||||
import { getAuditLogPath } from "./audit-log";
|
import { getAuditLogPath } from "./audit-log";
|
||||||
import { getDebugSetupCheck } from "./debug-setup";
|
|
||||||
import { getLogFilePath } from "./logger";
|
import { getLogFilePath } from "./logger";
|
||||||
import { getPackageLogPath } from "./package-log";
|
import { getPackageLogPath } from "./package-log";
|
||||||
import { getSessionLogPath } from "./session-log";
|
import { getSessionLogPath } from "./session-log";
|
||||||
@ -90,7 +89,6 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
addJson(zip, "overview/debug-setup.json", getDebugSetupCheck(baseDir));
|
|
||||||
addJson(zip, "overview/history.json", {
|
addJson(zip, "overview/history.json", {
|
||||||
total: history.length,
|
total: history.length,
|
||||||
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
||||||
@ -117,10 +115,8 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
addFileIfExists(zip, getLogFilePath(), "logs/rd_downloader.log");
|
||||||
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
addFileIfExists(zip, `${getLogFilePath()}.old`, "logs/rd_downloader.log.old");
|
||||||
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
addFileIfExists(zip, getAuditLogPath(), "logs/audit.log");
|
||||||
addFileIfExists(zip, getAuditLogPath() ? `${getAuditLogPath()}.old` : null, "logs/audit.log.old");
|
|
||||||
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
addFileIfExists(zip, getSessionLogPath(), "logs/session.log");
|
||||||
addFileIfExists(zip, getTraceLogPath(), "logs/trace.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, "session-logs"), "logs/session-logs");
|
||||||
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
addDirectoryIfExists(zip, path.join(baseDir, "package-logs"), "logs/package-logs");
|
||||||
|
|||||||
@ -7,16 +7,12 @@ type TraceLevel = "INFO" | "WARN" | "ERROR";
|
|||||||
|
|
||||||
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
const TRACE_LOG_FLUSH_INTERVAL_MS = 200;
|
||||||
const TRACE_CONFIG_FILE = "trace_config.json";
|
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 = {
|
const DEFAULT_TRACE_CONFIG: SupportTraceConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
includeMainLog: true,
|
includeMainLog: true,
|
||||||
includeAudit: true,
|
includeAudit: true,
|
||||||
logDebugRequests: true,
|
logDebugRequests: true,
|
||||||
autoDisableAt: null,
|
|
||||||
updatedAt: new Date(0).toISOString()
|
updatedAt: new Date(0).toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +21,6 @@ let traceConfigPath: string | null = null;
|
|||||||
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
let traceConfig: SupportTraceConfig = { ...DEFAULT_TRACE_CONFIG };
|
||||||
let pendingLines: string[] = [];
|
let pendingLines: string[] = [];
|
||||||
let flushTimer: NodeJS.Timeout | null = null;
|
let flushTimer: NodeJS.Timeout | null = null;
|
||||||
let autoDisableTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
function sanitizeFieldValue(value: unknown): string {
|
function sanitizeFieldValue(value: unknown): string {
|
||||||
if (value === undefined || value === null) {
|
if (value === undefined || value === null) {
|
||||||
@ -67,37 +62,6 @@ 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 {
|
function scheduleFlush(): void {
|
||||||
if (flushTimer) {
|
if (flushTimer) {
|
||||||
return;
|
return;
|
||||||
@ -112,14 +76,6 @@ function appendTraceLine(line: string): void {
|
|||||||
if (!traceLogPath) {
|
if (!traceLogPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rotateIfNeeded(traceLogPath);
|
|
||||||
if (!fs.existsSync(traceLogPath)) {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
|
||||||
} catch {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pendingLines.push(line);
|
pendingLines.push(line);
|
||||||
scheduleFlush();
|
scheduleFlush();
|
||||||
}
|
}
|
||||||
@ -134,9 +90,6 @@ function normalizeTraceConfig(raw: unknown): SupportTraceConfig {
|
|||||||
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
includeMainLog: value.includeMainLog === undefined ? DEFAULT_TRACE_CONFIG.includeMainLog : Boolean(value.includeMainLog),
|
||||||
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
includeAudit: value.includeAudit === undefined ? DEFAULT_TRACE_CONFIG.includeAudit : Boolean(value.includeAudit),
|
||||||
logDebugRequests: value.logDebugRequests === undefined ? DEFAULT_TRACE_CONFIG.logDebugRequests : Boolean(value.logDebugRequests),
|
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()
|
updatedAt: typeof value.updatedAt === "string" && value.updatedAt.trim()
|
||||||
? value.updatedAt
|
? value.updatedAt
|
||||||
: DEFAULT_TRACE_CONFIG.updatedAt
|
: DEFAULT_TRACE_CONFIG.updatedAt
|
||||||
@ -173,58 +126,11 @@ const mainLogListener = (line: string): void => {
|
|||||||
appendTraceLine(line);
|
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 {
|
export function initTraceLog(baseDir: string): void {
|
||||||
traceLogPath = path.join(baseDir, "trace.log");
|
traceLogPath = path.join(baseDir, "trace.log");
|
||||||
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
traceConfigPath = path.join(baseDir, TRACE_CONFIG_FILE);
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(baseDir, { recursive: true });
|
fs.mkdirSync(baseDir, { recursive: true });
|
||||||
cleanupOldBackup(traceLogPath);
|
|
||||||
if (!fs.existsSync(traceLogPath)) {
|
|
||||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
|
||||||
}
|
|
||||||
rotateIfNeeded(traceLogPath);
|
|
||||||
if (!fs.existsSync(traceLogPath)) {
|
if (!fs.existsSync(traceLogPath)) {
|
||||||
fs.writeFileSync(traceLogPath, "", "utf8");
|
fs.writeFileSync(traceLogPath, "", "utf8");
|
||||||
}
|
}
|
||||||
@ -238,7 +144,6 @@ export function initTraceLog(baseDir: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addLogListener(mainLogListener);
|
addLogListener(mainLogListener);
|
||||||
scheduleAutoDisable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTraceLogPath(): string | null {
|
export function getTraceLogPath(): string | null {
|
||||||
@ -266,17 +171,13 @@ export function updateTraceConfig(patch: Partial<SupportTraceConfig>): SupportTr
|
|||||||
updatedAt: new Date().toISOString()
|
updatedAt: new Date().toISOString()
|
||||||
});
|
});
|
||||||
persistTraceConfig();
|
persistTraceConfig();
|
||||||
scheduleAutoDisable();
|
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Konfiguration aktualisiert${formatFields(traceConfig)}\n`);
|
||||||
return getTraceConfig();
|
return getTraceConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setTraceEnabled(enabled: boolean, note = "", durationMs: number = TRACE_DEFAULT_AUTO_DISABLE_MS): SupportTraceConfig {
|
export function setTraceEnabled(enabled: boolean, note = ""): SupportTraceConfig {
|
||||||
const autoDisableAt = enabled && durationMs > 0
|
const next = updateTraceConfig({ enabled });
|
||||||
? new Date(Date.now() + durationMs).toISOString()
|
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note })}\n`);
|
||||||
: null;
|
|
||||||
const next = updateTraceConfig({ enabled, autoDisableAt });
|
|
||||||
appendTraceLine(`${new Date().toISOString()} [INFO] [trace] Support-Trace ${enabled ? "aktiviert" : "deaktiviert"}${formatFields({ note, autoDisableAt })}\n`);
|
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -297,7 +198,6 @@ export function logTraceEvent(
|
|||||||
|
|
||||||
export function shutdownTraceLog(): void {
|
export function shutdownTraceLog(): void {
|
||||||
removeLogListener(mainLogListener);
|
removeLogListener(mainLogListener);
|
||||||
clearAutoDisableTimer();
|
|
||||||
if (!traceLogPath) {
|
if (!traceLogPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -63,9 +63,8 @@ const api: ElectronApi = {
|
|||||||
openTraceLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_TRACE_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),
|
||||||
getDebugSetupCheck: () => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBUG_SETUP_CHECK),
|
|
||||||
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
getTraceConfig: () => ipcRenderer.invoke(IPC_CHANNELS.GET_TRACE_CONFIG),
|
||||||
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => ipcRenderer.invoke(IPC_CHANNELS.SET_TRACE_ENABLED, enabled, note, durationMinutes),
|
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),
|
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),
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import type {
|
|||||||
AppSettings,
|
AppSettings,
|
||||||
AppTheme,
|
AppTheme,
|
||||||
BandwidthScheduleEntry,
|
BandwidthScheduleEntry,
|
||||||
DebugSetupCheckResult,
|
|
||||||
DebridFallbackProvider,
|
DebridFallbackProvider,
|
||||||
DebridLinkHostLimitInfo,
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
@ -50,10 +49,8 @@ interface ConfirmPromptState {
|
|||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
confirmLabel: string;
|
confirmLabel: string;
|
||||||
cancelLabel?: string;
|
|
||||||
danger?: boolean;
|
danger?: boolean;
|
||||||
details?: string;
|
details?: string;
|
||||||
detailsLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContextMenuState {
|
interface ContextMenuState {
|
||||||
@ -140,44 +137,6 @@ interface ConfiguredAccountEntry {
|
|||||||
debridLinkKeys: DebridLinkAccountKeyEntry[];
|
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[] = [
|
const ACCOUNT_OPTIONS: AccountOption[] = [
|
||||||
{
|
{
|
||||||
kind: "realdebrid-api",
|
kind: "realdebrid-api",
|
||||||
@ -3454,34 +3413,14 @@ export function App(): ReactElement {
|
|||||||
closeMenus();
|
closeMenus();
|
||||||
const nextEnabled = !supportTraceEnabled;
|
const nextEnabled = !supportTraceEnabled;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle", 120);
|
const result = await window.rd.setTraceEnabled(nextEnabled, "UI support toggle");
|
||||||
setSupportTraceEnabled(result.enabled);
|
setSupportTraceEnabled(result.enabled);
|
||||||
showToast(result.enabled ? "Support-Trace für 2 Stunden aktiviert" : "Support-Trace deaktiviert", 2600);
|
showToast(result.enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", 2400);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Support-Trace fehlgeschlagen: ${String(error)}`, 2800);
|
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> => {
|
const onRotateDebugToken = async (): Promise<void> => {
|
||||||
closeMenus();
|
closeMenus();
|
||||||
const confirmed = await askConfirmPrompt({
|
const confirmed = await askConfirmPrompt({
|
||||||
@ -3828,9 +3767,6 @@ export function App(): ReactElement {
|
|||||||
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
<button className="menu-dropdown-item" onClick={() => { void onToggleSupportTrace(); }}>
|
||||||
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
<span>{supportTraceEnabled ? "Support-Trace deaktivieren" : "Support-Trace aktivieren"}</span>
|
||||||
</button>
|
</button>
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRunDebugSetupCheck(); }}>
|
|
||||||
<span>Debug-Setup prüfen</span>
|
|
||||||
</button>
|
|
||||||
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
<button className="menu-dropdown-item" onClick={() => { void onRotateDebugToken(); }}>
|
||||||
<span>Debug-Token rotieren</span>
|
<span>Debug-Token rotieren</span>
|
||||||
</button>
|
</button>
|
||||||
@ -4919,12 +4855,12 @@ export function App(): ReactElement {
|
|||||||
<p style={{ whiteSpace: "pre-line" }}>{confirmPrompt.message}</p>
|
<p style={{ whiteSpace: "pre-line" }}>{confirmPrompt.message}</p>
|
||||||
{confirmPrompt.details && (
|
{confirmPrompt.details && (
|
||||||
<details className="modal-details">
|
<details className="modal-details">
|
||||||
<summary>{confirmPrompt.detailsLabel || "Details anzeigen"}</summary>
|
<summary>Changelog anzeigen</summary>
|
||||||
<pre>{confirmPrompt.details}</pre>
|
<pre>{confirmPrompt.details}</pre>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
<div className="modal-actions">
|
<div className="modal-actions">
|
||||||
<button className="btn" onClick={() => closeConfirmPrompt(false)}>{confirmPrompt.cancelLabel || "Abbrechen"}</button>
|
<button className="btn" onClick={() => closeConfirmPrompt(false)}>Abbrechen</button>
|
||||||
<button
|
<button
|
||||||
className={confirmPrompt.danger ? "btn danger" : "btn"}
|
className={confirmPrompt.danger ? "btn danger" : "btn"}
|
||||||
onClick={() => closeConfirmPrompt(true)}
|
onClick={() => closeConfirmPrompt(true)}
|
||||||
|
|||||||
@ -43,7 +43,6 @@ export const IPC_CHANNELS = {
|
|||||||
OPEN_TRACE_LOG: "app:open-trace-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_DEBUG_SETUP_CHECK: "app:get-debug-setup-check",
|
|
||||||
GET_TRACE_CONFIG: "app:get-trace-config",
|
GET_TRACE_CONFIG: "app:get-trace-config",
|
||||||
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
SET_TRACE_ENABLED: "app:set-trace-enabled",
|
||||||
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
ROTATE_DEBUG_TOKEN: "app:rotate-debug-token",
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import type {
|
|||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
DebugSetupCheckResult,
|
|
||||||
DebridLinkHostLimitInfo,
|
DebridLinkHostLimitInfo,
|
||||||
DebridProvider,
|
DebridProvider,
|
||||||
DuplicatePolicy,
|
DuplicatePolicy,
|
||||||
@ -60,9 +59,8 @@ export interface ElectronApi {
|
|||||||
openTraceLog: () => 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>;
|
||||||
getDebugSetupCheck: () => Promise<DebugSetupCheckResult>;
|
|
||||||
getTraceConfig: () => Promise<SupportTraceConfig>;
|
getTraceConfig: () => Promise<SupportTraceConfig>;
|
||||||
setTraceEnabled: (enabled: boolean, note?: string, durationMinutes?: number) => Promise<SupportTraceConfig>;
|
setTraceEnabled: (enabled: boolean, note?: string) => Promise<SupportTraceConfig>;
|
||||||
rotateDebugToken: () => Promise<{ path: string }>;
|
rotateDebugToken: () => Promise<{ path: string }>;
|
||||||
openRealDebridLogin: () => Promise<void>;
|
openRealDebridLogin: () => Promise<void>;
|
||||||
openAllDebridLogin: () => Promise<void>;
|
openAllDebridLogin: () => Promise<void>;
|
||||||
|
|||||||
@ -342,37 +342,9 @@ export interface SupportTraceConfig {
|
|||||||
includeMainLog: boolean;
|
includeMainLog: boolean;
|
||||||
includeAudit: boolean;
|
includeAudit: boolean;
|
||||||
logDebugRequests: boolean;
|
logDebugRequests: boolean;
|
||||||
autoDisableAt: string | null;
|
|
||||||
updatedAt: string;
|
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 {
|
export interface HistoryEntry {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@ -29,20 +29,4 @@ describe("audit-log", () => {
|
|||||||
expect(content).toContain("Settings changed");
|
expect(content).toContain("Settings changed");
|
||||||
expect(content).toContain("changedKeys");
|
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,7 +341,6 @@ describe("debug-server", () => {
|
|||||||
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
expect(manifest.debugServer?.port).toBeGreaterThan(0);
|
||||||
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
||||||
expect(manifest.quickstart?.[1]).toContain("server IP");
|
expect(manifest.quickstart?.[1]).toContain("server IP");
|
||||||
expect(manifest.setupCheckEndpoint).toBe("/debug/setup");
|
|
||||||
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
expect(manifest.runtimeFiles?.tokenFile).toContain("debug_token.txt");
|
||||||
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
expect(manifest.endpoints?.some((entry: Record<string, any>) => entry.path === "/diagnostics")).toBe(true);
|
||||||
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
expect(JSON.stringify(manifest)).not.toContain(fixture.token);
|
||||||
@ -352,24 +351,6 @@ describe("debug-server", () => {
|
|||||||
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
expect(metaPayload.supportFiles?.aiManifest).toBe(manifestPath);
|
||||||
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
||||||
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
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 () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
@ -468,7 +449,6 @@ describe("debug-server", () => {
|
|||||||
const entries = zip.getEntries().map((entry) => entry.entryName);
|
const entries = zip.getEntries().map((entry) => entry.entryName);
|
||||||
expect(entries).toContain("overview/settings.json");
|
expect(entries).toContain("overview/settings.json");
|
||||||
expect(entries).toContain("overview/accounts.json");
|
expect(entries).toContain("overview/accounts.json");
|
||||||
expect(entries).toContain("overview/debug-setup.json");
|
|
||||||
expect(entries).toContain("overview/trace-config.json");
|
expect(entries).toContain("overview/trace-config.json");
|
||||||
expect(entries).toContain("logs/audit.log");
|
expect(entries).toContain("logs/audit.log");
|
||||||
expect(entries).toContain("logs/trace.log");
|
expect(entries).toContain("logs/trace.log");
|
||||||
|
|||||||
@ -48,43 +48,6 @@ describe("trace-log", () => {
|
|||||||
|
|
||||||
const traceConfig = getTraceConfig();
|
const traceConfig = getTraceConfig();
|
||||||
expect(traceConfig.enabled).toBe(true);
|
expect(traceConfig.enabled).toBe(true);
|
||||||
expect(traceConfig.autoDisableAt).toBeTruthy();
|
|
||||||
expect(JSON.parse(fs.readFileSync(traceConfigPath!, "utf8")).enabled).toBe(true);
|
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