Skip to content

feat: AI conversations viewer with search and replay#121

Merged
BillChirico merged 28 commits intomainfrom
feat/ai-conversations
Feb 28, 2026
Merged

feat: AI conversations viewer with search and replay#121
BillChirico merged 28 commits intomainfrom
feat/ai-conversations

Conversation

@BillChirico
Copy link
Collaborator

Summary

Dashboard page to browse, search, and replay all AI conversations the bot has had.

Backend (src/api/routes/conversations.js)

  • GET /:id/conversations — List conversations (grouped by channel + 15min window), with search, user/channel filters, pagination
  • GET /:id/conversations/:conversationId — Conversation detail with all messages
  • GET /:id/conversations/stats — Token usage, cost estimates, conversation counts
  • GET /:id/conversations/flags — List flagged messages
  • POST /:id/conversations/:conversationId/flag — Flag a problematic response with notes
  • Rate limited: 60 req/min

Dashboard UI

  • Conversation list page — Searchable table with user, channel, message count, token usage, timestamps, pagination
  • Conversation replay page — Chat-style bubbles (user right/blue, assistant left/gray), hover-to-flag on assistant messages, token/cost display, jump-to-Discord links
  • Flag dialog — Modal to flag problematic responses with reason + notes

Migration

  • 012_flagged_messages.cjs — flagged_messages table for tracking flagged responses

Proxy Routes

  • 5 Next.js API proxy routes following existing patterns

Tests

  • 38 new tests covering conversation grouping, CRUD, auth, error handling
  • All 2361 tests passing, 85%+ branch coverage

Closes #34

Copilot AI review requested due to automatic review settings February 28, 2026 04:13
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f6a80a and b63d639.

📒 Files selected for processing (16)
  • migrations/012_flagged_messages.cjs
  • src/api/index.js
  • src/api/routes/conversations.js
  • src/utils/escapeIlike.js
  • tests/api/routes/conversations.test.js
  • web/src/app/api/guilds/[guildId]/conversations/[conversationId]/flag/route.ts
  • web/src/app/api/guilds/[guildId]/conversations/[conversationId]/route.ts
  • web/src/app/api/guilds/[guildId]/conversations/flags/route.ts
  • web/src/app/api/guilds/[guildId]/conversations/route.ts
  • web/src/app/api/guilds/[guildId]/conversations/stats/route.ts
  • web/src/app/dashboard/conversations/[conversationId]/page.tsx
  • web/src/app/dashboard/conversations/page.tsx
  • web/src/components/dashboard/conversation-replay.tsx
  • web/src/components/layout/sidebar.tsx
  • web/src/components/ui/label.tsx
  • web/src/components/ui/textarea.tsx

📝 Walkthrough

Summary by CodeRabbit

Release Notes

New Features

  • Added Conversations dashboard section to browse, search, and filter past conversations by channel, participant, or date range
  • Added ability to flag problematic AI responses with reason and optional notes
  • Added conversation analytics displaying totals, message statistics, top participants, and token usage
  • Added detailed conversation view with message replay and flagging controls

Tests

  • Added comprehensive test coverage for conversation management and flagging

Walkthrough

Introduces a conversation viewer feature for the bot dashboard, including a database migration for flagged messages, bot API endpoints for listing and retrieving conversations with stats and flagging capabilities, web API proxy routes, and a complete frontend UI with conversation list, detail replay, and flagging workflow.

Changes

