import type { AutomationSnapshot } from './automation-module.js'; import type { CallFeatureStatus, CallsSnapshot } from './calls-module.js'; import type { CellularSnapshot } from './cellular-module.js'; import type { DeviceNetworkSnapshot } from './device-network-module.js'; import type { EsimSafeLabel, EsimSnapshot } from './esim-module.js'; import type { NotificationsSnapshot } from './notifications-module.js'; import type { OtaSnapshot } from './ota-module.js'; import type { OverviewFieldValue, OverviewSnapshot } from './overview-system.js'; /** * Bridge between the control-plane instance module proxy and the device panel modules. * * The API reads the device's own read-only endpoints, classifies each probe, and hands back a * flat list of sections. This file is the single place that knows how those device sections map * onto the console's presentation contracts, so every module renders live data instead of an * empty shell. */ export type InstanceModuleState = 'ok' | 'empty' | 'auth-required' | 'unsupported' | 'failed'; export interface InstanceModuleSection { readonly key: string; readonly path: string; readonly state: InstanceModuleState; readonly status: number; readonly data: unknown; } export interface InstanceModuleSnapshot { readonly instanceId: string; readonly module: string; readonly observedAt: string; readonly authenticated: boolean; readonly sections: readonly InstanceModuleSection[]; } export interface InstanceModuleReader { read(instanceId: string, module: string, signal?: AbortSignal): Promise; } const MAX_ENTRIES = 40; const MAX_STRING_LENGTH = 240; function isRecord(value: unknown): value is Readonly> { return typeof value === 'object' && value !== null && !Array.isArray(value); } function parseSnapshot(value: unknown, instanceId: string, module: string): InstanceModuleSnapshot { if ( !isRecord(value) || typeof value.instanceId !== 'string' || value.instanceId !== instanceId || typeof value.module !== 'string' || typeof value.observedAt !== 'string' || typeof value.authenticated !== 'boolean' || !Array.isArray(value.sections) ) throw new Error('服务端返回的模块快照无效。'); const sections = value.sections.slice(0, 64).flatMap((entry): InstanceModuleSection[] => { if (!isRecord(entry) || typeof entry.key !== 'string' || typeof entry.path !== 'string') return []; const state = entry.state; return [ { key: entry.key.slice(0, 64), path: entry.path.slice(0, 256), state: state === 'ok' || state === 'empty' || state === 'auth-required' || state === 'unsupported' || state === 'failed' ? state : 'failed', status: typeof entry.status === 'number' && Number.isFinite(entry.status) ? entry.status : 0, data: entry.data, }, ]; }); return { instanceId: value.instanceId, module: value.module || module, observedAt: value.observedAt, authenticated: value.authenticated, sections, }; } export function createInstanceModuleApiReader(fetcher: typeof fetch = fetch): InstanceModuleReader { return { async read(instanceId, module, signal) { const url = `/api/v1/instances/${encodeURIComponent(instanceId)}/modules/${encodeURIComponent( module, )}`; const response = await fetcher(url, { method: 'GET', credentials: 'same-origin', headers: { accept: 'application/json' }, ...(signal ? { signal } : {}), }); if (!response.ok) throw new Error(`请求失败 (${response.status})`); return parseSnapshot(await response.json(), instanceId, module); }, }; } function section(snapshot: InstanceModuleSnapshot, key: string): InstanceModuleSection | undefined { return snapshot.sections.find((candidate) => candidate.key === key); } function sectionRecord( snapshot: InstanceModuleSnapshot, key: string, ): Readonly> { const entry = section(snapshot, key); if (!entry) return {}; if (isRecord(entry.data)) return entry.data; if (Array.isArray(entry.data)) return { items: entry.data }; return {}; } /** First present value among candidate device field names; device payloads are not uniform. */ function pick(source: Readonly>, keys: readonly string[]): unknown { for (const key of keys) { const value = source[key]; if (value !== undefined && value !== null) return value; } return undefined; } function text(value: unknown): string | undefined { if (typeof value === 'string') return value.length > MAX_STRING_LENGTH ? `${value.slice(0, MAX_STRING_LENGTH)}…` : value; if (typeof value === 'number' && Number.isFinite(value)) return String(value); if (typeof value === 'boolean') return value ? 'true' : 'false'; return undefined; } function flag(value: unknown): boolean | undefined { if (typeof value === 'boolean') return value; if (typeof value === 'number') return value !== 0; if (typeof value === 'string') { const lowered = value.toLocaleLowerCase(); if (['true', 'yes', 'on', 'enabled', '1'].includes(lowered)) return true; if (['false', 'no', 'off', 'disabled', '0'].includes(lowered)) return false; } return undefined; } function count(value: unknown): number | undefined { if (typeof value === 'number' && Number.isFinite(value)) return value; if (typeof value === 'string') { const parsed = Number(value); if (Number.isFinite(parsed)) return parsed; } return undefined; } /** Keep only display-safe primitives, preserving device key names for the label table. */ function primitives(source: Readonly>): Record { const result: Record = {}; for (const [key, value] of Object.entries(source)) { if (Object.keys(result).length >= MAX_ENTRIES) break; if (typeof value === 'string') { result[key] = text(value) ?? null; } else if (typeof value === 'number') { result[key] = Number.isFinite(value) ? value : null; } else if (typeof value === 'boolean') { result[key] = value; } else if (Array.isArray(value)) { const entries = value .slice(0, 6) .map((item) => (isRecord(item) ? text(pick(item, ['name', 'label', 'value'])) : text(item))) .filter((item): item is string => item !== undefined); result[key] = entries.length ? entries.join('、') : null; } else { result[key] = null; } } return result; } function arrayFrom( source: Readonly>, keys: readonly string[], ): readonly unknown[] { for (const key of keys) { const value = source[key]; if (Array.isArray(value)) return value.slice(0, MAX_ENTRIES); } return []; } function tally(items: readonly unknown[], predicate: (item: unknown) => boolean): number { return items.reduce((total, item) => (predicate(item) ? total + 1 : total), 0); } function statusOf(items: readonly unknown[]): 'healthy' | 'degraded' | 'failed' | 'idle' { if (items.length === 0) return 'idle'; const failed = tally(items, (item) => { const record = isRecord(item) ? item : {}; const value = text(pick(record, ['status', 'state', 'result']))?.toLocaleLowerCase(); return value === 'failed' || value === 'error'; }); if (failed === 0) return 'healthy'; return failed >= items.length ? 'failed' : 'degraded'; } function observed(snapshot: InstanceModuleSnapshot): { readonly observedAt: string } { return { observedAt: snapshot.observedAt }; } export function mapOverview(snapshot: InstanceModuleSnapshot): OverviewSnapshot { const stats = sectionRecord(snapshot, 'stats'); // Anchored on word boundaries: "download_bytes" contains "load" but is throughput, not a load average. const cpuKeys = Object.keys(stats).filter((key) => /(^|_)(cpu|memory|mem|temp|thermal|load)(_|$)/u.test(key), ); const cpu: Record = {}; const rest: Record = {}; for (const [key, value] of Object.entries(primitives(stats))) { if (cpuKeys.includes(key)) cpu[key] = value; else rest[key] = value; } // /stats/cpu is a separate probe; its core summary belongs beside the load averages. Object.assign(cpu, primitives(sectionRecord(snapshot, 'cpu'))); // Message counters read better beside the traffic totals than in their own card. Object.assign(rest, primitives(sectionRecord(snapshot, 'smsStats'))); return { ...observed(snapshot), device: primitives(sectionRecord(snapshot, 'device')), sim: primitives(sectionRecord(snapshot, 'sim')), network: { ...primitives(sectionRecord(snapshot, 'network')), ...primitives(sectionRecord(snapshot, 'data')), }, stats: rest, cpu, connectivity: primitives(sectionRecord(snapshot, 'connectivity')), }; } export function mapCellular(snapshot: InstanceModuleSnapshot): CellularSnapshot { const network = sectionRecord(snapshot, 'network'); const signal = sectionRecord(snapshot, 'signalStrength'); const cells = isRecord(sectionRecord(snapshot, 'cells').cells) ? (sectionRecord(snapshot, 'cells').cells as Readonly>) : sectionRecord(snapshot, 'cells'); const location = sectionRecord(snapshot, 'cellLocation'); const operators = sectionRecord(snapshot, 'operators'); const available = arrayFrom(operators, ['operators', 'available', 'items', 'list']).flatMap( (item): string[] => { const name = isRecord(item) ? text(pick(item, ['name', 'long_name', 'alpha'])) : text(item); return name ? [name] : []; }, ); const roaming = flag(pick(network, ['roaming', 'is_roaming', 'roaming_active'])) ? 'roaming' : text(pick(sectionRecord(snapshot, 'roaming'), ['state', 'status', 'roaming'])); return { ...observed(snapshot), networkRegistration: { state: text(pick(network, ['registration_state', 'register_state', 'state', 'status'])), mode: text(pick(network, ['radio_mode', 'mode', 'network_mode', 'preferred_mode'])), operator: text(pick(network, ['operator_name', 'operator', 'carrier', 'name'])), roaming, }, signal: { rssi: text(pick(signal, ['rssi', 'signal_dbm', 'dbm', 'strength'])), rsrp: text(pick(signal, ['rsrp', 'lte_rsrp', 'nr_rsrp'])), rsrq: text(pick(signal, ['rsrq', 'lte_rsrq'])), sinr: text(pick(signal, ['sinr', 'lte_sinr', 'nr_sinr'])), quality: text(pick(signal, ['signal_percent', 'level', 'quality', 'asu'])), }, cellsLocation: { cell: text(pick(cells, ['cell_id', 'cellId', 'eci', 'cid', 'nr_cell_id'])), area: text(pick(cells, ['tac', 'tracking_area', 'area_code', 'rac'])), technology: text( pick(cells, ['network_type', 'technology', 'rat']) ?? pick(network, ['network_type', 'technology', 'rat']), ), latitude: text(pick(location, ['latitude', 'lat'])), longitude: text(pick(location, ['longitude', 'lng', 'lon'])), mcc: text(pick(location, ['mcc']) ?? pick(cells, ['mcc'])), mnc: text(pick(location, ['mnc']) ?? pick(cells, ['mnc'])), pci: text(pick(location, ['pci']) ?? pick(cells, ['pci', 'phys_cell_id'])), arfcn: text( pick(location, ['earfcn', 'arfcn', 'nr_arfcn']) ?? pick(cells, ['earfcn', 'arfcn', 'nr_arfcn']), ), }, operators: { current: text(pick(network, ['operator_name', 'operator', 'carrier'])), available: available.length ? available.slice(0, 12).join('、') : undefined, }, }; } export function mapDeviceNetwork(snapshot: InstanceModuleSnapshot): DeviceNetworkSnapshot { const wlanStatus = sectionRecord(snapshot, 'wlanStatus'); const wlanProfiles = sectionRecord(snapshot, 'wlanProfiles'); const interfaces = sectionRecord(snapshot, 'interfaces'); const addresses = sectionRecord(snapshot, 'addresses'); const ddnsStatus = sectionRecord(snapshot, 'ddnsStatus'); const ddnsConfig = sectionRecord(snapshot, 'ddnsConfig'); const ddnsLogs = sectionRecord(snapshot, 'ddnsLogs'); const profileItems = arrayFrom(wlanProfiles, ['profiles', 'items', 'list', 'networks']); const interfaceItems = arrayFrom(interfaces, ['interfaces', 'items', 'list']); const logItems = arrayFrom(ddnsLogs, ['logs', 'entries', 'items']); const addressList = (keys: readonly string[]): readonly string[] => arrayFrom(addresses, keys) .map((item) => text(item)) .filter((item): item is string => item !== undefined) .slice(0, 8); const ipv4 = addressList(['ipv4', 'ipv4_addresses', 'v4']); const ipv6 = addressList(['ipv6', 'ipv6_addresses', 'v6']); return { ...observed(snapshot), connectionAddresses: { ipv4, ipv6 }, wlan: { status: { enabled: flag(pick(wlanStatus, ['enabled', 'wlan_enabled', 'is_enabled'])), radioState: text(pick(wlanStatus, ['radio_state', 'radioState', 'state'])), connectionState: text( pick(wlanStatus, ['connection_state', 'connectionState', 'status', 'state']), ), activeProfile: text(pick(wlanStatus, ['active_profile', 'activeProfile', 'profile'])), ssid: text(pick(wlanStatus, ['ssid', 'network_name'])), }, profiles: profileItems.flatMap((item) => isRecord(item) ? [ { name: text(pick(item, ['name', 'profile_name'])), ssid: text(pick(item, ['ssid'])), security: text(pick(item, ['security', 'key_management', 'encryption'])), enabled: flag(pick(item, ['enabled', 'active'])), priority: count(pick(item, ['priority', 'order'])), }, ] : [], ), }, interfaces: interfaceItems.flatMap((item) => isRecord(item) ? [ { name: text(pick(item, ['name', 'interface', 'ifname'])), kind: text(pick(item, ['kind', 'type', 'driver'])), state: text(pick(item, ['state', 'status', 'operstate'])), macAddress: text(pick(item, ['mac_address', 'macAddress', 'mac'])), mtu: count(pick(item, ['mtu'])), addresses: arrayFrom(item, ['addresses', 'ips']).flatMap((address) => isRecord(address) ? [ { family: text(pick(address, ['family', 'scope'])), address: text(pick(address, ['address', 'ip', 'value'])), prefixLength: count(pick(address, ['prefix_length', 'prefix', 'cidr'])), scope: text(pick(address, ['scope'])), }, ] : typeof address === 'string' ? [{ address, family: undefined, prefixLength: undefined, scope: undefined }] : [], ), }, ] : [], ), ddns: { status: { enabled: flag(pick(ddnsStatus, ['enabled', 'configured'])), state: text(pick(ddnsStatus, ['state', 'status', 'last_result'])), lastUpdateAt: text(pick(ddnsStatus, ['last_update_at', 'lastUpdateAt', 'updated_at'])), }, config: { provider: text(pick(ddnsConfig, ['provider', 'service'])), hostname: text(pick(ddnsConfig, ['hostname', 'domain'])), updateIntervalSeconds: count( pick(ddnsConfig, ['update_interval_seconds', 'interval_seconds', 'interval']), ), }, logSummary: { totalEntries: count(pick(ddnsLogs, ['total', 'total_entries', 'count'])) ?? logItems.length, successfulUpdates: count(pick(ddnsLogs, ['successful', 'success', 'successful_updates'])) ?? tally(logItems, (item) => { const value = text( pick(isRecord(item) ? item : {}, ['status', 'result', 'state']), )?.toLocaleLowerCase(); return value === 'success' || value === 'ok' || value === 'updated'; }), failedUpdates: count(pick(ddnsLogs, ['failed', 'failures', 'failed_updates'])) ?? tally(logItems, (item) => { const value = text( pick(isRecord(item) ? item : {}, ['status', 'result', 'state']), )?.toLocaleLowerCase(); return value === 'failed' || value === 'error'; }), lastEventAt: text(pick(ddnsLogs, ['last_event_at', 'latest', 'last_at'])), }, }, }; } export function mapEsim(snapshot: InstanceModuleSnapshot): EsimSnapshot { const profiles = sectionRecord(snapshot, 'profiles'); const euicc = sectionRecord(snapshot, 'euicc'); const lpac = sectionRecord(snapshot, 'lpacStatus'); const items = arrayFrom(profiles, ['profiles', 'list', 'items', 'data']).flatMap((item) => isRecord(item) ? [item] : [], ); const enabled = tally(items, (item) => { const record = isRecord(item) ? item : {}; return ( flag(pick(record, ['enabled', 'is_enabled'])) === true || text(pick(record, ['status', 'state']))?.toLocaleLowerCase() === 'enabled' ); }); const lpacValue = text( pick(lpac, ['status', 'state', 'available']) ?? pick(euicc, ['lpac_status', 'status']), )?.toLocaleLowerCase(); const workModeValue = text(pick(euicc, ['work_mode', 'workMode']))?.toLocaleLowerCase(); const labels: EsimSafeLabel[] = []; for (const item of items.slice(0, 8)) { const record = isRecord(item) ? item : {}; const enabled = flag(pick(record, ['enabled', 'is_enabled'])); const itemStatus = text(pick(record, ['status', 'state']))?.toLocaleLowerCase(); const label: EsimSafeLabel = itemStatus === 'error' || itemStatus === 'failed' ? 'Error' : itemStatus === 'pending' || itemStatus === 'downloading' ? 'Pending' : enabled === true || itemStatus === 'enabled' ? 'Enabled' : enabled === false || itemStatus === 'disabled' ? 'Disabled' : 'Provisioned'; if (!labels.includes(label)) labels.push(label); } return { profileCount: items.length, enabledProfileCount: enabled, lpacStatus: lpacValue === 'available' || lpacValue === 'unavailable' || lpacValue === 'degraded' ? lpacValue : lpacValue === 'true' || flag(pick(lpac, ['available', 'supported'])) === true ? 'available' : 'unknown', workMode: workModeValue === 'idle' || workModeValue === 'working' || workModeValue === 'disabled' ? workModeValue : 'unknown', labels, }; } /** Call capabilities the Hub surfaces as separate panels; each maps to one probe. */ const CALL_FEATURE_PROBES: readonly { readonly key: string; readonly name: string }[] = [ { key: 'forwarding', name: '呼叫转移' }, { key: 'volume', name: '通话音量' }, { key: 'voicemail', name: '语音信箱' }, { key: 'ims', name: 'IMS 语音' }, ]; export function mapCalls(snapshot: InstanceModuleSnapshot): CallsSnapshot { const calls = sectionRecord(snapshot, 'calls'); const history = sectionRecord(snapshot, 'history'); const settings = sectionRecord(snapshot, 'settings'); const active = count(pick(calls, ['active', 'active_calls', 'current'])) ?? tally(arrayFrom(calls, ['calls', 'items', 'data', 'list']), (item) => { const value = text( pick(isRecord(item) ? item : {}, ['state', 'status']), )?.toLocaleLowerCase(); return value === 'active' || value === 'dialing' || value === 'ringing'; }); const historyItems = arrayFrom(history, ['calls', 'items', 'history', 'list', 'data']); return { ...observed(snapshot), calls: { state: text(pick(calls, ['state', 'status', 'call_status'])), total: count(pick(calls, ['total', 'count'])) ?? historyItems.length, active, ringing: count(pick(calls, ['ringing', 'incoming'])), held: count(pick(calls, ['held', 'hold'])), failed: count(pick(calls, ['failed', 'error_count'])), }, devices: { state: text(pick(settings, ['state', 'status'])) ?? text(pick(calls, ['state', 'status'])), total: count(pick(settings, ['total', 'line_count', 'slots'])), online: count(pick(settings, ['online'])), offline: count(pick(settings, ['offline'])), busy: active && active > 0 ? active : count(pick(settings, ['busy'])), }, features: CALL_FEATURE_PROBES.flatMap(({ key, name }): CallFeatureStatus[] => { const entry = section(snapshot, key); // A probe the firmware never answered is unknown; one that answered is a live verdict. if (!entry || entry.state === 'failed') return []; if (entry.state === 'unsupported' || entry.state === 'auth-required') return [{ name, state: 'unavailable' as const }]; const message = isRecord(entry.data) ? text(entry.data['message']) : undefined; return [{ name, state: 'available' as const, ...(message ? { detail: message } : {}) }]; }), }; } export function mapNotifications(snapshot: InstanceModuleSnapshot): NotificationsSnapshot { const config = sectionRecord(snapshot, 'config'); const queue = sectionRecord(snapshot, 'queue'); const logs = sectionRecord(snapshot, 'logs'); const channels = arrayFrom(config, ['channels', 'items', 'list', 'webhooks']); const queueItems = arrayFrom(queue, ['items', 'jobs', 'queue', 'notifications', 'data']); const logItems = arrayFrom(logs, ['items', 'logs', 'entries', 'data']); const enabledChannels = tally(channels, (item) => { const record = isRecord(item) ? item : {}; return flag(pick(record, ['enabled', 'active'])) !== false; }); return { ...observed(snapshot), channels: { status: flag(pick(config, ['enabled'])) === false ? 'unavailable' : enabledChannels === 0 && channels.length > 0 ? 'unavailable' : enabledChannels < channels.length ? 'degraded' : statusOf(channels), total: count(pick(config, ['total', 'count'])) ?? channels.length, enabled: enabledChannels, disabled: Math.max( 0, (count(pick(config, ['total', 'count'])) ?? channels.length) - enabledChannels, ), }, queue: { status: statusOf(queueItems), total: count(pick(queue, ['total', 'count'])) ?? queueItems.length, pending: count(pick(queue, ['pending', 'queued'])) ?? tally(queueItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['status', 'state'])) ?.toLocaleLowerCase() .trim(); return value === 'pending' || value === 'queued'; }), processing: count(pick(queue, ['processing', 'in_progress'])) ?? tally(queueItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['status', 'state'])) ?.toLocaleLowerCase() .trim(); return value === 'processing' || value === 'sending'; }), delivered: count(pick(queue, ['delivered', 'sent', 'succeeded'])) ?? tally(queueItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['status', 'state'])) ?.toLocaleLowerCase() .trim(); return value === 'delivered' || value === 'sent' || value === 'success'; }), failed: count(pick(queue, ['failed', 'errors'])) ?? tally(queueItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['status', 'state'])) ?.toLocaleLowerCase() .trim(); return value === 'failed' || value === 'error'; }), }, logs: { status: statusOf(logItems), total: count(pick(logs, ['total', 'count'])) ?? logItems.length, info: tally(logItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['level']))?.toLocaleLowerCase(); return value === 'info' || value === 'debug'; }), warning: tally(logItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['level']))?.toLocaleLowerCase(); return value === 'warn' || value === 'warning'; }), error: tally(logItems, (item) => { const value = text(pick(isRecord(item) ? item : {}, ['level']))?.toLocaleLowerCase(); return value === 'error' || value === 'fatal'; }), }, }; } export function mapAutomation(snapshot: InstanceModuleSnapshot): AutomationSnapshot { const config = sectionRecord(snapshot, 'config'); const logs = sectionRecord(snapshot, 'logs'); const rules = arrayFrom(config, ['rules', 'tasks', 'items', 'list', 'jobs']); const logItems = arrayFrom(logs, ['items', 'executions', 'logs', 'entries']); const enabledRules = tally( rules, (item) => flag(pick(isRecord(item) ? item : {}, ['enabled'])) !== false, ); const failedRuns = tally(logItems, (item) => { const value = text( pick(isRecord(item) ? item : {}, ['status', 'result', 'state']), )?.toLocaleLowerCase(); return value === 'failed' || value === 'error'; }); const scheduler = text(pick(config, ['scheduler', 'scheduler_state']))?.toLocaleLowerCase(); return { ...observed(snapshot), status: { state: flag(pick(config, ['enabled'])) === false ? 'disabled' : failedRuns > 0 ? 'degraded' : logItems.length || rules.length ? 'healthy' : 'unknown', scheduler: scheduler === 'active' || scheduler === 'idle' || scheduler === 'paused' || scheduler === 'disabled' || scheduler === 'unavailable' ? scheduler : flag(pick(config, ['enabled'])) === false ? 'disabled' : rules.length ? 'active' : 'idle', workers: flag(pick(config, ['enabled'])) === false ? 'disabled' : failedRuns > 0 ? 'degraded' : logItems.length ? 'available' : 'unavailable', }, tasks: { total: count(pick(config, ['total', 'count'])) ?? rules.length, enabled: enabledRules, disabled: Math.max( 0, (count(pick(config, ['total', 'count'])) ?? rules.length) - enabledRules, ), running: count(pick(config, ['running'])), queued: count(pick(config, ['queued'])), succeeded: tally(logItems, (item) => { const value = text( pick(isRecord(item) ? item : {}, ['status', 'result', 'state']), )?.toLocaleLowerCase(); return value === 'success' || value === 'ok' || value === 'completed'; }), failed: failedRuns, }, }; } export function mapOta(snapshot: InstanceModuleSnapshot): OtaSnapshot { const status = sectionRecord(snapshot, 'status'); const state = text(pick(status, ['status', 'state', 'phase']))?.toLocaleLowerCase(); const progress = count(pick(status, ['progress_percent', 'progressPercent', 'progress'])); const available = flag(pick(status, ['update_available', 'updateAvailable', 'available'])); return { currentVersion: text(pick(status, ['current_version', 'currentVersion', 'version'])), status: state === 'idle' || state === 'checking' || state === 'up-to-date' || state === 'available' || state === 'downloading' || state === 'verifying' || state === 'installing' || state === 'rebooting' || state === 'completed' || state === 'failed' ? state : 'unknown', progressPercent: progress === undefined ? undefined : Math.max(0, Math.min(100, progress)), updateAvailable: available ?? state === 'available', }; } export interface InstanceModuleDataSources { readonly overview: { load(instanceId: string, signal: AbortSignal): Promise }; readonly cellular: { load(instanceId: string, signal: AbortSignal): Promise }; readonly deviceNetwork: { load(instanceId: string, signal: AbortSignal): Promise; }; readonly esim: { load(instanceId: string, signal: AbortSignal): Promise }; readonly calls: { load(instanceId: string, signal: AbortSignal): Promise }; readonly notifications: { load(instanceId: string, signal: AbortSignal): Promise; }; readonly automation: { load(instanceId: string, signal: AbortSignal): Promise }; readonly ota: { load(instanceId: string, signal: AbortSignal): Promise }; } /** * One reader, one in-flight cache per module, eight module contracts. The device panel and the * fleet drill-down share this so a tab switch never re-probes a device that was just read. */ export function createInstanceModuleDataSources( reader: InstanceModuleReader = createInstanceModuleApiReader(), ttlMs = 15_000, ): InstanceModuleDataSources { const cache = new Map(); const inflight = new Map>(); const read = (module: string) => (instanceId: string, signal: AbortSignal) => { const key = `${instanceId}:${module}`; const cached = cache.get(key); if (cached && Date.now() - cached.at < ttlMs) return Promise.resolve(cached.value); const pending = inflight.get(key); if (pending) return pending; const next = reader.read(instanceId, module, signal).then( (value) => { cache.set(key, { at: Date.now(), value }); inflight.delete(key); return value; }, (error: unknown) => { inflight.delete(key); throw error; }, ); inflight.set(key, next); return next; }; return { overview: { load: async (id, signal) => mapOverview(await read('overview')(id, signal)) }, cellular: { load: async (id, signal) => mapCellular(await read('cellular')(id, signal)) }, deviceNetwork: { load: async (id, signal) => mapDeviceNetwork(await read('device-network')(id, signal)), }, esim: { load: async (id, signal) => mapEsim(await read('esim')(id, signal)) }, calls: { load: async (id, signal) => mapCalls(await read('calls')(id, signal)) }, notifications: { load: async (id, signal) => mapNotifications(await read('notifications')(id, signal)), }, automation: { load: async (id, signal) => mapAutomation(await read('automation')(id, signal)), }, ota: { load: async (id, signal) => mapOta(await read('ota')(id, signal)) }, }; }