Files
geo/apps/desktop-client/src/main/runtime-snapshot.ts
T

158 lines
5.8 KiB
TypeScript
Raw Normal View History

import { app } from "electron/main";
import { getCircuitBreakerState } from "./circuit-breaker";
import { collectRecoverySnapshot } from "./crash-recovery";
import { getKeepAlivePlan } from "./keep-alive";
import { getLeaseManagerSnapshot } from "./lease-manager";
import { getRateLimiterSnapshot } from "./rate-limiter";
import { getRuntimeControllerSnapshot } from "./runtime-controller";
import { getSchedulerState } from "./scheduler";
import { getSessionHandle, listSessionHandleSnapshots } from "./session-registry";
import { getTransportSnapshot } from "./transport/api-client";
import { describeVaultBackend } from "./vault";
import { listHotViews } from "./view-pool";
import { normalizeAccountPlatformUid } from "../shared/account-identity";
function minutesAhead(now: number, minutes: number): number {
return now + minutes * 60_000;
}
function parseTimestamp(value: string | null | undefined): number | null {
if (!value) {
return null;
}
const timestamp = Date.parse(value);
return Number.isNaN(timestamp) ? null : timestamp;
}
export function createRuntimeSnapshot() {
return createLiveRuntimeSnapshot(getRuntimeControllerSnapshot());
}
function createLiveRuntimeSnapshot(controller: ReturnType<typeof getRuntimeControllerSnapshot>) {
const now = Date.now();
// Electron Session instances are not safe to ship across IPC to the renderer.
const sessions = listSessionHandleSnapshots();
const hotViews = listHotViews();
const leaseManager = getLeaseManagerSnapshot();
const rateLimiter = getRateLimiterSnapshot();
const scheduler = getSchedulerState();
const transport = getTransportSnapshot();
const primaryClientId = controller.client?.id ?? controller.session?.desktop_client?.id ?? "desktop-self";
const lastSeenAt =
controller.lastHeartbeatAt
|| parseTimestamp(controller.client?.last_seen_at)
|| now;
const clientOnline = controller.running && controller.lastHeartbeatStatus !== "failed";
const queueDepth = controller.tasks.filter((task) =>
["queued", "in_progress"].includes(task.status),
).length;
const clients = [
{
id: primaryClientId,
label: "This device",
deviceName: controller.client?.device_name ?? controller.session?.desktop_client?.device_name ?? "Current Desktop",
os: controller.client?.os ?? process.platform,
status: clientOnline ? "online" : "offline",
lastSeenAt,
queueDepth,
heartbeat: clientOnline ? "healthy" : "stale",
channel: controller.client?.channel ?? "dev",
},
] as const;
const accounts = controller.accounts.map((account) => {
const sessionHandle = getSessionHandle(account.id);
const isHot = hotViews.some((item) => item.accountId === account.id);
const accountQueueDepth = controller.tasks.filter((task) =>
task.accountId === account.id && ["queued", "in_progress"].includes(task.status),
).length;
return {
id: account.id,
platform: account.platform,
displayName: account.display_name,
platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid,
avatarUrl: account.avatar_url ?? null,
health: account.health,
tags: account.tags,
syncVersion: account.sync_version,
clientId: account.client_id ?? primaryClientId,
partition: sessionHandle?.partition ?? `persist:acc-${account.id}`,
sessionState: isHot ? "hot" : sessionHandle ? "warm" : "cold",
lastSyncAt:
controller.lastAccountsSyncAt
|| parseTimestamp(account.verified_at)
|| now,
queueDepth: accountQueueDepth,
online: clientOnline,
};
});
const tasks = controller.tasks.map((task) => ({ ...task }));
const healthCounts = accounts.reduce<Record<string, number>>((acc, item) => {
acc[item.health] = (acc[item.health] ?? 0) + 1;
return acc;
}, {});
return {
generatedAt: now,
app: {
name: "GEO Rankly Desktop",
version: app.getVersion(),
channel: process.env.NODE_ENV === "development" ? "dev" : "release",
platform: process.platform,
sessionCount: sessions.length,
hotViewCount: hotViews.length,
},
workspace: {
id: `ws-${controller.client?.workspace_id ?? controller.session?.desktop_client?.workspace_id ?? "current"}`,
name: `Workspace #${controller.client?.workspace_id ?? controller.session?.desktop_client?.workspace_id ?? "current"}`,
strategy: controller.sseConnected ? "rabbitmq-first / db-fallback" : "db-fallback / heartbeat recovery",
nextSweepAt: scheduler.nextPullAt ?? minutesAhead(now, 1),
lastFullSyncAt: controller.lastAccountsSyncAt || now,
},
summary: {
onlineClients: clients.filter((item) => item.status === "online").length,
accountsBound: accounts.length,
queuedTasks: tasks.filter((item) => item.status === "queued").length,
issuesOpen:
tasks.filter((item) => ["unknown", "failed"].includes(item.status)).length
+ accounts.filter((item) => item.health !== "live").length,
healthCounts,
},
clients,
accounts,
tasks,
activity: controller.activity,
subsystems: {
transport,
circuitBreaker: getCircuitBreakerState(),
keepAlive: getKeepAlivePlan(),
leaseManager,
rateLimiter,
recovery: collectRecoverySnapshot(),
runtimeController: {
running: controller.running,
sseConnected: controller.sseConnected,
queueDepth: controller.queueDepth,
lastHeartbeatAt: controller.lastHeartbeatAt,
lastHeartbeatStatus: controller.lastHeartbeatStatus,
lastPullAt: controller.lastPullAt,
lastPullStatus: controller.lastPullStatus,
lastAccountsSyncAt: controller.lastAccountsSyncAt,
lastServerTime: controller.lastServerTime,
lastError: controller.lastError,
},
scheduler,
sessions,
hotViews,
vault: describeVaultBackend(),
},
};
}