Cohort / File(s) Summary
Database Migration
migrations/012_flagged_messages.cjs
Adds flagged_messages table with columns for tracking problematic responses (id, guild_id, conversation_first_id, message_id FK, flagged_by, reason, notes, status with defaults, resolved_by, resolved_at, created_at). Includes indexes on guild_id and (guild_id, status).
Bot API Core
src/api/index.js, src/api/routes/conversations.js
Mounts conversations router at /guilds/:id/conversations with auth. Implements GET / (grouped conversations with search, user, channel, date filters and pagination), GET /stats (metrics, top users, daily activity, token estimates), GET /flags (flagged messages with status filtering), GET /:conversationId (detailed message view with 2-hour window and flag status), POST /:conversationId/flag (validates and records message flags). Includes groupMessagesIntoConversations utility and rate limiting (60 req/min per IP).
Bot API Utilities
src/utils/escapeIlike.js
Escapes PostgreSQL ILIKE wildcard characters (\%, \_, \\) in user input for safe search queries.
Bot API Tests
tests/api/routes/conversations.test.js
Comprehensive test suite covering grouping logic, all endpoints (list, stats, flags, detail, flag creation), filtering, pagination, authentication, validation, wildcard escaping, and error handling.
Web API Proxy Routes
web/src/app/api/guilds/[guildId]/conversations/route.ts, web/src/app/api/guilds/[guildId]/conversations/stats/route.ts, web/src/app/api/guilds/[guildId]/conversations/flags/route.ts, web/src/app/api/guilds/[guildId]/conversations/[conversationId]/route.ts, web/src/app/api/guilds/[guildId]/conversations/[conversationId]/flag/route.ts
Next.js API routes that proxy requests to bot API with guild admin authorization. Forward allowed query params (page, limit, search, user, channel, from, to, status) and handle upstream responses.
Web Dashboard Pages
web/src/app/dashboard/conversations/page.tsx, web/src/app/dashboard/conversations/[conversationId]/page.tsx
Conversations list page with debounced search (300ms), channel filtering, pagination (25 per page), and table displaying channel, participants, message count, duration, preview, and date. Detail page renders conversation replay with flag support and refresh on flag submission.
Web UI Components
web/src/components/dashboard/conversation-replay.tsx, web/src/components/ui/label.tsx, web/src/components/ui/textarea.tsx
ConversationReplay component renders chat-style message replay with 5-minute timestamp grouping, flagged message indicators, and modal-based flagging workflow (reason selection, optional notes, validation, POST submission). Label and Textarea UI primitives support form interactions.
Navigation
web/src/components/layout/sidebar.tsx
Adds "Conversations" navigation item with MessagesSquare icon pointing to /dashboard/conversations.

Possibly related PRs

  • PR #18: Introduces the flagged_messages table migration with foreign key to conversations.id, which this PR directly depends on for message flagging functionality.
  • PR #119: Also modifies src/api/index.js to mount new resource routers before the generic /guilds routes, following the same pattern used here for conversations routes.
✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/ai-conversations

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.

@claude
Copy link

claude bot commented Feb 28, 2026

Claude finished @BillChirico's task in 2m 57s —— View job


Review of PR #121 — 3 issues found

  • Read project conventions (AGENTS.md, CLAUDE.md)
  • Review backend routes (src/api/routes/conversations.js)
  • Review migration (012_flagged_messages.cjs)
  • Review utility (src/utils/escapeIlike.js)
  • Review tests (tests/api/routes/conversations.test.js)
  • Review Next.js proxy routes (5 files)
  • Review dashboard pages and components
  • Review sidebar changes
  • Review UI primitives (label, textarea)
  • Post inline comments for issues found
  • Post top-level summary and verdict

🟡 Warning (3)

  1. List endpoint returns oldest messages, drops newest (src/api/routes/conversations.js:210-219) — ORDER BY created_at ASC LIMIT 10000 fetches the oldest 10,000 rows. For guilds exceeding this within the 30-day window, the most recent conversations are silently unreachable and the UI total is wrong. Change to DESC and reverse before grouping.

  2. Duplicate escapeIlike in logQuery.js (src/utils/escapeIlike.js) — PR correctly extracted escapeIlike into a shared utility, but src/utils/logQuery.js:13-14 still has its own local copy.

  3. Raw Discord snowflake in detail page (web/src/app/dashboard/conversations/[conversationId]/page.tsx:84) — Shows data.channelId.slice(-4) instead of a human-readable channel name. The list endpoint resolves channelName but the detail endpoint doesn't return it.

Documentation

  • AGENTS.md Key Files table not updated — Per project conventions, src/api/routes/conversations.js should be added.
AI prompt to fix all issues
Fix the following issues in the volvox-bot repo on branch feat/ai-conversations:

1. src/api/routes/conversations.js lines 210-219:
   Change `ORDER BY created_at ASC` to `ORDER BY created_at DESC` in the LIMIT 10000 query.
   Then reverse the rows before grouping:
   const allConversations = groupMessagesIntoConversations(result.rows.reverse());
   This ensures the most recent conversations (most relevant) are returned first
   when there are more than 10,000 messages in the 30-day time window.

