import { app } from "electron/main"; import { getProjectedAccountHealth } from "./account-health"; import { getProcessMetricsSnapshot } from "./process-metrics"; import { getCircuitBreakerState } from "./circuit-breaker"; import { collectRecoverySnapshot } from "./crash-recovery"; import { getKeepAlivePlan } from "./keep-alive"; import { getLeaseManagerSnapshot } from "./lease-manager"; import { getMonitorSchedulerSnapshot } from "./monitor-scheduler"; import { getPublishSchedulerSnapshot } from "./publish-scheduler"; import { getObservedRequestSnapshot } from "./network-observer"; import { getHiddenPlaywrightSnapshot } from "./playwright-cdp"; 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 { getHotViewPoolPolicy, 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; } type RuntimeControllerSnapshot = ReturnType; type RuntimeControllerAccount = RuntimeControllerSnapshot["accounts"][number]; function createRuntimeAccountView( controller: RuntimeControllerSnapshot, account: RuntimeControllerAccount, context: { hotViews: ReturnType; primaryClientId: string; clientOnline: boolean; lastAccountsSyncAt: number; now: number; }, ) { const sessionHandle = getSessionHandle(account.id); const isHot = context.hotViews.some((item) => item.accountId === account.id); const accountQueueDepth = controller.tasks.filter((task) => task.accountId === account.id && ["queued", "in_progress"].includes(task.status), ).length; const projected = getProjectedAccountHealth({ accountId: account.id, platform: account.platform, health: account.health, verifiedAt: account.verified_at, }); return { id: account.id, platform: account.platform, displayName: projected.profile?.displayName ?? account.display_name, platformUid: normalizeAccountPlatformUid(account.platform_uid) || account.platform_uid, avatarUrl: projected.profile?.avatarUrl ?? account.avatar_url ?? null, health: projected.health, authState: projected.authState, probeState: projected.probeState, authReason: projected.authReason, tags: account.tags, syncVersion: account.sync_version, clientId: account.client_id ?? context.primaryClientId, partition: projected.partition || sessionHandle?.partition || `persist:acc-${account.id}`, sessionState: isHot ? "hot" : sessionHandle ? "warm" : "cold", lastSyncAt: context.lastAccountsSyncAt || parseTimestamp(account.verified_at) || context.now, lastVerifiedAt: projected.lastVerifiedAt, lastProbeAt: projected.lastProbeAt, nextProbeAt: projected.nextProbeAt, queueDepth: accountQueueDepth, online: context.clientOnline, }; } export function createRuntimeSnapshot() { return createLiveRuntimeSnapshot(getRuntimeControllerSnapshot()); } export function createRuntimeAccountSnapshot(accountId: string) { const controller = getRuntimeControllerSnapshot(); const now = Date.now(); const primaryClientId = controller.client?.id ?? controller.session?.desktop_client?.id ?? "desktop-self"; const clientOnline = controller.running && controller.lastHeartbeatStatus !== "failed"; const account = controller.accounts.find((item) => item.id === accountId); if (!account) { return null; } return createRuntimeAccountView(controller, account, { hotViews: listHotViews(), primaryClientId, clientOnline, lastAccountsSyncAt: controller.lastAccountsSyncAt, now, }); } function createLiveRuntimeSnapshot(controller: RuntimeControllerSnapshot) { 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) => createRuntimeAccountView(controller, account, { hotViews, primaryClientId, clientOnline, lastAccountsSyncAt: controller.lastAccountsSyncAt, now, }), ); const tasks = controller.tasks.map((task) => ({ ...task })); const healthCounts = accounts.reduce>((acc, item) => { acc[item.health] = (acc[item.health] ?? 0) + 1; return acc; }, {}); const blockingAccountCount = accounts.filter((item) => ["expired", "revoked", "challenge_required"].includes(item.authState), ).length; 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.dispatchConnected ? "websocket-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 + blockingAccountCount, healthCounts, }, clients, accounts, tasks, activity: controller.activity, subsystems: { transport, circuitBreaker: getCircuitBreakerState(), keepAlive: getKeepAlivePlan(), leaseManager, rateLimiter, recovery: collectRecoverySnapshot(), runtimeController: { running: controller.running, dispatchConnected: controller.dispatchConnected, 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, publishScheduler: getPublishSchedulerSnapshot(), monitorScheduler: getMonitorSchedulerSnapshot(), sessions, hotViews, hotViewPool: getHotViewPoolPolicy(), networkDebug: getObservedRequestSnapshot(), playwright: getHiddenPlaywrightSnapshot(), processMetrics: getProcessMetricsSnapshot(), vault: describeVaultBackend(), }, }; }