Fix promotion blockers, identify audit log executors, track unban/timeout#255
Fix promotion blockers, identify audit log executors, track unban/timeout#255
Conversation
There’s a race condition in attributing bans, because the Audit Log Entry object isn’t created immediately as the Ban event fires. We’ll need to wait for a few seconds to make sure that the Audit Log Entry isn’t still pending before we post. We could perhaps change this to post immediately and edit the message once attribution details come back.
Extends the mod action logging system to capture timeout and unban events. Timeout logs include human-readable duration, and only manual timeout removals (before natural expiry) are logged. Refactors handlers to use Effect-TS with thin async wrappers that execute Effects. Extracts shared audit log fetching logic into a reusable helper. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates public-facing promotion blockers (title + invite link) and expands moderation reporting to capture more actions (unban/timeout) with better attribution via audit logs.
Changes:
- Update app page title metadata for production branding.
- Update the “Add to server” invite link and supporting UI entry point.
- Refactor moderation/audit-log reporting to use Effect-based handlers and add logging for unbans and timeouts.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| app/root.tsx | Updates the app’s <title> metadata for branding/promotion readiness. |
| app/components/DiscordLayout.tsx | Updates the Discord OAuth invite URL used by the dashboard UI. |
| app/commands/report/modActionLogger.ts | Refactors mod-action event handling, adds audit log retries, and introduces unban/timeout tracking. |
| app/commands/report/modActionLog.ts | Extends the mod-action report union and formats new action types in user-thread logs. |
| app/commands/report/automodLog.ts | Exports logAutomod for reuse by the updated logger. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const entry = findEntry(auditLogs.entries); | ||
| if (entry?.executor) { | ||
| yield* logEffect("debug", "ModActionLogger", `record found`, { | ||
| attempt: attempt + 1, | ||
| }); | ||
| return entry; | ||
| } |
There was a problem hiding this comment.
fetchAuditLogEntry only returns a found audit-log record when entry.executor is truthy. Audit log entries can exist without an executor (or the executor can be unavailable), which means this will incorrectly treat real bans/kicks/unbans as “not found” (e.g., kicks become left) and will also drop reason data. Return when an entry is found (and let downstream code handle executor === null) instead of gating on executor.
| // Determine if this is a timeout applied or removed | ||
| const isTimeoutApplied = newTimeout !== null && newTimeout > Date.now(); | ||
| const isTimeoutRemoved = | ||
| oldTimeout !== null && oldTimeout > Date.now() && newTimeout === null; | ||
|
|
There was a problem hiding this comment.
isTimeoutApplied is true for any member update while a timeout is active (newTimeout > Date.now()), even when oldTimeout === newTimeout. Since GuildMemberUpdate fires for many unrelated changes, this will spam duplicate “timeout” logs. Detect an actual timeout change (e.g., compare oldTimeout vs newTimeout, or compute hadTimeout/hasTimeout and only log when transitioning).
| // Determine if this is a timeout applied or removed | |
| const isTimeoutApplied = newTimeout !== null && newTimeout > Date.now(); | |
| const isTimeoutRemoved = | |
| oldTimeout !== null && oldTimeout > Date.now() && newTimeout === null; | |
| // Determine if this is a timeout applied, updated, or removed | |
| const hadTimeout = | |
| oldTimeout !== null && oldTimeout > Date.now(); | |
| const hasTimeout = | |
| newTimeout !== null && newTimeout > Date.now(); | |
| // Treat as "applied" if a timeout has become active or its expiry changed | |
| const isTimeoutApplied = | |
| hasTimeout && (!hadTimeout || oldTimeout !== newTimeout); | |
| // Treat as "removed" if a previously active timeout is no longer active | |
| const isTimeoutRemoved = hadTimeout && !hasTimeout; |
|
|
||
| const reasonText = reason ? ` ${reason}` : " for no reason"; | ||
| const durationText = | ||
| actionType === "timeout" ? ` for ${report.duration}` : ""; |
There was a problem hiding this comment.
durationText reads report.duration based on actionType === "timeout", but actionType was destructured from report, so TypeScript typically won’t narrow report here (and duration doesn’t exist on the other union variants). This is likely a type error under strict. Narrow on report.actionType (or use an if (report.actionType === "timeout") block) before accessing report.duration.
| actionType === "timeout" ? ` for ${report.duration}` : ""; | |
| report.actionType === "timeout" ? ` for ${report.duration}` : ""; |
| { | ||
| userId, | ||
| guildId: guild.id, | ||
| ruleId: autoModerationRule?.name, |
There was a problem hiding this comment.
In the structured log context for the timeout-skip branch, the key is ruleId but the value being logged is autoModerationRule?.name. This makes the log payload misleading; use ruleName for the name, or log the actual rule ID if you want ruleId.
| ruleId: autoModerationRule?.name, | |
| ruleName: autoModerationRule?.name, |
Preview environment removedThe preview for this PR has been cleaned up. |
Partially addresses #252