2. src/utils/logQuery.js lines 13-15:
   Remove the local `escapeIlike` function definition:
     function escapeIlike(str) {
       return str.replace(/[\\%_]/g, (ch) => `\\${ch}`);
     }
   And add an import at the top of the file (after the existing imports on line 9):
     import { escapeIlike } from './escapeIlike.js';

3. src/api/routes/conversations.js GET /:conversationId handler (around line 495-500):
   Add channelName to the response JSON by resolving via req.guild:
     const channelName = req.guild?.channels?.cache?.get(anchor.channel_id)?.name || anchor.channel_id;
   Then include it in the response:
     res.json({
       messages: enrichedMessages,
       channelId: anchor.channel_id,
       channelName,
       ...
     });

4. web/src/app/dashboard/conversations/[conversationId]/page.tsx:
   Update the ConversationDetailResponse interface to include channelName?: string
   Update line 84 to display channelName instead of raw channelId:
     Channel {data.channelName || data.channelId.slice(-4)} · {data.messages.length} messages

5. AGENTS.md Key Files table:
   Add a new row after the `src/api/routes/guilds.js` entry:
   | `src/api/routes/conversations.js` | Conversation REST API endpoints (list, detail, stats, flags) |

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary — 6 issues found

🔴 Critical (1)

  1. ILIKE search not escaped (src/api/routes/conversations.js:167) — User search input passed directly to ILIKE without escaping %, _, \ wildcards. The codebase has escapeIlike() in src/utils/logQuery.js — extract and reuse it.

🟡 Warning (4)

  1. Unbounded query in list endpoint (src/api/routes/conversations.js:204-210) — Fetches ALL messages into memory for in-memory grouping. Will OOM on large guilds.
  2. Unbounded query in stats endpoint (src/api/routes/conversations.js:278-284) — Same full-table scan issue, 5 DB queries per request.
  3. Unbounded query in detail endpoint (src/api/routes/conversations.js:426-432) — Loads every message in a channel to find one conversation. Use a time-window around the anchor.
  4. Unused variable _totalChars (src/api/routes/conversations.js:450) — Computed but never used. Biome should flag this.

🔵 Nitpick (1)

  1. Duplicate sidebar icon (web/src/components/layout/sidebar.tsx:42) — "Conversations" uses same MessageSquare icon as "AI Chat".

Missing Documentation

  • AGENTS.md Key Files table not updated with the new src/api/routes/conversations.js entry (per project conventions).

@greptile-apps
Copy link

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR adds a comprehensive AI conversations viewer to the dashboard, enabling admins to browse, search, and replay all bot conversations with flagging support for problematic responses.

Backend (src/api/routes/conversations.js):

  • Implements 5 endpoints: list, detail, stats, flags list, and flag creation
  • Groups messages by channel + 15-minute time windows to form logical conversations
  • Uses parameterized queries throughout and Winston logging (good security practices)
  • Rate limited at 60 req/min per IP
  • Performance consideration: loads up to 10k messages for grouping before pagination (already noted in previous review)

Frontend:

  • List page with search, channel filter, and pagination
  • Replay page with chat-style bubbles and hover-to-flag functionality
  • 5 Next.js API proxy routes following established patterns

Database:

  • Migration creates flagged_messages table with proper indexes and FK constraint

Code Quality:

  • 38 comprehensive tests added, all 2361 tests passing
  • Minor style issue: hardcoded 'en-US' locale in date formatters (should use undefined)
  • All auth, error handling, and logging patterns followed correctly

Confidence Score: 4/5

  • This PR is safe to merge with only minor style issues to address
  • Score reflects well-tested, comprehensive implementation with proper security practices (parameterized queries, auth, rate limiting, Winston logging). Only minor style violations (hardcoded locales) and a pre-existing performance consideration prevent a 5/5.
  • No files require special attention - the hardcoded locale fixes are trivial style improvements

Important Files Changed

Filename Overview
migrations/012_flagged_messages.cjs creates flagged_messages table with proper indexes and FK constraint, follows migration format correctly
src/api/routes/conversations.js comprehensive conversation routes with grouping, search, stats, and flagging - performance concern about loading 10k messages for grouping already noted
tests/api/routes/conversations.test.js 38 comprehensive tests covering grouping logic, CRUD, auth, and error handling
web/src/app/dashboard/conversations/page.tsx conversation list page with search, filters, pagination - has hardcoded locale in formatDate
web/src/components/dashboard/conversation-replay.tsx chat-style replay component with flag dialog and timestamp formatting - has hardcoded locale

