Skip to content

Enhanced Error Handling#6

Merged
BillChirico merged 1 commit intomainfrom
auto-claude/003-enhanced-error-handling
Feb 4, 2026
Merged

Enhanced Error Handling#6
BillChirico merged 1 commit intomainfrom
auto-claude/003-enhanced-error-handling

Conversation

@BillChirico
Copy link
Collaborator

Implement comprehensive error handling with user-friendly error messages, automatic retry for transient failures, and detailed error logging for debugging.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Enhanced error handling with user-friendly messages and actionable guidance for common issues including network failures, timeouts, rate limiting, and Discord-specific problems.
    • Automatic retry mechanism with intelligent backoff to gracefully handle transient errors.

Walkthrough

Two new utility modules are introduced: errors.js provides error classification, user-friendly messaging, and actionable guidance across categories like network, timeout, API, Discord, and config errors. retry.js implements configurable async retry logic with exponential backoff, integrating error classification to determine retryability.

Changes

Cohort / File(s) Summary
Error Classification Utility
src/utils/errors.js
New module exports ErrorType enum with 13 error categories and four functions: classifyError (maps errors to types), getUserFriendlyMessage (localized messaging), getSuggestedNextSteps (actionable guidance), and isRetryable (transience check for NETWORK, TIMEOUT, API_SERVER_ERROR, API_RATE_LIMIT).
Retry Mechanism Utility
src/utils/retry.js
New module exports withRetry (async retry executor with exponential backoff, configurable maxRetries, baseDelay, maxDelay, and shouldRetry predicate) and createRetryWrapper (factory for pre-configured retry wrappers). Integrates error classification to determine eligibility for retry and includes context-aware logging.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant withRetry
    participant ErrorClassifier
    participant Operation
    participant Backoff

    Client->>withRetry: call withRetry(fn, options)
    
    loop Retry Loop (up to maxRetries)
        withRetry->>Operation: execute operation
        alt Operation Succeeds
            Operation-->>withRetry: return result
            withRetry-->>Client: return success
        else Operation Fails
            Operation-->>withRetry: throw error
            withRetry->>ErrorClassifier: classifyError(error)
            ErrorClassifier-->>withRetry: ErrorType
            withRetry->>ErrorClassifier: isRetryable(error)
            ErrorClassifier-->>withRetry: boolean
            
            alt Retryable & Retries Remaining
                withRetry->>Backoff: calculateBackoff(attempt)
                Backoff-->>withRetry: delay time
                withRetry->>withRetry: sleep(delay)
                Note over withRetry: Log retry attempt, loop continues
            else Non-Retryable or No Retries Left
                withRetry-->>Client: throw error
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Enhanced Error Handling' is concise (23 characters), clearly descriptive, and directly reflects the main changeset which adds comprehensive error handling utilities.
Description check ✅ Passed The description accurately describes the changeset, mentioning error classification, user-friendly messages, automatic retries for transient failures, and error logging—all core features implemented in the added modules.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch auto-claude/003-enhanced-error-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Add errors.js with error classification, user-friendly messages, and retry detection
- Add retry.js with exponential backoff for transient failures
- Integrate with existing Winston logger from main branch
@BillChirico BillChirico force-pushed the auto-claude/003-enhanced-error-handling branch from 005cfd4 to c2bfc4a Compare February 4, 2026 02:37
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@src/utils/errors.js`:
- Around line 91-94: The current detection only inspects the error message
string (`message`) to map to ErrorType.CONFIG_MISSING; update the logic in the
function in src/utils/errors.js to also check the error object's `code` property
(e.g., `code === 'ENOENT'`) and handle cases where the input may be an Error
object rather than a string, so that when either message includes 'config.json
not found' or `code === 'ENOENT'` you return ErrorType.CONFIG_MISSING; reference
the existing `message` variable and the `ErrorType.CONFIG_MISSING` symbol when
making this change.
- Around line 114-146: The messages map inside getUserFriendlyMessage is
recreated on every call; move the messages object to module scope (e.g., const
MESSAGES = { ... }) and have getUserFriendlyMessage reference
MESSAGES[errorType]. Update references to ErrorType keys accordingly and keep
the default fallback to MESSAGES[ErrorType.UNKNOWN] so behavior is unchanged
while avoiding repeated allocations.
- Around line 51-56: The ETIMEDOUT branch is currently classified as NETWORK and
thus never reaches the TIMEOUT check; update the logic in the function that maps
error codes (uses the variables code, message and the ErrorType enum) so
ETIMEDOUT maps to ErrorType.TIMEOUT: remove ETIMEDOUT from the first conditional
(the one returning ErrorType.NETWORK) or explicitly check ETIMEDOUT before the
NETWORK branch, and ensure the second conditional (checking ETIMEDOUT or
message.includes('timeout')) returns ErrorType.TIMEOUT.

In `@src/utils/retry.js`:
- Around line 27-33: The calculateBackoff function uses pure exponential backoff
which risks synchronized retries; modify calculateBackoff(attempt, baseDelay,
maxDelay) to add randomized jitter to the computed delay (for example by
applying a random factor or selecting a random value within a range around the
exponential value) and still cap the result at maxDelay: compute the exponential
delay as now, apply jitter via Math.random to spread retries, then return
Math.min(jitteredDelay, maxDelay). Ensure the jitter keeps delays non-negative
and documented in the function comment.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0286a77 and c2bfc4a.

📒 Files selected for processing (2)
  • src/utils/errors.js
  • src/utils/retry.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-26T01:57:34.920Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-11-26T01:57:34.920Z
Learning: Applies to src/**/*.{ts,tsx} : Use `reportError(context, error)` from `src/lib/logger.ts` to report errors to Sentry with context metadata, falling back to console.error if Sentry is disabled

Applied to files:

  • src/utils/errors.js
🧬 Code graph analysis (2)
src/utils/retry.js (2)
src/utils/errors.js (5)
  • isRetryable (192-204)
  • errorType (115-115)
  • errorType (156-156)
  • errorType (193-193)
  • classifyError (43-105)
src/logger.js (3)
  • warn (225-227)
  • error (232-234)
  • debug (211-213)
src/utils/errors.js (2)
src/logger.js (1)
  • error (232-234)
src/utils/retry.js (2)
  • errorType (67-67)
  • isRetryable (49-55)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cursor Bugbot
🔇 Additional comments (7)
src/utils/errors.js (3)

11-34: LGTM!

The ErrorType enum is well-organized with clear categorization of error types (network, API, Discord-specific, configuration, and unknown). Good practice to use string values for easier debugging and logging.


155-183: LGTM!

The function appropriately returns null for error types where no actionable suggestion exists (API_ERROR, UNKNOWN). The documented return type correctly indicates this behavior.


192-204: LGTM!

Good selection of retryable error types. Rate limits are correctly included since they're transient. The actual backoff handling in retry.js will manage the delay appropriately.

src/utils/retry.js (4)

8-9: LGTM!

Imports are appropriate - using the error classification utilities from the sibling module and logging functions from the logger.


16-18: LGTM!

Standard sleep implementation using Promise and setTimeout.


48-118: LGTM!

The retry logic is well-structured:

  • Correctly handles the initial attempt plus retries
  • Logs appropriately at different levels (warn on first failure, debug for retry attempts, error on final failure)
  • Properly integrates with the error classification system
  • Clean separation of retryable vs non-retryable error handling

126-129: LGTM!

Clean factory pattern allowing pre-configured retry wrappers. Option merging order correctly allows per-call overrides.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +51 to +56
if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
return ErrorType.NETWORK;
}
if (code === 'ETIMEDOUT' || message.includes('timeout')) {
return ErrorType.TIMEOUT;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ETIMEDOUT classification is unreachable for TIMEOUT.

ETIMEDOUT is checked at line 51 and returns NETWORK, so the ETIMEDOUT check at line 54 for TIMEOUT will never execute. Since ETIMEDOUT specifically indicates a connection timeout, it should likely return TIMEOUT, not NETWORK.

🐛 Proposed fix
   // Network errors
-  if (code === 'ECONNREFUSED' || code === 'ENOTFOUND' || code === 'ETIMEDOUT') {
+  if (code === 'ECONNREFUSED' || code === 'ENOTFOUND') {
     return ErrorType.NETWORK;
   }
   if (code === 'ETIMEDOUT' || message.includes('timeout')) {
     return ErrorType.TIMEOUT;
   }
🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 51 - 56, The ETIMEDOUT branch is currently
classified as NETWORK and thus never reaches the TIMEOUT check; update the logic
in the function that maps error codes (uses the variables code, message and the
ErrorType enum) so ETIMEDOUT maps to ErrorType.TIMEOUT: remove ETIMEDOUT from
the first conditional (the one returning ErrorType.NETWORK) or explicitly check
ETIMEDOUT before the NETWORK branch, and ensure the second conditional (checking
ETIMEDOUT or message.includes('timeout')) returns ErrorType.TIMEOUT.

Comment on lines +91 to +94
// Config errors
if (message.includes('config.json not found') || message.includes('enoent')) {
return ErrorType.CONFIG_MISSING;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

ENOENT detection may miss file-not-found errors.

Node.js filesystem errors set error.code = 'ENOENT', but this code only checks the message. Consider also checking the code variable.

🛠️ Proposed fix
   // Config errors
-  if (message.includes('config.json not found') || message.includes('enoent')) {
+  if (message.includes('config.json not found') || message.includes('enoent') || code === 'ENOENT') {
     return ErrorType.CONFIG_MISSING;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Config errors
if (message.includes('config.json not found') || message.includes('enoent')) {
return ErrorType.CONFIG_MISSING;
}
// Config errors
if (message.includes('config.json not found') || message.includes('enoent') || code === 'ENOENT') {
return ErrorType.CONFIG_MISSING;
}
🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 91 - 94, The current detection only
inspects the error message string (`message`) to map to
ErrorType.CONFIG_MISSING; update the logic in the function in
src/utils/errors.js to also check the error object's `code` property (e.g.,
`code === 'ENOENT'`) and handle cases where the input may be an Error object
rather than a string, so that when either message includes 'config.json not
found' or `code === 'ENOENT'` you return ErrorType.CONFIG_MISSING; reference the
existing `message` variable and the `ErrorType.CONFIG_MISSING` symbol when
making this change.

Comment on lines +114 to +146
export function getUserFriendlyMessage(error, context = {}) {
const errorType = classifyError(error, context);

const messages = {
[ErrorType.NETWORK]: "I'm having trouble connecting to my brain right now. Check if the AI service is running and try again!",

[ErrorType.TIMEOUT]: "That took too long to process. Try again with a shorter message, or wait a moment and retry!",

[ErrorType.API_RATE_LIMIT]: "Whoa, too many requests! Let's take a quick breather. Try again in a minute.",

[ErrorType.API_UNAUTHORIZED]: "I'm having authentication issues with the AI service. An admin needs to check the API credentials.",

[ErrorType.API_NOT_FOUND]: "The AI service endpoint isn't responding. Please check if it's configured correctly.",

[ErrorType.API_SERVER_ERROR]: "The AI service is having technical difficulties. It should recover automatically - try again in a moment!",

[ErrorType.API_ERROR]: "Something went wrong with the AI service. Give it another shot in a moment!",

[ErrorType.DISCORD_PERMISSION]: "I don't have permission to do that! An admin needs to check my role permissions.",

[ErrorType.DISCORD_CHANNEL_NOT_FOUND]: "I can't find that channel. It might have been deleted, or I don't have access to it.",

[ErrorType.DISCORD_MISSING_ACCESS]: "I don't have access to that resource. Please check my permissions!",

[ErrorType.CONFIG_MISSING]: "Configuration file not found! Please create a config.json file (you can copy from config.example.json).",

[ErrorType.CONFIG_INVALID]: "The configuration file has errors. Please check config.json for syntax errors or missing required fields.",

[ErrorType.UNKNOWN]: "Something unexpected happened. Try again, and if it keeps happening, check the logs for details.",
};

return messages[errorType] || messages[ErrorType.UNKNOWN];
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider moving the messages object outside the function.

The messages object is recreated on every call. Moving it to module scope would avoid repeated object creation, though the impact is minor.

♻️ Proposed refactor
+const USER_FRIENDLY_MESSAGES = {
+  [ErrorType.NETWORK]: "I'm having trouble connecting to my brain right now. Check if the AI service is running and try again!",
+  [ErrorType.TIMEOUT]: "That took too long to process. Try again with a shorter message, or wait a moment and retry!",
+  // ... remaining messages
+  [ErrorType.UNKNOWN]: "Something unexpected happened. Try again, and if it keeps happening, check the logs for details.",
+};
+
 export function getUserFriendlyMessage(error, context = {}) {
   const errorType = classifyError(error, context);
-  const messages = {
-    // ... all messages
-  };
-  return messages[errorType] || messages[ErrorType.UNKNOWN];
+  return USER_FRIENDLY_MESSAGES[errorType] || USER_FRIENDLY_MESSAGES[ErrorType.UNKNOWN];
 }
🤖 Prompt for AI Agents
In `@src/utils/errors.js` around lines 114 - 146, The messages map inside
getUserFriendlyMessage is recreated on every call; move the messages object to
module scope (e.g., const MESSAGES = { ... }) and have getUserFriendlyMessage
reference MESSAGES[errorType]. Update references to ErrorType keys accordingly
and keep the default fallback to MESSAGES[ErrorType.UNKNOWN] so behavior is
unchanged while avoiding repeated allocations.

Comment on lines +27 to +33
function calculateBackoff(attempt, baseDelay, maxDelay) {
// Exponential backoff: baseDelay * 2^attempt
const delay = baseDelay * Math.pow(2, attempt);

// Cap at maxDelay
return Math.min(delay, maxDelay);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding jitter to prevent thundering herd.

Pure exponential backoff without jitter can cause synchronized retry storms when multiple operations fail simultaneously. Adding randomized jitter spreads out retries.

♻️ Proposed fix with jitter
 function calculateBackoff(attempt, baseDelay, maxDelay) {
   // Exponential backoff: baseDelay * 2^attempt
   const delay = baseDelay * Math.pow(2, attempt);
 
-  // Cap at maxDelay
-  return Math.min(delay, maxDelay);
+  // Cap at maxDelay and add jitter (±25%)
+  const cappedDelay = Math.min(delay, maxDelay);
+  const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
+  return Math.max(0, Math.floor(cappedDelay + jitter));
 }
🤖 Prompt for AI Agents
In `@src/utils/retry.js` around lines 27 - 33, The calculateBackoff function uses
pure exponential backoff which risks synchronized retries; modify
calculateBackoff(attempt, baseDelay, maxDelay) to add randomized jitter to the
computed delay (for example by applying a random factor or selecting a random
value within a range around the exponential value) and still cap the result at
maxDelay: compute the exponential delay as now, apply jitter via Math.random to
spread retries, then return Math.min(jitteredDelay, maxDelay). Ensure the jitter
keeps delays non-negative and documented in the function comment.

@BillChirico BillChirico merged commit cdedff8 into main Feb 4, 2026
2 checks passed
@BillChirico BillChirico deleted the auto-claude/003-enhanced-error-handling branch February 4, 2026 02:40
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before Autofix could start.

}
if (code === 'ETIMEDOUT' || message.includes('timeout')) {
return ErrorType.TIMEOUT;
}
Copy link

Choose a reason for hiding this comment

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

ETIMEDOUT classified as NETWORK instead of TIMEOUT

Medium Severity

The error code ETIMEDOUT is checked in the network errors condition (line 51) before the timeout condition (line 54). When an error has code === 'ETIMEDOUT', it returns ErrorType.NETWORK instead of ErrorType.TIMEOUT. The second check for ETIMEDOUT on line 54 becomes unreachable for that code path. This causes timeout errors to show network-related user messages instead of timeout-specific guidance.

Fix in Cursor Fix in Web

BillChirico added a commit that referenced this pull request Feb 15, 2026
…cord values

- getThreadConfig now validates reuseWindowMinutes (must be positive number)
  and autoArchiveMinutes (must be valid number), falling back to safe defaults
- Add snapAutoArchiveDuration() that snaps to nearest Discord-allowed value
  (60, 1440, 4320, or 10080 minutes)
- Invalid config values (NaN, negative, non-number) gracefully degrade

Addresses PR #57 review threads #3 and #6.
BillChirico added a commit that referenced this pull request Feb 15, 2026
…cord values

- getThreadConfig now validates reuseWindowMinutes (must be positive number)
  and autoArchiveMinutes (must be valid number), falling back to safe defaults
- Add snapAutoArchiveDuration() that snaps to nearest Discord-allowed value
  (60, 1440, 4320, or 10080 minutes)
- Invalid config values (NaN, negative, non-number) gracefully degrade

Addresses PR #57 review threads #3 and #6.
BillChirico added a commit that referenced this pull request Feb 16, 2026
Add privacy documentation to the memory module explaining that user
messages are sent to the mem0 hosted platform for processing. Update
the /memory command description to indicate external storage. Users
can view and delete their data via /memory view and /memory forget.

Resolves review thread #6.
BillChirico added a commit that referenced this pull request Feb 16, 2026
Add privacy documentation to the memory module explaining that user
messages are sent to the mem0 hosted platform for processing. Update
the /memory command description to indicate external storage. Users
can view and delete their data via /memory view and /memory forget.

Resolves review thread #6.
BillChirico added a commit that referenced this pull request Feb 16, 2026
Add privacy documentation to the memory module explaining that user
messages are sent to the mem0 hosted platform for processing. Update
the /memory command description to indicate external storage. Users
can view and delete their data via /memory view and /memory forget.

Resolves review thread #6.
BillChirico added a commit that referenced this pull request Feb 16, 2026
…uard

- Add cursor-based pagination with `after` param to fetchUserGuilds,
  looping until all guilds are fetched (issue #2)
- Propagate RefreshTokenError to session and auto-redirect to sign-in
  via SessionGuard component in Providers (issue #3)
- Add dockerContext = ".." to railway.toml for monorepo Docker builds (issue #4)
- Create /api/health returning 200 JSON; update railway.toml healthcheckPath (issue #5)
- Conditionally render Add to Server buttons only when CLIENT_ID is set (issue #6)
- Add AbortController cleanup to guild fetch useEffect in ServerSelector (issue #7)
- Refuse unauthenticated bot API requests when BOT_API_SECRET is missing (issue #9)
- Add retry + empty/error states to ServerSelector (issue #13 partial)
BillChirico added a commit that referenced this pull request Feb 17, 2026
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from
  requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST
  admin check with slash-command isAdmin (#1, #2, #12)
- Add botOwners startup warning when using default upstream ID (#3)
- Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to
  README deployment table (#4)
- Pass actual permission level to getPermissionError so modlog denial
  says 'moderator' not 'administrator' (#5)
- Guard _seedOAuthState with NODE_ENV production check (#6)
- Add test: valid JWT with no server-side session (#7)
- Add DiscordApiError class with HTTP status (#8)
- Add moderatorRoleId support to isModerator (#9)
- Remove no-op delete override from SessionStore (#10)
- Cap oauthStates at 10k entries (#11)
- Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12)
- Handle dashboard URL fragment collision (#13)
- Cap guildCache at 10k entries (#14)
- Add SESSION_TTL_MS co-location comment with JWT expiry (#15)
- Cache SESSION_SECRET via lazy getter in verifyJwt (#16)
- Remove PII (username) from OAuth auth log (#17)
BillChirico added a commit that referenced this pull request Feb 17, 2026
- Differentiate requireGuildAdmin (ADMINISTRATOR only) from
  requireGuildModerator (ADMINISTRATOR | MANAGE_GUILD), aligning REST
  admin check with slash-command isAdmin (#1, #2, #12)
- Add botOwners startup warning when using default upstream ID (#3)
- Add SESSION_SECRET, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI to
  README deployment table (#4)
- Pass actual permission level to getPermissionError so modlog denial
  says 'moderator' not 'administrator' (#5)
- Guard _seedOAuthState with NODE_ENV production check (#6)
- Add test: valid JWT with no server-side session (#7)
- Add DiscordApiError class with HTTP status (#8)
- Add moderatorRoleId support to isModerator (#9)
- Remove no-op delete override from SessionStore (#10)
- Cap oauthStates at 10k entries (#11)
- Fix hasOAuthGuildPermission docstring for bitwise OR semantics (#12)
- Handle dashboard URL fragment collision (#13)
- Cap guildCache at 10k entries (#14)
- Add SESSION_TTL_MS co-location comment with JWT expiry (#15)
- Cache SESSION_SECRET via lazy getter in verifyJwt (#16)
- Remove PII (username) from OAuth auth log (#17)
BillChirico added a commit that referenced this pull request Feb 28, 2026
…utes (#6)

Each route previously inlined its prefix string on every call.
Extracts to module-scope const matching the pattern used by
config/members/roles routes.
BillChirico added a commit that referenced this pull request Feb 28, 2026
* feat(conversations): add flagged_messages migration

* feat(conversations): add API endpoints for list, detail, search, flag, stats

* feat(conversations): mount router in API index

* feat(conversations): add conversation list and replay dashboard pages

* feat(conversations): add Next.js API proxy routes

* test(conversations): add API and grouping tests

* fix(conversations): escape ILIKE wildcards to prevent wildcard injection

* fix(conversations): remove unused _totalChars variable

* fix(conversations): cap list query to 5000 rows to prevent memory exhaustion

* fix(conversations): replace in-memory stats grouping with SQL aggregates

* fix(conversations): bound conversation detail query by time window instead of full channel scan

* style: alphabetize imports and format authorizeGuildAdmin calls

* test(conversations): fix stats mock to match SQL conversation counting

* test(conversations): add POST flag endpoint to guild validation test

The auth test already covered all 5 endpoints including POST .../flag,
but the guild-validation test only checked 4 GET endpoints, leaving the
flag endpoint's guild validation untested.

Resolves review thread PRRT_kwDORICdSM5xTeiw

* fix(conversations): parameterize SQL interval and fix flag button a11y

Thread 3: Replace string interpolation of CONVERSATION_GAP_MINUTES in
the window-function SQL with a $2 parameter to avoid hardcoded literals.
Passes CONVERSATION_GAP_MINUTES as a query value instead.

Thread 4: Change flag button wrapper from `focus-within:opacity-100`
to `group-focus-within:opacity-100` so the button becomes visible
whenever any element in the message bubble group receives keyboard focus,
not just when the button's own children are focused — matching the
group-hover pattern and ensuring proper keyboard accessibility.

Also: biome --write reformatted label.tsx and textarea.tsx (pre-existing
style issues).

* test: add ILIKE wildcard escape coverage for % and _ characters

Adds test cases verifying that % and _ characters in conversation
search queries are properly escaped before being used in ILIKE
patterns, preventing them from acting as SQL wildcards.

* fix: deterministic flag status for duplicate flagged_messages rows

When a message has been flagged multiple times (flagged_messages has no
UNIQUE constraint on message_id), the previous Map construction would
silently overwrite entries in iteration order, making the displayed
status non-deterministic.

Order the SELECT by created_at DESC so the first row per message_id
that lands in the Map is always the most recently created flag, giving
a predictable 'latest wins' behaviour.

* refactor: extract escapeIlike utility from logQuery inline impl

Creates src/utils/escapeIlike.js as a shared, exported utility.
Conversations route now imports it instead of duplicating the regex.

* fix(conversations): use escapeIlike(), fix non-deterministic flag status, add 30-day default window, verify conversationId on flag POST

- Import escapeIlike() instead of inline regex (DRY #4)
- Default to last 30 days when no `from` filter to prevent unbounded LIMIT 5000 scan (#3)
- Fix Map construction: iterate ORDER BY DESC rows and only set first occurrence per key so most-recent flag status wins (#1)
- Verify flagged messageId belongs to the conversation's channel before inserting (#2)

* test(conversations): add ILIKE backslash escape test and fix flag mocks for new anchor check

- Add test for backslash (\) escaping in ILIKE search (#7)
- Update 'flag a message' mock to include anchorCheck query result

* refactor(web): add LOG_PREFIX constant to all 5 conversation proxy routes (#6)

Each route previously inlined its prefix string on every call.
Extracts to module-scope const matching the pattern used by
config/members/roles routes.

* fix(ui): use MessagesSquare icon for Conversations sidebar entry (#8)

AI Chat already uses MessageSquare. Conversations now uses MessagesSquare
to distinguish the two nav items visually.

* fix(ui): show last 4 digits of channel snowflake instead of raw ID (#9)

Raw Discord snowflakes are 18+ digit numbers. Showing `${channelId.slice(-4)}`
gives a minimal but less jarring display until a proper channel name resolver
is wired up.

* refactor(ui): extract PAGE_SIZE constant in conversations page (#5)

Replaces two hardcoded 25s with a single PAGE_SIZE = 25 constant.

* docs(migration): explain missing FK on conversation_first_id (#10)

conversation_first_id has no FK because conversations are not a separate
table with their own PK. They are virtual groups derived from message rows.
message_id already carries a FK for referential integrity.

* fix(lint): suppress pre-existing biome a11y errors in label component

* fix(conversations): stray $ in JSX channel display, increase query limit to 10k
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.

1 participant