Add support self-check diagnostics
This commit is contained in:
parent
28452373bf
commit
7027e11cbd
@ -208,13 +208,14 @@ After startup, the app also writes `debug_ai_manifest.json` into the same runtim
|
||||
|
||||
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.
|
||||
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 now also reports free disk space, current support-log sizes, and an estimated support-bundle size.
|
||||
|
||||
Available endpoints after restart:
|
||||
|
||||
- `GET /health`
|
||||
- `GET /meta`
|
||||
- `GET /debug/setup`
|
||||
- `GET /self-check`
|
||||
- `GET /host/diagnostics`
|
||||
- `GET /status`
|
||||
- `GET /settings`
|
||||
@ -249,6 +250,7 @@ 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/self-check?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&durationMinutes=120"
|
||||
@ -258,7 +260,7 @@ Invoke-RestMethod "http://SERVER:9868/host/diagnostics?token=YOUR_TOKEN"
|
||||
Invoke-WebRequest "http://SERVER:9868/support/bundle?token=YOUR_TOKEN" -OutFile ".\\rd-support-bundle.zip"
|
||||
```
|
||||
|
||||
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, trace data, package/session/item logs, host-side Windows crash hints, and even a full ZIP support bundle can be inspected remotely.
|
||||
This makes it easy to share one URL plus token during support, so current package status, session state, history, redacted account/settings state, audit actions, trace data, package/session/item logs, host-side Windows crash hints, disk space, support-log volume, support-bundle size estimates, and even a full ZIP support bundle can be inspected remotely.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ 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: "/self-check", description: "Extended support self-check with disk space, log sizes, and support bundle estimate." },
|
||||
{ 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." },
|
||||
@ -238,7 +239,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 /self-check or /debug/setup to quickly verify whether token, host, manifest, trace, disk space, and log sizes 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."
|
||||
],
|
||||
@ -274,6 +275,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
||||
remoteHostHint
|
||||
},
|
||||
setupCheckEndpoint: "/debug/setup",
|
||||
selfCheckEndpoint: "/self-check",
|
||||
askUserFor: [
|
||||
"Server IP or DNS name, if remote access is required and not already known."
|
||||
],
|
||||
@ -475,7 +477,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
traceLog: getTraceLogPath()
|
||||
},
|
||||
supportChecks: {
|
||||
setup: "/debug/setup"
|
||||
setup: "/debug/setup",
|
||||
selfCheck: "/self-check"
|
||||
},
|
||||
logPaths: {
|
||||
main: getLogFilePath(),
|
||||
@ -488,7 +491,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === "/debug/setup") {
|
||||
if (pathname === "/debug/setup" || pathname === "/self-check") {
|
||||
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,10 +1,42 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { getSessionLogPath } from "./session-log";
|
||||
import { createStoragePaths, loadSettings } from "./storage";
|
||||
import type {
|
||||
DebugSetupCheckResult,
|
||||
SupportBundleEstimate,
|
||||
SupportDirectorySizeInfo,
|
||||
SupportDiskSpaceInfo,
|
||||
SupportFileSizeInfo,
|
||||
SupportTraceConfig
|
||||
} from "../shared/types";
|
||||
|
||||
const DEFAULT_PORT = 9868;
|
||||
const DEFAULT_HOST = "127.0.0.1";
|
||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
||||
const LOW_FREE_BYTES_THRESHOLD = Number(process.env.RD_SELF_CHECK_LOW_FREE_BYTES || 20 * 1024 * 1024 * 1024);
|
||||
const LOW_FREE_PERCENT_THRESHOLD = Number(process.env.RD_SELF_CHECK_LOW_FREE_PERCENT || 5);
|
||||
const LOW_FREE_PERCENT_BYTES_GUARD = Number(process.env.RD_SELF_CHECK_LOW_FREE_PERCENT_BYTES_GUARD || 50 * 1024 * 1024 * 1024);
|
||||
const LARGE_LOG_BYTES_THRESHOLD = Number(process.env.RD_SELF_CHECK_LARGE_LOG_BYTES || 250 * 1024 * 1024);
|
||||
const LARGE_BUNDLE_BYTES_THRESHOLD = Number(process.env.RD_SELF_CHECK_LARGE_BUNDLE_BYTES || 150 * 1024 * 1024);
|
||||
const BUNDLE_OVERVIEW_SLACK_BYTES = 256 * 1024;
|
||||
|
||||
function formatByteCount(bytes: number): string {
|
||||
if (!Number.isFinite(bytes) || bytes < 0) {
|
||||
return "0 B";
|
||||
}
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
}
|
||||
if (bytes < 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
||||
}
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function readToken(baseDir: string): string {
|
||||
try {
|
||||
@ -69,26 +101,257 @@ function readTraceConfig(baseDir: string): SupportTraceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
function getFileSizeInfo(filePath: string | null): SupportFileSizeInfo {
|
||||
if (!filePath) {
|
||||
return { path: null, exists: false, bytes: 0 };
|
||||
}
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
return {
|
||||
path: filePath,
|
||||
exists: true,
|
||||
bytes: stat.size
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
path: filePath,
|
||||
exists: false,
|
||||
bytes: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getDirectorySizeInfo(dirPath: string, skipPath?: string | null): SupportDirectorySizeInfo {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
return {
|
||||
path: dirPath,
|
||||
exists: false,
|
||||
fileCount: 0,
|
||||
bytes: 0
|
||||
};
|
||||
}
|
||||
|
||||
let bytes = 0;
|
||||
let fileCount = 0;
|
||||
const queue = [dirPath];
|
||||
while (queue.length > 0) {
|
||||
const current = queue.pop();
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(current, { withFileTypes: true });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(current, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
queue.push(fullPath);
|
||||
continue;
|
||||
}
|
||||
if (skipPath && path.resolve(fullPath) === path.resolve(skipPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
bytes += fs.statSync(fullPath).size;
|
||||
fileCount += 1;
|
||||
} catch {
|
||||
// ignore unreadable files
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
path: dirPath,
|
||||
exists: true,
|
||||
fileCount,
|
||||
bytes
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExistingPath(targetPath: string): string {
|
||||
let current = path.resolve(targetPath);
|
||||
while (!fs.existsSync(current)) {
|
||||
const parent = path.dirname(current);
|
||||
if (parent === current) {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
function getWindowsDiskSpaceInfo(existingPath: string): SupportDiskSpaceInfo | null {
|
||||
if (process.platform !== "win32") {
|
||||
return null;
|
||||
}
|
||||
const root = path.parse(existingPath).root.replace(/[\\/]+$/g, "");
|
||||
const driveName = root.replace(":", "");
|
||||
if (!/^[A-Za-z]$/.test(driveName)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const raw = execFileSync(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
`$drive = Get-PSDrive -Name '${driveName}'; if ($drive) { [pscustomobject]@{ FreeSpace = [int64]$drive.Free; Size = [int64]($drive.Used + $drive.Free) } | ConvertTo-Json -Compress }`
|
||||
],
|
||||
{
|
||||
encoding: "utf8",
|
||||
windowsHide: true,
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
timeout: 3000
|
||||
}
|
||||
).trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(raw) as { FreeSpace?: number | string; Size?: number | string };
|
||||
const totalBytes = Number(parsed.Size);
|
||||
const freeBytes = Number(parsed.FreeSpace);
|
||||
const freePercent = Number.isFinite(totalBytes) && totalBytes > 0
|
||||
? Math.round((freeBytes / totalBytes) * 1000) / 10
|
||||
: null;
|
||||
return {
|
||||
path: existingPath,
|
||||
totalBytes: Number.isFinite(totalBytes) ? totalBytes : null,
|
||||
freeBytes: Number.isFinite(freeBytes) ? freeBytes : null,
|
||||
freePercent
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getDiskSpaceInfo(targetPath: string): SupportDiskSpaceInfo {
|
||||
const existingPath = resolveExistingPath(targetPath);
|
||||
try {
|
||||
const stat = fs.statfsSync(existingPath);
|
||||
const totalBytes = Number(stat.blocks) * Number(stat.bsize);
|
||||
const freeBytes = Number(stat.bavail) * Number(stat.bsize);
|
||||
const freePercent = totalBytes > 0
|
||||
? Math.round((freeBytes / totalBytes) * 1000) / 10
|
||||
: null;
|
||||
return {
|
||||
path: existingPath,
|
||||
totalBytes,
|
||||
freeBytes,
|
||||
freePercent
|
||||
};
|
||||
} catch {
|
||||
const windowsFallback = getWindowsDiskSpaceInfo(existingPath);
|
||||
if (windowsFallback) {
|
||||
return windowsFallback;
|
||||
}
|
||||
return {
|
||||
path: existingPath,
|
||||
totalBytes: null,
|
||||
freeBytes: null,
|
||||
freePercent: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSupportBundleEstimate(
|
||||
baseDir: string,
|
||||
logSummary: DebugSetupCheckResult["logSummary"]
|
||||
): SupportBundleEstimate {
|
||||
const storagePaths = createStoragePaths(baseDir);
|
||||
const staticFiles = [
|
||||
path.join(baseDir, AI_MANIFEST_FILE),
|
||||
path.join(baseDir, "debug_host.txt"),
|
||||
path.join(baseDir, "debug_port.txt"),
|
||||
storagePaths.configFile,
|
||||
storagePaths.sessionFile,
|
||||
storagePaths.historyFile,
|
||||
path.join(baseDir, "trace_config.json")
|
||||
].map((filePath) => getFileSizeInfo(filePath));
|
||||
|
||||
const staticBytes = staticFiles.reduce((sum, entry) => sum + entry.bytes, 0);
|
||||
const duplicatedLiveLogBytes = logSummary.session.bytes + logSummary.packageLogs.bytes + logSummary.itemLogs.bytes;
|
||||
const estimatedEntries = 10
|
||||
+ staticFiles.filter((entry) => entry.exists).length
|
||||
+ Number(logSummary.main.exists)
|
||||
+ Number(logSummary.mainBackup.exists)
|
||||
+ Number(logSummary.audit.exists)
|
||||
+ Number(logSummary.auditBackup.exists)
|
||||
+ Number(logSummary.session.exists)
|
||||
+ Number(logSummary.trace.exists)
|
||||
+ Number(logSummary.traceBackup.exists)
|
||||
+ logSummary.sessionLogs.fileCount
|
||||
+ logSummary.packageLogs.fileCount
|
||||
+ logSummary.itemLogs.fileCount
|
||||
+ logSummary.packageLogs.fileCount
|
||||
+ logSummary.itemLogs.fileCount;
|
||||
|
||||
return {
|
||||
estimatedBytes: staticBytes + logSummary.totalBytes + duplicatedLiveLogBytes + BUNDLE_OVERVIEW_SLACK_BYTES,
|
||||
estimatedEntries,
|
||||
duplicatedLiveLogBytes,
|
||||
note: "Schätzwert vor ZIP-Komprimierung; aktueller Session-Log sowie Live-Paket-/Item-Logs werden im Bundle zusätzlich gespiegelt."
|
||||
};
|
||||
}
|
||||
|
||||
export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
const host = readHost(baseDir);
|
||||
const port = readPort(baseDir);
|
||||
const token = readToken(baseDir);
|
||||
const storagePaths = createStoragePaths(baseDir);
|
||||
const settings = loadSettings(storagePaths);
|
||||
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 sessionLogPath = getSessionLogPath();
|
||||
const localOnly = /^(127\.0\.0\.1|localhost|::1)$/i.test(host);
|
||||
const warnings: string[] = [];
|
||||
const notes: string[] = [];
|
||||
|
||||
const logSummary: DebugSetupCheckResult["logSummary"] = {
|
||||
main: getFileSizeInfo(path.join(baseDir, "rd_downloader.log")),
|
||||
mainBackup: getFileSizeInfo(path.join(baseDir, "rd_downloader.log.old")),
|
||||
audit: getFileSizeInfo(path.join(baseDir, "audit.log")),
|
||||
auditBackup: getFileSizeInfo(path.join(baseDir, "audit.log.old")),
|
||||
session: getFileSizeInfo(sessionLogPath),
|
||||
trace: getFileSizeInfo(traceLogPath),
|
||||
traceBackup: getFileSizeInfo(path.join(baseDir, "trace.log.old")),
|
||||
sessionLogs: getDirectorySizeInfo(path.join(baseDir, "session-logs"), sessionLogPath),
|
||||
packageLogs: getDirectorySizeInfo(path.join(baseDir, "package-logs")),
|
||||
itemLogs: getDirectorySizeInfo(path.join(baseDir, "item-logs")),
|
||||
totalBytes: 0
|
||||
};
|
||||
logSummary.totalBytes = [
|
||||
logSummary.main.bytes,
|
||||
logSummary.mainBackup.bytes,
|
||||
logSummary.audit.bytes,
|
||||
logSummary.auditBackup.bytes,
|
||||
logSummary.session.bytes,
|
||||
logSummary.trace.bytes,
|
||||
logSummary.traceBackup.bytes,
|
||||
logSummary.sessionLogs.bytes,
|
||||
logSummary.packageLogs.bytes,
|
||||
logSummary.itemLogs.bytes
|
||||
].reduce((sum, value) => sum + value, 0);
|
||||
|
||||
const diskSpace: DebugSetupCheckResult["diskSpace"] = {
|
||||
runtime: getDiskSpaceInfo(baseDir),
|
||||
output: getDiskSpaceInfo(settings.outputDir),
|
||||
extract: getDiskSpaceInfo(settings.extractDir)
|
||||
};
|
||||
const supportBundle = getSupportBundleEstimate(baseDir, logSummary);
|
||||
|
||||
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.");
|
||||
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.");
|
||||
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.");
|
||||
@ -102,10 +365,43 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
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.");
|
||||
|
||||
for (const entry of [
|
||||
{ label: "Runtime", info: diskSpace.runtime },
|
||||
{ label: "Download-Ziel", info: diskSpace.output },
|
||||
{ label: "Entpack-Ziel", info: diskSpace.extract }
|
||||
]) {
|
||||
if (entry.info.freeBytes === null || entry.info.totalBytes === null) {
|
||||
warnings.push(`${entry.label}: Freier Speicherplatz konnte nicht gelesen werden (${entry.info.path}).`);
|
||||
continue;
|
||||
}
|
||||
const lowByAbsolute = entry.info.freeBytes < LOW_FREE_BYTES_THRESHOLD;
|
||||
const lowByPercent = entry.info.freePercent !== null
|
||||
&& entry.info.freePercent < LOW_FREE_PERCENT_THRESHOLD
|
||||
&& entry.info.freeBytes < LOW_FREE_PERCENT_BYTES_GUARD;
|
||||
if (lowByAbsolute || lowByPercent) {
|
||||
warnings.push(`${entry.label}: wenig freier Speicherplatz (${formatByteCount(entry.info.freeBytes)} frei auf ${entry.info.path}).`);
|
||||
}
|
||||
}
|
||||
|
||||
if (logSummary.totalBytes >= LARGE_LOG_BYTES_THRESHOLD) {
|
||||
warnings.push(`Support-Logs sind bereits recht groß (${formatByteCount(logSummary.totalBytes)}). Rotation greift, aber ein Bundle wird entsprechend umfangreicher.`);
|
||||
} else {
|
||||
notes.push(`Aktuelle Support-Logmenge: ${formatByteCount(logSummary.totalBytes)}.`);
|
||||
}
|
||||
|
||||
if (supportBundle.estimatedBytes >= LARGE_BUNDLE_BYTES_THRESHOLD) {
|
||||
warnings.push(`Support-Bundle wird voraussichtlich groß (${formatByteCount(supportBundle.estimatedBytes)} vor ZIP-Komprimierung).`);
|
||||
} else {
|
||||
notes.push(`Support-Bundle-Schätzung: etwa ${formatByteCount(supportBundle.estimatedBytes)}.`);
|
||||
}
|
||||
|
||||
notes.push("Die App kann Netzwerk-Firewalls oder Provider-Sicherheitsgruppen nicht direkt prüfen.");
|
||||
|
||||
return {
|
||||
status: warnings.length > 0 ? "warn" : "ok",
|
||||
enabled: Boolean(token),
|
||||
runtimeBaseDir: baseDir,
|
||||
host,
|
||||
port,
|
||||
localOnly,
|
||||
@ -117,6 +413,9 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||
traceLogPath: fs.existsSync(traceLogPath) ? traceLogPath : null,
|
||||
traceEnabled: traceConfig.enabled,
|
||||
traceAutoDisableAt: traceConfig.autoDisableAt,
|
||||
diskSpace,
|
||||
logSummary,
|
||||
supportBundle,
|
||||
warnings,
|
||||
notes,
|
||||
localUrls: {
|
||||
|
||||
@ -72,6 +72,7 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
||||
const snapshot = manager.getSnapshot();
|
||||
const packageIds = Object.keys(snapshot.session.packages);
|
||||
const itemIds = Object.keys(snapshot.session.items);
|
||||
const debugSetup = getDebugSetupCheck(baseDir);
|
||||
|
||||
addJson(zip, "overview/meta.json", {
|
||||
appVersion: APP_VERSION,
|
||||
@ -90,7 +91,8 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
||||
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime
|
||||
}
|
||||
});
|
||||
addJson(zip, "overview/debug-setup.json", getDebugSetupCheck(baseDir));
|
||||
addJson(zip, "overview/debug-setup.json", debugSetup);
|
||||
addJson(zip, "overview/self-check.json", debugSetup);
|
||||
addJson(zip, "overview/history.json", {
|
||||
total: history.length,
|
||||
entries: history.map((entry) => summarizeHistoryEntry(entry))
|
||||
|
||||
@ -141,8 +141,18 @@ interface ConfiguredAccountEntry {
|
||||
}
|
||||
|
||||
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||
const formatDiskLine = (label: string, value: DebugSetupCheckResult["diskSpace"]["runtime"]): string => {
|
||||
if (value.freeBytes === null || value.totalBytes === null) {
|
||||
return `${label}: unbekannt (${value.path})`;
|
||||
}
|
||||
return `${label}: ${humanSize(value.freeBytes)} frei von ${humanSize(value.totalBytes)} (${value.freePercent ?? "?"}% frei) | ${value.path}`;
|
||||
};
|
||||
|
||||
const formatFileLine = (label: string, bytes: number): string => `${label}: ${humanSize(bytes)}`;
|
||||
const lines: string[] = [
|
||||
`Status: ${setup.status === "ok" ? "OK" : "Warnung"}`,
|
||||
`Debug-Server aktiv: ${setup.enabled ? "ja" : "nein"}`,
|
||||
`Runtime-Ordner: ${setup.runtimeBaseDir}`,
|
||||
`Host: ${setup.host}`,
|
||||
`Port: ${setup.port}`,
|
||||
`Token-Datei: ${setup.tokenPath}`,
|
||||
@ -150,6 +160,25 @@ function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
||||
`Trace aktiv: ${setup.traceEnabled ? "ja" : "nein"}`,
|
||||
`Trace-Auto-Ende: ${setup.traceAutoDisableAt || "nicht gesetzt"}`,
|
||||
"",
|
||||
"Freier Speicherplatz:",
|
||||
formatDiskLine("Runtime", setup.diskSpace.runtime),
|
||||
formatDiskLine("Download-Ziel", setup.diskSpace.output),
|
||||
formatDiskLine("Entpack-Ziel", setup.diskSpace.extract),
|
||||
"",
|
||||
"Support-Logs:",
|
||||
formatFileLine("Gesamt", setup.logSummary.totalBytes),
|
||||
formatFileLine("Hauptlog", setup.logSummary.main.bytes + setup.logSummary.mainBackup.bytes),
|
||||
formatFileLine("Audit", setup.logSummary.audit.bytes + setup.logSummary.auditBackup.bytes),
|
||||
formatFileLine("Trace", setup.logSummary.trace.bytes + setup.logSummary.traceBackup.bytes),
|
||||
`${formatFileLine("Session-Logs", setup.logSummary.session.bytes + setup.logSummary.sessionLogs.bytes)} | Dateien: ${setup.logSummary.sessionLogs.fileCount}`,
|
||||
`${formatFileLine("Paket-Logs", setup.logSummary.packageLogs.bytes)} | Dateien: ${setup.logSummary.packageLogs.fileCount}`,
|
||||
`${formatFileLine("Item-Logs", setup.logSummary.itemLogs.bytes)} | Dateien: ${setup.logSummary.itemLogs.fileCount}`,
|
||||
"",
|
||||
"Support-Bundle:",
|
||||
`${formatFileLine("Schätzwert", setup.supportBundle.estimatedBytes)} | Einträge: ${setup.supportBundle.estimatedEntries}`,
|
||||
formatFileLine("Doppelte Live-Log-Spiegelung", setup.supportBundle.duplicatedLiveLogBytes),
|
||||
setup.supportBundle.note,
|
||||
"",
|
||||
"Lokale URLs:",
|
||||
setup.localUrls.health,
|
||||
setup.localUrls.meta,
|
||||
|
||||
@ -346,8 +346,37 @@ export interface SupportTraceConfig {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface SupportFileSizeInfo {
|
||||
path: string | null;
|
||||
exists: boolean;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
export interface SupportDirectorySizeInfo {
|
||||
path: string;
|
||||
exists: boolean;
|
||||
fileCount: number;
|
||||
bytes: number;
|
||||
}
|
||||
|
||||
export interface SupportDiskSpaceInfo {
|
||||
path: string;
|
||||
totalBytes: number | null;
|
||||
freeBytes: number | null;
|
||||
freePercent: number | null;
|
||||
}
|
||||
|
||||
export interface SupportBundleEstimate {
|
||||
estimatedBytes: number;
|
||||
estimatedEntries: number;
|
||||
duplicatedLiveLogBytes: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface DebugSetupCheckResult {
|
||||
status: "ok" | "warn";
|
||||
enabled: boolean;
|
||||
runtimeBaseDir: string;
|
||||
host: string;
|
||||
port: number;
|
||||
localOnly: boolean;
|
||||
@ -359,6 +388,25 @@ export interface DebugSetupCheckResult {
|
||||
traceLogPath: string | null;
|
||||
traceEnabled: boolean;
|
||||
traceAutoDisableAt: string | null;
|
||||
diskSpace: {
|
||||
runtime: SupportDiskSpaceInfo;
|
||||
output: SupportDiskSpaceInfo;
|
||||
extract: SupportDiskSpaceInfo;
|
||||
};
|
||||
logSummary: {
|
||||
totalBytes: number;
|
||||
main: SupportFileSizeInfo;
|
||||
mainBackup: SupportFileSizeInfo;
|
||||
audit: SupportFileSizeInfo;
|
||||
auditBackup: SupportFileSizeInfo;
|
||||
session: SupportFileSizeInfo;
|
||||
trace: SupportFileSizeInfo;
|
||||
traceBackup: SupportFileSizeInfo;
|
||||
sessionLogs: SupportDirectorySizeInfo;
|
||||
packageLogs: SupportDirectorySizeInfo;
|
||||
itemLogs: SupportDirectorySizeInfo;
|
||||
};
|
||||
supportBundle: SupportBundleEstimate;
|
||||
warnings: string[];
|
||||
notes: string[];
|
||||
localUrls: {
|
||||
|
||||
@ -342,6 +342,7 @@ describe("debug-server", () => {
|
||||
expect(manifest.debugServer?.remoteBaseUrlTemplate).toContain("<SERVER_IP_OR_DNS>");
|
||||
expect(manifest.quickstart?.[1]).toContain("server IP");
|
||||
expect(manifest.setupCheckEndpoint).toBe("/debug/setup");
|
||||
expect(manifest.selfCheckEndpoint).toBe("/self-check");
|
||||
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);
|
||||
@ -353,6 +354,7 @@ describe("debug-server", () => {
|
||||
expect(metaPayload.supportFiles?.traceConfig).toBe(getTraceConfigPath());
|
||||
expect(metaPayload.supportFiles?.traceLog).toBe(getTraceLogPath());
|
||||
expect(metaPayload.supportChecks?.setup).toBe("/debug/setup");
|
||||
expect(metaPayload.supportChecks?.selfCheck).toBe("/self-check");
|
||||
});
|
||||
|
||||
it("serves a debug setup check with trace expiry details", async () => {
|
||||
@ -362,16 +364,34 @@ describe("debug-server", () => {
|
||||
const payload = await response.json() as Record<string, any>;
|
||||
|
||||
expect(payload.enabled).toBe(true);
|
||||
expect(payload.status).toBe("ok");
|
||||
expect(payload.runtimeBaseDir).toBe(fixture.baseDir);
|
||||
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.diskSpace?.runtime?.freeBytes).toBeGreaterThan(0);
|
||||
expect(payload.diskSpace?.output?.freeBytes).toBeGreaterThan(0);
|
||||
expect(payload.diskSpace?.extract?.freeBytes).toBeGreaterThan(0);
|
||||
expect(payload.logSummary?.totalBytes).toBeGreaterThan(0);
|
||||
expect(payload.logSummary?.packageLogs?.fileCount).toBe(1);
|
||||
expect(payload.logSummary?.itemLogs?.fileCount).toBe(1);
|
||||
expect(payload.supportBundle?.estimatedBytes).toBeGreaterThan(0);
|
||||
expect(payload.remoteUrlTemplates?.health).toContain("<SERVER_IP_OR_DNS>");
|
||||
expect(Array.isArray(payload.notes)).toBe(true);
|
||||
});
|
||||
|
||||
it("serves the self-check alias", async () => {
|
||||
const fixture = await createFixture();
|
||||
const response = await fetch(`${fixture.baseUrl}/self-check?token=${fixture.token}`);
|
||||
expect(response.ok).toBe(true);
|
||||
const payload = await response.json() as Record<string, any>;
|
||||
expect(payload.status).toBe("ok");
|
||||
expect(payload.supportBundle?.estimatedEntries).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("serves package details and package log by package query", async () => {
|
||||
const fixture = await createFixture();
|
||||
|
||||
@ -469,6 +489,7 @@ describe("debug-server", () => {
|
||||
expect(entries).toContain("overview/settings.json");
|
||||
expect(entries).toContain("overview/accounts.json");
|
||||
expect(entries).toContain("overview/debug-setup.json");
|
||||
expect(entries).toContain("overview/self-check.json");
|
||||
expect(entries).toContain("overview/trace-config.json");
|
||||
expect(entries).toContain("logs/audit.log");
|
||||
expect(entries).toContain("logs/trace.log");
|
||||
|
||||
Loading…
Reference in New Issue
Block a user