Sequence Diagram

sequenceDiagram
    participant User
    participant Dashboard
    participant NextProxy as Next.js API Proxy
    participant Express as Express API
    participant DB as PostgreSQL

    User->>Dashboard: Browse /dashboard/conversations
    Dashboard->>NextProxy: GET /api/guilds/{id}/conversations?search=...
    NextProxy->>Express: GET /guilds/{id}/conversations (with x-api-secret)
    Express->>DB: SELECT messages WHERE guild_id=$1 LIMIT 10000
    DB-->>Express: message rows
    Express->>Express: groupMessagesIntoConversations (15min gap)
    Express->>Express: paginate grouped conversations
    Express-->>NextProxy: {conversations, total, page}
    NextProxy-->>Dashboard: JSON response
    Dashboard->>User: Display conversation list

    User->>Dashboard: Click conversation
    Dashboard->>NextProxy: GET /api/guilds/{id}/conversations/{convId}
    NextProxy->>Express: GET /guilds/{id}/conversations/{convId}
    Express->>DB: SELECT anchor message + 2hr window
    DB-->>Express: message rows
    Express->>Express: group and find target conversation
    Express->>DB: SELECT flags for message_ids
    DB-->>Express: flag statuses
    Express-->>NextProxy: {messages, channelId, duration, tokenEstimate}
    NextProxy-->>Dashboard: JSON response
    Dashboard->>User: Display chat replay

    User->>Dashboard: Flag problematic response
    Dashboard->>NextProxy: POST /api/guilds/{id}/conversations/{convId}/flag
    NextProxy->>Express: POST with {messageId, reason, notes}
    Express->>DB: INSERT INTO flagged_messages
    DB-->>Express: flag created
    Express-->>NextProxy: {flagId, status}
    NextProxy-->>Dashboard: success
    Dashboard->>User: Show flagged indicator
Loading

Last reviewed commit: b63d639

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

15 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements a full AI conversations viewer for the dashboard as described in issue #34. It adds backend routes for browsing, searching, flagging, and getting stats on AI conversations, along with a React dashboard UI for conversation list and chat-style replay, plus new proxy API routes in the Next.js web app.

Changes:

  • New backend Express route module (src/api/routes/conversations.js) with 5 endpoints: list, detail, stats, flags, and flag creation; mounted in src/api/index.js
  • New dashboard pages (conversations/page.tsx, conversations/[conversationId]/page.tsx) and a shared ConversationReplay component with flag dialog; sidebar updated with new nav entry
  • New database migration (012_flagged_messages.cjs) for the flagged_messages table and new UI primitive components (textarea.tsx, label.tsx)

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
src/api/routes/conversations.js Core backend routes for conversation CRUD, search, grouping, flags, and stats
src/api/index.js Mounts the new conversations router
web/src/app/dashboard/conversations/page.tsx Conversation list UI with search, channel filter, and pagination
web/src/app/dashboard/conversations/[conversationId]/page.tsx Conversation detail / replay page
web/src/components/dashboard/conversation-replay.tsx Chat-style replay component with flag dialog
web/src/components/layout/sidebar.tsx Adds Conversations nav entry
web/src/app/api/guilds/[guildId]/conversations/route.ts Next.js proxy for conversation list
web/src/app/api/guilds/[guildId]/conversations/[conversationId]/route.ts Next.js proxy for conversation detail
web/src/app/api/guilds/[guildId]/conversations/[conversationId]/flag/route.ts Next.js proxy for flag POST
web/src/app/api/guilds/[guildId]/conversations/flags/route.ts Next.js proxy for flags list
web/src/app/api/guilds/[guildId]/conversations/stats/route.ts Next.js proxy for stats
migrations/012_flagged_messages.cjs Creates flagged_messages table and indexes
web/src/components/ui/textarea.tsx New Textarea UI primitive
web/src/components/ui/label.tsx New Label UI primitive
tests/api/routes/conversations.test.js 38 tests covering routes and grouping logic

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 28, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 28, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary — 3 issues found

