+ Authentication is required before overview data can be read for this instance.
+
+ );
+
+ if (!dataSource)
+ return (
+
+ No safe overview read data source is available. This console will not invent or call an
+ uncontracted production endpoint.
+
+ );
+
+ const retainedSnapshot =
+ state.kind === 'loading' || state.kind === 'error' ? state.snapshot : undefined;
+
+ if ((state.kind === 'loading' || state.kind === 'idle') && !retainedSnapshot)
+ return (
+
+ {state.kind === 'loading' ?
Refreshing overview…
: null}
+ {state.kind === 'error' ? (
+
+
Refresh failed; showing the last known overview.
+
+
+ ) : null}
+ {instance.freshness !== 'fresh' ? (
+
+ Overview data is {instance.freshness}; verify freshness before relying on these values.
+
+ ) : null}
+ {snapshot.observedAt ?
Observed {snapshot.observedAt}
: null}
+
+ {SECTIONS.map(([key, label]) => (
+
+ ))}
+
+
+ System actions
+
+ Restart is unavailable because the backend R3 restart operation is not implemented. No
+ action is offered.
+
+
+
+ );
+}
diff --git a/apps/web/src/operations/operation-client.test.ts b/apps/web/src/operations/operation-client.test.ts
new file mode 100644
index 0000000..3140c2e
--- /dev/null
+++ b/apps/web/src/operations/operation-client.test.ts
@@ -0,0 +1,216 @@
+import { describe, expect, it, vi } from 'vitest';
+import { OperationClientError, createOperationClient } from './operation-client.js';
+
+const response = (body: unknown, init: ResponseInit = {}) =>
+ new Response(JSON.stringify(body), {
+ status: 200,
+ headers: { 'content-type': 'application/json', ...init.headers },
+ ...init,
+ });
+
+const entry = {
+ operationId: 'safeOp',
+ title: 'Safe operation',
+ risk: 'R1',
+ capability: 'command',
+ batchable: false,
+ parameterSchemaId: 'safe.params.v1',
+};
+const page = { items: [entry], page: { page: 1, pageSize: 25, total: 1 } };
+const preparation = {
+ id: 'prep-1',
+ status: 'prepared',
+ operationId: 'safeOp',
+ risk: 'R1',
+ expiresAt: '2030-01-01T00:00:00.000Z',
+ confirmationToken: 'opaque-secret',
+ confirmationPrompt: 'Confirm safe operation',
+ targetCount: 1,
+};
+const job = {
+ id: 'job-1',
+ operationId: 'safeOp',
+ status: 'succeeded',
+ rootJobId: 'job-1',
+ items: [{ id: 'item-1', targetId: 'instance-1', state: 'succeeded' }],
+ attempts: [
+ {
+ id: 'attempt-1',
+ state: 'succeeded',
+ startedAt: '2029-01-01T00:00:00.000Z',
+ finishedAt: '2029-01-01T00:00:01.000Z',
+ },
+ ],
+ createdAt: '2029-01-01T00:00:00.000Z',
+};
+
+describe('safe operation client', () => {
+ it('loads a strictly validated catalog using same-origin credentials', async () => {
+ const fetcher = vi.fn(async () => response(page));
+ const client = createOperationClient(fetcher as typeof fetch);
+
+ await expect(client.list({ risk: 'R1', search: 'safe value' })).resolves.toEqual(page);
+ expect(fetcher).toHaveBeenCalledWith('/api/v1/operations?risk=R1&search=safe+value', {
+ method: 'GET',
+ credentials: 'same-origin',
+ headers: { accept: 'application/json' },
+ });
+ });
+
+ it.each([
+ { ...page, leaked: true },
+ { ...page, items: [{ ...entry, method: 'POST' }] },
+ { ...page, items: [{ ...entry, risk: 'R9' }] },
+ { ...page, page: { ...page.page, total: -1 } },
+ ])('rejects malformed or expanded catalog envelopes', async (body) => {
+ const client = createOperationClient(vi.fn(async () => response(body)) as typeof fetch);
+ await expect(client.list()).rejects.toThrow('Operation catalog response is invalid.');
+ });
+
+ it('only prepares an operation from the fetched catalog and retains its token privately', async () => {
+ const fetcher = vi
+ .fn()
+ .mockResolvedValueOnce(response(page))
+ .mockResolvedValueOnce(response(preparation));
+ const client = createOperationClient(fetcher as typeof fetch);
+ await client.list();
+
+ const result = await client.prepare({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'instance-1', revision: 2 }],
+ parameters: { parameterSchemaId: 'safe.params.v1', fields: [] },
+ });
+ expect(result).toEqual({
+ id: 'prep-1',
+ status: 'prepared',
+ operationId: 'safeOp',
+ risk: 'R1',
+ expiresAt: preparation.expiresAt,
+ confirmationPrompt: 'Confirm safe operation',
+ targetCount: 1,
+ });
+ expect(JSON.stringify(result)).not.toContain('opaque-secret');
+ expect(fetcher.mock.calls[1]).toEqual([
+ '/api/v1/operations/prepare',
+ expect.objectContaining({
+ method: 'POST',
+ credentials: 'same-origin',
+ body: JSON.stringify({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'instance-1', revision: 2 }],
+ parameters: { parameterSchemaId: 'safe.params.v1', fields: [] },
+ }),
+ }),
+ ]);
+ });
+
+ it('refuses uncatalogued operations and mismatched schemas without a request', async () => {
+ const fetcher = vi.fn(async () => response(page));
+ const client = createOperationClient(fetcher as typeof fetch);
+ await client.list();
+
+ await expect(
+ client.prepare({
+ operationId: 'hiddenOp',
+ targets: [{ instanceId: 'i' }],
+ parameters: { parameterSchemaId: 'hidden.v1', fields: [] },
+ }),
+ ).rejects.toThrow('Operation is not present in the loaded catalog.');
+ await expect(
+ client.prepare({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'i' }],
+ parameters: { parameterSchemaId: 'wrong', fields: [] },
+ }),
+ ).rejects.toThrow('Operation parameter schema does not match the catalog.');
+ expect(fetcher).toHaveBeenCalledOnce();
+ });
+
+ it('executes with the in-memory token exactly once and validates public Job fields', async () => {
+ const fetcher = vi
+ .fn()
+ .mockResolvedValueOnce(response(page))
+ .mockResolvedValueOnce(response(preparation))
+ .mockResolvedValueOnce(response(job, { status: 202 }));
+ const client = createOperationClient(fetcher as typeof fetch);
+ await client.list();
+ await client.prepare({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'instance-1' }],
+ parameters: { parameterSchemaId: 'safe.params.v1', fields: [] },
+ });
+
+ await expect(client.execute('prep-1')).resolves.toEqual(job);
+ expect(JSON.parse(fetcher.mock.calls[2][1].body)).toEqual({
+ preparationId: 'prep-1',
+ confirmationToken: 'opaque-secret',
+ });
+ await expect(client.execute('prep-1')).rejects.toThrow(
+ 'No in-memory confirmation is available for this preparation.',
+ );
+ expect(fetcher).toHaveBeenCalledTimes(3);
+ });
+
+ it('rejects expanded preparation and Job payloads', async () => {
+ const badPreparationClient = createOperationClient(
+ vi
+ .fn()
+ .mockResolvedValueOnce(response(page))
+ .mockResolvedValueOnce(
+ response({ ...preparation, transportPath: '/private' }),
+ ) as typeof fetch,
+ );
+ await badPreparationClient.list();
+ await expect(
+ badPreparationClient.prepare({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'i' }],
+ parameters: { parameterSchemaId: 'safe.params.v1', fields: [] },
+ }),
+ ).rejects.toThrow('Operation preparation response is invalid.');
+
+ const badJobClient = createOperationClient(
+ vi
+ .fn()
+ .mockResolvedValueOnce(response(page))
+ .mockResolvedValueOnce(response(preparation))
+ .mockResolvedValueOnce(
+ response({ ...job, upstreamResponse: 'secret' }, { status: 202 }),
+ ) as typeof fetch,
+ );
+ await badJobClient.list();
+ await badJobClient.prepare({
+ operationId: 'safeOp',
+ targets: [{ instanceId: 'i' }],
+ parameters: { parameterSchemaId: 'safe.params.v1', fields: [] },
+ });
+ await expect(badJobClient.execute('prep-1')).rejects.toThrow(
+ 'Operation execution response is invalid.',
+ );
+ });
+
+ it('redacts Problem Details detail, validation messages, and unknown response fields', async () => {
+ const problem = {
+ type: 'https://example.invalid/problem',
+ title: 'Bad Request',
+ status: 400,
+ detail: 'token=server-secret',
+ code: 'VALIDATION_FAILED',
+ requestId: 'request-1',
+ validation: [{ field: 'parameters', code: 'INVALID', message: 'secret field value' }],
+ debug: 'private stack',
+ };
+ const client = createOperationClient(
+ vi.fn(async () =>
+ response(problem, { status: 400, headers: { 'content-type': 'application/problem+json' } }),
+ ) as typeof fetch,
+ );
+
+ const error = await client.list().catch((caught: unknown) => caught);
+ expect(error).toBeInstanceOf(OperationClientError);
+ expect(error).toMatchObject({ status: 400, code: 'VALIDATION_FAILED', requestId: 'request-1' });
+ expect(JSON.stringify(error)).not.toContain('server-secret');
+ expect(JSON.stringify(error)).not.toContain('secret field value');
+ expect(JSON.stringify(error)).not.toContain('private stack');
+ });
+});
diff --git a/apps/web/src/operations/operation-client.ts b/apps/web/src/operations/operation-client.ts
new file mode 100644
index 0000000..a84b63e
--- /dev/null
+++ b/apps/web/src/operations/operation-client.ts
@@ -0,0 +1,440 @@
+import type {
+ OperationCatalogEntry,
+ OperationPage,
+ OperationPageQuery,
+ PrepareOperationRequest,
+ RiskLevel,
+} from '@multi-simadmin/contracts';
+
+type OperationCatalogPage = OperationPage;
+type OperationCatalogQuery = OperationPageQuery;
+type PrepareOperationInput = PrepareOperationRequest;
+export type OperationCapability = OperationCatalogEntry['capability'];
+export type {
+ OperationCatalogEntry,
+ OperationPage as OperationCatalogPage,
+ OperationPageQuery as OperationCatalogQuery,
+ PrepareOperationRequest as PrepareOperationInput,
+ RiskLevel,
+};
+
+/** The confirmation token is deliberately absent from this public view. */
+export interface PreparedOperation {
+ readonly id: string;
+ readonly status: 'prepared';
+ readonly operationId: string;
+ readonly risk: RiskLevel;
+ readonly expiresAt: string;
+ readonly confirmationPrompt: string;
+ readonly targetCount: number;
+}
+
+export type JobStatus =
+ | 'queued'
+ | 'running'
+ | 'cancelling'
+ | 'succeeded'
+ | 'partially-succeeded'
+ | 'failed'
+ | 'cancelled'
+ | 'unknown-result';
+export type JobItemState = 'succeeded' | 'failed' | 'skipped' | 'cancelled' | 'unknown-result';
+export type AttemptState = 'running' | 'succeeded' | 'failed' | 'cancelled' | 'unknown-result';
+export interface OperationJob {
+ readonly id: string;
+ readonly operationId: string;
+ readonly status: JobStatus;
+ readonly retryOfJobId?: string;
+ readonly rootJobId: string;
+ readonly items: readonly Readonly<{
+ id: string;
+ targetId: string;
+ state: JobItemState;
+ sourceJobItemId?: string;
+ error?: RedactedProblem;
+ }>[];
+ readonly attempts: readonly Readonly<{
+ id: string;
+ state: AttemptState;
+ startedAt: string;
+ finishedAt?: string;
+ }>[];
+ readonly createdAt: string;
+}
+
+export interface RedactedProblem {
+ readonly status: number;
+ readonly code: string;
+ readonly requestId: string;
+}
+
+export class OperationClientError extends Error implements RedactedProblem {
+ override readonly name = 'OperationClientError';
+ constructor(
+ readonly status: number,
+ readonly code: string,
+ readonly requestId: string,
+ ) {
+ super(`Operation request failed (${status}, ${code}).`);
+ }
+}
+
+export interface OperationClient {
+ list(query?: OperationCatalogQuery): Promise