Skip to content

Admin debug/utility commands: /ping, /status, /workspace repair·rebuild·purge, /admin reset-database#38

Merged
HandyS11 merged 9 commits into
developfrom
feat/admin-utility-commands
Jul 1, 2026
Merged

Admin debug/utility commands: /ping, /status, /workspace repair·rebuild·purge, /admin reset-database#38
HandyS11 merged 9 commits into
developfrom
feat/admin-utility-commands

Conversation

@HandyS11

@HandyS11 HandyS11 commented Jul 1, 2026

Copy link
Copy Markdown
Owner

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

Command Home Gating Action
/ping DiagnosticsModule none Discord gateway latency + REST round-trip
/status DiagnosticsModule none uptime, latency, per-server Rust+ connection status, guild/server counts
/workspace repair /workspace group ManageGuild non-destructive reconcile — recreate any missing channels/messages
/workspace rebuild /workspace group ManageGuild + danger flag + confirm button teardown + re-provision
/workspace purge /workspace group ManageGuild + danger flag + confirm button delete all of this guild's bot data (channels + servers + settings)
/admin reset-database new /admin group ManageGuild + danger flag + typed RESET clear all DB rows across all guilds (keeps schema) + restart notice

Supporting 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 its RustServers (FK cascade clears per-server rows incl. credentials), then explicit deletes for the guild-keyed non-cascaded tables (EventSubscription, PairedEntity, GuildSettings, FcmRegistration).

Design decisions

  • Auth: destructive commands require ManageGuild + the existing EnableDangerCommands flag. MaintenanceOptions binds the same Workspace config section as WorkspaceOptions, so one flag governs every danger command.
  • DB reset semantics: clear rows (not drop schema); response advises restarting the bot for a clean in-memory state.
  • Localization: plain English (operator-facing), consistent with the existing admin commands — no new .resx keys (parity stays at 260).
  • Diagnostics + admin modules are thin Discord I/O (coverage-excluded per repo convention); the two services carry the TDD tests.

Testing

  • Full suite: 797 passed / 0 failed across 16 assemblies (-maxcpucount:1).
  • Solution build 0 warnings / 0 errors (-warnaserror); jb cleanupcode clean.
  • New service tests: 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-database is intentionally guild-admin-triggerable when the danger flag is on (no owner tier, per design) — the flag defaults off and the typed RESET confirmation is the backstop.
  • Running the DB reset live is safe (hosted services use per-operation scoped DbContexts) but leaves in-memory state stale until the advised restart.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 1, 2026 17:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /ping and /status diagnostics commands.
  • Adds new dangerous maintenance/admin capabilities: /workspace repair|rebuild|purge and /admin reset-database.
  • Implements and tests DatabaseMaintenanceService (global row wipe) and GuildPurgeService (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.

Comment on lines +21 to +44
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);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +61 to +65
// 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +31 to +35
if (!options.Value.EnableDangerCommands)
{
await RespondAsync("Developer commands are disabled.", ephemeral: true).ConfigureAwait(false);
return;
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a2dc8bf. The /admin danger-off reply is now: "Dangerous maintenance commands are disabled. Set Workspace:EnableDangerCommands to enable them."

Comment on lines +19 to +28
// 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);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@HandyS11 HandyS11 merged commit 338d09f into develop Jul 1, 2026
3 checks passed
@HandyS11 HandyS11 deleted the feat/admin-utility-commands branch July 1, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants