-- Migration: 009_session_machine_soft_delete.sql -- Purpose: Give connect_machines and connect_sessions a soft-delete marker -- (deleted_at) so an operator can PURGE a stale machine/session — -- removing it from the live console — without destroying its audit -- history (SPEC-004 / v2-stable-identity Task 5). -- -- Task 5 is the operator-removal mechanism that finally purges the ~14 live -- ghost connect_machines rows left by the duplicate-registration bug. The -- live-only admin "disconnect" (DELETE /api/sessions/:id) and the legacy hard -- DELETE /api/machines/:agent_id stay as they were; the NEW purge path -- (`?purge=true`) sets deleted_at, drops the in-memory session via -- SessionManager::remove_session, and writes an audit row. A soft delete keeps -- the row (and its connect_session_events history) for the audit trail per the -- project convention "prefer deleted_at over hard deletes" (CLAUDE.md), while -- every list/get query filters `deleted_at IS NULL` so the purged unit -- disappears from the dashboard and the startup reconcile never restores it. -- -- Idempotent: ADD COLUMN IF NOT EXISTS. The columns are NULLABLE with no default, -- so adding them is a metadata-only change on Postgres (no table rewrite, no row -- locks held for a scan) — safe to apply online. A NULL deleted_at means "live"; -- a non-null timestamp means "removed at that instant". Applied on server startup -- by sqlx::migrate!(); never pre-applied via psql. Ordered after 008. -- See .claude/standards/gururmm/sqlx-migrations.md. -- 1. connect_sessions.deleted_at: when set, the session was operator-purged and is -- excluded from every list/get query. NULL = live. ALTER TABLE connect_sessions ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; -- 2. connect_machines.deleted_at: when set, the machine was operator-purged. This -- is the marker that hides the ghost duplicate rows from /api/machines and the -- startup reconcile. NULL = live. ALTER TABLE connect_machines ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; -- 3. Partial indexes so the hot "live rows only" filter (deleted_at IS NULL) used -- by every list query stays an index scan as the soft-deleted set grows. The -- predicate matches the WHERE clause the queries use. CREATE INDEX IF NOT EXISTS idx_connect_machines_live ON connect_machines (hostname) WHERE deleted_at IS NULL; CREATE INDEX IF NOT EXISTS idx_connect_sessions_live ON connect_sessions (started_at DESC) WHERE deleted_at IS NULL;