Admin debug/utility commands: /ping, /status, /workspace repair·rebuild·purge, /admin reset-database#38
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds operator-facing Discord slash commands for diagnostics and destructive maintenance/workspace operations, backed by new persistence/workspace services and accompanying unit tests.
Changes:
- Introduces
/pingand/statusdiagnostics commands. - Adds new dangerous maintenance/admin capabilities:
/workspace repair|rebuild|purgeand/admin reset-database. - Implements and tests
DatabaseMaintenanceService(global row wipe) andGuildPurgeService(single-guild data purge), and wires them into DI/options.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/RustPlusBot.Persistence.Tests/Maintenance/DatabaseMaintenanceServiceTests.cs | Adds coverage for database-wide row wipe preserving schema. |
| tests/RustPlusBot.Features.Workspace.Tests/Teardown/GuildPurgeServiceTests.cs | Adds coverage for purging a single guild while leaving other guild data intact. |
| src/RustPlusBot.Persistence/PersistenceServiceCollectionExtensions.cs | Registers IDatabaseMaintenanceService in persistence DI. |
| src/RustPlusBot.Persistence/Maintenance/IDatabaseMaintenanceService.cs | Adds interface for database-wide maintenance operations. |
| src/RustPlusBot.Persistence/Maintenance/DatabaseMaintenanceService.cs | Implements database-wide row wipe via raw SQL deletes. |
| src/RustPlusBot.Host/Program.cs | Binds MaintenanceOptions from the Workspace config section. |
| src/RustPlusBot.Features.Workspace/WorkspaceServiceCollectionExtensions.cs | Registers IGuildPurgeService in workspace DI. |
| src/RustPlusBot.Features.Workspace/Teardown/IGuildPurgeService.cs | Adds interface for guild-scoped purge behavior. |
| src/RustPlusBot.Features.Workspace/Teardown/GuildPurgeService.cs | Implements purge flow: teardown workspace + delete guild/server-scoped rows. |
| src/RustPlusBot.Features.Workspace/Modules/WorkspaceAdminModule.cs | Adds /workspace repair, /workspace rebuild, /workspace purge commands and confirmation flows. |
| src/RustPlusBot.Features.Commands/Modules/MaintenanceModule.cs | Adds /admin reset-database with typed confirmation + danger gating. |
| src/RustPlusBot.Features.Commands/Modules/DiagnosticsModule.cs | Adds /ping and /status read-only diagnostics. |
| src/RustPlusBot.Features.Commands/MaintenanceOptions.cs | Introduces options model for danger-gating maintenance command(s). |
| src/RustPlusBot.Features.Commands/CommandServiceCollectionExtensions.cs | Registers MaintenanceOptions in the commands feature DI. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| await context.Database.OpenConnectionAsync(cancellationToken).ConfigureAwait(false); | ||
| try | ||
| { | ||
| await context.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = OFF", cancellationToken) | ||
| .ConfigureAwait(false); | ||
|
|
||
| foreach (var table in tables) | ||
| { | ||
| // Deletion order is deliberately irrelevant: PRAGMA foreign_keys = OFF (above) suspends | ||
| // FK enforcement for the wipe, so do not "fix" this by adding a topological sort. | ||
| // Table names come from the EF model (never user input); the identifier guard keeps | ||
| // the raw statement demonstrably injection-safe for the Sonar gate. | ||
| if (!IsSafeIdentifier(table!)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var sql = string.Create(CultureInfo.InvariantCulture, $"DELETE FROM \"{table}\""); | ||
| await context.Database.ExecuteSqlRawAsync(sql, cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| await context.Database.ExecuteSqlRawAsync("PRAGMA foreign_keys = ON", cancellationToken) | ||
| .ConfigureAwait(false); | ||
| } |
There was a problem hiding this comment.
Fixed in a2dc8bf. ClearAllAsync now runs the DELETEs inside a single transaction with PRAGMA defer_foreign_keys = ON (FK enforcement deferred to commit, and it auto-resets when the transaction ends). An interrupted/cancelled wipe now rolls back instead of leaving the DB partially cleared, and it no longer leaves a connection-level foreign_keys pragma toggled off on a pooled connection. The identifier guard now throws InvalidOperationException on an unexpected table name instead of silently skipping it.
| // Cap server fields so the embed stays under Discord's 25-field limit (4 header fields above). | ||
| foreach (var server in known.Take(20)) | ||
| { | ||
| var state = await connections.GetStateAsync(Context.Guild.Id, server.Id).ConfigureAwait(false); | ||
| var line = state is null |
There was a problem hiding this comment.
Fixed in a2dc8bf. Added IConnectionStore.GetStatesForGuildAsync(guildId) (one query). /status now fetches all of the guild's connection states once and populates the embed from an in-memory dictionary keyed by RustServerId, instead of calling GetStateAsync per server.
| if (!options.Value.EnableDangerCommands) | ||
| { | ||
| await RespondAsync("Developer commands are disabled.", ephemeral: true).ConfigureAwait(false); | ||
| return; | ||
| } |
There was a problem hiding this comment.
Fixed in a2dc8bf. The /admin danger-off reply is now: "Dangerous maintenance commands are disabled. Set Workspace:EnableDangerCommands to enable them."
| // 1) Delete provisioned Discord channels/categories/messages (Discord side + records). | ||
| await teardown.ResetGuildAsync(guildId, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| // 2) Remove each server; the RustServer FK cascade clears its per-server rows | ||
| // (connection state, command/map settings, switches, alarms, storage monitors, credentials). | ||
| var known = await servers.ListAsync(guildId, cancellationToken).ConfigureAwait(false); | ||
| foreach (var server in known) | ||
| { | ||
| await servers.RemoveAsync(guildId, server.Id, cancellationToken).ConfigureAwait(false); | ||
| } |
There was a problem hiding this comment.
Fixed in a2dc8bf. PurgeGuildAsync now acquires the per-guild IProvisioningLock and holds it across the whole purge (teardown + server/row deletes), calling a new lock-free WorkspaceTeardownService.ResetGuildCoreAsync to avoid re-acquiring it. This blocks concurrent reconciliation — notably the self-heal that fires when teardown deletes channels — from re-provisioning into a half-purged guild.
… lock) - DatabaseMaintenanceService: wipe in a single transaction with defer_foreign_keys so an interruption rolls back instead of leaving a partially-cleared DB; throw on an unexpected table identifier instead of silently skipping it (and reporting a misleading success). - DiagnosticsModule /status: replace the per-server GetStateAsync N+1 with a single IConnectionStore.GetStatesForGuildAsync bulk read + dictionary lookup. - MaintenanceModule: clearer danger-off message for the operator-facing /admin group instead of "Developer commands are disabled". - GuildPurgeService: hold the per-guild provisioning lock across the whole purge (teardown + domain deletes) so concurrent reconciliation — including the self-heal triggered by teardown deleting channels — cannot re-provision into a half-purged guild. Adds a lock-free WorkspaceTeardownService core. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Summary
Adds six operator-facing admin/debug/utility slash commands, reusing the existing authorization model (no new bot-owner concept). Fills the gaps identified in an inventory of the current surface (
/help,/uptime,/setup,/workspace reset).Commands
/ping/status/workspace repair/workspacegroup/workspace rebuild/workspacegroup/workspace purge/workspacegroup/admin reset-database/admingroupRESETSupporting services (unit-tested)
DatabaseMaintenanceService(Persistence) —ClearAllAsync()clears every table's rows while keeping the schema (live-safe: single held-open connection +PRAGMA foreign_keys = OFF). Not a drop/recreate.GuildPurgeService(Features.Workspace) — deletes exactly one guild's data: teardown provisioned channels, remove itsRustServers (FK cascade clears per-server rows incl. credentials), then explicit deletes for the guild-keyed non-cascaded tables (EventSubscription,PairedEntity,GuildSettings,FcmRegistration).Design decisions
ManageGuild+ the existingEnableDangerCommandsflag.MaintenanceOptionsbinds the sameWorkspaceconfig section asWorkspaceOptions, so one flag governs every danger command..resxkeys (parity stays at 260).Testing
-maxcpucount:1).-warnaserror);jb cleanupcodeclean.DatabaseMaintenanceServiceTests(empties every table incl. an FK-cascade seed, schema survives),GuildPurgeServiceTests(target guild fully purged, a second guild untouched).Notes for reviewers
/admin reset-databaseis intentionally guild-admin-triggerable when the danger flag is on (no owner tier, per design) — the flag defaults off and the typedRESETconfirmation is the backstop.DbContexts) but leaves in-memory state stale until the advised restart.🤖 Generated with Claude Code