feat(web): add console update panel, system maintenance path and connection settings UI

- Add online update workflow with plan, backup and install states.

- Expose maintenance paths, component backup jobs and connection authorization modes.

- Align auth and automation screens with the fused control plane.
This commit is contained in:
chick
2026-09-07 00:45:44 +08:00
parent 6d251dab7b
commit f6319374cd
20 changed files with 1985 additions and 67 deletions
@@ -8,7 +8,7 @@ import type { SystemMaintenanceDataSource } from './system-maintenance-api-data-
afterEach(cleanup);
const defaults = { heartbeatSeconds: 30, offlineSeconds: 90 };
const defaults = { heartbeatSeconds: 30, offlineSeconds: 90, authorizationMode: 'auto' };
function source(overrides: Partial<SystemMaintenanceDataSource> = {}): SystemMaintenanceDataSource {
return {
@@ -36,6 +36,10 @@ function field(region: HTMLElement, label: string): HTMLInputElement {
return node;
}
function modeButton(region: HTMLElement, label: string): HTMLButtonElement {
return within(region).getByRole('button', { name: label }) as HTMLButtonElement;
}
describe('ConnectionSettingsPanel', () => {
it('loads the saved cadence into the inputs', async () => {
const { data, region } = await mount();
@@ -44,6 +48,30 @@ describe('ConnectionSettingsPanel', () => {
expect(field(region, '离线判定(秒)').value).toBe('90');
});
it('starts new-device authorization in automatic mode', async () => {
const { region } = await mount();
expect(modeButton(region, '自动授权').getAttribute('aria-pressed')).toBe('true');
expect(modeButton(region, '人工确认').getAttribute('aria-pressed')).toBe('false');
});
it('saves a selected manual authorization mode and echoes it', async () => {
const { data, region } = await mount();
await userEvent.click(modeButton(region, '人工确认'));
const save = within(region).getByRole('button', { name: '保存设置' }) as HTMLButtonElement;
expect(save.disabled).toBe(false);
await userEvent.click(save);
expect(data.updateConnection).toHaveBeenCalledWith({
heartbeatSeconds: 30,
offlineSeconds: 90,
authorizationMode: 'manual',
});
const notice = await within(region).findByRole('status');
expect(notice.textContent).toContain('新设备人工确认');
});
it('keeps save disabled until a valid change is made', async () => {
const { region } = await mount();
const save = within(region).getByRole('button', { name: '保存设置' }) as HTMLButtonElement;
@@ -69,9 +97,12 @@ describe('ConnectionSettingsPanel', () => {
expect(data.updateConnection).toHaveBeenCalledWith({
heartbeatSeconds: 45,
offlineSeconds: 180,
authorizationMode: 'auto',
});
const notice = await within(region).findByRole('status');
expect(notice.textContent).toContain('连接设置已保存,心跳 45 秒,离线判定 180 秒。');
expect(notice.textContent).toContain(
'连接设置已保存,新设备自动授权,心跳 45 秒,离线判定 180 秒。',
);
});
it('blocks a save when the offline window drops below twice the heartbeat', async () => {
@@ -103,7 +134,7 @@ describe('ConnectionSettingsPanel', () => {
await userEvent.type(field(region, '离线判定(秒)'), '150');
await userEvent.click(within(region).getByRole('button', { name: '保存设置' }));
const problem = await within(region).findByRole('alert');
expect(problem.textContent).toContain('连接设置未保存,请检查心跳与离线判定的取值范围。');
expect(problem.textContent).toContain('连接设置未保存,请检查各项设置的取值范围。');
expect(field(region, '心跳间隔(秒)').value).toBe('60');
});