fix(support): expose notification health aggregates
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user