🔴 Critical (1)

  1. Stray $ in JSX renders literal $ in UI (web/src/app/dashboard/conversations/[conversationId]/page.tsx:84) — Channel ${data.channelId.slice(-4)} is JSX, not a template literal. The $ renders as visible text. Should be Channel {data.channelId.slice(-4)}.

🟡 Warning (2)

  1. List endpoint silently drops newest conversations (src/api/routes/conversations.js:210-214) — ORDER BY created_at ASC LIMIT 5000 fetches the oldest 5000 rows. For active guilds exceeding 5000 messages in 30 days, the most recent conversations are unreachable. Change to DESC and reverse in-memory.
  2. Duplicate escapeIlike in logQuery.js (src/utils/logQuery.js:13-14) — Identical to the new shared src/utils/escapeIlike.js. Import the shared utility instead of maintaining two copies.

Documentation

  • AGENTS.md Key Files table not updated with src/api/routes/conversations.js (per project conventions in AGENTS.md § Documentation).

AI prompt to fix all issues
Fix the following issues in the volvox-bot repo on branch feat/ai-conversations:

1. web/src/app/dashboard/conversations/[conversationId]/page.tsx line 84:
   The line reads: Channel ${data.channelId.slice(-4)} · {data.messages.length} messages
   The $ is a stray character in JSX (not inside template literals).
   Change it to: Channel {data.channelId.slice(-4)} · {data.messages.length} messages

2. src/api/routes/conversations.js lines 210-215:
   Change `ORDER BY created_at ASC` to `ORDER BY created_at DESC` in the LIMIT 5000 query.
   Then reverse the rows before grouping:
   const allConversations = groupMessagesIntoConversations(result.rows.reverse());
   This ensures the most recent conversations (most relevant) are returned first
   when there are more than 5000 messages in the time window.

3. src/utils/logQuery.js lines 13-14:
   Remove the local `escapeIlike` function definition:
     function escapeIlike(str) {
       return str.replace(/[\\%_]/g, (ch) => `\\${ch}`);
     }
   And add an import at the top of the file (after the existing imports):
     import { escapeIlike } from './escapeIlike.js';

4. AGENTS.md Key Files table:
   Add a new row after the `src/api/routes/guilds.js` entry:
   | `src/api/routes/conversations.js` | Conversation REST API endpoints (list, detail, stats, flags) |

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary — 3 issues found

🟡 Warning (3)

  1. List endpoint returns oldest messages, drops newest (src/api/routes/conversations.js:210-219) — ORDER BY created_at ASC LIMIT 10000 fetches the oldest 10,000 rows. For guilds exceeding this within the 30-day window, the most recent conversations are silently unreachable and the UI total count is wrong. Change to DESC and reverse before grouping.

  2. Duplicate escapeIlike in logQuery.js (src/utils/escapeIlike.js) — This PR correctly extracted escapeIlike into a shared utility, but src/utils/logQuery.js:13-14 still has its own local copy. Two independent implementations of the same function.

  3. Raw Discord snowflake in detail page (web/src/app/dashboard/conversations/[conversationId]/page.tsx:84) — Shows data.channelId.slice(-4) (last 4 digits of a snowflake) instead of a human-readable channel name. The list endpoint already resolves channelName but the detail endpoint doesn't return it.

Documentation

  • AGENTS.md Key Files table not updated — Per project conventions (AGENTS.md § Documentation), src/api/routes/conversations.js should be added as a key file.

AI prompt to fix all issues
Fix the following issues in the volvox-bot repo on branch feat/ai-conversations:

1. src/api/routes/conversations.js lines 210-219:
   Change `ORDER BY created_at ASC` to `ORDER BY created_at DESC` in the LIMIT 10000 query.
   Then reverse the rows before grouping:
   const allConversations = groupMessagesIntoConversations(result.rows.reverse());
   This ensures the most recent conversations (most relevant) are returned first
   when there are more than 10,000 messages in the 30-day time window.

2. src/utils/logQuery.js lines 13-15:
   Remove the local `escapeIlike` function definition:
     function escapeIlike(str) {
       return str.replace(/[\\%_]/g, (ch) => `\\${ch}`);
     }
   And add an import at the top of the file (after the existing imports on line 9):
     import { escapeIlike } from './escapeIlike.js';

