Files
multi-simadmin/apps/web/src/instances/instance-module-api-data-source.ts
T
chick a75badeb58 feat(web): rebuild the console around the fused control plane
Render the Hub feature set in the existing Animal Island styling: fleet
messages, notification centre, log centre, organisation panel, per-instance
device module panels and the settings backup, connection and maintenance
surfaces.
2026-09-05 18:53:04 +08:00

750 lines
30 KiB
TypeScript

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<InstanceModuleSnapshot>;
}
const MAX_ENTRIES = 40;
const MAX_STRING_LENGTH = 240;
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
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<Record<string, unknown>> {
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<Record<string, unknown>>, 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<string, unknown>>): Record<string, OverviewFieldValue> {
const result: Record<string, OverviewFieldValue> = {};
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<Record<string, unknown>>,
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<number>((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<string, OverviewFieldValue> = {};
const rest: Record<string, OverviewFieldValue> = {};
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<Record<string, unknown>>)
: 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<OverviewSnapshot> };
readonly cellular: { load(instanceId: string, signal: AbortSignal): Promise<CellularSnapshot> };
readonly deviceNetwork: {
load(instanceId: string, signal: AbortSignal): Promise<DeviceNetworkSnapshot>;
};
readonly esim: { load(instanceId: string, signal: AbortSignal): Promise<unknown> };
readonly calls: { load(instanceId: string, signal: AbortSignal): Promise<CallsSnapshot> };
readonly notifications: {
load(instanceId: string, signal: AbortSignal): Promise<unknown>;
};
readonly automation: { load(instanceId: string, signal: AbortSignal): Promise<unknown> };
readonly ota: { load(instanceId: string, signal: AbortSignal): Promise<unknown> };
}
/**
* 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<string, { at: number; value: InstanceModuleSnapshot }>();
const inflight = new Map<string, Promise<InstanceModuleSnapshot>>();
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)) },
};
}