fix(support): expose notification health aggregates

This commit is contained in:
Sucukdeluxe
2026-08-22 08:00:25 +02:00
parent 16679e73b5
commit 1b8f6fe4b6
7 changed files with 219 additions and 47 deletions
+17 -7
View File
@@ -70,7 +70,7 @@ import { getDebugSetupCheck } from "./debug-setup";
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
import { getDesktopRenameLogPath, initDesktopRenameLogAt, shutdownDesktopRenameLog } from "./desktop-rename-log";
import { buildAccountSummary, diffAccountSummary } from "./support-data";
import { buildAccountSummary, buildNotificationSupportPayload, diffAccountSummary, type NotificationSupportPayload } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
@@ -239,7 +239,7 @@ export class AppController {
} catch (err) {
logger.warn(`Health-Check uebersprungen (Fehler): ${String((err as Error).message || err)}`);
}
startDebugServer(this.manager, this.storagePaths.baseDir);
startDebugServer(this.manager, this.storagePaths.baseDir, () => this.getNotificationSupportPayload());
this.runtimeStatsTimer = setInterval(() => {
this.manager.persistRuntimeStats();
this.settings = this.manager.getSettings();
@@ -1166,21 +1166,31 @@ export class AppController {
return { restored: true, relaunch: false, message: "Einstellungen aus Online-Sicherung wiederhergestellt" };
}
public async exportSupportBundle(): Promise<{ buffer: Buffer; defaultFileName: string }> {
public async exportSupportBundle(): Promise<{ buffer: Buffer; defaultFileName: string }> {
this.audit("INFO", "Support-Bundle exportiert");
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
});
return {
buffer: await buildSupportBundle(this.manager, this.storagePaths.baseDir, { hostDiagnosticsMode: "cached" }),
buffer: await buildSupportBundle(this.manager, this.storagePaths.baseDir, {
hostDiagnosticsMode: "cached",
notificationStatus: this.getNotificationSupportPayload()
}),
defaultFileName: getSupportBundleDefaultFileName()
};
}
public getSupportBundleDefaultFileName(): string {
return getSupportBundleDefaultFileName();
}
public getSupportBundleDefaultFileName(): string {
return getSupportBundleDefaultFileName();
}
public getNotificationSupportPayload(): NotificationSupportPayload {
return buildNotificationSupportPayload(
this.notificationOutbox.getStatus(),
this.downloadHealthMonitor.getState()
);
}
public importBackup(data: Buffer, passphrase?: string): { restored: boolean; relaunch: boolean; message: string } {
let parsed: Record<string, unknown>;
+33 -15
View File
@@ -12,7 +12,7 @@ import { getSessionLogPath } from "./session-log";
import { getPackageLogPath as getPersistedPackageLogPath } from "./package-log";
import { getRenameLogPath } from "./rename-log";
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, normalizeNotificationSupportPayload, summarizeHistoryEntry, type NotificationSupportPayload } from "./support-data";
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
import { getTraceConfig, getTraceConfigPath, getTraceLogPath, logTraceEvent, setTraceEnabled, updateTraceConfig } from "./trace-log";
import { getConversionLogPath } from "./conversion-trace";
@@ -56,7 +56,8 @@ const DEBUG_ENDPOINTS: DebugEndpointDescriptor[] = [
{ 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: "/providers", description: "Live provider runtime state: per-account/key cooldowns (until/remaining/reason/category), in-flight depth, Mega rotation cursor, empty-response streaks. The 'why is it cooling down right now' view." },
{ 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." },
{ method: "GET", path: "/notifications", description: "Returns safe notification delivery and incident aggregates." },
{ method: "GET", path: "/history", queryExample: "limit=50&status=completed", description: "Returns history entries with optional filters." },
{ method: "GET", path: "/status", description: "Returns a live high-level status overview." },
{ method: "GET", path: "/packages", queryExample: "package=Release&includeItems=1", description: "Lists packages and optional per-item detail." },
@@ -74,6 +75,7 @@ let bindPort = DEFAULT_PORT;
let runtimeBaseDir = "";
let allowlist: string[] = [];
let requestLimits = new Map<string, { startedAt: number; count: number }>();
let notificationStatusProvider: (() => NotificationSupportPayload) | null = null;
export interface DebugServerRuntimeStatus {
running: boolean;
@@ -92,9 +94,13 @@ function readSupportSettings() {
return loadSettings(getStoragePaths());
}
function readSupportHistory() {
return loadHistory(getStoragePaths());
}
function readSupportHistory() {
return loadHistory(getStoragePaths());
}
function readNotificationStatus(): NotificationSupportPayload {
return normalizeNotificationSupportPayload(notificationStatusProvider?.());
}
function extractDebugClientIp(req: http.IncomingMessage): string {
const forwarded = req.headers["x-forwarded-for"];
@@ -908,7 +914,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
return;
}
if (pathname === "/stats") {
if (pathname === "/stats") {
if (!manager) {
jsonResponse(res, 503, { error: "Manager not initialized" });
return;
@@ -923,8 +929,13 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
}
});
return;
}
return;
}
if (pathname === "/notifications") {
jsonResponse(res, 200, readNotificationStatus());
return;
}
if (pathname === "/history") {
const entries = readSupportHistory();
@@ -1046,7 +1057,7 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
return;
}
const fileName = getSupportBundleDefaultFileName();
buildSupportBundle(manager, runtimeBaseDir)
buildSupportBundle(manager, runtimeBaseDir, { notificationStatus: readNotificationStatus() })
.then((body) => {
logTraceEvent("INFO", "support", "Support-Bundle über Debug-Server heruntergeladen", {
fileName,
@@ -1088,7 +1099,8 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
},
status: buildStatusPayload(snapshot),
settings: buildRedactedSettingsPayload(readSupportSettings()),
stats: buildStatsPayload(snapshot),
stats: buildStatsPayload(snapshot),
notifications: readNotificationStatus(),
accounts: buildAccountSummary(readSupportSettings()),
providers: getProviderRuntimeSnapshot(),
history: {
@@ -1184,11 +1196,16 @@ function openServerSocket(): Promise<void> {
});
}
export function startDebugServer(mgr: DownloadManager, baseDir: string): void {
runtimeBaseDir = baseDir;
manager = mgr;
void openServerSocket();
}
export function startDebugServer(
mgr: DownloadManager,
baseDir: string,
readCurrentNotificationStatus?: () => NotificationSupportPayload
): void {
runtimeBaseDir = baseDir;
manager = mgr;
notificationStatusProvider = readCurrentNotificationStatus || null;
void openServerSocket();
}
export async function restartDebugServer(): Promise<DebugServerRuntimeStatus> {
const old = server;
@@ -1256,6 +1273,7 @@ export function clearDebugToken(): void {
}
export function stopDebugServer(): void {
notificationStatusProvider = null;
if (server) {
server.close();
try {
+10 -7
View File
@@ -13,7 +13,7 @@ import { getRenameLogPath } from "./rename-log";
import { getDesktopRenameLogPath } from "./desktop-rename-log";
import { getSessionLogPath } from "./session-log";
import { createStoragePaths, loadHistory, loadSettings } from "./storage";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, summarizeHistoryEntry } from "./support-data";
import { buildAccountSummary, buildRedactedSettingsPayload, buildStatsPayload, normalizeNotificationSupportPayload, summarizeHistoryEntry, type NotificationSupportPayload } from "./support-data";
import { getTraceConfig, getTraceConfigPath, getTraceLogPath } from "./trace-log";
import { getCachedWindowsHostDiagnostics, getWindowsHostDiagnostics } from "./windows-host-diagnostics";
import type { DownloadManager } from "./download-manager";
@@ -99,9 +99,10 @@ export function getSupportBundleDefaultFileName(): string {
type HostDiagnosticsMode = "full" | "cached" | "none";
interface BuildSupportBundleOptions {
hostDiagnosticsMode?: HostDiagnosticsMode;
}
interface BuildSupportBundleOptions {
hostDiagnosticsMode?: HostDiagnosticsMode;
notificationStatus?: NotificationSupportPayload;
}
function createDeferredHostDiagnostics(reason: string): unknown {
return {
@@ -144,7 +145,8 @@ export async function buildSupportBundle(manager: DownloadManager, baseDir: stri
const snapshot = manager.getSnapshot();
const packageIds = Object.keys(snapshot.session.packages);
const itemIds = Object.keys(snapshot.session.items);
const debugSetup = getDebugSetupCheck(baseDir);
const debugSetup = getDebugSetupCheck(baseDir);
const notificationStatus = normalizeNotificationSupportPayload(options.notificationStatus);
addJson(zip, "overview/meta.json", {
appVersion: APP_VERSION,
@@ -156,14 +158,15 @@ export async function buildSupportBundle(manager: DownloadManager, baseDir: stri
addJson(zip, "overview/status.json", snapshot.session);
addJson(zip, "overview/settings.json", buildRedactedSettingsPayload(settings));
addJson(zip, "overview/accounts.json", buildAccountSummary(settings));
addJson(zip, "overview/stats.json", {
addJson(zip, "overview/stats.json", {
...buildStatsPayload(snapshot),
allTime: {
totalDownloadedAllTime: settings.totalDownloadedAllTime,
totalCompletedFilesAllTime: settings.totalCompletedFilesAllTime,
totalRuntimeAllTimeMs: settings.totalRuntimeAllTimeMs
}
});
});
addJson(zip, "overview/notifications.json", notificationStatus);
addJson(zip, "overview/debug-setup.json", debugSetup);
addJson(zip, "overview/self-check.json", debugSetup);
addJson(zip, "overview/history.json", {
+22 -1
View File
@@ -16,6 +16,27 @@ export interface NotificationSupportPayload {
incidentAgeMs: number | null;
}
export function normalizeNotificationSupportPayload(
value?: Partial<NotificationSupportPayload> | null
): NotificationSupportPayload {
const rawQueued = value?.queued;
const rawLastSuccessAt = value?.lastSuccessAt;
const rawIncidentAgeMs = value?.incidentAgeMs;
const queued = typeof rawQueued === "number" && Number.isFinite(rawQueued)
? Math.max(0, Math.floor(rawQueued))
: 0;
const lastSuccessAt = typeof rawLastSuccessAt === "number" && Number.isFinite(rawLastSuccessAt) && rawLastSuccessAt > 0
? Math.floor(rawLastSuccessAt)
: null;
const incidentType = value?.incidentType === "scheduler" || value?.incidentType === "no_data"
? value.incidentType
: null;
const incidentAgeMs = incidentType && typeof rawIncidentAgeMs === "number" && Number.isFinite(rawIncidentAgeMs) && rawIncidentAgeMs >= 0
? Math.floor(rawIncidentAgeMs)
: null;
return { queued, lastSuccessAt, incidentType, incidentAgeMs };
}
export function buildNotificationSupportPayload(
outbox: Pick<NotificationOutboxStatus, "queued" | "lastSuccessAt">,
health: Pick<DownloadHealthState, "incidentType" | "incidentStartedAt">,
@@ -34,7 +55,7 @@ export function buildNotificationSupportPayload(
const incidentAgeMs = incidentType && incidentStartedAt > 0
? Math.max(0, Math.floor(Number.isFinite(now) ? now : Date.now()) - incidentStartedAt)
: null;
return { queued, lastSuccessAt, incidentType, incidentAgeMs };
return normalizeNotificationSupportPayload({ queued, lastSuccessAt, incidentType, incidentAgeMs });
}
export function buildAccountSummary(settings: AppSettings): Record<string, unknown> {