Harden support logging and debug setup

This commit is contained in:
Sucukdeluxe
2026-03-09 02:47:49 +01:00
parent af73934b0f
commit fc4fafa0d6
16 changed files with 512 additions and 24 deletions
+8 -3
View File
@@ -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;
}
+43
View File
@@ -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`,
+30 -7
View File
@@ -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&note=support", description: "Reads or updates the support trace configuration." },
{ method: "GET", path: "/trace/config", queryExample: "enable=1&note=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
View 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>"}`
}
};
}
+7 -2
View File
@@ -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 () => {
+4
View File
@@ -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");
+103 -3
View File
@@ -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;
}