feat(server): operator removal of stale sessions/machines (SPEC-004 Task 5, server)
All checks were successful
Build and Test / Build Agent (Windows) (push) Successful in 7m29s
Build and Test / Build Server (Linux) (push) Successful in 10m58s
Build and Test / Security Audit (push) Successful in 4m4s
Build and Test / Build Summary (push) Successful in 8s

Admin-gated soft-delete + purge so operators can clear ghost machines/sessions
(the ~15-rows-for-one-host accumulation) from the console.

- migration 009: deleted_at on connect_sessions + connect_machines, with partial
  indexes WHERE deleted_at IS NULL.
- DELETE /api/machines/:agent_id?purge=true and DELETE /api/sessions/:id?purge=true
  soft-delete the row and purge the in-memory session (remove_session); the
  non-purge path keeps the legacy hard-delete / live-only disconnect. POST
  /api/machines/bulk-remove handles multi-select (batch cap 500). All admin-gated
  (AdminUser -> 403; tightens the prior any-user delete) and audited to
  connect_session_events (actor + target + trusted client IP).
- list/get queries filter deleted_at IS NULL so removed units leave the console;
  upsert revives (deleted_at = NULL) a genuinely-reconnecting machine. The
  keyed-reattach identity resolver (get_machine_by_id) is intentionally unfiltered.

Dashboard removal UI is the A3b follow-up. 86 server tests pass; fmt/clippy/test
clean. Implements specs/v2-stable-identity/plan.md Task 5 (server portion).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-31 13:52:36 -07:00
parent cef1928379
commit 5ee6675337
8 changed files with 1105 additions and 130 deletions

View File

@@ -6,6 +6,7 @@ pub mod changelog;
pub mod downloads;
pub mod machine_keys;
pub mod releases;
pub mod removal;
pub mod sessions;
pub mod users;
@@ -172,7 +173,7 @@ impl From<db::sessions::DbSession> for SessionRecord {
#[derive(Debug, Serialize)]
pub struct EventRecord {
pub id: i64,
pub session_id: String,
pub session_id: Option<String>,
pub event_type: String,
pub timestamp: String,
pub viewer_id: Option<String>,
@@ -185,7 +186,7 @@ impl From<db::events::SessionEvent> for EventRecord {
fn from(e: db::events::SessionEvent) -> Self {
Self {
id: e.id,
session_id: e.session_id.to_string(),
session_id: e.session_id.map(|id| id.to_string()),
event_type: e.event_type,
timestamp: e.timestamp.to_rfc3339(),
viewer_id: e.viewer_id,
@@ -208,12 +209,17 @@ pub struct MachineHistory {
/// Query parameters for machine deletion
#[derive(Debug, Deserialize)]
pub struct DeleteMachineParams {
/// If true, send uninstall command to agent (if online)
/// If true, send uninstall command to agent (if online). Legacy (non-purge) path.
#[serde(default)]
pub uninstall: bool,
/// If true, include history in response before deletion
/// If true, include history in response before deletion. Legacy (non-purge) path.
#[serde(default)]
pub export: bool,
/// If true, take the Task-5 SOFT-DELETE path: set `connect_machines.deleted_at`,
/// drop the live in-memory session, and audit the removal — instead of the legacy
/// hard delete. This is the operator-removal mechanism that purges ghost rows.
#[serde(default)]
pub purge: bool,
}
/// Response for machine deletion