feat(dashboard): operator removal UI for stale machines/sessions (SPEC-004 Task 5)
All checks were successful
All checks were successful
Admin-only per-row Remove + multi-select bulk removal on the machines view, plus per-row purge Remove on the sessions view, wired to the Task-5 admin API (DELETE /api/machines|sessions/:id?purge=true, POST /api/machines/bulk-remove). Confirm modals (danger-styled, focus-trapped), TanStack refetch so purged rows leave the console, structured ApiError surfacing, honest partial-bulk summary, and admin-gating via useAuth().isAdmin as defense-in-depth over the server 403. Replaces the legacy all-user delete trigger. typecheck/lint/build clean. Implements specs/v2-stable-identity/plan.md Task 5 (dashboard portion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { http } from "./client";
|
||||
import type {
|
||||
BulkRemoveResponse,
|
||||
CreatedKey,
|
||||
DeleteMachineParams,
|
||||
DeleteMachineResponse,
|
||||
@@ -29,12 +30,21 @@ export function getMachineHistory(
|
||||
);
|
||||
}
|
||||
|
||||
/** DELETE /api/machines/:agent_id — remove a machine, optionally uninstall/export. */
|
||||
/**
|
||||
* DELETE /api/machines/:agent_id — remove a machine (admin only).
|
||||
*
|
||||
* Two server-side modes, selected by the query flags:
|
||||
* - `purge: true` → soft-delete + purge the in-memory session (Task 5
|
||||
* operator removal of ghost rows). Mutually exclusive with uninstall/export.
|
||||
* - otherwise → the legacy hard delete, optionally commanding the agent
|
||||
* to uninstall and/or returning full history in the response.
|
||||
*/
|
||||
export function deleteMachine(
|
||||
agentId: string,
|
||||
params: DeleteMachineParams = {},
|
||||
): Promise<DeleteMachineResponse> {
|
||||
const qs = new URLSearchParams();
|
||||
if (params.purge) qs.set("purge", "true");
|
||||
if (params.uninstall) qs.set("uninstall", "true");
|
||||
if (params.export) qs.set("export", "true");
|
||||
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
||||
@@ -43,6 +53,22 @@ export function deleteMachine(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/machines/bulk-remove — remove many machines at once (admin only).
|
||||
* Each id is soft-deleted + its session purged when `purge` is true. Invalid or
|
||||
* unknown ids are reported per-id in the response rather than failing the batch;
|
||||
* the server caps the batch at 500.
|
||||
*/
|
||||
export function bulkRemoveMachines(
|
||||
ids: string[],
|
||||
purge = true,
|
||||
): Promise<BulkRemoveResponse> {
|
||||
return http.post<BulkRemoveResponse>("/api/machines/bulk-remove", {
|
||||
ids,
|
||||
purge,
|
||||
});
|
||||
}
|
||||
|
||||
// --- Admin: per-agent keys --------------------------------------------------
|
||||
|
||||
/** GET /api/machines/:agent_id/keys — list key metadata (admin only). */
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { http } from "./client";
|
||||
import type { Session, ViewerTokenResponse } from "./types";
|
||||
import type {
|
||||
RemoveSessionResponse,
|
||||
Session,
|
||||
ViewerTokenResponse,
|
||||
} from "./types";
|
||||
|
||||
/**
|
||||
* GET /api/sessions — all live sessions known to the relay's in-memory session
|
||||
@@ -27,10 +31,25 @@ export function mintViewerToken(
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/sessions/:id — disconnect/end a live session. The relay sends a
|
||||
* Disconnect to the agent. Returns 200 on success, 404 if the session is not
|
||||
* found. Requires an authenticated dashboard JWT (not admin-gated server-side).
|
||||
* DELETE /api/sessions/:id — disconnect/end a live session (admin only). The
|
||||
* relay sends a Disconnect to the agent. Returns 200 on success, 404 if the
|
||||
* session is not live in memory. This is the live-only path (no `purge`); it
|
||||
* does not soft-delete any persisted row.
|
||||
*/
|
||||
export function endSession(sessionId: string): Promise<void> {
|
||||
return http.del<void>(`/api/sessions/${encodeURIComponent(sessionId)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/sessions/:id?purge=true — operator removal of a session (admin
|
||||
* only). Soft-deletes the persisted `connect_sessions` row and drops any live
|
||||
* in-memory session, clearing a ghost/stale session from the console. 404 only
|
||||
* when neither a live nor a persisted session exists.
|
||||
*/
|
||||
export function purgeSession(
|
||||
sessionId: string,
|
||||
): Promise<RemoveSessionResponse> {
|
||||
return http.del<RemoveSessionResponse>(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}?purge=true`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,6 +164,13 @@ export interface DeleteMachineParams {
|
||||
uninstall?: boolean;
|
||||
/** Include full history in the delete response before removal. */
|
||||
export?: boolean;
|
||||
/**
|
||||
* Operator-removal (Task 5): soft-delete the machine and purge its in-memory
|
||||
* session so a ghost row disappears from the console. Selects the server's
|
||||
* `?purge=true` path (admin-only). Mutually exclusive with the legacy
|
||||
* `uninstall`/`export` hard-delete options.
|
||||
*/
|
||||
purge?: boolean;
|
||||
}
|
||||
|
||||
export interface DeleteMachineResponse {
|
||||
@@ -173,6 +180,38 @@ export interface DeleteMachineResponse {
|
||||
history: MachineHistory | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-id outcome in a bulk machine removal. Mirrors
|
||||
* `api::removal::BulkRemoveItem`. `status` is one of `removed` | `not_found` |
|
||||
* `invalid` | `error` (widened to string for forward compatibility).
|
||||
*/
|
||||
export interface BulkRemoveItem {
|
||||
agent_id: string;
|
||||
status: "removed" | "not_found" | "invalid" | "error" | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Body for `POST /api/machines/bulk-remove`. Mirrors
|
||||
* `api::removal::BulkRemoveRequest`. The server caps the batch at 500 ids and
|
||||
* defaults `purge` to true; we always send it explicitly for the operator
|
||||
* removal workflow.
|
||||
*/
|
||||
export interface BulkRemoveRequest {
|
||||
ids: string[];
|
||||
purge: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Summary body for a bulk removal. Mirrors `api::removal::BulkRemoveResponse`.
|
||||
* `requested` is the batch size, `removed` the count that actually soft-deleted,
|
||||
* and `results` the per-id outcomes.
|
||||
*/
|
||||
export interface BulkRemoveResponse {
|
||||
requested: number;
|
||||
removed: number;
|
||||
results: BulkRemoveItem[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sessions (live relay state)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -221,6 +260,18 @@ export interface Session {
|
||||
consent_state: ConsentState | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Response from `DELETE /api/sessions/:id?purge=true`. Mirrors
|
||||
* `api::removal::RemoveSessionResponse`. `soft_deleted` is whether a persisted
|
||||
* `connect_sessions` row was marked deleted (false when the session was only
|
||||
* live in memory, e.g. an attended session that never persisted).
|
||||
*/
|
||||
export interface RemoveSessionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
soft_deleted: boolean;
|
||||
}
|
||||
|
||||
/** Access mode the relay grants a minted viewer token. */
|
||||
export type ViewerAccess = "control" | "view_only";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user