3. src/api/routes/conversations.js GET /:conversationId handler (around line 495-500):
   Add channelName to the response JSON by resolving via req.guild:
     const channelName = req.guild?.channels?.cache?.get(anchor.channel_id)?.name || anchor.channel_id;
   Then include it in the response:
     res.json({
       messages: enrichedMessages,
       channelId: anchor.channel_id,
       channelName,
       ...
     });

4. web/src/app/dashboard/conversations/[conversationId]/page.tsx:
   Update the ConversationDetailResponse interface to include channelName?: string
   Update line 84 to display channelName instead of raw channelId:
     Channel {data.channelName || data.channelId.slice(-4)} · {data.messages.length} messages

5. AGENTS.md Key Files table:
   Add a new row after the `src/api/routes/guilds.js` entry:
   | `src/api/routes/conversations.js` | Conversation REST API endpoints (list, detail, stats, flags) |

@BillChirico BillChirico merged commit 2f894ce into main Feb 28, 2026
10 checks passed
@BillChirico BillChirico deleted the feat/ai-conversations branch February 28, 2026 05:38
BillChirico pushed a commit that referenced this pull request Mar 1, 2026
- Date validation fallback: restructure from-filter logic so the 30-day
  default window applies whenever no valid 'from' param is provided,
  including when the param is present but an invalid date (previously
  left an unbounded query in that case)

- Extract parsePagination: export the function from guilds.js and import
  it in conversations.js instead of duplicating the implementation

- ORDER BY DESC: fetch rows newest-first then reverse before grouping so
  the 10k row cap retains the most recent conversations instead of
  silently dropping them in favour of the oldest records
BillChirico pushed a commit that referenced this pull request Mar 1, 2026
- Add tests/utils/escapeIlike.test.js with 16 tests covering:
  - Plain strings (no-op cases)
  - Percent sign (%) escaping
  - Underscore (_) escaping
  - Backslash (\) escaping
  - Combinations of all three special chars
- Remove inline escapeIlike() copy from logQuery.js (lines 13-15)
- Import shared escapeIlike from ./escapeIlike.js instead

