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.
|
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:
|
Available endpoints after restart:
|
||||||
|
|
||||||
- `GET /health`
|
- `GET /health`
|
||||||
- `GET /meta`
|
- `GET /meta`
|
||||||
- `GET /debug/setup`
|
- `GET /debug/setup`
|
||||||
|
- `GET /self-check`
|
||||||
- `GET /host/diagnostics`
|
- `GET /host/diagnostics`
|
||||||
- `GET /status`
|
- `GET /status`
|
||||||
- `GET /settings`
|
- `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/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/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/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&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"
|
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
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,7 @@ 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: "/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: "/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." },
|
||||||
@ -238,7 +239,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
"Read debug_token.txt and debug_port.txt from this runtime folder.",
|
||||||
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
"If remote access is needed, ask the user only for the server IP or DNS name.",
|
||||||
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
"Call /meta first to confirm the server is reachable and to re-read the endpoint list.",
|
||||||
"Use /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.",
|
"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."
|
||||||
],
|
],
|
||||||
@ -274,6 +275,7 @@ function buildAiManifest(baseDir: string): Record<string, unknown> {
|
|||||||
remoteHostHint
|
remoteHostHint
|
||||||
},
|
},
|
||||||
setupCheckEndpoint: "/debug/setup",
|
setupCheckEndpoint: "/debug/setup",
|
||||||
|
selfCheckEndpoint: "/self-check",
|
||||||
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."
|
||||||
],
|
],
|
||||||
@ -475,7 +477,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
traceLog: getTraceLogPath()
|
traceLog: getTraceLogPath()
|
||||||
},
|
},
|
||||||
supportChecks: {
|
supportChecks: {
|
||||||
setup: "/debug/setup"
|
setup: "/debug/setup",
|
||||||
|
selfCheck: "/self-check"
|
||||||
},
|
},
|
||||||
logPaths: {
|
logPaths: {
|
||||||
main: getLogFilePath(),
|
main: getLogFilePath(),
|
||||||
@ -488,7 +491,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pathname === "/debug/setup") {
|
if (pathname === "/debug/setup" || pathname === "/self-check") {
|
||||||
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
jsonResponse(res, 200, getDebugSetupCheck(runtimeBaseDir));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,42 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
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_PORT = 9868;
|
||||||
const DEFAULT_HOST = "127.0.0.1";
|
const DEFAULT_HOST = "127.0.0.1";
|
||||||
const AI_MANIFEST_FILE = "debug_ai_manifest.json";
|
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 {
|
function readToken(baseDir: string): string {
|
||||||
try {
|
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 {
|
export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
||||||
const host = readHost(baseDir);
|
const host = readHost(baseDir);
|
||||||
const port = readPort(baseDir);
|
const port = readPort(baseDir);
|
||||||
const token = readToken(baseDir);
|
const token = readToken(baseDir);
|
||||||
|
const storagePaths = createStoragePaths(baseDir);
|
||||||
|
const settings = loadSettings(storagePaths);
|
||||||
const tokenPath = path.join(baseDir, "debug_token.txt");
|
const tokenPath = path.join(baseDir, "debug_token.txt");
|
||||||
const aiManifestPath = path.join(baseDir, AI_MANIFEST_FILE);
|
const aiManifestPath = path.join(baseDir, AI_MANIFEST_FILE);
|
||||||
const traceConfigPath = path.join(baseDir, "trace_config.json");
|
const traceConfigPath = path.join(baseDir, "trace_config.json");
|
||||||
const traceLogPath = path.join(baseDir, "trace.log");
|
const traceLogPath = path.join(baseDir, "trace.log");
|
||||||
const traceConfig = readTraceConfig(baseDir);
|
const traceConfig = readTraceConfig(baseDir);
|
||||||
|
const sessionLogPath = getSessionLogPath();
|
||||||
const localOnly = /^(127\.0\.0\.1|localhost|::1)$/i.test(host);
|
const localOnly = /^(127\.0\.0\.1|localhost|::1)$/i.test(host);
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
const notes: 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) {
|
if (!token) {
|
||||||
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
warnings.push("debug_token.txt fehlt oder ist leer. Der Debug-Server startet dann nicht.");
|
||||||
}
|
}
|
||||||
if (localOnly) {
|
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 {
|
} 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)) {
|
if (!fs.existsSync(aiManifestPath)) {
|
||||||
warnings.push("debug_ai_manifest.json fehlt. App einmal neu starten, damit die KI-Support-Datei neu geschrieben wird.");
|
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) {
|
if (traceConfig.enabled && traceConfig.autoDisableAt) {
|
||||||
notes.push(`Support-Trace aktiv bis ${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 {
|
return {
|
||||||
|
status: warnings.length > 0 ? "warn" : "ok",
|
||||||
enabled: Boolean(token),
|
enabled: Boolean(token),
|
||||||
|
runtimeBaseDir: baseDir,
|
||||||
host,
|
host,
|
||||||
port,
|
port,
|
||||||
localOnly,
|
localOnly,
|
||||||
@ -117,6 +413,9 @@ export function getDebugSetupCheck(baseDir: string): DebugSetupCheckResult {
|
|||||||
traceLogPath: fs.existsSync(traceLogPath) ? traceLogPath : null,
|
traceLogPath: fs.existsSync(traceLogPath) ? traceLogPath : null,
|
||||||
traceEnabled: traceConfig.enabled,
|
traceEnabled: traceConfig.enabled,
|
||||||
traceAutoDisableAt: traceConfig.autoDisableAt,
|
traceAutoDisableAt: traceConfig.autoDisableAt,
|
||||||
|
diskSpace,
|
||||||
|
logSummary,
|
||||||
|
supportBundle,
|
||||||
warnings,
|
warnings,
|
||||||
notes,
|
notes,
|
||||||
localUrls: {
|
localUrls: {
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export function buildSupportBundle(manager: DownloadManager, baseDir: string): B
|
|||||||
const snapshot = manager.getSnapshot();
|
const snapshot = manager.getSnapshot();
|
||||||
const packageIds = Object.keys(snapshot.session.packages);
|
const packageIds = Object.keys(snapshot.session.packages);
|
||||||
const itemIds = Object.keys(snapshot.session.items);
|
const itemIds = Object.keys(snapshot.session.items);
|
||||||
|
const debugSetup = getDebugSetupCheck(baseDir);
|
||||||
|
|
||||||
addJson(zip, "overview/meta.json", {
|
addJson(zip, "overview/meta.json", {
|
||||||
appVersion: APP_VERSION,
|
appVersion: APP_VERSION,
|
||||||
@ -90,7 +91,8 @@ 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/debug-setup.json", debugSetup);
|
||||||
|
addJson(zip, "overview/self-check.json", debugSetup);
|
||||||
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))
|
||||||
|
|||||||
@ -141,8 +141,18 @@ interface ConfiguredAccountEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
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[] = [
|
const lines: string[] = [
|
||||||
|
`Status: ${setup.status === "ok" ? "OK" : "Warnung"}`,
|
||||||
`Debug-Server aktiv: ${setup.enabled ? "ja" : "nein"}`,
|
`Debug-Server aktiv: ${setup.enabled ? "ja" : "nein"}`,
|
||||||
|
`Runtime-Ordner: ${setup.runtimeBaseDir}`,
|
||||||
`Host: ${setup.host}`,
|
`Host: ${setup.host}`,
|
||||||
`Port: ${setup.port}`,
|
`Port: ${setup.port}`,
|
||||||
`Token-Datei: ${setup.tokenPath}`,
|
`Token-Datei: ${setup.tokenPath}`,
|
||||||
@ -150,6 +160,25 @@ function buildDebugSetupDetails(setup: DebugSetupCheckResult): string {
|
|||||||
`Trace aktiv: ${setup.traceEnabled ? "ja" : "nein"}`,
|
`Trace aktiv: ${setup.traceEnabled ? "ja" : "nein"}`,
|
||||||
`Trace-Auto-Ende: ${setup.traceAutoDisableAt || "nicht gesetzt"}`,
|
`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:",
|
"Lokale URLs:",
|
||||||
setup.localUrls.health,
|
setup.localUrls.health,
|
||||||
setup.localUrls.meta,
|
setup.localUrls.meta,
|
||||||
|
|||||||
@ -346,8 +346,37 @@ export interface SupportTraceConfig {
|
|||||||
updatedAt: string;
|
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 {
|
export interface DebugSetupCheckResult {
|
||||||
|
status: "ok" | "warn";
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
runtimeBaseDir: string;
|
||||||
host: string;
|
host: string;
|
||||||
port: number;
|
port: number;
|
||||||
localOnly: boolean;
|
localOnly: boolean;
|
||||||
@ -359,6 +388,25 @@ export interface DebugSetupCheckResult {
|
|||||||
traceLogPath: string | null;
|
traceLogPath: string | null;
|
||||||
traceEnabled: boolean;
|
traceEnabled: boolean;
|
||||||
traceAutoDisableAt: string | null;
|
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[];
|
warnings: string[];
|
||||||
notes: string[];
|
notes: string[];
|
||||||
localUrls: {
|
localUrls: {
|
||||||
|
|||||||
@ -342,6 +342,7 @@ describe("debug-server", () => {
|
|||||||
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.setupCheckEndpoint).toBe("/debug/setup");
|
||||||
|
expect(manifest.selfCheckEndpoint).toBe("/self-check");
|
||||||
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);
|
||||||
@ -353,6 +354,7 @@ describe("debug-server", () => {
|
|||||||
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");
|
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 () => {
|
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>;
|
const payload = await response.json() as Record<string, any>;
|
||||||
|
|
||||||
expect(payload.enabled).toBe(true);
|
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.host).toBe("0.0.0.0");
|
||||||
expect(payload.localOnly).toBe(false);
|
expect(payload.localOnly).toBe(false);
|
||||||
expect(payload.tokenConfigured).toBe(true);
|
expect(payload.tokenConfigured).toBe(true);
|
||||||
expect(payload.aiManifestPresent).toBe(true);
|
expect(payload.aiManifestPresent).toBe(true);
|
||||||
expect(payload.traceEnabled).toBe(true);
|
expect(payload.traceEnabled).toBe(true);
|
||||||
expect(payload.traceAutoDisableAt).toBeTruthy();
|
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(payload.remoteUrlTemplates?.health).toContain("<SERVER_IP_OR_DNS>");
|
||||||
expect(Array.isArray(payload.notes)).toBe(true);
|
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 () => {
|
it("serves package details and package log by package query", async () => {
|
||||||
const fixture = await createFixture();
|
const fixture = await createFixture();
|
||||||
|
|
||||||
@ -469,6 +489,7 @@ describe("debug-server", () => {
|
|||||||
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/debug-setup.json");
|
||||||
|
expect(entries).toContain("overview/self-check.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");
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user