feat(fleet): card-only UI with progressive load and restart ops
Drop the redundant advanced table so fleet stays resource-first cards. Keep multi-select batch service/system restart via prepare→execute, card restarts, overview system ops, and progressive fleet loading.
This commit is contained in:
@@ -23,6 +23,8 @@ describe('createSafeControlPlaneUpstream', () => {
|
||||
body: '[REDACTED]',
|
||||
});
|
||||
await upstream.postNetworkRegisterAuto('http://192.168.1.20');
|
||||
await upstream.postServiceRestart('http://192.168.1.20');
|
||||
await upstream.postSystemReboot('http://192.168.1.20', 3);
|
||||
expect(calls).toEqual([
|
||||
{ method: 'GET', url: 'http://192.168.1.20/api/health' },
|
||||
{
|
||||
@@ -37,6 +39,18 @@ describe('createSafeControlPlaneUpstream', () => {
|
||||
headers: {},
|
||||
body: '',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'http://192.168.1.20/api/service/restart',
|
||||
headers: {},
|
||||
body: '',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
url: 'http://192.168.1.20/api/system/reboot',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: '{"delay_seconds":3}',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import { SafeUpstreamGateway, type SafeUpstreamTransport } from './safe-upstream
|
||||
export interface SafeControlPlaneUpstream extends ConnectionTransport {
|
||||
request: UpstreamSessionClientOptions['request'];
|
||||
postNetworkRegisterAuto(origin: string): Promise<{ readonly status: number }>;
|
||||
postServiceRestart(origin: string): Promise<{ readonly status: number }>;
|
||||
postSystemReboot(origin: string, delaySeconds: number): Promise<{ readonly status: number }>;
|
||||
}
|
||||
export function createSafeControlPlaneUpstream(
|
||||
transport: SafeUpstreamTransport,
|
||||
@@ -14,5 +16,7 @@ export function createSafeControlPlaneUpstream(
|
||||
get: (url) => transport.get(url),
|
||||
request: (request) => gateway.request(request),
|
||||
postNetworkRegisterAuto: (origin) => gateway.postNetworkRegisterAuto(origin),
|
||||
postServiceRestart: (origin) => gateway.postServiceRestart(origin),
|
||||
postSystemReboot: (origin, delaySeconds) => gateway.postSystemReboot(origin, delaySeconds),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -161,4 +161,27 @@ describe('SafeUpstreamGateway', () => {
|
||||
}),
|
||||
).rejects.toThrow('UPSTREAM_REQUEST_INVALID');
|
||||
});
|
||||
|
||||
it('dispatches audited service restart and system reboot with fixed delay body', async () => {
|
||||
const calls: unknown[] = [];
|
||||
const gateway = new SafeUpstreamGateway({
|
||||
transport: {
|
||||
get: async () => ({ status: 200, headers: {}, body: '' }),
|
||||
post: async (url, headers, body) => {
|
||||
calls.push({ url, headers, body });
|
||||
return { status: 204, headers: {}, body: '' };
|
||||
},
|
||||
},
|
||||
});
|
||||
await gateway.postServiceRestart('http://192.168.1.20:8080');
|
||||
await gateway.postSystemReboot('http://192.168.1.20:8080', 3);
|
||||
expect(calls).toEqual([
|
||||
{ url: 'http://192.168.1.20:8080/api/service/restart', headers: {}, body: '' },
|
||||
{
|
||||
url: 'http://192.168.1.20:8080/api/system/reboot',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: '{"delay_seconds":3}',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface SafeUpstreamTransport {
|
||||
}
|
||||
export class SafeUpstreamGateway {
|
||||
constructor(private readonly options: { readonly transport: SafeUpstreamTransport }) {}
|
||||
async postNetworkRegisterAuto(origin: string): Promise<UpstreamResponse> {
|
||||
private assertOrigin(origin: string): string {
|
||||
let parsed: URL;
|
||||
try {
|
||||
parsed = new URL(origin);
|
||||
@@ -32,7 +32,12 @@ export class SafeUpstreamGateway {
|
||||
parsed.hash
|
||||
)
|
||||
throw this.notDispatched();
|
||||
const url = `${parsed.origin}/api/network/register-auto`;
|
||||
return parsed.origin;
|
||||
}
|
||||
|
||||
private async postZeroBody(origin: string, path: string): Promise<UpstreamResponse> {
|
||||
const base = this.assertOrigin(origin);
|
||||
const url = `${base}${path}`;
|
||||
try {
|
||||
return await this.options.transport.post(url, {}, '');
|
||||
} catch (error) {
|
||||
@@ -44,6 +49,34 @@ export class SafeUpstreamGateway {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async postNetworkRegisterAuto(origin: string): Promise<UpstreamResponse> {
|
||||
return this.postZeroBody(origin, '/api/network/register-auto');
|
||||
}
|
||||
|
||||
async postServiceRestart(origin: string): Promise<UpstreamResponse> {
|
||||
return this.postZeroBody(origin, '/api/service/restart');
|
||||
}
|
||||
|
||||
async postSystemReboot(origin: string, delaySeconds: number): Promise<UpstreamResponse> {
|
||||
if (delaySeconds !== 3) throw this.notDispatched();
|
||||
const base = this.assertOrigin(origin);
|
||||
const url = `${base}/api/system/reboot`;
|
||||
try {
|
||||
return await this.options.transport.post(
|
||||
url,
|
||||
{ 'content-type': 'application/json' },
|
||||
JSON.stringify({ delay_seconds: 3 }),
|
||||
);
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof UpstreamError &&
|
||||
(error.code === 'UNSAFE_ORIGIN' || error.code === 'UNSAFE_RESOLUTION')
|
||||
)
|
||||
throw new OperationNotDispatchedError(error.code);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async request(request: UpstreamRequest): Promise<UpstreamResponse> {
|
||||
const url = new URL(request.url);
|
||||
const headerKeys = Object.keys(request.headers).sort().join(',');
|
||||
|
||||
Reference in New Issue
Block a user