Addresses PR #121 review comment: utility function was duplicated
between logQuery.js and escapeIlike.js.
BillChirico pushed a commit that referenced this pull request Mar 1, 2026
BillChirico pushed a commit that referenced this pull request Mar 1, 2026
BillChirico pushed a commit that referenced this pull request Mar 1, 2026
BillChirico added a commit that referenced this pull request Mar 2, 2026
Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.
BillChirico added a commit that referenced this pull request Mar 2, 2026
Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.
BillChirico added a commit that referenced this pull request Mar 2, 2026
Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.
BillChirico added a commit that referenced this pull request Mar 2, 2026
Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.
BillChirico added a commit that referenced this pull request Mar 2, 2026
* security: escape user content in triage prompt delimiters (#164)

Add escapePromptDelimiters() to HTML-encode < and > in user-supplied
message content before it is inserted between XML-style section tags
in the LLM prompt.

Without escaping, a crafted message containing the literal text
`</messages-to-evaluate>` could break out of the user-content section
and inject attacker-controlled instructions into the prompt structure.

Changes:
- Add escapePromptDelimiters(text) utility exported from triage-prompt.js
- Apply escape to m.content and m.replyTo.content in buildConversationText()
- Add 13 new tests covering the escape function and injection scenarios

Closes #164

* security: escape & chars and author fields in prompt delimiters

* fix(security): escape & in prompt delimiters and escape author fields

- Add & → &amp; escape first in escapePromptDelimiters() to prevent
  HTML entity bypass attacks (e.g. &lt;/messages-to-evaluate&gt;)
- Also escape m.author and m.replyTo.author since Discord display
  names are user-controlled and can contain < / > characters

Addresses review feedback on PR #204.

* fix: guard replyTo.content before .slice() to handle null/undefined

* perf: SQL-based conversation pagination + missing DB indexes (#221)

Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.

* feat: channel-level quiet mode via bot mention (#173) (#213)

* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx

* feat: audit log improvements — CSV/JSON export and real-time WebSocket stream (#215)

* feat: audit log improvements — CSV/JSON export, real-time WebSocket stream

- Add GET /:id/audit-log/export endpoint (CSV and JSON, up to 10k rows)
- Add /ws/audit-log WebSocket server for real-time audit entry broadcast
- Refactor buildFilters() shared helper to eliminate duplication
- Hook broadcastAuditEntry() into insertAuditEntry (RETURNING id+created_at)
- Wire setupAuditStream/stopAuditStream into startServer/stopServer lifecycle
- Add escapeCsvValue/rowsToCsv helpers with full test coverage
- 30 route tests + 17 WebSocket stream tests, all green

Closes #136

* fix: PR #215 review feedback - audit stream fixes

- ws.ping() crash: guard with readyState check + try/catch to avoid
  crashing heartbeat interval when socket not OPEN
- stopAuditStream race: make setupAuditStream async and await
  stopAuditStream() to prevent concurrent WebSocketServer creation
- Query param array coercion: add typeof === 'string' checks for
  startDate/endDate to handle Express string|string[]|undefined
- CSV CRLF quoting: add \r to RFC 4180 special-char check for proper
  Windows line ending handling
- Test timeouts: make AUTH_TIMEOUT_MS configurable via
  AUDIT_STREAM_AUTH_TIMEOUT_MS env var, use 100ms in tests

* feat: voice channel activity tracking — join/leave/move, leaderboard, export (#212)

* feat: add voice_sessions migration (#135)

* feat: add voice tracking module — join/leave/move/flush/leaderboard (#135)

* feat: wire voiceStateUpdate handler into event registration (#135)

* feat: add /voice command — leaderboard, stats, export subcommands (#135)

* feat: add voice config defaults to config.json (#135)

* feat: wire voice flush start/stop into bot lifecycle (#135)

* feat: add voice to config API allowlist (#135)

* fix: SQL UPDATE subquery for closeSession, fix import order (#135)

* fix(voice): resolve race conditions and missing config schema

- Fix openSession: update in-memory state only AFTER DB INSERT succeeds
- Fix closeSession: delete from in-memory state only AFTER DB UPDATE succeeds
- Fix: allow closeSession on leave/move even when feature is disabled
- Fix migration: add UNIQUE constraint to partial index to prevent duplicates
- Fix: move 'Voice join' log to after openSession succeeds
- Add voice config to CONFIG_SCHEMA for validation

---------

Co-authored-by: Bill <[email protected]>

* feat(dashboard): auto-save config with 500ms debounce (#199)

* feat(dashboard): replace manual save with auto-save (500ms debounce)

- Remove 'Save Changes' button; saving now fires automatically 500ms
  after the last config change (no changes → no network call)
- Add saveStatus state ('idle' | 'saving' | 'saved' | 'error') with
  AutoSaveStatus component showing spinner, check, or error+retry
- Add isLoadingConfigRef guard so the initial fetchConfig load never
  triggers a spurious PATCH
- Ctrl+S still works: clears debounce timer and saves immediately
- Keep 'beforeunload' warning for validation errors that block save
- Replace yellow unsaved-changes banner with a destructive validation
  error banner (only shown when save is actually blocked)
- Error state shows 'Save failed' + 'Retry' button for user recovery

Closes #189

* test(dashboard): add auto-save tests for ConfigEditor

- No PATCH on initial config load
- Validation error banner suppresses auto-save
- 'Saving...' spinner visible while PATCH in-flight
- 'Save failed' + Retry button on PATCH error

* fix(dashboard): prevent fetchConfig from overwriting saveStatus after successful save

Add skipSaveStatusReset parameter to fetchConfig so that post-save reloads
preserve the 'saved' status indicator instead of immediately resetting to 'idle'.

* test(dashboard): use fake timers, restore vi.stubGlobal, fix assertions, add idle/saved coverage

- Replace real setTimeout delays with vi.useFakeTimers() + vi.advanceTimersByTimeAsync()
  for deterministic, fast debounce tests
- Add afterEach cleanup: vi.unstubAllGlobals() + vi.useRealTimers()
- Replace toBeTruthy() with toBeInTheDocument() for Testing Library queries
- Add idle state test (no status indicator shown after load)
- Add saved state test (shows 'Saved' after successful save)
- Update file-level comment to list all four states

---------

Co-authored-by: Bill Chirico <[email protected]>

* feat: Reaction role menus (#162) (#205)

* feat: reaction role menus - core module, command, event hooks, migration

Implements issue #162: reaction role menus.

- Add migration 004 creating reaction_role_menus and reaction_role_entries tables
- Add src/modules/reactionRoles.js with DB helpers, embed builder, event handlers
- Add src/commands/reactionrole.js with /reactionrole create|add|remove|delete|list
- Wire handleReactionRoleAdd/Remove into registerReactionHandlers in events.js

Roles are granted on reaction add and revoked on reaction remove.
All mappings persist in PostgreSQL across bot restarts.

* test: reaction role menus - 40 tests covering module and command

- tests/modules/reactionRoles.test.js: resolveEmojiString, buildReactionRoleEmbed,
  all DB helpers, handleReactionRoleAdd, handleReactionRoleRemove
- tests/commands/reactionrole.test.js: all 5 subcommands (create, add, remove,
  delete, list) including error paths and guild ownership checks
- Fix biome lint: import sort order + unused import removal

* fix: remove unused import in reactionrole command

---------

Co-authored-by: Bill Chirico <[email protected]>

* fix(security): validate GitHub owner/repo format before gh CLI call (#198)

* fix(security): validate GitHub owner/repo format before gh CLI call

Prevents API path traversal by validating owner/repo segments against
a strict allowlist regex before interpolating them into the gh CLI
invocation.

Adds:
- VALID_GH_NAME regex (/^[a-zA-Z0-9._-]+$/)
- isValidGhRepo() helper (exported for testing)
- Guard in fetchRepoEvents() — returns [] and warns on invalid input
- Strengthened guard in pollGuildFeed() split logic

Fixes #160

* test(security): add validation tests for GitHub owner/repo format

Covers isValidGhRepo(), VALID_GH_NAME regex, and fetchRepoEvents()
validation guard introduced in fix for #160.

19 new tests verify:
- Valid alphanumeric/dot/hyphen/underscore names pass
- Path traversal (../../etc/passwd) is rejected at both entry points
- Slashes, empty strings, non-strings, spaces all rejected
- Shell metacharacters (; && $()) blocked
- gh CLI is NOT invoked when validation fails
- warn() fires with the invalid values (observable audit trail)
- Valid owner/repo still reach gh CLI unchanged

* fix(security): reject pure-dot owner/repo names to prevent path traversal

* test(githubFeed): add tests for pure-dot path traversal bypass

---------

Co-authored-by: Bill Chirico <[email protected]>

---------

Co-authored-by: Bill <[email protected]>
Co-authored-by: Bill Chirico <[email protected]>
BillChirico added a commit that referenced this pull request Mar 2, 2026
* feat: add role_menu_templates migration (#135)

* feat: add roleMenuTemplates module with built-ins, CRUD, and share (#135)

* feat: add /rolemenu command with template CRUD, apply, share (#135)

* feat: seed built-in role menu templates on startup (#135)

* test: add roleMenuTemplates tests — 36 passing (#135)

* test: add /rolemenu command tests — 19 passing (#135)

* fix: typo hasModeatorPerms → hasModeratorPerms

* perf: SQL-based conversation pagination + missing DB indexes (#221)

Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.

* feat: channel-level quiet mode via bot mention (#173) (#213)

* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx

* Fix: unterminated string in rolemenu.js

* Fix: lint issues and formatting

* fix: deterministic template lookup and correct roleId precedence

- Add ORDER BY to getTemplateByName for deterministic results
- Fix roleId precedence to preserve existing roleIds during merge
- Truncate Discord embed field values to 1024 chars

* fix: test assertion matches comment intent

The test expected template roleId to win, but the comment said existing
should take precedence. Fixed assertion to match documented behavior.

* fix: filter empty roleIds and only enable when valid options exist

- Filter out options with empty roleIds before saving
- Only enable role menu for non-built-in templates with valid options
- Add user-facing note when options are filtered

* chore: remove unused _MAX_DESCRIPTION_LEN constant

* fix: case-insensitive unique index for template names

Use LOWER(name) in unique index to match case-insensitive queries
and prevent duplicate templates differing only by case.

* fix(roleMenuTemplates): add type validation for roleId and description

- validateTemplateOptions now validates that optional roleId and
  description fields are strings when present
- Update JSDoc @see reference from issue #135 (voice tracking) to
  issue #216 (role menu templates)
- Update ON CONFLICT clause to use constraint name for consistency
  with the new LOWER(name) index

---------

Co-authored-by: Bill <[email protected]>
Co-authored-by: Bill Chirico <[email protected]>
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.

Web dashboard: AI conversations viewer with search and replay

2 participants