[Overview]
Implement Feature 6 (Admin Panel for Guild Management) and Feature 7 (Audit Log) for the DrusaBoT music Discord bot. The Admin page is an owner-only interface listing every guild the bot is in, letting the owner set a per-guild command prefix and manage per-guild blacklist/whitelist user restrictions. The Audit Log page surfaces every admin/mod action from the existing audit_logs table. This fills a confirmed gap: today access control (prefix, blacklist, whitelist) is global-only and only reachable via Discord text commands; this adds a web UI plus the data layer to support it on a per-guild basis.
Scope is Option A (per-guild), confirmed by the user. The bot is currently single-guild-configured (config.guild_id) with a hardcoded command_prefix="!", global blacklisted_users/approved_users tables, and owner-only Discord admin cogs that already write to audit_logs. The plan adds: three new per-guild tables, a prefix resolver callable in the bot core, per-guild enforcement in is_authorized, owner-only admin API routes, an audit manager module, and a React admin + audit-log SPA surface. No existing user-facing behavior is removed; global lists keep working; default prefix stays ! and is backward compatible.
[Types]
Backend Python (no new public types; route payloads use Pydantic models):
PrefixUpdate(Pydantic BaseModel, routes.py) —prefix: str = Field(..., min_length=1, max_length=5, pattern=r"^[^\s]{1,5}$"). Validates a custom prefix is a single non-whitespace token, max 5 chars.BulkUserRef(Pydantic BaseModel, routes.py) —user_id: str(resolved Discord id),username: str,display_name: str. For add-blacklist/whitelist.- Reuses existing
SettingsUpdate,GuildSettingsfrom routes.py/guild_settings. - Reuses DB row dicts (
sqlite3.Row→dict) returned by managers, matching thefavorites_manager/guild_settingspattern (functions returnlist[dict]/dict).
Frontend TypeScript (web/src/lib/types.ts additions):
AdminGuild { id: string; name: string; member_count?: number | null; icon_url?: string | null; prefix: string; blacklist_count: number; whitelist_count: number }PrefixUpdate { prefix: string }AccessEntry { user_id: string; username: string; display_name: string; added_by: string; added_at: string }AuditLog { id: number; action: string; target_user_id: string; target_username: string; moderator_id: string; moderator_name?: string; timestamp: string }AuditLogsResponse { logs: AuditLog[]; total: number; limit: number; offset: number; actions: string[] }AdminActionunion:"set_prefix" | "add_blacklist" | "remove_blacklist" | "add_whitelist" | "remove_whitelist"
[Files]
New files:
bot/database/admin_manager.py— per-guild prefix, blacklist, whitelist CRUD (mirrors favorites_manager.py: plain functions takingdb_path, usingget_connection). Includes an in-memory_PREFIX_CACHEdict for prefix lookups.bot/database/audit_manager.py—log_action(...),get_logs(...),get_actions(...),count_logs(...). Canonical writer foraudit_logs; cogs call through here.web/src/pages/AdminPage.tsx— owner-only guild management page (guild selector + prefix editor + blacklist/whitelist panels).web/src/features/admin/AuditLog.tsx— paginated, filterable audit-log viewer.web/src/hooks/use-admin.ts— React Query hooks for admin API (/api/admin/...).web/src/hooks/use-audit.ts— React Query hook for/api/admin/audit.tests/test_admin_manager.py— unit tests for admin_manager (temp DB).tests/test_audit_manager.py— unit tests for audit_manager (temp DB).tests/test_dashboard_admin_routes.py— TestClient tests for admin + audit routes (owner auth, 403 for admin role, prefix validation, blacklist/whitelist CRUD).
Modified files: Backend:
bot/database/database.py— addguild_prefixes,guild_blacklist,guild_whitelistCREATE TABLE + indexes in_create_tables.bot/database/repository.py— add the same three tables + indexes in_create_tables_postgres(PostgreSQL parity).bot/core/bot.py—command_prefix="!"→command_prefix=get_prefix_resolvercallable. Add_PREFIX_CACHEimport-free lazy resolver in admin_manager, consumed here.bot/dashboard/routes.py— add owner-only admin routes + audit route. Add_require_ownerhelper (shared secret OR OAuthrole=="owner"session; 403 for admin-role, 401 for missing/invalid). Signatureregister_routes(app, bot, security, check_write_auth)unchanged.bot/cogs/admin/base.py—log_auditdelegates toaudit_manager.log_action(single code path, DRY).bot/cogs/music/base.py—is_authorizedadditionally checks per-guild blacklist and per-guild whitelist mode before global checks (owner always passes; adds no-op when tables empty).
Frontend:
web/src/lib/types.ts— add AdminGuild, PrefixUpdate, AccessEntry, AuditLog, AuditLogsResponse, AdminAction.web/src/router.tsx— add/adminchild route (owner-guarded) and/admin/auditroute.web/src/components/layout/Sidebar.tsx— add conditional Admin nav item (shields icon) shown whensessionUser?.role === "owner".web/src/components/layout/ProtectedRoute.tsx— unchanged (login guard); admin gating lives in AdminPage.
Files deleted/moved: none.
Configuration: no .env / settings changes. New behavior is DB-driven and backward-compatible.
[Functions]
New functions:
Backend (bot/database/admin_manager.py):
get_prefix(guild_id: str, db_path: str) -> str— returns stored prefix or"!". Reads_PREFIX_CACHEfirst.set_prefix(guild_id: str, prefix: str, db_path: str) -> str— upsertsguild_prefixesrow, invalidates_PREFIX_CACHE, returns prefix.list_blacklist(guild_id: str, db_path: str) -> list[dict]add_blacklist(guild_id, user_id, username, display_name, added_by, db_path) -> bool(True if newly added; False already present)remove_blacklist(guild_id, user_id, db_path) -> boollist_whitelist(guild_id, db_path) -> list[dict]add_whitelist(...)-> bool;remove_whitelist(...) -> boolguild_admin_summary(guild_id, db_path) -> dict—{prefix, blacklist_count, whitelist_count}is_guild_blacklisted(guild_id, user_id, db_path) -> boolandis_guild_whitelisted(guild_id, user_id, db_path) -> bool— used by enforcement.get_prefix_resolver(bot)/resolve_prefix(bot, message)helper used bycommand_prefixcallable — lazy imports admin_manager to avoid import cycles.
Backend (bot/database/audit_manager.py):
log_action(action, target_user_id, target_username, moderator_id, db_path) -> int— insert, returns row id.get_logs(db_path, limit=50, offset=0, action=None) -> list[dict]get_actions(db_path) -> list[str]— distinct actions for filter dropdown.count_logs(db_path) -> int
Backend (bot/dashboard/routes.py, inside register_routes):
_require_owner(credentials) -> None— raises HTTPException(403/401). Accepts shared secret OR OAuth session withrole=="owner".GET /api/admin/guilds— all guilds enriched with prefix + counts (owner-only).GET /api/admin/guilds/{guild_id}— single guild summary.POST /api/admin/guilds/{guild_id}/prefix— set prefix (PrefixUpdate).GET /api/admin/guilds/{guild_id}/blacklist— list.POST /api/admin/guilds/{guild_id}/blacklist— add (Body user_id, username, display_name; resolves/normalizes).DELETE /api/admin/guilds/{guild_id}/blacklist/{user_id}— remove.GET /api/admin/guilds/{guild_id}/whitelist— list.POST /api/admin/guilds/{guild_id}/whitelist— add.DELETE /api/admin/guilds/{guild_id}/whitelist/{user_id}— remove.GET /api/admin/audit— owner-only paginated audit list (limit,offset,action). Resolves moderator_name frombot.get_usercache.
Modified functions:
bot/cogs/admin/base.py::log_audit— body becomesreturn await audit_manager.log_action(...)(sync; called viaawaitalready works since callersawait log_audit). Keep async wrapper for backward compatibility.bot/cogs/music/base.py::is_authorized(ctx, bot)— after owner check and before global blacklist/approved checks, add: ifctx.guildandis_guild_blacklisted(guild_id, user_id, db_path)→ deny with "blacklisted in this server"; if guild has any whitelist entries and user not whitelisted → deny with "not in server allowlist". Falls through to existing global checks. No-op when tables empty (fresh DB), so existing tests unaffected.bot/core/bot.pyBot.__init__— replacecommand_prefix="!"withcommand_prefix=resolve_prefixcallable.
New classes: none significant. AuditResult not needed (manager returns plain dicts).
[Classes]
Backend: no new classes. (Bot subclass modified only to swap command_prefix value to a callable.)
Frontend: AdminPage and AuditLog are functional components (project convention: pages/features use function components, no classes).
[Dependencies]
Backend: no new packages. Reuses sqlite3 (stdlib), existing get_connection. PostgreSQL path updated in repository.py for parity only.
Frontend: no new packages. Reuses lucide-react (add Shield icon import), @tanstack/react-query, zustand, react-router-dom. No changes to package.json.
Deploy: cd web && npm run build after frontend changes; bot restart to load new prefix callable + cogs (cogs already auto-load).
[Testing]
New test files:
tests/test_admin_manager.py: temp DB, init tables; test set/get prefix (cache invalidation), blacklist add/remove/list (dedup), whitelist same, guild_admin_summary counts.tests/test_audit_manager.py: temp DB; log_action then get_logs (pagination, action filter, ordering), get_actions, count_logs.tests/test_dashboard_admin_routes.py: mirrorstest_dashboard_routes.pypattern — simple bot SimpleNamespace withguilds,get_user,config.database_path,config.dashboard_secret_key. Cases:GET /api/admin/guildsreturns 401 without token, 200 with shared-secret, 403 withrole=adminOAuth token, 200 withrole=ownertoken.- Prefix:
POST .../prefixrejectsprefix="", acceptsprefix="?",get_prefixreturns?. - Blacklist add/remove/list round-trip. Whitelist add/remove/list round-trip.
GET /api/admin/auditreturns paginated logs.
Existing tests kept green: run pytest tests/ -x. The is_authorized change is a no-op on empty tables. The command_prefix change is config-level; existing music/cog tests that build a Bot with a fake prefix won't break (they don't invoke real command dispatch). Verify with tests/test_dashboard_routes.py and tests/test_database.py.
Lint/typecheck: ruff check bot/ && black bot/; cd web && npm run lint && npx tsc --noEmit.
Validation strategy:
- Local:
pytest tests/ -xgreen.ruff + blackclean. Frontendtsc+oxlintclean. - Frontend dev:
cd web && npm run dev, open/adminwith owner OAuth session → guild list loads, prefix editable, blacklist/whitelist panels work,/admin/auditshows logs with moderator names. - Backend: hit
/api/admin/guildswith owner OAuth token → 200; with admin-role token → 403. - Deploy: rebuild frontend, restart bot, login as owner, exercise admin page, trigger actions, confirm audit log entries appear; issue
!help/commands to confirm custom per-guild prefix works and blacklisted/whitelisted enforcement behaves.
[Implementation Order]
- DB schema — add the three tables + indexes to
database.py::_create_tablesANDrepository.py::_create_tables_postgres(non-destructiveCREATE TABLE IF NOT EXISTS; no migration script needed). bot/database/admin_manager.py— prefix/blacklist/whitelist CRUD + cache + enforcement helpers + prefix resolver.bot/database/audit_manager.py— log getter + writer; refactorbot/cogs/admin/base.py::log_auditto delegate.bot/core/bot.py— swapcommand_prefixto the resolver callable.bot/cogs/music/base.py::is_authorized— add per-guild blacklist/whitelist enforcement.bot/dashboard/routes.py— add_require_owner+ all admin + audit API routes.- Frontend types + hooks (
types.ts,use-admin.ts,use-audit.ts). web/src/pages/AdminPage.tsx+web/src/features/admin/AuditLog.tsx.web/src/router.tsx+web/src/components/layout/Sidebar.tsx+ TopBar/AdminPage gating.- Tests (3 new test files) + run
pytest -x,ruff,black, frontendtsc/oxlint. - Frontend production build (
cd web && npm run build) →web/dist. - Git commit + push; restart bot; smoke test (owner auth 403-for-admin, prefix works, audit visible).