feat(web): localize and redesign operations console

This commit is contained in:
chick
2026-07-18 22:22:41 +08:00
parent d575ab4d9d
commit abfd07f8a3
34 changed files with 1536 additions and 1058 deletions
@@ -44,18 +44,16 @@ describe('isolated Automation read module', () => {
it('loads through the injected exact-owner source and renders only aggregate task status', async () => {
const pending = deferredSource();
render(<AutomationModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Automation loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '自动化加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const counts = await screen.findByRole('region', { name: 'Task counts' });
const counts = await screen.findByRole('region', { name: '任务计数' });
expect(within(counts).getByText('12')).toBeTruthy();
expect(within(counts).getByText('9')).toBeTruthy();
expect(screen.getByRole('region', { name: 'Automation status' }).textContent).toContain(
'healthy',
);
expect(screen.queryByRole('region', { name: 'Safe labels' })).toBeNull();
expect(screen.getByText(/Observed 2026-07-17T13:00:00Z/)).toBeTruthy();
expect(screen.getByRole('region', { name: '自动化状态' }).textContent).toContain('正常');
expect(screen.queryByRole('region', { name: '安全标签' })).toBeNull();
expect(screen.getByText(/观测时间:2026-07-17T13:00:00Z/)).toBeTruthy();
});
it('omits labels and never exposes task config, endpoints, tokens, phones, messages, payloads, logs, or operations', async () => {
@@ -72,12 +70,12 @@ describe('isolated Automation read module', () => {
labels: ['Daily jobs'],
};
render(<AutomationModule instance={owner} dataSource={{ load: async () => unsafe }} />);
expect(await screen.findByText(/read-only aggregate view/i)).toBeTruthy();
expect(await screen.findByText(/只读汇总视图/i)).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/Daily jobs|secret\.example|secret-token|555|private message|private payload|private raw log|cron config|execute|nested-token|nested-message/i,
);
expect(screen.queryByRole('button')).toBeNull();
expect(screen.getByText(/read-only aggregate view/i)).toBeTruthy();
expect(screen.getByText(/只读汇总视图/i)).toBeTruthy();
});
it('copies and bounds enums, counts, and timestamps while dropping all labels', async () => {
@@ -102,13 +100,13 @@ describe('isolated Automation read module', () => {
],
};
render(<AutomationModule instance={owner} dataSource={{ load: async () => unsafe }} />);
await screen.findByRole('region', { name: 'Task counts' });
expect(within(screen.getByRole('region', { name: 'Task counts' })).getByText('2')).toBeTruthy();
expect(within(screen.getByRole('region', { name: 'Task counts' })).getByText('3')).toBeTruthy();
await screen.findByRole('region', { name: '任务计数' });
expect(within(screen.getByRole('region', { name: '任务计数' })).getByText('2')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '任务计数' })).getByText('3')).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/scheduler-token|state-secret|scheduler-secret|worker-secret|Safe [1-9]|script|Infinity|1\.5|-1/,
);
expect(screen.queryByText(/^Observed /)).toBeNull();
expect(screen.queryByText(/^观测时间:/)).toBeNull();
unsafe.labels[0] = 'Mutated';
unsafe.tasks.disabled = 99;
expect(screen.queryByText('Mutated')).toBeNull();
@@ -123,12 +121,12 @@ describe('isolated Automation read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(load).not.toHaveBeenCalled();
rerender(<AutomationModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Automation unavailable' }).textContent).toMatch(
/no safe Automation read data source.*will not invent.*production endpoint/i,
expect(screen.getByRole('status', { name: '自动化不可用' }).textContent).toMatch(
/没有可用的安全自动化只读数据源.*不会臆造.*生产端点/i,
);
});
@@ -143,19 +141,17 @@ describe('isolated Automation read module', () => {
const { rerender } = render(
<AutomationModule instance={owner} dataSource={source} refreshSignal={0} />,
);
const counts = await screen.findByRole('region', { name: 'Task counts' });
const counts = await screen.findByRole('region', { name: '任务计数' });
expect(within(counts).getByText('12')).toBeTruthy();
rerender(<AutomationModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/showing the last known Automation data/i);
expect(alert.textContent).toMatch(/正在显示上次已知的自动化数据/i);
expect(alert.textContent).not.toMatch(/secret endpoint token/i);
expect(within(screen.getByRole('region', { name: '任务计数' })).getByText('12')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '重试加载自动化' }));
expect(
within(screen.getByRole('region', { name: 'Task counts' })).getByText('12'),
).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Automation' }));
expect(
await within(screen.getByRole('region', { name: 'Task counts' })).findByText('13'),
await within(screen.getByRole('region', { name: '任务计数' })).findByText('13'),
).toBeTruthy();
});
@@ -190,7 +186,7 @@ describe('isolated Automation read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Automation data could not be loaded.');
expect(alert.textContent).toContain('无法加载自动化数据。');
expect(alert.textContent).not.toContain('private automation token');
});
});
+30 -30
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Aggregate task counts only; per-task records and configuration are intentionally unsupported. */
export interface AutomationTaskCounts {
@@ -46,7 +47,7 @@ type ReadState =
| { kind: 'ready'; ownerId: string; snapshot: AutomationSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: AutomationSnapshot };
const SAFE_LOAD_ERROR = 'Automation data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载自动化数据。';
const STATES = new Set<AutomationState>(['healthy', 'degraded', 'failed', 'disabled', 'unknown']);
const SCHEDULERS = new Set<AutomationScheduler>([
'active',
@@ -138,13 +139,13 @@ function Fields({
values: readonly (readonly [string, string | number | null | undefined])[];
}) {
const supplied = values.filter(([, value]) => value !== undefined);
if (!supplied.length) return <p>No aggregate status was supplied.</p>;
if (!supplied.length) return <p>未提供汇总状态。</p>;
return (
<dl>
{supplied.map(([label, value]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
<dd>{displayValue(value)}</dd>
</div>
))}
</dl>
@@ -156,28 +157,28 @@ function SnapshotView({ snapshot }: { snapshot: AutomationSnapshot }) {
const tasks = snapshot.tasks;
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
<p>Read-only aggregate view; task details and operational controls are excluded.</p>
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<p>此处仅显示只读汇总视图,不包含任务详情和操作控件。</p>
<div className="automation-grid">
<Section label="Automation status">
<Section label="自动化状态">
<Fields
values={[
['State', status.state],
['Scheduler', status.scheduler],
['Workers', status.workers],
['状态', status.state],
['调度器', status.scheduler],
['工作进程', status.workers],
]}
/>
</Section>
<Section label="Task counts">
<Section label="任务计数">
<Fields
values={[
['Total', tasks.total],
['Enabled', tasks.enabled],
['Disabled', tasks.disabled],
['Running', tasks.running],
['Queued', tasks.queued],
['Succeeded', tasks.succeeded],
['Failed', tasks.failed],
['总计', tasks.total],
['已启用', tasks.enabled],
['已禁用', tasks.disabled],
['运行中', tasks.running],
['排队中', tasks.queued],
['成功', tasks.succeeded],
['失败', tasks.failed],
]}
/>
</Section>
@@ -238,15 +239,14 @@ export function AutomationModule({ instance, dataSource, refreshSignal }: Automa
if (instance.authentication !== 'authenticated')
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Automation data can be read for this instance.
需要先完成认证,才能读取自动化数据。
</div>
);
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Automation unavailable">
No safe Automation read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="自动化不可用">
没有可用的安全自动化只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
@@ -256,17 +256,17 @@ export function AutomationModule({ instance, dataSource, refreshSignal }: Automa
: undefined;
if ((state.kind === 'idle' || state.kind === 'loading') && !retained)
return (
<p role="status" aria-label="Automation loading status">
Loading Automation…
<p role="status" aria-label="自动化加载状态">
正在加载自动化数据…
</p>
);
if (state.kind === 'error' && !retained)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Automation: {SAFE_LOAD_ERROR}</p>
<p>{SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Automation
重试加载自动化
</button>
</div>
);
@@ -276,18 +276,18 @@ export function AutomationModule({ instance, dataSource, refreshSignal }: Automa
if (!snapshot) return null;
return (
<div className="automation-module">
{state.kind === 'loading' ? <p role="status">Refreshing Automation…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新自动化数据…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Automation data.</p>
<p>刷新失败,正在显示上次已知的自动化数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Automation
重试加载自动化
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Automation freshness">
Automation data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="自动化数据时效性">
自动化数据可能不是最新数据,请核实时效性后再使用这些数值。
</p>
) : null}
<SnapshotView snapshot={snapshot} />
+15 -15
View File
@@ -40,16 +40,16 @@ describe('isolated Calls read module', () => {
it('loads only through the injected source and renders aggregate call and device status', async () => {
const pending = deferredSource();
render(<CallsModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Calls loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '通话加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const calls = await screen.findByRole('region', { name: 'Call status' });
const devices = screen.getByRole('region', { name: 'Device status' });
expect(within(calls).getByText('active')).toBeTruthy();
const calls = await screen.findByRole('region', { name: '通话状态' });
const devices = screen.getByRole('region', { name: '设备状态' });
expect(within(calls).getByText('活跃')).toBeTruthy();
expect(within(calls).getByText('2')).toBeTruthy();
expect(within(devices).getByText('available')).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T12:30:00Z/)).toBeTruthy();
expect(within(devices).getByText('可用')).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T12:30:00Z/)).toBeTruthy();
});
it('strictly allowlists aggregate fields and exposes no phone numbers, audio, logs, or controls', async () => {
@@ -62,7 +62,7 @@ describe('isolated Calls read module', () => {
devices: { ...snapshot.devices, phoneNumber: '+1-555-0102', audioUrl: 'secret-audio-url' },
} as CallsSnapshot;
render(<CallsModule instance={owner} dataSource={{ load: async () => unsafe }} />);
expect(await screen.findByText('available')).toBeTruthy();
expect(await screen.findByText('可用')).toBeTruthy();
expect(document.body.textContent).not.toMatch(/555|private|secret-audio/i);
expect(screen.queryByRole('button')).toBeNull();
});
@@ -75,12 +75,12 @@ describe('isolated Calls read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(load).not.toHaveBeenCalled();
rerender(<CallsModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Calls unavailable' }).textContent).toMatch(
/no safe calls read data source.*will not invent.*production endpoint/i,
expect(screen.getByRole('status', { name: '通话不可用' }).textContent).toMatch(
/没有可用的安全通话只读数据源.*不会臆造.*生产端点/i,
);
});
@@ -95,16 +95,16 @@ describe('isolated Calls read module', () => {
const { rerender } = render(
<CallsModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('available')).toBeTruthy();
expect(await screen.findByText('可用')).toBeTruthy();
rerender(<CallsModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/showing the last known Calls data/i);
expect(alert.textContent).toMatch(/正在显示上次已知的通话数据/i);
expect(alert.textContent).not.toMatch(/secret endpoint|telephone/i);
expect(screen.getByText('available')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Calls' }));
expect(screen.getByText('可用')).toBeTruthy();
await user.click(screen.getByRole('button', { name: '重试加载通话' }));
expect(
await within(screen.getByRole('region', { name: 'Call status' })).findByText('3'),
await within(screen.getByRole('region', { name: '通话状态' })).findByText('3'),
).toBeTruthy();
});
+30 -30
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Bounded values permitted in aggregate call/device status. */
export type CallsStatusValue = string | number | boolean | null;
@@ -49,23 +50,23 @@ type ReadState =
| { kind: 'ready'; snapshot: OwnedSnapshot }
| { kind: 'error'; snapshot?: OwnedSnapshot };
const SAFE_LOAD_ERROR = 'Calls data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载通话数据。';
const CALL_FIELDS = [
['State', 'state'],
['Total', 'total'],
['Active', 'active'],
['Ringing', 'ringing'],
['Held', 'held'],
['Failed', 'failed'],
['状态', 'state'],
['总计', 'total'],
['活动中', 'active'],
['响铃中', 'ringing'],
['保持中', 'held'],
['失败', 'failed'],
] as const;
const DEVICE_FIELDS = [
['State', 'state'],
['Total', 'total'],
['Online', 'online'],
['Offline', 'offline'],
['Busy', 'busy'],
['状态', 'state'],
['总计', 'total'],
['在线', 'online'],
['离线', 'offline'],
['忙碌', 'busy'],
] as const;
function Section({ label, children }: { label: string; children: ReactNode }) {
@@ -86,13 +87,13 @@ function AggregateFields({
}) {
const safeValues = values as Readonly<Record<string, CallsStatusValue | undefined>>;
const supplied = fields.filter(([, key]) => safeValues[key] !== undefined);
if (!supplied.length) return <p>No aggregate status was supplied.</p>;
if (!supplied.length) return <p>未提供汇总状态。</p>;
return (
<dl>
{supplied.map(([label, key]) => (
<div key={key}>
<dt>{label}</dt>
<dd>{safeValues[key] == null ? 'Unavailable' : String(safeValues[key])}</dd>
<dd>{displayValue(safeValues[key])}</dd>
</div>
))}
</dl>
@@ -102,12 +103,12 @@ function AggregateFields({
function SnapshotView({ snapshot }: { snapshot: CallsSnapshot }) {
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<div className="calls-grid">
<Section label="Call status">
<Section label="通话状态">
<AggregateFields values={snapshot.calls} fields={CALL_FIELDS} />
</Section>
<Section label="Device status">
<Section label="设备状态">
<AggregateFields values={snapshot.devices} fields={DEVICE_FIELDS} />
</Section>
</div>
@@ -162,15 +163,14 @@ export function CallsModule({ instance, dataSource, refreshSignal }: CallsModule
if (instance.authentication !== 'authenticated')
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Calls data can be read for this instance.
需要先完成认证,才能读取通话数据。
</div>
);
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Calls unavailable">
No safe Calls read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="通话不可用">
没有可用的安全通话只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
@@ -179,17 +179,17 @@ export function CallsModule({ instance, dataSource, refreshSignal }: CallsModule
if ((state.kind === 'idle' || state.kind === 'loading') && !snapshot)
return (
<p role="status" aria-label="Calls loading status">
Loading Calls…
<p role="status" aria-label="通话加载状态">
正在加载通话数据…
</p>
);
if (state.kind === 'error' && !snapshot)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Calls: {SAFE_LOAD_ERROR}</p>
<p>{SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Calls
重试加载通话
</button>
</div>
);
@@ -198,18 +198,18 @@ export function CallsModule({ instance, dataSource, refreshSignal }: CallsModule
return (
<div className="calls-module">
{state.kind === 'loading' ? <p role="status">Refreshing Calls…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新通话数据…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Calls data.</p>
<p>刷新失败,正在显示上次已知的通话数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Calls
重试加载通话
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Calls freshness">
Calls data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="通话数据时效性">
通话数据可能不是最新数据,请核实时效性后再使用这些数值。
</p>
) : null}
<SnapshotView snapshot={snapshot.value} />
+20 -19
View File
@@ -23,7 +23,7 @@ const owner: InstanceContext = {
const snapshot: CellularSnapshot = {
observedAt: '2026-07-17T12:00:00Z',
networkRegistration: { state: 'registered', roaming: false },
networkRegistration: { state: 'online', roaming: false },
signal: { rssi: '-71 dBm', quality: 82 },
cellsLocation: { cell: '12345', area: 19, latitude: null },
operators: { current: 'Example Mobile', available: 3 },
@@ -54,27 +54,30 @@ describe('Phase 6.2 Cellular read module', () => {
const pending = deferredSource();
render(<CellularModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Cellular loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '蜂窝网络加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
for (const heading of ['Network registration', 'Signal', 'Cells / location', 'Operators']) {
for (const heading of ['网络注册', '信号', '小区与位置', '运营商']) {
expect(await screen.findByRole('heading', { name: heading })).toBeTruthy();
}
expect(
within(screen.getByRole('region', { name: 'Signal' })).getByText('-71 dBm'),
).toBeTruthy();
expect(screen.getByText('Unavailable')).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T12:00:00Z/)).toBeTruthy();
expect(within(screen.getByRole('region', { name: '信号' })).getByText('-71 dBm')).toBeTruthy();
const registration = screen.getByRole('region', { name: '网络注册' });
expect(within(registration).getByText('在线')).toBeTruthy();
expect(within(registration).getByText('否')).toBeTruthy();
expect(within(registration).queryByText('online')).toBeNull();
expect(within(registration).queryByText('false')).toBeNull();
expect(screen.getByText('不可用')).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T12:00:00Z/)).toBeTruthy();
expect(screen.queryByRole('button', { name: /register|operator|network/i })).toBeNull();
expect(screen.getByText(/automatic network registration is available only/i)).toBeTruthy();
expect(screen.getByText(/audited R2 prepare, confirm, and execute flow/i)).toBeTruthy();
expect(screen.getByText(/自动网络注册仅可通过/i)).toBeTruthy();
expect(screen.getByText(/经审计的 R2 准备、确认和执行流程/i)).toBeTruthy();
});
it('is honest when no read source is injected', () => {
render(<CellularModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Cellular unavailable' }).textContent).toMatch(
/no safe cellular read data source.*will not invent.*production endpoint/i,
expect(screen.getByRole('status', { name: '蜂窝网络不可用' }).textContent).toMatch(
/没有可用的安全蜂窝网络只读数据源.*不会臆造.*生产端点/i,
);
});
@@ -95,7 +98,7 @@ describe('Phase 6.2 Cellular read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(screen.queryByText('-71 dBm')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
});
@@ -114,13 +117,11 @@ describe('Phase 6.2 Cellular read module', () => {
expect(await screen.findByText('-71 dBm')).toBeTruthy();
rerender(<CellularModule instance={owner} dataSource={source} refreshSignal={1} />);
expect(
await screen.findByText(/Refresh failed; showing the last known cellular data/i),
).toBeTruthy();
expect(await screen.findByText(/刷新失败,正在显示上次已知的蜂窝网络数据/i)).toBeTruthy();
expect(screen.getByText('-71 dBm')).toBeTruthy();
expect(screen.queryByText(/secret upstream details/i)).toBeNull();
await user.click(screen.getByRole('button', { name: 'Retry loading cellular data' }));
await user.click(screen.getByRole('button', { name: '重试加载蜂窝网络数据' }));
expect(await screen.findByText('-65 dBm')).toBeTruthy();
expect(load).toHaveBeenCalledTimes(3);
});
@@ -134,9 +135,9 @@ describe('Phase 6.2 Cellular read module', () => {
render(<CellularModule instance={owner} dataSource={{ load }} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/Cellular data could not be loaded/i);
expect(alert.textContent).toMatch(/无法加载蜂窝网络数据/i);
expect(alert.textContent).not.toMatch(/private failure/i);
await user.click(screen.getByRole('button', { name: 'Retry loading cellular data' }));
await user.click(screen.getByRole('button', { name: '重试加载蜂窝网络数据' }));
expect(await screen.findByText('-71 dBm')).toBeTruthy();
});
+41 -40
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Cellular data is deliberately limited to display-safe primitive values. */
export type CellularFieldValue = string | number | boolean | null;
@@ -59,42 +60,42 @@ type ReadState =
const SECTIONS = [
[
'networkRegistration',
'Network registration',
'网络注册',
[
['State', 'state'],
['Mode', 'mode'],
['Operator', 'operator'],
['Roaming', 'roaming'],
['状态', 'state', true],
['模式', 'mode', true],
['运营商', 'operator'],
['漫游', 'roaming', true],
],
],
[
'signal',
'Signal',
'信号',
[
['RSSI', 'rssi'],
['RSRP', 'rsrp'],
['RSRQ', 'rsrq'],
['SINR', 'sinr'],
['Quality', 'quality'],
['质量', 'quality', true],
],
],
[
'cellsLocation',
'Cells / location',
'小区与位置',
[
['Cell', 'cell'],
['Area', 'area'],
['Technology', 'technology'],
['Latitude', 'latitude'],
['Longitude', 'longitude'],
['小区', 'cell'],
['区域', 'area'],
['技术', 'technology', true],
['纬度', 'latitude'],
['经度', 'longitude'],
],
],
[
'operators',
'Operators',
'运营商',
[
['Current', 'current'],
['Available', 'available'],
['当前', 'current'],
['可用', 'available'],
],
],
] as const;
@@ -106,7 +107,7 @@ function StructuredSection({
}: {
label: string;
values: object;
fields: readonly (readonly [string, string])[];
fields: readonly (readonly [string, string, boolean?])[];
}) {
const safeValues = values as Readonly<Record<string, CellularFieldValue | undefined>>;
const entries = fields.filter(([, key]) => safeValues[key] !== undefined);
@@ -115,18 +116,20 @@ function StructuredSection({
<h2>{label}</h2>
{entries.length ? (
<dl>
{entries.map(([labelText, fieldKey]) => {
{entries.map(([labelText, fieldKey, localized]) => {
const value = safeValues[fieldKey];
return (
<div key={fieldKey}>
<dt>{labelText}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
<dd>
{localized ? displayValue(value) : value == null ? '不可用' : String(value)}
</dd>
</div>
);
})}
</dl>
) : (
<p>No {label.toLocaleLowerCase()} data was supplied.</p>
<p>未提供{label}数据。</p>
)}
</section>
);
@@ -169,7 +172,7 @@ export function CellularModule({ instance, dataSource, refreshSignal }: Cellular
: undefined;
return {
kind: 'error',
message: 'Cellular data could not be loaded.',
message: '无法加载蜂窝网络数据。',
...(snapshot ? { snapshot } : {}),
};
});
@@ -182,16 +185,15 @@ export function CellularModule({ instance, dataSource, refreshSignal }: Cellular
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before cellular data can be read for this instance.
需要先完成认证,才能读取蜂窝网络数据。
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="Cellular unavailable">
No safe cellular read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="蜂窝网络不可用">
没有可用的安全蜂窝网络只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
}
@@ -201,8 +203,8 @@ export function CellularModule({ instance, dataSource, refreshSignal }: Cellular
if ((state.kind === 'idle' || state.kind === 'loading') && !ownedSnapshot) {
return (
<p role="status" aria-label="Cellular loading status">
Loading cellular data…
<p role="status" aria-label="蜂窝网络加载状态">
正在加载蜂窝网络数据…
</p>
);
}
@@ -210,9 +212,9 @@ export function CellularModule({ instance, dataSource, refreshSignal }: Cellular
if (state.kind === 'error' && !ownedSnapshot) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load cellular data: {state.message}</p>
<p>{state.message}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading cellular data
重试加载蜂窝网络数据
</button>
</div>
);
@@ -223,32 +225,31 @@ export function CellularModule({ instance, dataSource, refreshSignal }: Cellular
return (
<div className="cellular-module">
{state.kind === 'loading' ? <p role="status">Refreshing cellular data…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新蜂窝网络数据…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known cellular data.</p>
<p>刷新失败,正在显示上次已知的蜂窝网络数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading cellular data
重试加载蜂窝网络数据
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Cellular freshness">
Cellular data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="蜂窝网络数据新鲜度">
蜂窝网络数据可能已过期;使用这些值前请核实数据新鲜度。
</p>
) : null}
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<div className="cellular-grid">
{SECTIONS.map(([key, label, fields]) => (
<StructuredSection key={key} label={label} values={snapshot[key]} fields={fields} />
))}
</div>
<section className="state-panel" aria-label="Network registration operations">
<h2>Network registration operations</h2>
<section className="state-panel" aria-label="网络注册操作">
<h2>网络注册操作</h2>
<p>
Automatic network registration is available only through the audited R2 prepare, confirm,
and execute flow. Manual registration and monitoring controls remain unavailable. This
read-only panel does not bypass those gates.
自动网络注册仅可通过经审计的 R2 准备、确认和执行流程进行。手动注册和监控控件仍不可用。
此只读面板不会绕过这些限制。
</p>
</section>
</div>
@@ -26,8 +26,8 @@ const snapshot: DeviceNetworkSnapshot = {
wlan: {
status: {
enabled: true,
radioState: 'on',
connectionState: 'connected',
radioState: 'enabled',
connectionState: 'active',
activeProfile: 'office',
ssid: 'Operations Wi-Fi',
},
@@ -101,22 +101,30 @@ describe('Phase 6.3 Device Network read module', () => {
const pending = deferredSource();
render(<DeviceNetworkModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Device Network loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '设备网络加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
expect(await screen.findByRole('heading', { name: 'WLAN status' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'WLAN profiles' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'Interfaces and addresses' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS status' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS configuration' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS log summary' })).toBeTruthy();
expect(await screen.findByRole('heading', { name: 'WLAN 状态' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'WLAN 配置文件' })).toBeTruthy();
expect(screen.getByRole('heading', { name: '接口与地址' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS 状态' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS 配置' })).toBeTruthy();
expect(screen.getByRole('heading', { name: 'DDNS 日志摘要' })).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'WLAN profiles' })).getByText('WPA3'),
within(screen.getByRole('region', { name: 'WLAN 配置文件' })).getByText('WPA3'),
).toBeTruthy();
const wlanStatus = screen.getByRole('region', { name: 'WLAN 状态' });
expect(within(wlanStatus).getAllByText('已启用')).toHaveLength(2);
expect(within(wlanStatus).getByText('活跃')).toBeTruthy();
expect(within(wlanStatus).getByText('是')).toBeTruthy();
expect(within(wlanStatus).queryByText('enabled')).toBeNull();
expect(within(wlanStatus).queryByText('active')).toBeNull();
expect(within(wlanStatus).queryByText('true')).toBeNull();
expect(screen.getByText('192.0.2.10/24')).toBeTruthy();
expect(screen.getByText('gateway.example.test')).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T11:00:00Z/)).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T11:00:00Z/)).toBeTruthy();
});
it('does not render passwords, credentials, arbitrary fields, log bodies, or write controls', async () => {
@@ -127,14 +135,14 @@ describe('Phase 6.3 Device Network read module', () => {
expect(document.body.textContent).not.toContain('private-password');
expect(screen.queryByText(/password/i)).toBeNull();
expect(screen.queryByRole('button')).toBeNull();
expect(screen.getByText(/R1.*unavailable.*executable backend support/i)).toBeTruthy();
expect(screen.getByText(/R2.*unavailable.*executable backend support/i)).toBeTruthy();
expect(screen.getByText(/可执行后端支持.*R1.*不可用/i)).toBeTruthy();
expect(screen.getByText(/可执行后端支持.*R2.*不可用/i)).toBeTruthy();
});
it('is honest when no source is injected and does not invent an endpoint', () => {
render(<DeviceNetworkModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Device Network unavailable' }).textContent).toMatch(
/no safe device network read data source.*uncontracted production endpoint/i,
expect(screen.getByRole('status', { name: '设备网络不可用' }).textContent).toMatch(
/没有可用的安全设备网络只读数据源.*未签订契约的生产端点/i,
);
});
@@ -155,7 +163,7 @@ describe('Phase 6.3 Device Network read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(screen.queryByText('gateway.example.test')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
});
@@ -169,9 +177,9 @@ describe('Phase 6.3 Device Network read module', () => {
render(<DeviceNetworkModule instance={owner} dataSource={{ load }} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Device Network data could not be loaded.');
expect(alert.textContent).toContain('无法加载设备网络数据。');
expect(alert.textContent).not.toContain('secret upstream');
await user.click(screen.getByRole('button', { name: 'Retry loading Device Network' }));
await user.click(screen.getByRole('button', { name: '重试加载设备网络' }));
expect(await screen.findByText('gateway.example.test')).toBeTruthy();
expect(load).toHaveBeenCalledTimes(2);
});
@@ -193,9 +201,9 @@ describe('Phase 6.3 Device Network read module', () => {
expect(await screen.findByText('gateway.example.test')).toBeTruthy();
rerender(<DeviceNetworkModule instance={owner} dataSource={source} refreshSignal={1} />);
expect((await screen.findByRole('alert')).textContent).toMatch(/showing the last known/i);
expect((await screen.findByRole('alert')).textContent).toMatch(/正在显示上次已知/i);
expect(screen.getByText('gateway.example.test')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Device Network' }));
await user.click(screen.getByRole('button', { name: '重试加载设备网络' }));
expect(await screen.findByText('new.example.test')).toBeTruthy();
});
@@ -250,9 +258,9 @@ describe('Phase 6.3 Device Network read module', () => {
dataSource={{ load: async () => snapshot }}
/>,
);
expect(
(await screen.findByRole('status', { name: 'Device Network freshness' })).textContent,
).toMatch(/stale/i);
expect((await screen.findByRole('status', { name: '设备网络数据新鲜度' })).textContent).toMatch(
/可能已过期/i,
);
expect(screen.getByText('gateway.example.test')).toBeTruthy();
});
});
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Only these bounded primitives may cross the injected read boundary into the UI. */
export type DeviceNetworkValue = string | number | boolean | null;
@@ -90,23 +91,27 @@ type ReadState =
| { kind: 'ready'; ownerId: string; snapshot: DeviceNetworkSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: DeviceNetworkSnapshot };
const SAFE_LOAD_ERROR = 'Device Network data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载设备网络数据。';
function display(value: DeviceNetworkValue | undefined): string {
return value == null ? 'Unavailable' : String(value);
function displayRaw(value: DeviceNetworkValue | undefined): string {
return value == null ? '不可用' : String(value);
}
function Fields({
values,
}: {
values: readonly (readonly [label: string, value: DeviceNetworkValue | undefined])[];
values: readonly (readonly [
label: string,
value: DeviceNetworkValue | undefined,
localized?: boolean,
])[];
}) {
return (
<dl>
{values.map(([label, value]) => (
{values.map(([label, value, localized]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{display(value)}</dd>
<dd>{localized ? displayValue(value) : displayRaw(value)}</dd>
</div>
))}
</dl>
@@ -129,115 +134,115 @@ function SnapshotView({ snapshot }: { snapshot: DeviceNetworkSnapshot }) {
const logs = snapshot.ddns.logSummary;
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<div className="device-network-grid">
<Section label="WLAN status">
<Section label="WLAN 状态">
<Fields
values={[
['Enabled', status.enabled],
['Radio state', status.radioState],
['Connection state', status.connectionState],
['Active profile', status.activeProfile],
['已启用', status.enabled, true],
['无线电状态', status.radioState, true],
['连接状态', status.connectionState, true],
['活动配置文件', status.activeProfile],
['SSID', status.ssid],
]}
/>
</Section>
<Section label="WLAN profiles">
<Section label="WLAN 配置文件">
{snapshot.wlan.profiles.length ? (
snapshot.wlan.profiles.map((profile, index) => (
<article key={`${profile.name ?? 'profile'}-${index}`}>
<Fields
values={[
['Name', profile.name],
['名称', profile.name],
['SSID', profile.ssid],
['Security', profile.security],
['Enabled', profile.enabled],
['Priority', profile.priority],
['安全性', profile.security, true],
['已启用', profile.enabled, true],
['优先级', profile.priority],
]}
/>
</article>
))
) : (
<p>No WLAN profiles were supplied.</p>
<p>未提供 WLAN 配置。</p>
)}
</Section>
<Section label="Interfaces and addresses">
<Section label="接口与地址">
{snapshot.interfaces.length ? (
snapshot.interfaces.map((networkInterface, index) => (
<article key={`${networkInterface.name ?? 'interface'}-${index}`}>
<Fields
values={[
['Name', networkInterface.name],
['Kind', networkInterface.kind],
['State', networkInterface.state],
['MAC address', networkInterface.macAddress],
['名称', networkInterface.name],
['类型', networkInterface.kind, true],
['状态', networkInterface.state, true],
['MAC 地址', networkInterface.macAddress],
['MTU', networkInterface.mtu],
]}
/>
<h3>Addresses</h3>
<h3>地址</h3>
{networkInterface.addresses.length ? (
<ul>
{networkInterface.addresses.map((address, addressIndex) => (
<li key={`${address.address ?? 'address'}-${addressIndex}`}>
<Fields
values={[
['Family', address.family],
['地址族', address.family, true],
[
'Address',
'地址',
address.address == null
? null
: address.prefixLength == null
? address.address
: `${address.address}/${address.prefixLength}`,
],
['Scope', address.scope],
['作用域', address.scope, true],
]}
/>
</li>
))}
</ul>
) : (
<p>No addresses were supplied.</p>
<p>未提供地址。</p>
)}
</article>
))
) : (
<p>No network interfaces were supplied.</p>
<p>未提供网络接口。</p>
)}
</Section>
<Section label="DDNS status">
<Section label="DDNS 状态">
<Fields
values={[
['Enabled', ddnsStatus.enabled],
['State', ddnsStatus.state],
['Last update', ddnsStatus.lastUpdateAt],
['已启用', ddnsStatus.enabled, true],
['状态', ddnsStatus.state, true],
['最近更新', ddnsStatus.lastUpdateAt],
]}
/>
</Section>
<Section label="DDNS configuration">
<Section label="DDNS 配置">
<Fields
values={[
['Provider', config.provider],
['Hostname', config.hostname],
['Update interval (seconds)', config.updateIntervalSeconds],
['服务商', config.provider],
['主机名', config.hostname],
['更新间隔(秒)', config.updateIntervalSeconds],
]}
/>
</Section>
<Section label="DDNS log summary">
<Section label="DDNS 日志摘要">
<Fields
values={[
['Total entries', logs.totalEntries],
['Successful updates', logs.successfulUpdates],
['Failed updates', logs.failedUpdates],
['Last event', logs.lastEventAt],
['条目总数', logs.totalEntries],
['成功更新', logs.successfulUpdates],
['失败更新', logs.failedUpdates],
['最近事件', logs.lastEventAt],
]}
/>
</Section>
</div>
<section className="state-panel" aria-label="Device Network actions">
<h2>Device Network actions</h2>
<p>R1 configuration actions are unavailable until executable backend support exists.</p>
<p>R2 operational actions are unavailable until executable backend support exists.</p>
<section className="state-panel" aria-label="设备网络操作">
<h2>设备网络操作</h2>
<p>在具备可执行后端支持前,R1 配置操作不可用。</p>
<p>在具备可执行后端支持前,R2 运行操作不可用。</p>
</section>
</>
);
@@ -294,16 +299,15 @@ export function DeviceNetworkModule({
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Device Network data can be read for this instance.
需要先完成认证,才能读取设备网络数据。
</div>
);
}
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Device Network unavailable">
No safe Device Network read data source is available. This console will not invent or call
an uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="设备网络不可用">
没有可用的安全设备网络只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
@@ -314,17 +318,17 @@ export function DeviceNetworkModule({
if ((state.kind === 'idle' || state.kind === 'loading') && !retainedSnapshot)
return (
<p role="status" aria-label="Device Network loading status">
Loading Device Network…
<p role="status" aria-label="设备网络加载状态">
正在加载设备网络…
</p>
);
if (state.kind === 'error' && !retainedSnapshot)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Device Network: {SAFE_LOAD_ERROR}</p>
<p>{SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Device Network
重试加载设备网络
</button>
</div>
);
@@ -335,19 +339,18 @@ export function DeviceNetworkModule({
return (
<div className="device-network-module">
{state.kind === 'loading' ? <p role="status">Refreshing Device Network…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新设备网络…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Device Network data.</p>
<p>刷新失败,正在显示上次已知的设备网络数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Device Network
重试加载设备网络
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Device Network freshness">
Device Network data is {instance.freshness}; verify freshness before relying on these
values.
<p className="state-panel" role="status" aria-label="设备网络数据新鲜度">
设备网络数据可能已过期;使用这些值前请核实数据新鲜度。
</p>
) : null}
<SnapshotView snapshot={currentSnapshot} />
+21 -23
View File
@@ -36,19 +36,19 @@ describe('isolated safe eSIM read module', () => {
render(<EsimModule instance={owner} dataSource={pending.source} />);
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const section = await screen.findByRole('region', { name: 'eSIM safe summary' });
const section = await screen.findByRole('region', { name: 'eSIM 安全摘要' });
expect(within(section).getByText('4')).toBeTruthy();
expect(within(section).getByText('2')).toBeTruthy();
expect(within(section).getByText('available')).toBeTruthy();
expect(within(section).getByText('idle')).toBeTruthy();
expect(within(section).getByText('Provisioned')).toBeTruthy();
expect(within(section).getByText('可用')).toBeTruthy();
expect(within(section).getByText('空闲')).toBeTruthy();
expect(within(section).getByText('已配置')).toBeTruthy();
expect(screen.queryByRole('button')).toBeNull();
});
it('has no endpoint fallback and requires the exact authenticated owner', async () => {
const load = vi.fn<EsimDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<EsimModule instance={owner} dataSource={{ load }} />);
expect(await screen.findByText('Provisioned')).toBeTruthy();
expect(await screen.findByText('已配置')).toBeTruthy();
rerender(
<EsimModule
instance={{
@@ -60,13 +60,13 @@ describe('isolated safe eSIM read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.queryByText('Provisioned')).toBeNull();
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(screen.queryByText('已配置')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
rerender(<EsimModule instance={owner} />);
expect(screen.getByRole('status', { name: 'eSIM unavailable' }).textContent).toMatch(
/no safe esim read data source.*will not invent or call.*endpoint/i,
expect(screen.getByRole('status', { name: 'eSIM 不可用' }).textContent).toMatch(
/没有可用的安全 eSIM 只读数据源.*不会臆造或调用.*端点/i,
);
});
@@ -91,7 +91,7 @@ describe('isolated safe eSIM read module', () => {
providerData: { secret: 'raw-provider-secret' },
} as unknown as EsimSnapshot;
render(<EsimModule instance={owner} dataSource={{ load: async () => hostile }} />);
expect(await screen.findByText('Enabled')).toBeTruthy();
expect(await screen.findByText('已启用')).toBeTruthy();
for (const forbidden of [
'EID-secret',
'ICCID-secret',
@@ -109,7 +109,7 @@ describe('isolated safe eSIM read module', () => {
expect(document.body.textContent).not.toContain(forbidden);
expect(document.querySelector('img')).toBeNull();
expect(document.querySelector('script')).toBeNull();
expect(screen.getAllByText('Unavailable')).toHaveLength(4);
expect(screen.getAllByText('不可用')).toHaveLength(4);
});
it('retains same-owner data during refresh and uses fixed safe errors with retry', async () => {
@@ -123,14 +123,12 @@ describe('isolated safe eSIM read module', () => {
const { rerender } = render(
<EsimModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('Provisioned')).toBeTruthy();
expect(await screen.findByText('已配置')).toBeTruthy();
rerender(<EsimModule instance={owner} dataSource={source} refreshSignal={1} />);
expect(
await screen.findByText(/Refresh failed; showing the last known eSIM summary/i),
).toBeTruthy();
expect(await screen.findByText(/刷新失败,正在显示上次已知的 eSIM 摘要/i)).toBeTruthy();
expect(document.body.textContent).not.toContain('EID-secret');
expect(screen.getByText('4')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading eSIM data' }));
await user.click(screen.getByRole('button', { name: '重试加载 eSIM 数据' }));
expect(await screen.findByText('5')).toBeTruthy();
});
@@ -146,7 +144,7 @@ describe('isolated safe eSIM read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('eSIM data could not be loaded.');
expect(alert.textContent).toContain('无法加载 eSIM 数据。');
expect(alert.textContent).not.toContain('private code');
});
@@ -163,8 +161,8 @@ describe('isolated safe eSIM read module', () => {
expect(alphaSignal?.aborted).toBe(true);
alpha.resolve({ ...snapshot, labels: ['Error'] });
bravo.resolve({ ...snapshot, labels: ['Pending'] });
expect(await screen.findByText('Pending')).toBeTruthy();
expect(screen.queryByText('Error')).toBeNull();
expect(await screen.findByText('等待中')).toBeTruthy();
expect(screen.queryByText('错误')).toBeNull();
});
it('caps labels at eight and defensively sanitizes unknown and null payloads', async () => {
@@ -176,13 +174,13 @@ describe('isolated safe eSIM read module', () => {
const { rerender } = render(
<EsimModule instance={owner} dataSource={{ load: async () => ({ labels }) }} />,
);
const summary = await screen.findByRole('region', { name: 'eSIM safe summary' });
const summary = await screen.findByRole('region', { name: 'eSIM 安全摘要' });
expect(within(summary).getAllByRole('listitem')).toHaveLength(8);
rerender(
<EsimModule instance={owner} dataSource={{ load: async () => null }} refreshSignal={1} />,
);
expect(await screen.findByText('No safe labels were supplied.')).toBeTruthy();
expect(screen.getAllByText('Unavailable')).toHaveLength(4);
expect(await screen.findByText('未提供安全标签。')).toBeTruthy();
expect(screen.getAllByText('不可用')).toHaveLength(4);
});
it('turns a synchronous source throw into the fixed initial-load error', async () => {
@@ -197,7 +195,7 @@ describe('isolated safe eSIM read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('eSIM data could not be loaded.');
expect(alert.textContent).toContain('无法加载 eSIM 数据。');
expect(alert.textContent).not.toContain('private eSIM identifier');
});
});
+32 -24
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Bounded status vocabularies are the only strings this module may present. */
export type EsimLpacStatus = 'available' | 'unavailable' | 'degraded' | 'unknown';
@@ -53,7 +54,7 @@ const SAFE_LABELS = new Set<EsimSafeLabel>([
'Pending',
'Error',
]);
const SAFE_LOAD_ERROR = 'eSIM data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载 eSIM 数据。';
const MAX_SAFE_LABELS = 8;
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
@@ -93,40 +94,48 @@ function sanitizeSnapshot(value: unknown): SafeSnapshot {
}
function display(value: number | string | undefined): string {
return value === undefined ? 'Unavailable' : String(value);
return displayValue(value);
}
const SAFE_LABEL_DISPLAY_VALUES: Readonly<Record<EsimSafeLabel, string>> = {
Provisioned: '已配置',
Enabled: 'enabled',
Disabled: 'disabled',
Pending: 'pending',
Error: 'error',
};
function SnapshotView({ snapshot }: { snapshot: SafeSnapshot }) {
return (
<section className="esim-card" aria-label="eSIM safe summary">
<h2>eSIM safe summary</h2>
<section className="esim-card" aria-label="eSIM 安全摘要">
<h2>eSIM 安全摘要</h2>
<dl>
<div>
<dt>Profile count</dt>
<dt>配置数量</dt>
<dd>{display(snapshot.profileCount)}</dd>
</div>
<div>
<dt>Enabled profile count</dt>
<dt>已启用配置数量</dt>
<dd>{display(snapshot.enabledProfileCount)}</dd>
</div>
<div>
<dt>lpac status</dt>
<dt>LPAC 状态</dt>
<dd>{display(snapshot.lpacStatus)}</dd>
</div>
<div>
<dt>Work mode</dt>
<dt>工作模式</dt>
<dd>{display(snapshot.workMode)}</dd>
</div>
</dl>
<h3>Safe labels</h3>
<h3>安全标签</h3>
{snapshot.labels.length ? (
<ul>
{snapshot.labels.map((label, index) => (
<li key={`${label}-${index}`}>{label}</li>
<li key={`${label}-${index}`}>{displayValue(SAFE_LABEL_DISPLAY_VALUES[label])}</li>
))}
</ul>
) : (
<p>No safe labels were supplied.</p>
<p>未提供安全标签。</p>
)}
</section>
);
@@ -187,16 +196,15 @@ export function EsimModule({ instance, dataSource, refreshSignal }: EsimModulePr
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before eSIM data can be read for this instance.
需要先完成认证,才能读取 eSIM 数据。
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="eSIM unavailable">
No safe eSIM read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="eSIM 不可用">
没有可用的安全 eSIM 只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
}
@@ -208,8 +216,8 @@ export function EsimModule({ instance, dataSource, refreshSignal }: EsimModulePr
if ((state.kind === 'idle' || state.kind === 'loading') && !retained) {
return (
<p role="status" aria-label="eSIM loading status">
Loading eSIM summary…
<p role="status" aria-label="eSIM 加载状态">
正在加载 eSIM 摘要…
</p>
);
}
@@ -217,9 +225,9 @@ export function EsimModule({ instance, dataSource, refreshSignal }: EsimModulePr
if (state.kind === 'error' && !retained) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load eSIM data: {SAFE_LOAD_ERROR}</p>
<p>{SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading eSIM data
重试加载 eSIM 数据
</button>
</div>
);
@@ -231,18 +239,18 @@ export function EsimModule({ instance, dataSource, refreshSignal }: EsimModulePr
return (
<div className="esim-module">
{state.kind === 'loading' ? <p role="status">Refreshing eSIM summary…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新 eSIM 摘要…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known eSIM summary.</p>
<p>刷新失败,正在显示上次已知的 eSIM 摘要。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading eSIM data
重试加载 eSIM 数据
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="eSIM freshness">
eSIM data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="eSIM 数据时效性">
eSIM 数据可能不是最新数据,请核实时效性后再使用这些数值。
</p>
) : null}
<SnapshotView snapshot={snapshot} />
+24 -26
View File
@@ -33,13 +33,13 @@ describe('Instance CRUD form', () => {
const source = dataSource();
render(<InstanceEditor mode="create" dataSource={source} />);
await user.type(screen.getByLabelText('Name'), 'Lab modem');
await user.type(screen.getByLabelText('Origin'), 'https://lab.example/admin');
await user.type(screen.getByLabelText('Tags'), 'lab, west, lab');
await user.selectOptions(screen.getByLabelText('Authentication method'), 'password');
await user.type(screen.getByLabelText('Password'), 'do-not-render');
await user.type(screen.getByLabelText('名称'), 'Lab modem');
await user.type(screen.getByLabelText('源地址'), 'https://lab.example/admin');
await user.type(screen.getByLabelText('标签'), 'lab, west, lab');
await user.selectOptions(screen.getByLabelText('认证方式'), 'password');
await user.type(screen.getByLabelText('密码'), 'do-not-render');
expect(document.body.textContent).not.toContain('do-not-render');
await user.click(screen.getByRole('button', { name: 'Add instance' }));
await user.click(screen.getByRole('button', { name: '添加实例' }));
expect(source.create).toHaveBeenCalledWith({
name: 'Lab modem',
@@ -58,17 +58,17 @@ describe('Instance CRUD form', () => {
expect(await screen.findByDisplayValue('Owner modem')).toBeTruthy();
expect(source.get).toHaveBeenCalledWith('owner');
await user.clear(screen.getByLabelText('Name'));
await user.type(screen.getByLabelText('Name'), 'Renamed');
await user.click(screen.getByRole('button', { name: 'Save changes' }));
await user.clear(screen.getByLabelText('名称'));
await user.type(screen.getByLabelText('名称'), 'Renamed');
await user.click(screen.getByRole('button', { name: '保存更改' }));
expect(source.update).toHaveBeenLastCalledWith(
'owner',
3,
expect.objectContaining({ password: { action: 'preserve' } }),
);
await user.selectOptions(screen.getByLabelText('Password action'), 'clear');
await user.click(screen.getByRole('button', { name: 'Save changes' }));
await user.selectOptions(screen.getByLabelText('密码操作'), 'clear');
await user.click(screen.getByRole('button', { name: '保存更改' }));
expect(source.update).toHaveBeenLastCalledWith(
'owner',
4,
@@ -76,7 +76,7 @@ describe('Instance CRUD form', () => {
);
rerender(<InstanceEditor mode="edit" instanceId="intruder" dataSource={source} />);
expect((await screen.findByRole('alert')).textContent).toMatch(/does not match this route/i);
expect((await screen.findByRole('alert')).textContent).toMatch(/实例操作失败,请稍后重试/i);
expect(screen.queryByDisplayValue('Owner modem')).toBeNull();
});
@@ -84,19 +84,17 @@ describe('Instance CRUD form', () => {
const user = userEvent.setup();
const source = dataSource({
update: vi.fn(async () => {
throw new Error('Could not save this instance.');
throw new Error('无法保存此实例。');
}),
});
render(<InstanceEditor mode="edit" instanceId="owner" dataSource={source} />);
await screen.findByDisplayValue('Owner modem');
await user.click(screen.getByRole('button', { name: 'Test connection' }));
expect((await screen.findByRole('status')).textContent).toMatch(/reachable and authenticated/i);
await user.click(screen.getByRole('button', { name: '测试连接' }));
expect((await screen.findByRole('status')).textContent).toMatch(/连接可达且已认证/i);
await user.click(screen.getByRole('button', { name: 'Save changes' }));
expect((await screen.findByRole('alert')).textContent).toContain(
'Could not save this instance.',
);
await user.type(screen.getByLabelText('Name'), ' still here');
await user.click(screen.getByRole('button', { name: '保存更改' }));
expect((await screen.findByRole('alert')).textContent).toContain('实例操作失败,请稍后重试。');
await user.type(screen.getByLabelText('名称'), ' still here');
expect(screen.getByRole('alert')).toBeTruthy();
});
@@ -105,12 +103,12 @@ describe('Instance CRUD form', () => {
const source = dataSource();
render(<InstanceEditor mode="edit" instanceId="owner" dataSource={source} />);
await screen.findByDisplayValue('Owner modem');
await user.click(screen.getByRole('button', { name: 'Delete instance' }));
expect(
(screen.getByRole('button', { name: 'Confirm deletion' }) as HTMLButtonElement).disabled,
).toBe(true);
await user.type(screen.getByLabelText('Type owner to confirm'), 'owner');
await user.click(screen.getByRole('button', { name: 'Confirm deletion' }));
await user.click(screen.getByRole('button', { name: '删除实例' }));
expect((screen.getByRole('button', { name: '确认删除' }) as HTMLButtonElement).disabled).toBe(
true,
);
await user.type(screen.getByLabelText('输入 owner 以确认'), 'owner');
await user.click(screen.getByRole('button', { name: '确认删除' }));
expect(source.delete).toHaveBeenCalledWith('owner', 3);
});
});
+39 -40
View File
@@ -16,8 +16,8 @@ export interface InstanceEditorProps {
}
type PasswordAction = 'preserve' | 'set' | 'clear';
function message(error: unknown): string {
return error instanceof Error && error.message ? error.message : 'The instance operation failed.';
function message(): string {
return '实例操作失败,请稍后重试。';
}
export function InstanceEditor({
@@ -43,7 +43,7 @@ export function InstanceEditor({
useEffect(() => {
if (mode !== 'edit') return;
if (!instanceId) {
setError('The instance route is missing an owner.');
setError('实例路由缺少所有者。');
return;
}
let active = true;
@@ -53,8 +53,7 @@ export function InstanceEditor({
.get(instanceId)
.then((loaded) => {
if (!active) return;
if (loaded.id !== instanceId)
throw new Error('The returned instance does not match this route.');
if (loaded.id !== instanceId) throw new Error('返回的实例与当前路由不匹配。');
setOwner(loaded);
setName(loaded.name);
setOrigin(loaded.origin);
@@ -63,7 +62,7 @@ export function InstanceEditor({
setPasswordAction('preserve');
setPassword('');
})
.catch((cause: unknown) => active && setError(message(cause)));
.catch(() => active && setError(message()));
return () => {
active = false;
};
@@ -88,14 +87,14 @@ export function InstanceEditor({
? await dataSource.create(input)
: await dataSource.update(owner!.id, owner!.revision, input);
if (mode === 'edit' && saved.id !== instanceId)
throw new Error('The returned instance does not match this route.');
throw new Error('返回的实例与当前路由不匹配。');
setOwner(saved);
setPassword('');
setPasswordAction('preserve');
setStatus(mode === 'create' ? 'Instance added.' : 'Changes saved.');
setStatus(mode === 'create' ? '实例已添加。' : '更改已保存。');
setError(undefined);
} catch (cause) {
setError(message(cause));
} catch {
setError(message());
} finally {
setBusy(false);
}
@@ -109,13 +108,13 @@ export function InstanceEditor({
setStatus(
result.reachable
? result.authenticated
? 'Connection is reachable and authenticated.'
: 'Connection is reachable; authentication is required.'
: 'Connection is not reachable.',
? '连接可达且已认证。'
: '连接可达,但需要认证。'
: '连接不可达。',
);
setError(undefined);
} catch (cause) {
setError(message(cause));
} catch {
setError(message());
} finally {
setBusy(false);
}
@@ -126,11 +125,11 @@ export function InstanceEditor({
setBusy(true);
try {
await dataSource.delete(owner.id, owner.revision);
setStatus('Instance deletion accepted.');
setStatus('实例删除请求已接受。');
setError(undefined);
setConfirming(false);
} catch (cause) {
setError(message(cause));
} catch {
setError(message());
} finally {
setBusy(false);
}
@@ -142,12 +141,12 @@ export function InstanceEditor({
{error}
</p>
) : (
<p role="status">Loading instance…</p>
<p role="status">正在加载实例…</p>
);
return (
<section className="instance-editor">
<h1>{mode === 'create' ? 'Add instance' : 'Instance settings'}</h1>
<h1>{mode === 'create' ? '添加实例' : '实例设置'}</h1>
{error ? (
<p role="alert" className="state-panel state-error">
{error}
@@ -160,11 +159,11 @@ export function InstanceEditor({
) : null}
<form onSubmit={submit}>
<label>
Name
名称
<input required value={name} onChange={(event) => setName(event.target.value)} />
</label>
<label>
Origin
源地址
<input
required
type="url"
@@ -173,29 +172,29 @@ export function InstanceEditor({
/>
</label>
<label>
Tags
标签
<input
value={tags}
onChange={(event) => setTags(event.target.value)}
aria-describedby="tags-help"
/>
</label>
<small id="tags-help">Comma-separated tags</small>
<small id="tags-help">使用英文逗号分隔标签</small>
<label>
Authentication method
认证方式
<select
value={authMethod}
onChange={(event) => setAuthMethod(event.target.value as 'none' | 'password')}
>
<option value="none">None</option>
<option value="password">Password</option>
<option value="none">无</option>
<option value="password">密码</option>
</select>
</label>
{authMethod === 'password' ? (
<>
{mode === 'edit' ? (
<label>
Password action
密码操作
<select
value={passwordAction}
onChange={(event) => {
@@ -203,15 +202,15 @@ export function InstanceEditor({
setPassword('');
}}
>
<option value="preserve">Keep saved password</option>
<option value="set">Set new password</option>
<option value="clear">Clear saved password</option>
<option value="preserve">保留已保存的密码</option>
<option value="set">设置新密码</option>
<option value="clear">清除已保存的密码</option>
</select>
</label>
) : null}
{mode === 'create' || passwordAction === 'set' ? (
<label>
Password
密码
<input
required
type="password"
@@ -225,27 +224,27 @@ export function InstanceEditor({
) : null}
<div className="form-actions">
<button disabled={busy} type="submit">
{mode === 'create' ? 'Add instance' : 'Save changes'}
{mode === 'create' ? '添加实例' : '保存更改'}
</button>
{mode === 'edit' ? (
<button disabled={busy} type="button" onClick={() => void testConnection()}>
Test connection
测试连接
</button>
) : null}
</div>
</form>
{mode === 'edit' ? (
<section className="danger-zone" aria-labelledby="danger-heading">
<h2 id="danger-heading">Danger zone</h2>
<h2 id="danger-heading">危险操作区</h2>
{!confirming ? (
<button type="button" onClick={() => setConfirming(true)}>
Delete instance
删除实例
</button>
) : (
<div>
<p>Deletion cannot be undone.</p>
<p>删除后无法撤销。</p>
<label>
Type {owner!.id} to confirm
输入 {owner!.id} 以确认
<input
value={confirmation}
onChange={(event) => setConfirmation(event.target.value)}
@@ -256,7 +255,7 @@ export function InstanceEditor({
type="button"
onClick={() => void remove()}
>
Confirm deletion
确认删除
</button>
<button
type="button"
@@ -265,7 +264,7 @@ export function InstanceEditor({
setConfirmation('');
}}
>
Cancel
取消
</button>
</div>
)}
+15 -15
View File
@@ -22,9 +22,9 @@ const owner: InstanceContext = {
const capabilities: InstanceCapabilityMap = {
overview: { state: 'supported' },
messages: { state: 'degraded', explanation: 'Message history is read-only.' },
calls: { state: 'unsupported', explanation: 'This modem has no voice support.' },
esim: { state: 'unknown', explanation: 'Capability discovery did not report eSIM.' },
messages: { state: 'degraded', explanation: '消息历史记录为只读。' },
calls: { state: 'unsupported', explanation: '此调制解调器不支持语音功能。' },
esim: { state: 'unknown', explanation: '能力探测结果未包含 eSIM。' },
};
describe('InstanceDetail', () => {
@@ -38,14 +38,14 @@ describe('InstanceDetail', () => {
/>,
);
expect(screen.getByRole('link', { name: 'Overview' }).getAttribute('href')).toBe(
expect(screen.getByRole('link', { name: '概览' }).getAttribute('href')).toBe(
'/instances/owner/overview',
);
expect(screen.queryByRole('link', { name: 'Messages' })).toBeNull();
expect(screen.getByText('Message history is read-only.')).toBeTruthy();
expect(screen.getByText('This modem has no voice support.')).toBeTruthy();
expect(screen.getByText('Capability discovery did not report eSIM.')).toBeTruthy();
expect(screen.getAllByText('Capability state is unknown.').length).toBeGreaterThan(0);
expect(screen.queryByRole('link', { name: '消息' })).toBeNull();
expect(screen.getByText('消息历史记录为只读。')).toBeTruthy();
expect(screen.getByText('此调制解调器不支持语音功能。')).toBeTruthy();
expect(screen.getByText('能力探测结果未包含 eSIM。')).toBeTruthy();
expect(screen.getAllByText('能力状态未知。').length).toBeGreaterThan(0);
});
it('never renders or loads context when the direct-route owner does not match', () => {
@@ -60,8 +60,8 @@ describe('InstanceDetail', () => {
);
expect(screen.queryByText('Owner modem')).toBeNull();
expect(screen.queryByRole('navigation', { name: 'Instance modules' })).toBeNull();
expect(screen.getByText(/Instance context is unavailable/i)).toBeTruthy();
expect(screen.queryByRole('navigation', { name: '实例模块' })).toBeNull();
expect(screen.getByText(/此路由缺少实例上下文/i)).toBeTruthy();
expect(load).not.toHaveBeenCalled();
});
@@ -98,7 +98,7 @@ describe('InstanceDetail', () => {
expect(pending.get('owner')?.signal.aborted).toBe(true);
pending.get('second')?.resolve({ overview: { state: 'supported' } });
expect(await screen.findByRole('link', { name: 'Overview' })).toBeTruthy();
expect(await screen.findByRole('link', { name: '概览' })).toBeTruthy();
pending
.get('owner')
?.resolve({ overview: { state: 'unsupported', explanation: 'Stale owner result' } });
@@ -107,9 +107,9 @@ describe('InstanceDetail', () => {
});
it.each<[InstanceModule, string]>([
['cellular', 'Cellular'],
['device-network', 'Device Network'],
['automation', 'Automation'],
['cellular', '蜂窝网络'],
['device-network', '设备网络'],
['automation', '自动化'],
['ota', 'OTA'],
])('uses the existing module contract for %s', (module, label) => {
render(
+25 -29
View File
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
import type { InstanceContext, InstanceModule } from '../app-shell.js';
import { canonicalHttpOrigin } from '../fleet/fleet-page.js';
import { displayStatus } from '../ui/locale.js';
export type CapabilityState = 'supported' | 'degraded' | 'unsupported' | 'unknown';
@@ -27,23 +28,23 @@ export interface InstanceDetailProps {
}
export const INSTANCE_MODULE_LABELS: Readonly<Record<InstanceModule, string>> = {
overview: 'Overview',
cellular: 'Cellular',
'device-network': 'Device Network',
messages: 'Messages',
calls: 'Calls',
overview: '概览',
cellular: '蜂窝网络',
'device-network': '设备网络',
messages: '消息',
calls: '通话',
esim: 'eSIM',
notifications: 'Notifications',
automation: 'Automation',
notifications: '通知',
automation: '自动化',
ota: 'OTA',
};
const MODULES = Object.keys(INSTANCE_MODULE_LABELS) as readonly InstanceModule[];
const DEFAULT_EXPLANATIONS: Readonly<Record<Exclude<CapabilityState, 'supported'>, string>> = {
degraded: 'This module is available with limited functionality.',
unsupported: 'This instance does not support this module.',
unknown: 'Capability state is unknown.',
degraded: '此模块可用,但功能受限。',
unsupported: '此实例不支持该模块。',
unknown: '能力状态未知。',
};
function capabilityFor(map: InstanceCapabilityMap, module: InstanceModule): InstanceCapability {
@@ -99,7 +100,7 @@ export function InstanceDetail({
(error: unknown) => {
void error;
if (!controller.signal.aborted && requestRef.current === request) {
setLoadError('Capability discovery failed.');
setLoadError('能力探测失败。');
setLoading(false);
}
},
@@ -111,7 +112,7 @@ export function InstanceDetail({
return (
<section>
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
<p>Instance context is unavailable for this route.</p>
<p>此路由缺少实例上下文。</p>
</section>
);
}
@@ -122,29 +123,29 @@ export function InstanceDetail({
return (
<section className="instance-detail">
<aside className="instance-context" aria-label="Current instance">
<aside className="instance-context" aria-label="当前实例">
<strong>{instance.name}</strong>
<code>{instance.id}</code>
<dl>
<div>
<dt>Status</dt>
<dd>{instance.status}</dd>
<dt>状态</dt>
<dd>{displayStatus(instance.status)}</dd>
</div>
<div>
<dt>Authentication</dt>
<dd>{instance.authentication}</dd>
<dt>认证</dt>
<dd>{displayStatus(instance.authentication)}</dd>
</div>
<div>
<dt>Freshness</dt>
<dd>{instance.freshness}</dd>
<dt>数据新鲜度</dt>
<dd>{displayStatus(instance.freshness)}</dd>
</div>
</dl>
{origin ? (
<a href={origin} target="_blank" rel="noopener noreferrer">
Open original site
打开源站
</a>
) : null}
<nav aria-label="Instance modules">
<nav aria-label="实例模块">
<ul>
{MODULES.map((item) => {
const capability = capabilityFor(map, item);
@@ -172,19 +173,14 @@ export function InstanceDetail({
</aside>
<div className="instance-module-detail">
<h1>{INSTANCE_MODULE_LABELS[module]}</h1>
{loading ? <p role="status">Loading capabilities…</p> : null}
{loadError ? <p role="alert">Capabilities unavailable: {loadError}</p> : null}
{loading ? <p role="status">正在加载能力…</p> : null}
{loadError ? <p role="alert">能力不可用:{loadError}</p> : null}
{!loading && canRender(activeCapability) ? (
<>
{activeCapability.state === 'degraded' ? (
<p data-capability-state="degraded">{explanation(activeCapability)}</p>
) : null}
{moduleContent ?? (
<p>
Inspect {INSTANCE_MODULE_LABELS[module].toLowerCase()} data and available
operations.
</p>
)}
{moduleContent ?? <p>查看{INSTANCE_MODULE_LABELS[module]}数据和可用操作。</p>}
</>
) : null}
{!loading && !canRender(activeCapability) ? (
+17 -17
View File
@@ -81,17 +81,19 @@ describe('Messages isolated read module', () => {
const pending = deferredSource();
render(<MessagesModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Messages loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '消息加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const sms = await screen.findByRole('region', { name: 'SMS aggregate' });
const sms = await screen.findByRole('region', { name: '短信汇总' });
expect(within(sms).getByText('42')).toBeTruthy();
expect(within(sms).getByText('2026-07-17T11:55:00Z')).toBeTruthy();
const devices = screen.getByRole('region', { name: 'Device message aggregates' });
const devices = screen.getByRole('region', { name: '设备消息汇总' });
expect(within(devices).getByText('modem-1')).toBeTruthy();
expect(within(devices).getByText('Primary modem')).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T12:00:00Z/)).toBeTruthy();
expect(within(devices).getByText('在线')).toBeTruthy();
expect(within(devices).queryByText('online')).toBeNull();
expect(screen.getByText(/观测时间:2026-07-17T12:00:00Z/)).toBeTruthy();
});
it('uses an explicit safe allowlist and exposes no bodies, content, recipients, credentials, or write actions', async () => {
@@ -111,16 +113,14 @@ describe('Messages isolated read module', () => {
}
expect(screen.queryByText(/recipient|phone number|password/i)).toBeNull();
expect(screen.queryByRole('button')).toBeNull();
expect(screen.getByText(/aggregate metadata only/i)).toBeTruthy();
expect(
screen.getByText(/sending, deleting, and changing messages are unavailable/i),
).toBeTruthy();
expect(screen.getByText(/仅显示汇总元数据/i)).toBeTruthy();
expect(screen.getByText(/此只读模块不支持发送、删除或修改消息/)).toBeTruthy();
});
it('does not invent an endpoint when no source is injected', () => {
render(<MessagesModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Messages unavailable' }).textContent).toMatch(
/no safe messages read data source.*uncontracted production endpoint/i,
expect(screen.getByRole('status', { name: '消息不可用' }).textContent).toMatch(
/没有可用的安全消息只读数据源.*未签订契约的生产端点/i,
);
});
@@ -135,7 +135,7 @@ describe('Messages isolated read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(screen.queryByText('Primary modem')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
});
@@ -149,9 +149,9 @@ describe('Messages isolated read module', () => {
render(<MessagesModule instance={owner} dataSource={{ load }} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Messages data could not be loaded.');
expect(alert.textContent).toContain('无法加载消息数据。');
expect(alert.textContent).not.toContain('secret URL');
await user.click(screen.getByRole('button', { name: 'Retry loading Messages' }));
await user.click(screen.getByRole('button', { name: '重试加载消息' }));
expect(await screen.findByText('Primary modem')).toBeTruthy();
expect(load).toHaveBeenCalledTimes(2);
});
@@ -171,9 +171,9 @@ describe('Messages isolated read module', () => {
rerender(<MessagesModule instance={owner} dataSource={source} refreshSignal={1} />);
expect(screen.getByText('Primary modem')).toBeTruthy();
expect((await screen.findByRole('alert')).textContent).toMatch(/showing the last known/i);
expect((await screen.findByRole('alert')).textContent).toMatch(/正在显示上次已知/i);
expect(screen.getByText('Primary modem')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Messages' }));
await user.click(screen.getByRole('button', { name: '重试加载消息' }));
expect(await screen.findByText('Replacement modem')).toBeTruthy();
});
@@ -207,8 +207,8 @@ describe('Messages isolated read module', () => {
dataSource={{ load: async () => snapshot }}
/>,
);
expect((await screen.findByRole('status', { name: 'Messages freshness' })).textContent).toMatch(
/stale/i,
expect((await screen.findByRole('status', { name: '消息数据新鲜度' })).textContent).toMatch(
/可能已过期/i,
);
expect(screen.getByText('Primary modem')).toBeTruthy();
});
+41 -37
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Bounded primitives permitted at the Messages presentation boundary. */
export type MessageMetadataValue = string | number | boolean | null;
@@ -47,33 +48,37 @@ type ReadState =
| { kind: 'ready'; ownerId: string; snapshot: MessagesSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: MessagesSnapshot };
const SAFE_LOAD_ERROR = 'Messages data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载消息数据。';
const AGGREGATE_FIELDS = [
['Total', 'total'],
['Inbound', 'inbound'],
['Outbound', 'outbound'],
['Unread', 'unread'],
['Failed', 'failed'],
['Queued', 'queued'],
['Last activity', 'lastActivityAt'],
['总计', 'total'],
['接收', 'inbound'],
['发送', 'outbound'],
['未读', 'unread'],
['失败', 'failed'],
['排队中', 'queued'],
['最近活动', 'lastActivityAt'],
] as const;
function display(value: MessageMetadataValue | undefined): string {
return value == null ? 'Unavailable' : String(value);
function displayRaw(value: MessageMetadataValue | undefined): string {
return value == null ? '不可用' : String(value);
}
function Fields({
values,
}: {
values: readonly (readonly [label: string, value: MessageMetadataValue | undefined])[];
values: readonly (readonly [
label: string,
value: MessageMetadataValue | undefined,
localized?: boolean,
])[];
}) {
return (
<dl>
{values.map(([label, value]) => (
{values.map(([label, value, localized]) => (
<div key={label}>
<dt>{label}</dt>
<dd>{display(value)}</dd>
<dd>{localized ? displayValue(value) : displayRaw(value)}</dd>
</div>
))}
</dl>
@@ -99,34 +104,34 @@ function aggregateFields(
function SnapshotView({ snapshot }: { snapshot: MessagesSnapshot }) {
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
<p>Aggregate metadata only; sensitive payload and addressing details are excluded.</p>
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<p>仅显示汇总元数据;敏感内容和寻址详情已排除。</p>
<div className="messages-grid">
<Section label="SMS aggregate">
<Section label="短信汇总">
<Fields values={aggregateFields(snapshot.sms)} />
</Section>
<Section label="Device message aggregates">
<Section label="设备消息汇总">
{snapshot.devices.length ? (
snapshot.devices.map((device, index) => (
<article key={`${device.deviceId ?? device.label ?? 'device'}-${index}`}>
<Fields
values={[
['Device ID', device.deviceId],
['Label', device.label],
['State', device.state],
['设备 ID', device.deviceId],
['标签', device.label],
['状态', device.state, true],
...aggregateFields(device),
]}
/>
</article>
))
) : (
<p>No device message aggregates were supplied.</p>
<p>未提供设备消息汇总。</p>
)}
</Section>
</div>
<section className="state-panel" aria-label="Messages actions">
<h2>Messages actions</h2>
<p>Sending, deleting, and changing messages are unavailable in this read-only module.</p>
<section className="state-panel" aria-label="消息操作">
<h2>消息操作</h2>
<p>此只读模块不支持发送、删除或修改消息。</p>
</section>
</>
);
@@ -183,16 +188,15 @@ export function MessagesModule({ instance, dataSource, refreshSignal }: Messages
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Messages data can be read for this instance.
需要先完成认证,才能读取消息数据。
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="Messages unavailable">
No safe Messages read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="消息不可用">
没有可用的安全消息只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
}
@@ -204,8 +208,8 @@ export function MessagesModule({ instance, dataSource, refreshSignal }: Messages
if ((state.kind === 'idle' || state.kind === 'loading') && !retainedSnapshot) {
return (
<p role="status" aria-label="Messages loading status">
Loading Messages…
<p role="status" aria-label="消息加载状态">
正在加载消息…
</p>
);
}
@@ -213,9 +217,9 @@ export function MessagesModule({ instance, dataSource, refreshSignal }: Messages
if (state.kind === 'error' && !retainedSnapshot) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Messages: {SAFE_LOAD_ERROR}</p>
<p>{SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Messages
重试加载消息
</button>
</div>
);
@@ -227,18 +231,18 @@ export function MessagesModule({ instance, dataSource, refreshSignal }: Messages
return (
<div className="messages-module">
{state.kind === 'loading' ? <p role="status">Refreshing Messages…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新消息…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Messages data.</p>
<p>刷新失败,正在显示上次已知的消息数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Messages
重试加载消息
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Messages freshness">
Messages data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="消息数据新鲜度">
消息数据可能已过期;使用这些值前请核实数据新鲜度。
</p>
) : null}
<SnapshotView snapshot={currentSnapshot} />
@@ -82,25 +82,21 @@ describe('Notifications isolated safe read module', () => {
const pending = deferredSource();
render(<NotificationsModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'Notifications loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: '通知加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const channels = await screen.findByRole('region', { name: 'Channel aggregate' });
expect(within(channels).getByText('healthy')).toBeTruthy();
const channels = await screen.findByRole('region', { name: '渠道汇总' });
expect(within(channels).getByText('正常', { selector: 'dd' })).toBeTruthy();
expect(within(channels).getByText('5')).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Queue aggregate' })).getByText('18'),
).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Log aggregate' })).getByText('30'),
).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T12:00:00Z/)).toBeTruthy();
expect(within(screen.getByRole('region', { name: '队列汇总' })).getByText('18')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '日志汇总' })).getByText('30')).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T12:00:00Z/)).toBeTruthy();
});
it('never exposes polymorphic config, endpoints, destinations, tokens, phone numbers, payloads, raw logs, or actions', async () => {
render(<NotificationsModule instance={owner} dataSource={{ load: async () => snapshot }} />);
expect(await screen.findByText(/aggregate counts and status only/i)).toBeTruthy();
expect(await screen.findByText(/仅显示汇总计数和状态/i)).toBeTruthy();
const text = document.body.textContent ?? '';
for (const secret of [
@@ -124,8 +120,8 @@ describe('Notifications isolated safe read module', () => {
it('fails closed without an injected source or authenticated owner', async () => {
const load = vi.fn<NotificationsDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<NotificationsModule instance={owner} />);
expect(screen.getByRole('status', { name: 'Notifications unavailable' }).textContent).toMatch(
/no safe notifications read data source.*uncontracted production endpoint/i,
expect(screen.getByRole('status', { name: '通知不可用' }).textContent).toMatch(
/没有可用的安全通知只读数据源.*未签订契约的生产端点/i,
);
rerender(
@@ -134,7 +130,7 @@ describe('Notifications isolated safe read module', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(load).not.toHaveBeenCalled();
});
@@ -160,8 +156,8 @@ describe('Notifications isolated safe read module', () => {
...snapshot,
channels: { ...snapshot.channels, status: 'degraded', total: 9 },
});
expect(await screen.findByText('degraded')).toBeTruthy();
expect(screen.queryByText('healthy')).toBeNull();
expect(await screen.findByText('功能受限', { selector: 'dd' })).toBeTruthy();
expect(screen.queryByText('正常', { selector: 'dd' })).toBeNull();
});
it('uses fixed errors and retains only same-owner last-good data across refresh failure and retry', async () => {
@@ -175,18 +171,16 @@ describe('Notifications isolated safe read module', () => {
const { rerender } = render(
<NotificationsModule instance={owner} dataSource={source} refreshSignal={0} />,
);
expect(await screen.findByText('healthy')).toBeTruthy();
expect(await screen.findByText('正常', { selector: 'dd' })).toBeTruthy();
rerender(<NotificationsModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/refresh failed; showing the last known notifications data/i);
expect(alert.textContent).toMatch(/刷新失败,正在显示上次已知的通知数据/i);
expect(alert.textContent).not.toContain('secret endpoint');
expect(screen.getByText('healthy')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading Notifications' }));
expect(screen.getByText('正常', { selector: 'dd' })).toBeTruthy();
await user.click(screen.getByRole('button', { name: '重试加载通知数据' }));
expect(
await within(screen.getByRole('region', { name: 'Channel aggregate' })).findByText(
'available',
),
await within(screen.getByRole('region', { name: '渠道汇总' })).findByText('可用'),
).toBeTruthy();
});
@@ -204,15 +198,13 @@ describe('Notifications isolated safe read module', () => {
logs: { status: { token: 'nested-secret' }, total: Number.MAX_SAFE_INTEGER + 1, error: 3 },
};
render(<NotificationsModule instance={owner} dataSource={{ load: async () => unsafe }} />);
const logs = await screen.findByRole('region', { name: 'Log aggregate' });
const logs = await screen.findByRole('region', { name: '日志汇总' });
expect(within(logs).getByText('3')).toBeTruthy();
expect(
within(screen.getByRole('region', { name: 'Channel aggregate' })).getByText('2'),
).toBeTruthy();
expect(within(screen.getByRole('region', { name: '渠道汇总' })).getByText('2')).toBeTruthy();
expect(document.body.textContent).not.toMatch(
/api-token|channel-secret|private payload|nested-secret|Infinity|1\.5|-1/,
);
expect(screen.queryByText(/^Observed /)).toBeNull();
expect(screen.queryByText(/^观测时间:/)).toBeNull();
unsafe.channels.disabled = 99;
expect(screen.queryByText('99')).toBeNull();
});
@@ -225,7 +217,7 @@ describe('Notifications isolated safe read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Notifications data could not be loaded.');
expect(alert.textContent).toContain('无法加载通知数据。');
expect(alert.textContent).not.toContain('private token');
});
@@ -241,7 +233,7 @@ describe('Notifications isolated safe read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('Notifications data could not be loaded.');
expect(alert.textContent).toContain('无法加载通知数据。');
expect(alert.textContent).not.toContain('private notification token');
});
});
+39 -40
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
/** Values permitted at the Notifications presentation boundary. */
export type NotificationStatus =
@@ -73,7 +74,7 @@ type ReadState =
| { kind: 'ready'; snapshot: OwnedSnapshot }
| { kind: 'error'; snapshot?: OwnedSnapshot };
const SAFE_LOAD_ERROR = 'Notifications data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载通知数据。';
const NOTIFICATION_STATUSES = new Set<NotificationStatus>([
'healthy',
'degraded',
@@ -139,30 +140,30 @@ export function sanitizeNotificationsSnapshot(value: unknown): NotificationsSnap
}
const CHANNEL_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Enabled', 'enabled'],
['Disabled', 'disabled'],
['Healthy', 'healthy'],
['Degraded', 'degraded'],
['Failed', 'failed'],
['状态', 'status'],
['总计', 'total'],
['已启用', 'enabled'],
['已禁用', 'disabled'],
['正常', 'healthy'],
['功能受限', 'degraded'],
['失败', 'failed'],
] as const;
const QUEUE_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Pending', 'pending'],
['Processing', 'processing'],
['Delivered', 'delivered'],
['Failed', 'failed'],
['状态', 'status'],
['总计', 'total'],
['等待中', 'pending'],
['处理中', 'processing'],
['已送达', 'delivered'],
['失败', 'failed'],
] as const;
const LOG_FIELDS = [
['Status', 'status'],
['Total', 'total'],
['Info', 'info'],
['Warning', 'warning'],
['Error', 'error'],
['状态', 'status'],
['总计', 'total'],
['信息', 'info'],
['警告', 'warning'],
['错误', 'error'],
] as const;
function Section({ label, children }: { label: string; children: ReactNode }) {
@@ -183,7 +184,7 @@ function AggregateFields({
}) {
// Constant-key projection is the security boundary: never enumerate or spread source objects.
const supplied = fields.filter(([, key]) => aggregate[key] !== undefined);
if (!supplied.length) return <p>No aggregate status was supplied.</p>;
if (!supplied.length) return <p>未提供汇总状态。</p>;
return (
<dl>
@@ -192,7 +193,7 @@ function AggregateFields({
return (
<div key={key}>
<dt>{label}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
<dd>{displayValue(value)}</dd>
</div>
);
})}
@@ -203,16 +204,16 @@ function AggregateFields({
function SnapshotView({ snapshot }: { snapshot: NotificationsSnapshot }) {
return (
<>
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
<p>Aggregate counts and status only; notification details and contents are excluded.</p>
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<p>仅显示汇总计数和状态;不包含通知详情和内容。</p>
<div className="notifications-grid">
<Section label="Channel aggregate">
<Section label="渠道汇总">
<AggregateFields aggregate={snapshot.channels} fields={CHANNEL_FIELDS} />
</Section>
<Section label="Queue aggregate">
<Section label="队列汇总">
<AggregateFields aggregate={snapshot.queue} fields={QUEUE_FIELDS} />
</Section>
<Section label="Log aggregate">
<Section label="日志汇总">
<AggregateFields aggregate={snapshot.logs} fields={LOG_FIELDS} />
</Section>
</div>
@@ -282,16 +283,15 @@ export function NotificationsModule({
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before Notifications data can be read for this instance.
需要先完成认证,才能读取通知数据。
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="Notifications unavailable">
No safe Notifications read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="通知不可用">
没有可用的安全通知只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
}
@@ -301,8 +301,8 @@ export function NotificationsModule({
if ((state.kind === 'idle' || state.kind === 'loading') && !snapshot) {
return (
<p role="status" aria-label="Notifications loading status">
Loading Notifications…
<p role="status" aria-label="通知加载状态">
正在加载通知数据…
</p>
);
}
@@ -310,9 +310,9 @@ export function NotificationsModule({
if (state.kind === 'error' && !snapshot) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load Notifications: {SAFE_LOAD_ERROR}</p>
<p>无法加载通知数据: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Notifications
重试加载通知数据
</button>
</div>
);
@@ -322,19 +322,18 @@ export function NotificationsModule({
return (
<div className="notifications-module">
{state.kind === 'loading' ? <p role="status">Refreshing Notifications…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新通知数据…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known Notifications data.</p>
<p>刷新失败,正在显示上次已知的通知数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading Notifications
重试加载通知数据
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Notifications freshness">
Notifications data is {instance.freshness}; verify freshness before relying on these
values.
<p className="state-panel" role="status" aria-label="通知数据时效性">
通知数据可能已过期;使用这些值前请先核实数据新鲜度。
</p>
) : null}
<SnapshotView snapshot={snapshot.value} />
+17 -17
View File
@@ -42,15 +42,15 @@ describe('isolated safe OTA read module', () => {
const pending = deferredSource();
render(<OtaModule instance={owner} dataSource={pending.source} />);
expect(screen.getByRole('status', { name: 'OTA loading status' })).toBeTruthy();
expect(screen.getByRole('status', { name: 'OTA 加载状态' })).toBeTruthy();
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
const summary = await screen.findByRole('region', { name: 'OTA safe summary' });
const summary = await screen.findByRole('region', { name: 'OTA 安全摘要' });
expect(within(summary).getByText('v2.4.1-build.7')).toBeTruthy();
expect(within(summary).getByText('downloading')).toBeTruthy();
expect(within(summary).getByText('下载中')).toBeTruthy();
expect(within(summary).getByText('42%')).toBeTruthy();
expect(within(summary).getByText('Available')).toBeTruthy();
expect(within(summary).getByText('是')).toBeTruthy();
});
it('strictly projects safe values and rejects URLs, proxy prefixes, notes, logs, and file paths', async () => {
@@ -67,8 +67,8 @@ describe('isolated safe OTA read module', () => {
} as unknown as OtaSnapshot;
render(<OtaModule instance={owner} dataSource={{ load: async () => unsafe }} />);
const summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(2);
const summary = await screen.findByRole('region', { name: 'OTA 安全摘要' });
expect(within(summary).getAllByText('不可用')).toHaveLength(2);
expect(document.body.textContent).not.toMatch(
/private\.example|admin\/ota|api\/proxy|private release notes|private raw OTA log|var\/lib|tmp\/private/i,
);
@@ -79,7 +79,7 @@ describe('isolated safe OTA read module', () => {
it('offers no endpoint or apply, cancel, or upload operations', async () => {
render(<OtaModule instance={owner} dataSource={{ load: async () => snapshot }} />);
expect(await screen.findByText('v2.4.1-build.7')).toBeTruthy();
expect(screen.getByText(/read-only OTA metadata/i)).toBeTruthy();
expect(screen.getByText(/只读 OTA 元数据/i)).toBeTruthy();
expect(screen.queryByText(/endpoint/i)).toBeNull();
expect(screen.queryByRole('button', { name: /apply|cancel|upload/i })).toBeNull();
expect(screen.queryByRole('textbox')).toBeNull();
@@ -88,14 +88,14 @@ describe('isolated safe OTA read module', () => {
it('fails closed without an injected source or authenticated owner', () => {
const load = vi.fn<OtaDataSource['load']>().mockResolvedValue(snapshot);
const { rerender } = render(<OtaModule instance={owner} />);
expect(screen.getByRole('status', { name: 'OTA unavailable' }).textContent).toMatch(
/no safe OTA read data source.*will not invent.*production endpoint/i,
expect(screen.getByRole('status', { name: 'OTA 不可用' }).textContent).toMatch(
/没有可用的安全 OTA 只读数据源.*不会臆造.*生产端点/i,
);
rerender(
<OtaModule instance={{ ...owner, authentication: 'auth-required' }} dataSource={{ load }} />,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(load).not.toHaveBeenCalled();
});
@@ -132,11 +132,11 @@ describe('isolated safe OTA read module', () => {
rerender(<OtaModule instance={owner} dataSource={source} refreshSignal={1} />);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toMatch(/refresh failed; showing the last known OTA data/i);
expect(alert.textContent).toMatch(/刷新失败,正在显示上次已知的 OTA 数据/i);
expect(alert.textContent).not.toMatch(/secret URL|var\/private/i);
expect(screen.getByText('v2.4.1-build.7')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'Retry loading OTA data' }));
await user.click(screen.getByRole('button', { name: '重试加载 OTA 数据' }));
expect(await screen.findByText('v2.4.2')).toBeTruthy();
});
@@ -148,7 +148,7 @@ describe('isolated safe OTA read module', () => {
/>,
);
const alert = await screen.findByRole('alert');
expect(alert.textContent).toContain('OTA data could not be loaded.');
expect(alert.textContent).toContain('无法加载 OTA 数据。');
expect(alert.textContent).not.toContain('private release URL');
});
@@ -160,13 +160,13 @@ describe('isolated safe OTA read module', () => {
dataSource={{ load: async () => ({ currentVersion: opaque }) }}
/>,
);
let summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(4);
let summary = await screen.findByRole('region', { name: 'OTA 安全摘要' });
expect(within(summary).getAllByText('不可用')).toHaveLength(4);
expect(document.body.textContent).not.toContain(opaque);
rerender(
<OtaModule instance={owner} dataSource={{ load: async () => null }} refreshSignal={1} />,
);
summary = await screen.findByRole('region', { name: 'OTA safe summary' });
expect(within(summary).getAllByText('Unavailable')).toHaveLength(4);
summary = await screen.findByRole('region', { name: 'OTA 安全摘要' });
expect(within(summary).getAllByText('不可用')).toHaveLength(4);
});
});
+24 -32
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import type { InstanceContext } from '../app-shell.js';
import { displayValue } from '../ui/locale.js';
export type OtaStatus =
| 'idle'
@@ -52,7 +53,7 @@ type ReadState =
| { kind: 'ready'; ownerId: string; snapshot: SafeOtaSnapshot }
| { kind: 'error'; ownerId: string; snapshot?: SafeOtaSnapshot };
const SAFE_LOAD_ERROR = 'OTA data could not be loaded.';
const SAFE_LOAD_ERROR = '无法加载 OTA 数据。';
const OTA_STATUSES = new Set<OtaStatus>([
'idle',
'checking',
@@ -107,40 +108,32 @@ function sanitizeSnapshot(value: unknown): SafeOtaSnapshot {
}
function display(value: string | undefined): string {
return value ?? 'Unavailable';
return displayValue(value);
}
function SnapshotView({ snapshot }: { snapshot: SafeOtaSnapshot }) {
return (
<section className="ota-card" aria-label="OTA safe summary">
<h2>OTA safe summary</h2>
<p>Read-only OTA metadata: update operations and source-specific details are excluded.</p>
<section className="ota-card" aria-label="OTA 安全摘要">
<h2>OTA 安全摘要</h2>
<p>仅显示只读 OTA 元数据;不包含更新操作和来源特定详情。</p>
<dl>
<div>
<dt>Current version</dt>
<dt>当前版本</dt>
<dd>{display(snapshot.currentVersion)}</dd>
</div>
<div>
<dt>Status</dt>
<dt>状态</dt>
<dd>{display(snapshot.status)}</dd>
</div>
<div>
<dt>Progress</dt>
<dt>进度</dt>
<dd>
{snapshot.progressPercent === undefined
? 'Unavailable'
: `${snapshot.progressPercent}%`}
{snapshot.progressPercent === undefined ? '不可用' : `${snapshot.progressPercent}%`}
</dd>
</div>
<div>
<dt>Update availability</dt>
<dd>
{snapshot.updateAvailable === undefined
? 'Unavailable'
: snapshot.updateAvailable
? 'Available'
: 'No update available'}
</dd>
<dt>更新可用性</dt>
<dd>{displayValue(snapshot.updateAvailable)}</dd>
</div>
</dl>
</section>
@@ -204,16 +197,15 @@ export function OtaModule({ instance, dataSource, refreshSignal }: OtaModuleProp
if (instance.authentication !== 'authenticated') {
return (
<div className="state-panel state-error" role="alert">
Authentication is required before OTA data can be read for this instance.
需要先完成认证,才能读取 OTA 数据。
</div>
);
}
if (!dataSource) {
return (
<div className="state-panel" role="status" aria-label="OTA unavailable">
No safe OTA read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="OTA 不可用">
没有可用的安全 OTA 只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
}
@@ -225,8 +217,8 @@ export function OtaModule({ instance, dataSource, refreshSignal }: OtaModuleProp
if ((state.kind === 'idle' || state.kind === 'loading') && !retained) {
return (
<p role="status" aria-label="OTA loading status">
Loading OTA data…
<p role="status" aria-label="OTA 加载状态">
正在加载 OTA 数据…
</p>
);
}
@@ -234,9 +226,9 @@ export function OtaModule({ instance, dataSource, refreshSignal }: OtaModuleProp
if (state.kind === 'error' && !retained) {
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load OTA data: {SAFE_LOAD_ERROR}</p>
<p>无法加载 OTA 数据: {SAFE_LOAD_ERROR}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading OTA data
重试加载 OTA 数据
</button>
</div>
);
@@ -248,18 +240,18 @@ export function OtaModule({ instance, dataSource, refreshSignal }: OtaModuleProp
return (
<div className="ota-module">
{state.kind === 'loading' ? <p role="status">Refreshing OTA data…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新 OTA 数据…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known OTA data.</p>
<p>刷新失败,正在显示上次已知的 OTA 数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading OTA data
重试加载 OTA 数据
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="OTA freshness">
OTA data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="OTA 数据时效性">
OTA 数据可能已过期;使用这些值前请先核实数据新鲜度。
</p>
) : null}
<SnapshotView snapshot={snapshot} />
+43 -20
View File
@@ -21,7 +21,7 @@ const owner: InstanceContext = {
authentication: 'authenticated',
freshness: 'fresh',
};
const overviewCapability: InstanceCapabilityMap = {
const overviewCapabilities: InstanceCapabilityMap = {
overview: { state: 'supported' },
};
const snapshot: OverviewSnapshot = {
@@ -61,26 +61,51 @@ describe('Phase 6.1 Overview / System read slice', () => {
<AppShell
pathname="/instances/alpha/overview"
instance={owner}
capabilities={overviewCapability}
capabilities={overviewCapabilities}
overviewDataSource={pending.source}
/>,
);
expect(screen.getByRole('status', { name: 'Overview loading status' }).textContent).toContain(
'Loading overview',
expect(screen.getByRole('status', { name: '概览加载状态' }).textContent).toContain(
'正在加载概览',
);
expect(pending.load).toHaveBeenCalledWith('alpha', expect.any(AbortSignal));
pending.resolve(snapshot);
for (const heading of ['Device', 'SIM', 'Network', 'Statistics', 'CPU', 'Connectivity']) {
for (const heading of ['设备', 'SIM', '网络', '统计', 'CPU', '连接状态']) {
expect(await screen.findByRole('heading', { name: heading })).toBeTruthy();
}
expect(
within(screen.getByRole('region', { name: 'Device' })).getByText('SIMBox 8'),
).toBeTruthy();
expect(screen.getByText(/Observed 2026-07-17T10:00:00Z/)).toBeTruthy();
expect(within(screen.getByRole('region', { name: '设备' })).getByText('SIMBox 8')).toBeTruthy();
expect(screen.getByText(/观测时间:2026-07-17T10:00:00Z/)).toBeTruthy();
expect(screen.queryByRole('button', { name: /restart/i })).toBeNull();
expect(screen.getByText(/Restart is unavailable.*R3/i)).toBeTruthy();
expect(screen.getByText(/重启不可用.*R3/i)).toBeTruthy();
});
it('translates only known field names and values while preserving unknown safe data', async () => {
render(
<OverviewSystemPage
instance={owner}
dataSource={{
load: async () => ({
...snapshot,
device: { Model: 'SIMBox 8', Enabled: true, Mystery: 'vendor-value' },
network: { Registration: 'registered' },
connectivity: { State: 'connected' },
}),
}}
/>,
);
const device = await screen.findByRole('region', { name: '设备' });
expect(within(device).getByText('型号')).toBeTruthy();
expect(within(device).getByText('Enabled')).toBeTruthy();
expect(within(device).getByText('是')).toBeTruthy();
expect(within(device).getByText('Mystery')).toBeTruthy();
expect(within(device).getByText('vendor-value')).toBeTruthy();
expect(within(screen.getByRole('region', { name: '网络' })).getByText('已注册')).toBeTruthy();
expect(
within(screen.getByRole('region', { name: '连接状态' })).getByText('已连接'),
).toBeTruthy();
});
it('is honest when no safe read source is injected and never attempts a production endpoint', () => {
@@ -88,11 +113,11 @@ describe('Phase 6.1 Overview / System read slice', () => {
<AppShell
pathname="/instances/alpha/overview"
instance={owner}
capabilities={overviewCapability}
capabilities={overviewCapabilities}
/>,
);
expect(screen.getByRole('status', { name: 'Overview unavailable' }).textContent).toMatch(
/no safe overview read data source/i,
expect(screen.getByRole('status', { name: '概览不可用' }).textContent).toMatch(
/没有可用的安全概览只读数据源/i,
);
expect(screen.queryByRole('button', { name: /restart/i })).toBeNull();
});
@@ -114,7 +139,7 @@ describe('Phase 6.1 Overview / System read slice', () => {
dataSource={{ load }}
/>,
);
expect(screen.getByRole('alert').textContent).toMatch(/authentication is required/i);
expect(screen.getByRole('alert').textContent).toMatch(/需要先完成认证/i);
expect(screen.queryByText('SIMBox 8')).toBeNull();
expect(load).toHaveBeenCalledTimes(1);
});
@@ -126,8 +151,8 @@ describe('Phase 6.1 Overview / System read slice', () => {
dataSource={{ load: async () => snapshot }}
/>,
);
expect((await screen.findByRole('status', { name: 'Overview freshness' })).textContent).toMatch(
/stale/i,
expect((await screen.findByRole('status', { name: '概览数据新鲜度' })).textContent).toMatch(
/可能已过期/i,
);
expect(screen.getByText('SIMBox 8')).toBeTruthy();
});
@@ -140,10 +165,8 @@ describe('Phase 6.1 Overview / System read slice', () => {
.mockResolvedValueOnce(snapshot);
render(<OverviewSystemPage instance={owner} dataSource={{ load }} />);
expect((await screen.findByRole('alert')).textContent).toContain(
'Overview data could not be loaded.',
);
await user.click(screen.getByRole('button', { name: 'Retry loading overview' }));
expect((await screen.findByRole('alert')).textContent).toContain('无法加载概览数据。');
await user.click(screen.getByRole('button', { name: '重试加载概览' }));
expect(await screen.findByText('SIMBox 8')).toBeTruthy();
expect(load).toHaveBeenCalledTimes(2);
});
+63 -28
View File
@@ -34,14 +34,53 @@ type ReadState =
| { kind: 'error'; message: string; snapshot?: OverviewSnapshot };
const SECTIONS = [
['device', 'Device'],
['device', '设备'],
['sim', 'SIM'],
['network', 'Network'],
['stats', 'Statistics'],
['network', '网络'],
['stats', '统计'],
['cpu', 'CPU'],
['connectivity', 'Connectivity'],
['connectivity', '连接状态'],
] as const;
const FIELD_LABELS: Readonly<Record<string, string>> = {
Model: '型号',
Uptime: '运行时间',
Slots: '卡槽数',
Active: '活跃数',
Operator: '运营商',
Registration: '注册状态',
'Messages today': '今日消息数',
Calls: '通话数',
Usage: '使用率',
Temperature: '温度',
State: '状态',
Latency: '延迟',
};
const ENUM_LABELS: Readonly<Record<string, string>> = {
connected: '已连接',
disconnected: '未连接',
connecting: '正在连接',
registered: '已注册',
unregistered: '未注册',
searching: '正在搜索',
roaming: '漫游中',
online: '在线',
offline: '离线',
unknown: '未知',
};
function displayFieldLabel(key: string): string {
return FIELD_LABELS[key] ?? key;
}
function displayFieldValue(value: OverviewFieldValue): string {
if (value == null) return '不可用';
if (typeof value === 'boolean') return value ? '是' : '否';
if (typeof value === 'string') return ENUM_LABELS[value.toLocaleLowerCase()] ?? value;
return String(value);
}
function StructuredSection({ label, values }: { label: string; values: OverviewSection }) {
const entries = Object.entries(values);
return (
@@ -51,13 +90,13 @@ function StructuredSection({ label, values }: { label: string; values: OverviewS
<dl>
{entries.map(([key, value]) => (
<div key={key}>
<dt>{key}</dt>
<dd>{value == null ? 'Unavailable' : String(value)}</dd>
<dt>{displayFieldLabel(key)}</dt>
<dd>{displayFieldValue(value)}</dd>
</div>
))}
</dl>
) : (
<p>No {label.toLocaleLowerCase()} data was supplied.</p>
<p>未提供{label}数据。</p>
)}
</section>
);
@@ -103,7 +142,7 @@ export function OverviewSystemPage({
if (request === requestOwner.current && !controller.signal.aborted)
setState((current) => ({
kind: 'error',
message: 'Overview data could not be loaded.',
message: '无法加载概览数据。',
...(current.kind === 'loading' && current.snapshot
? { snapshot: current.snapshot }
: {}),
@@ -116,15 +155,14 @@ export function OverviewSystemPage({
if (instance.authentication === 'auth-required')
return (
<div className="state-panel state-error" role="alert">
Authentication is required before overview data can be read for this instance.
需要先完成认证,才能读取概览数据。
</div>
);
if (!dataSource)
return (
<div className="state-panel" role="status" aria-label="Overview unavailable">
No safe overview read data source is available. This console will not invent or call an
uncontracted production endpoint.
<div className="state-panel" role="status" aria-label="概览不可用">
没有可用的安全概览只读数据源。此控制台不会臆造或调用未签订契约的生产端点。
</div>
);
@@ -133,17 +171,17 @@ export function OverviewSystemPage({
if ((state.kind === 'loading' || state.kind === 'idle') && !retainedSnapshot)
return (
<p role="status" aria-label="Overview loading status">
Loading overview…
<p role="status" aria-label="概览加载状态">
正在加载概览…
</p>
);
if (state.kind === 'error' && !state.snapshot)
return (
<div className="state-panel state-error" role="alert">
<p>Unable to load overview: {state.message}</p>
<p>无法加载概览: {state.message}</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading overview
重试加载概览
</button>
</div>
);
@@ -153,32 +191,29 @@ export function OverviewSystemPage({
return (
<div className="overview-system">
{state.kind === 'loading' ? <p role="status">Refreshing overview…</p> : null}
{state.kind === 'loading' ? <p role="status">正在刷新概览…</p> : null}
{state.kind === 'error' ? (
<div className="state-panel state-error" role="alert">
<p>Refresh failed; showing the last known overview.</p>
<p>刷新失败,正在显示上次已知的概览数据。</p>
<button type="button" onClick={() => setRetry((value) => value + 1)}>
Retry loading overview
重试加载概览
</button>
</div>
) : null}
{instance.freshness !== 'fresh' ? (
<p className="state-panel" role="status" aria-label="Overview freshness">
Overview data is {instance.freshness}; verify freshness before relying on these values.
<p className="state-panel" role="status" aria-label="概览数据新鲜度">
概览数据可能已过期;使用这些值前请先核实数据新鲜度。
</p>
) : null}
{snapshot.observedAt ? <p>Observed {snapshot.observedAt}</p> : null}
{snapshot.observedAt ? <p>观测时间:{snapshot.observedAt}</p> : null}
<div className="overview-grid">
{SECTIONS.map(([key, label]) => (
<StructuredSection key={key} label={label} values={snapshot[key]} />
))}
</div>
<section className="state-panel" aria-label="System actions">
<h2>System actions</h2>
<p>
Restart is unavailable because the backend R3 restart operation is not implemented. No
action is offered.
</p>
<section className="state-panel" aria-label="系统操作">
<h2>系统操作</h2>
<p>重启不可用,因为后端尚未实现 R3 重启操作,因此不提供任何操作。</p>
</section>
</div>
);