import { useEffect, useMemo, useRef, useState } from 'react'; import { buildFleetTableViewModel, fleetStatusKind, type FleetAuthFilter, type FleetFilter, type FleetInstance, type FleetSortColumn, type FleetStatus, type SortDirection, } from './fleet-table-view-model.js'; import type { InstanceDataSource } from '../instances/instance-crud.js'; import type { FleetMessageLoadState, FleetMessagesDataSource, } from './fleet-messages-api-data-source.js'; import { loadFleetMessageSummaries } from './fleet-messages-api-data-source.js'; import { createOperationClient, type OperationClient, } from '../operations/operation-client.js'; import { safeUiError } from '../ui/locale.js'; export interface FleetSnapshot { readonly instances: readonly FleetInstance[]; readonly statuses: ReadonlyMap; } export interface FleetDataSource { load( signal?: AbortSignal, onPartial?: (snapshot: FleetSnapshot) => void, ): Promise; } export interface FleetPageProps { readonly dataSource?: FleetDataSource; readonly messagesDataSource?: FleetMessagesDataSource; readonly instanceDataSource?: InstanceDataSource; readonly initialData?: FleetSnapshot; readonly refreshSignal?: number; readonly operationClient?: OperationClient; } type FleetMessageState = FleetMessageLoadState; const EMPTY_SNAPSHOT: FleetSnapshot = { instances: [], statuses: new Map() }; const STATUS_LABELS: Readonly> = { online: '在线', auth: '需要认证', offline: '离线', unknown: '未知', }; const SORT_COLUMN_LABELS: Readonly> = { name: '名称', status: '状态', latency: '延迟', version: '版本', capabilities: '能力', tags: '标签', freshness: '数据新鲜度', anomalies: '异常', }; const CAPABILITY_LABELS: Readonly> = { overview: '概览', cellular: '蜂窝网络', 'device-network': '设备网络', messages: '消息', calls: '通话', esim: 'eSIM', notifications: '通知', automation: '自动化', ota: 'OTA', }; function capabilityLabel(value: string): string { return CAPABILITY_LABELS[value] ?? value; } const percent = (value: number | undefined): string => value === undefined ? '暂未获取' : `${value.toFixed(1)}%`; const temperature = (value: number | undefined): string => value === undefined ? '暂未获取' : `${value.toFixed(1)} °C`; const messageDirection = (value: string): string => value === 'incoming' || value === 'received' ? '收到' : value === 'outgoing' || value === 'sent' ? '发送' : '未知方向'; const messageTime = (value: string): string => { const date = new Date(value); return Number.isNaN(date.getTime()) ? value : date.toLocaleString('zh-CN'); }; export function canonicalHttpOrigin(value: string): string | null { try { const url = new URL(value); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; url.username = ''; url.password = ''; return url.origin; } catch { return null; } } type BatchActionKind = 'service-restart' | 'system-reboot'; type BatchProgress = Readonly<{ action: BatchActionKind; total: number; completed: number; succeeded: number; failed: number; message: string; }>; const SERVICE_RESTART = { operationId: 'postServiceRestart', parameterSchemaId: 'simadmin.58e2204.postServiceRestart.parameters.v1', title: '重启服务', } as const; const SYSTEM_REBOOT = { operationId: 'postSystemReboot', parameterSchemaId: 'simadmin.58e2204.postSystemReboot.parameters.v1', title: '系统重启', delaySeconds: 3, } as const; export function FleetPage({ dataSource, messagesDataSource, instanceDataSource, initialData, refreshSignal = 0, operationClient, }: FleetPageProps) { const [snapshot, setSnapshot] = useState(initialData ?? null); const [error, setError] = useState(null); const [attempt, setAttempt] = useState(0); const [query, setQuery] = useState(''); const [filter, setFilter] = useState('all'); const [auth, setAuth] = useState('all'); const [capability, setCapability] = useState(''); const [version, setVersion] = useState(''); const [tag, setTag] = useState(''); const [page, setPage] = useState(1); const [sort, setSort] = useState<{ column: FleetSortColumn; direction: SortDirection }>({ column: 'name', direction: 'asc', }); const [selectedIds, setSelectedIds] = useState>(new Set()); const [batchOpen, setBatchOpen] = useState(false); const [batchBusy, setBatchBusy] = useState(false); const [batchProgress, setBatchProgress] = useState(null); const [cardAction, setCardAction] = useState<{ id: string; busy: boolean; message?: string; error?: string; } | null>(null); const [deleteId, setDeleteId] = useState(); const [deleteOwner, setDeleteOwner] = useState>(); const [deleteConfirmation, setDeleteConfirmation] = useState(''); const [deleteBusy, setDeleteBusy] = useState(false); const deleteRequestRef = useRef(0); const [deleteStatus, setDeleteStatus] = useState>(); const messagesOwner = useRef(0); const [messageStates, setMessageStates] = useState>( new Map(), ); const resolvedOperationClient = useMemo( () => operationClient ?? createOperationClient(), [operationClient], ); useEffect(() => { if (initialData && refreshSignal === 0) { setSnapshot(initialData); setError(null); return; } if (!dataSource) { setSnapshot(EMPTY_SNAPSHOT); return; } let active = true; const controller = new AbortController(); // Keep the last known instances visible while refreshing so the homepage stays interactive. setSnapshot((current) => current); setError(null); void dataSource .load(controller.signal, (partial) => { if (active) setSnapshot(partial); }) .then( (data) => { if (active) setSnapshot(data); }, (reason: unknown) => { if (!active) return; if (reason instanceof DOMException && reason.name === 'AbortError') return; setError('实例加载失败。'); }, ); return () => { active = false; controller.abort(); }; }, [attempt, dataSource, initialData, refreshSignal]); useEffect(() => { const request = ++messagesOwner.current; const controller = new AbortController(); const instances = snapshot?.instances; setMessageStates(new Map()); if (!messagesDataSource || !instances) return () => controller.abort(); void loadFleetMessageSummaries( messagesDataSource, instances.map((instance) => instance.id), controller.signal, ).then((entries) => { if (request === messagesOwner.current && !controller.signal.aborted) setMessageStates(entries); }); return () => controller.abort(); }, [messagesDataSource, snapshot]); const model = useMemo( () => buildFleetTableViewModel(snapshot?.instances ?? [], snapshot?.statuses ?? new Map(), { query, filter, auth, ...(capability ? { capability } : {}), ...(version ? { version } : {}), ...(tag ? { tag } : {}), sort, selectedIds, page, }), [auth, capability, filter, page, query, selectedIds, snapshot, sort, tag, version], ); const fleetSummary = useMemo(() => { const statuses = [...(snapshot?.statuses.values() ?? [])]; const online = statuses.filter( (status) => status.reachable && status.authenticated !== false, ).length; return { total: snapshot?.instances.length ?? 0, online, attention: (snapshot?.instances.length ?? 0) - online, }; }, [snapshot]); const statusFacetCounts = useMemo(() => { const counts: Record = { all: snapshot?.instances.length ?? 0, online: 0, auth: 0, offline: 0, unknown: 0, }; for (const instance of snapshot?.instances ?? []) { const kind = fleetStatusKind(snapshot?.statuses.get(instance.id)); counts[kind] += 1; } return counts; }, [snapshot]); const activeFilterChips = useMemo(() => { const chips: Array<{ key: string; label: string; onClear: () => void }> = []; const trimmedQuery = query.trim(); if (trimmedQuery) { chips.push({ key: 'query', label: `搜索:${trimmedQuery}`, onClear: () => { setQuery(''); setPage(1); }, }); } if (filter !== 'all') { chips.push({ key: 'status', label: `状态:${STATUS_LABELS[filter] ?? filter}`, onClear: () => { setFilter('all'); setPage(1); }, }); } if (auth !== 'all') { const authLabel = auth === 'authenticated' ? '已认证' : auth === 'required' ? '需要认证' : '未知'; chips.push({ key: 'auth', label: `认证:${authLabel}`, onClear: () => { setAuth('all'); setPage(1); }, }); } if (capability) { chips.push({ key: 'capability', label: `能力:${capabilityLabel(capability)}`, onClear: () => { setCapability(''); setPage(1); }, }); } if (version) { chips.push({ key: 'version', label: `版本:${version}`, onClear: () => { setVersion(''); setPage(1); }, }); } if (tag) { chips.push({ key: 'tag', label: `标签:${tag}`, onClear: () => { setTag(''); setPage(1); }, }); } return chips; }, [auth, capability, filter, query, tag, version]); const advancedFilterCount = useMemo( () => (auth !== 'all' ? 1 : 0) + (capability ? 1 : 0) + (version ? 1 : 0) + (tag ? 1 : 0), [auth, capability, tag, version], ); useEffect(() => { if (page !== model.page) setPage(model.page); }, [model.page, page]); function resetPage(action: () => void): void { action(); setPage(1); } function clearAllFilters(): void { setQuery(''); setFilter('all'); setAuth('all'); setCapability(''); setVersion(''); setTag(''); setPage(1); } function toggleOne(id: string, checked: boolean): void { setSelectedIds((current) => { const next = new Set(current); if (checked) next.add(id); else next.delete(id); return next; }); } function toggleVisible(checked: boolean): void { setSelectedIds((current) => { const next = new Set(current); for (const row of model.rows) { if (checked) next.add(row.id); else next.delete(row.id); } return next; }); } async function beginDelete(id: string): Promise { const request = ++deleteRequestRef.current; setDeleteId(id); setDeleteOwner(undefined); setDeleteConfirmation(''); setDeleteStatus(undefined); if (!instanceDataSource) { setDeleteStatus({ id, kind: 'error', message: '无法准备删除,请稍后重试。' }); return; } setDeleteBusy(true); try { const current = await instanceDataSource.get(id); if (request !== deleteRequestRef.current) return; if (current.id !== id) throw new Error('owner mismatch'); setDeleteOwner({ id: current.id, revision: current.revision }); } catch { if (request === deleteRequestRef.current) { setDeleteStatus({ id, kind: 'error', message: '无法准备删除,请稍后重试。' }); } } finally { if (request === deleteRequestRef.current) setDeleteBusy(false); } } async function remove(id: string): Promise { if (!instanceDataSource || deleteOwner?.id !== id || deleteConfirmation !== id) return; setDeleteBusy(true); setDeleteStatus(undefined); try { await instanceDataSource.delete(id, deleteOwner.revision); setDeleteStatus({ id, kind: 'success', message: '删除请求已提交。' }); setDeleteId(undefined); setDeleteConfirmation(''); } catch { setDeleteStatus({ id, kind: 'error', message: '删除请求失败,请稍后重试。' }); } finally { setDeleteBusy(false); } } function selectedTargets(): Array<{ instanceId: string; revision: number }> { const instances = snapshot?.instances ?? []; return model.selectedIds.flatMap((id) => { const instance = instances.find((item) => item.id === id); if (!instance || !instance.revision || instance.revision < 1) return []; return [{ instanceId: id, revision: instance.revision }]; }); } async function runOperation( kind: BatchActionKind, targets: ReadonlyArray<{ instanceId: string; revision: number }>, ): Promise<{ succeeded: number; failed: number }> { const catalog = await resolvedOperationClient.list({ pageSize: 100 }); const op = kind === 'service-restart' ? SERVICE_RESTART : SYSTEM_REBOOT; const entry = catalog.items.find((item) => item.operationId === op.operationId); if (!entry || entry.parameterSchemaId !== op.parameterSchemaId) throw new Error('Operation is not available.'); let succeeded = 0; let failed = 0; let completed = 0; for (const target of targets) { try { const prepared = await resolvedOperationClient.prepare({ operationId: op.operationId, targets: [target], parameters: { parameterSchemaId: op.parameterSchemaId, fields: kind === 'system-reboot' ? [{ fieldId: 'delay_seconds', kind: 'number', value: SYSTEM_REBOOT.delaySeconds }] : [], }, }); const confirmed = typeof window === 'undefined' ? true : window.confirm( `${prepared.confirmationPrompt} 风险等级 ${prepared.risk}。确认继续?`, ); if (!confirmed) { failed += 1; } else { const job = await resolvedOperationClient.execute(prepared.id); if (job.status === 'succeeded') succeeded += 1; else failed += 1; } } catch { failed += 1; } completed += 1; setBatchProgress({ action: kind, total: targets.length, completed, succeeded, failed, message: `${op.title}进度 ${completed}/${targets.length}`, }); } return { succeeded, failed }; } async function runBatch(kind: BatchActionKind): Promise { const targets = selectedTargets(); if (targets.length === 0) { setBatchProgress({ action: kind, total: 0, completed: 0, succeeded: 0, failed: 0, message: '所选实例缺少配置版本,请刷新后重试。', }); return; } const title = kind === 'service-restart' ? SERVICE_RESTART.title : SYSTEM_REBOOT.title; if ( typeof window !== 'undefined' && !window.confirm(`将对 ${targets.length} 个实例执行「${title}」。此操作为高风险,确认继续?`) ) return; setBatchBusy(true); setBatchProgress({ action: kind, total: targets.length, completed: 0, succeeded: 0, failed: 0, message: `正在执行${title}…`, }); try { const result = await runOperation(kind, targets); setBatchProgress({ action: kind, total: targets.length, completed: targets.length, succeeded: result.succeeded, failed: result.failed, message: `${title}完成:成功 ${result.succeeded},失败 ${result.failed}。`, }); } catch (error) { setBatchProgress({ action: kind, total: targets.length, completed: 0, succeeded: 0, failed: targets.length, message: safeUiError(error, `${title}失败,请稍后重试。`), }); } finally { setBatchBusy(false); } } async function runCardAction(id: string, kind: BatchActionKind): Promise { const instance = snapshot?.instances.find((item) => item.id === id); if (!instance?.revision || instance.revision < 1) { setCardAction({ id, busy: false, error: '缺少配置版本,请刷新后重试。' }); return; } const title = kind === 'service-restart' ? SERVICE_RESTART.title : SYSTEM_REBOOT.title; if ( typeof window !== 'undefined' && !window.confirm(`将对该实例执行「${title}」。此操作为高风险,确认继续?`) ) return; setCardAction({ id, busy: true, message: `正在${title}…` }); try { const result = await runOperation(kind, [{ instanceId: id, revision: instance.revision }]); setCardAction({ id, busy: false, message: result.succeeded === 1 ? `${title}已提交成功。` : `${title}失败,请稍后重试。`, ...(result.failed === 1 ? { error: `${title}失败,请稍后重试。` } : {}), }); } catch (error) { setCardAction({ id, busy: false, error: safeUiError(error, `${title}失败,请稍后重试。`), }); } } const selectFilter = ( label: string, value: string, setValue: (value: string) => void, choices: readonly string[], allLabel: string, displayChoice: (choice: string) => string = (choice) => choice, ) => ( ); return (

实例舰队

资源优先总览,查找并管理全部 SimAdmin 实例。

添加实例 已选择 {model.selectedIds.length} 项
{snapshot ? (
实例总数 {fleetSummary.total}
在线 {fleetSummary.online}
需处理 {fleetSummary.attention}
) : null} {batchOpen ? (
批量操作

已选择 {model.selectedIds.length} 项。重启将逐个实例安全确认后执行。

{batchProgress ? (

{batchProgress.message}

) : null}
) : null}
{( [ ['all', '全部'], ['online', '在线'], ['auth', '需认证'], ['offline', '离线'], ['unknown', '未知'], ] as const ).map(([value, label]) => ( ))}

显示 {model.filteredCount} / {model.totalCount} {model.pageCount > 1 ? ` · 第 ${model.page}/${model.pageCount} 页` : null}

{activeFilterChips.length > 0 ? (
{activeFilterChips.map((chip) => ( ))}
) : null}
更多筛选 {advancedFilterCount > 0 ? ( {advancedFilterCount} ) : null}
{selectFilter( '能力', capability, setCapability, model.facets.capabilities, '全部能力', capabilityLabel, )} {selectFilter('版本', version, setVersion, model.facets.versions, '全部版本')} {selectFilter('标签', tag, setTag, model.facets.tags, '全部标签')}
{!snapshot && !error ? (

正在加载实例列表…

) : null} {error ? (

无法加载实例。{error}

) : null} {snapshot && model.emptyReason ? (
{model.emptyReason === 'config' ? ( <>

尚未配置实例。

添加实例 ) : null} {model.emptyReason === 'search' ?

没有实例符合搜索与筛选条件。

: null} {model.emptyReason === 'filter' ?

没有实例符合当前筛选条件。

: null}
) : null} {snapshot && model.rows.length > 0 ? ( <>
{model.rows.map((row) => (

{row.displayName}

{row.id} 进入仪表盘
{STATUS_LABELS[row.statusKind]}
{(() => { const origin = canonicalHttpOrigin(row.instance.url); return (

{origin ?? '源地址无效'}

); })()} {row.anomalies.length > 0 ? (

{row.anomalies.join(', ')}

) : null}
手机号
{row.status?.summary?.resources?.phoneNumbers?.join('、') || '暂未获取'}
CPU
{row.status?.summary?.resources?.cpuPercent === undefined ? ( '暂未获取' ) : ( <> {percent(row.status.summary.resources.cpuPercent)} )}
内存
{row.status?.summary?.resources?.memoryPercent === undefined ? ( '暂未获取' ) : ( <> {percent(row.status.summary.resources.memoryPercent)} )}
最高温度
{temperature(row.status?.summary?.resources?.maxTemperatureCelsius)}
{row.capabilities.length > 0 ? (
{row.capabilities.map((item) => ( {capabilityLabel(item)} ))}
) : null}

最近短信

{messageStates.get(row.id)?.unavailable ? (

短信暂不可用

) : messageStates.get(row.id)?.latest ? (
方向
{messageDirection(messageStates.get(row.id)!.latest!.direction)}
号码
{messageStates.get(row.id)!.latest!.phoneNumber}
时间
{messageTime(messageStates.get(row.id)!.latest!.timestamp)}
) : (

暂无短信

)}
编辑
{cardAction?.id === row.id ? (

{cardAction.error ?? cardAction.message}

) : null} {deleteId === row.id ? (

删除后无法撤销。

) : null} {deleteStatus?.id === row.id ? (

{deleteStatus.message}

) : null}
))}
) : null}
); }