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:
@@ -12,6 +12,7 @@ const catalog = {
|
||||
rows: 7,
|
||||
},
|
||||
],
|
||||
directory: '/Users/chick/.local/opt/multi-simadmin/data/backups',
|
||||
};
|
||||
|
||||
const backup = {
|
||||
@@ -19,6 +20,7 @@ const backup = {
|
||||
createdAt: '2026-09-04T03:30:00.000Z',
|
||||
sizeBytes: 4096,
|
||||
appVersion: '0.1.0',
|
||||
formatVersion: 1,
|
||||
note: '升级前',
|
||||
automatic: true,
|
||||
integrity: 'ok',
|
||||
@@ -48,7 +50,7 @@ describe('component backup API data source', () => {
|
||||
const source = createComponentBackupApiDataSource(fetcher);
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
await expect(source.catalog(signal)).resolves.toEqual(catalog.items);
|
||||
await expect(source.catalog(signal)).resolves.toEqual(catalog);
|
||||
await expect(source.list(signal)).resolves.toEqual([backup]);
|
||||
await expect(source.autoSettings(signal)).resolves.toEqual(settings);
|
||||
expect(fetcher.mock.calls[0]?.[0]).toBe('/api/v1/system/component-backups/catalog');
|
||||
@@ -61,12 +63,16 @@ describe('component backup API data source', () => {
|
||||
const fetcher = vi
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValueOnce(json(backup, 201))
|
||||
.mockResolvedValueOnce(json({ devices: 7 }))
|
||||
.mockResolvedValueOnce(
|
||||
json({ written: { devices: 7 }, safetyBackup: 'multi-simadmin-components-auto-x.json' }),
|
||||
)
|
||||
.mockResolvedValueOnce(json({ filename: backup.filename }));
|
||||
const source = createComponentBackupApiDataSource(fetcher);
|
||||
|
||||
await expect(source.create(['devices'], '升级前')).resolves.toEqual(backup);
|
||||
await expect(source.restore(backup.filename, ['devices'])).resolves.toBeUndefined();
|
||||
await expect(source.restore(backup.filename, ['devices'])).resolves.toBe(
|
||||
'multi-simadmin-components-auto-x.json',
|
||||
);
|
||||
await expect(source.remove(backup.filename)).resolves.toBeUndefined();
|
||||
|
||||
expect(fetcher.mock.calls[0]?.[1]).toMatchObject({
|
||||
@@ -79,6 +85,20 @@ describe('component backup API data source', () => {
|
||||
expect(fetcher.mock.calls[2]?.[1]).toMatchObject({ method: 'DELETE' });
|
||||
});
|
||||
|
||||
it('previews an archive and builds a safe download URL', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValueOnce(json(backup));
|
||||
const source = createComponentBackupApiDataSource(fetcher);
|
||||
|
||||
await expect(source.preview(backup.filename)).resolves.toEqual(backup);
|
||||
expect(source.backupDownloadUrl(backup.filename)).toBe(
|
||||
`/api/v1/system/component-backups/${encodeURIComponent(backup.filename)}/download`,
|
||||
);
|
||||
expect(fetcher.mock.calls[0]?.[0]).toBe(
|
||||
`/api/v1/system/component-backups/${encodeURIComponent(backup.filename)}/preview`,
|
||||
);
|
||||
expect(fetcher.mock.calls[0]?.[1]).toMatchObject({ method: 'GET' });
|
||||
});
|
||||
|
||||
it('saves the schedule and rejects an unknown component key', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(json(settings));
|
||||
const source = createComponentBackupApiDataSource(fetcher);
|
||||
@@ -111,4 +131,18 @@ describe('component backup API data source', () => {
|
||||
createComponentBackupApiDataSource(fetcher).remove('../secret.json'),
|
||||
).rejects.toThrowError('Component backup response is invalid.');
|
||||
});
|
||||
|
||||
it('refuses a catalog that does not say where the backups live', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(json({ items: catalog.items }));
|
||||
await expect(createComponentBackupApiDataSource(fetcher).catalog()).rejects.toThrowError(
|
||||
'Component backup response is invalid.',
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses a restore that cannot prove it took a safety snapshot first', async () => {
|
||||
const fetcher = vi.fn<typeof fetch>().mockResolvedValue(json({ written: { devices: 7 } }));
|
||||
await expect(
|
||||
createComponentBackupApiDataSource(fetcher).restore(backup.filename, ['devices']),
|
||||
).rejects.toThrowError('Component backup response is invalid.');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user