Skip to content

System persistence and files#5

Merged
BillChirico merged 1 commit intoauto-claude/002-graceful-shutdown-handlingfrom
system-persistence-and-files-f6bb
Feb 4, 2026
Merged

System persistence and files#5
BillChirico merged 1 commit intoauto-claude/002-graceful-shutdown-handlingfrom
system-persistence-and-files-f6bb

Conversation

@BillChirico
Copy link
Collaborator

…utdown

  • Add mkdirSync to ensure data directory exists before saving state
  • Remove accidentally committed local dev files (.auto-claude-security.json, .auto-claude-status, .claude_settings.json) and add to .gitignore
  • Reorder graceful shutdown to wait for pending requests before saving state to ensure in-flight conversation history is captured

…utdown

- Add mkdirSync to ensure data directory exists before saving state
- Remove accidentally committed local dev files (.auto-claude-security.json,
  .auto-claude-status, .claude_settings.json) and add to .gitignore
- Reorder graceful shutdown to wait for pending requests before saving state
  to ensure in-flight conversation history is captured
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 4, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch system-persistence-and-files-f6bb

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.

@BillChirico BillChirico merged commit 3094a9c into auto-claude/002-graceful-shutdown-handling Feb 4, 2026
2 checks passed
@BillChirico BillChirico deleted the system-persistence-and-files-f6bb branch February 4, 2026 02:09
BillChirico added a commit that referenced this pull request Feb 4, 2026
* auto-claude: subtask-1-1 - Create data directory structure for state persistence

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* auto-claude: subtask-1-2 - Add state persistence functions (save/load convers

* auto-claude: subtask-1-3 - Add pending request tracking mechanism

Added pendingRequests Set to track active AI requests during shutdown.
The generateResponse function now registers requests on start and
removes them on completion (success or error) using try-finally.

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* auto-claude: subtask-1-4 - Implement graceful shutdown orchestration

- Add gracefulShutdown function that saves state, waits for pending requests, and destroys Discord client
- Handle SIGTERM and SIGINT signals for clean shutdown
- Add 5-second timeout for pending requests during shutdown
- Add loadState() call on startup to restore previous state
- Include detailed logging for shutdown process

* fix: correct state file location and shutdown timeout (qa-requested)

- Fix state.json path to use data/ directory (critical security fix)
- Update shutdown timeout from 5s to 10s per spec

Addresses QA review feedback from session 1

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

* fix: ensure data directory exists, remove local dev files, reorder shutdown (#5)

- Add mkdirSync to ensure data directory exists before saving state
- Remove accidentally committed local dev files (.auto-claude-security.json,
  .auto-claude-status, .claude_settings.json) and add to .gitignore
- Reorder graceful shutdown to wait for pending requests before saving state
  to ensure in-flight conversation history is captured

Co-authored-by: Cursor Agent <[email protected]>

---------

Co-authored-by: Claude Sonnet 4.5 <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
BillChirico added a commit that referenced this pull request Feb 11, 2026
When both topChannels and suggestedChannels are empty, channelText was
an empty string producing broken output like 'Check out — there's
always something interesting going on!'. Also handles voice-only
activity where getActivityLevel promotes to busy/hype but no text
channels exist. Each template now has a graceful fallback when no
channel references are available.

Resolves Bugbot review threads #1 and #5.
BillChirico added a commit that referenced this pull request Feb 15, 2026
- Truncate username when it would leave no room for content
- Add final safety clamp to guarantee ≤100 chars in all cases
- Handle edge case where username is ~98+ chars gracefully
- Add tests for very long username scenarios

Addresses PR #57 review threads #5 and #7.
BillChirico added a commit that referenced this pull request Feb 15, 2026
- Truncate username when it would leave no room for content
- Add final safety clamp to guarantee ≤100 chars in all cases
- Handle edge case where username is ~98+ chars gracefully
- Add tests for very long username scenarios

Addresses PR #57 review threads #5 and #7.
BillChirico added a commit that referenced this pull request Feb 16, 2026
…reation

The health check now performs a lightweight search against the mem0
platform to verify the SDK client actually works, rather than just
checking that the client object was created. This properly validates
connectivity with the hosted mem0 platform.

Also fixes misleading test name that asserted success but was named
as a failure case.

Resolves review threads #5, #11.
BillChirico added a commit that referenced this pull request Feb 16, 2026
…reation

The health check now performs a lightweight search against the mem0
platform to verify the SDK client actually works, rather than just
checking that the client object was created. This properly validates
connectivity with the hosted mem0 platform.

Also fixes misleading test name that asserted success but was named
as a failure case.

Resolves review threads #5, #11.
BillChirico added a commit that referenced this pull request Feb 16, 2026
…reation

The health check now performs a lightweight search against the mem0
platform to verify the SDK client actually works, rather than just
checking that the client object was created. This properly validates
connectivity with the hosted mem0 platform.

Also fixes misleading test name that asserted success but was named
as a failure case.

Resolves review threads #5, #11.
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
Create proxy routes following the existing bot-api-proxy pattern:
- GET /api/guilds/:guildId/members — enriched member list
- GET /api/guilds/:guildId/members/:userId — member detail
- POST /api/guilds/:guildId/members/:userId/xp — XP adjustment
- GET /api/guilds/:guildId/members/:userId/cases — mod case history
- GET /api/guilds/:guildId/members/export — CSV export (streamed)

All routes include guild admin authorization and proper error handling.
CSV export uses 30s timeout and streams the response body.

Addresses review comment #5 on PR #119.
BillChirico added a commit that referenced this pull request Feb 28, 2026
* feat(dashboard): add member table component with sort and pagination

* feat(dashboard): add members list page

* feat(dashboard): add member detail page with stats and admin actions

* feat(members): add member management API with detail, history, XP adjust, and CSV export

* feat(members): mount members router, export guild middleware, remove superseded basic members endpoint

* test(members): add API endpoint tests (26 tests)

* fix: lint import ordering in member dashboard components

* fix(members-api): add rate limiting, CSV injection protection, XP transaction safety

- CSV injection: escapeCsv now prefixes formula chars (=,+,-,@,\t,\r) with single quote
- XP bounds: cap adjustments to ±1,000,000
- XP transaction: wrap upsert + level update in BEGIN/COMMIT/ROLLBACK
- Warning filter: add AND action = 'warn' to recent warnings query in member detail
- CSV export: paginate guild.members.list() for guilds > 1000 members
- Pool safety: wrap getPool() in safeGetPool() with try/catch, return 503 on failure
- Search total: return filteredTotal alongside total when search is active

Addresses review comments #8-15 on PR #119.

* fix(dashboard): align member interfaces with backend API response shape

Frontend-backend contract fixes:
- MemberRow: id/displayName/avatar/messages_sent/warning_count/joinedAt (was snake_case)
- Avatar: use full URL from backend directly, remove hash-based CDN helper
- Pagination: cursor → nextAfter, param 'cursor' → 'after'
- MemberDetail: flat response shape with stats/reputation/warnings sub-objects
- SortColumn: restrict to API-supported values (messages/xp/warnings/joined)
- Role color: use hexColor string directly instead of number conversion
- XP progress: use next_level_xp from reputation object
- CSV export: add error state instead of silent catch
- Dependency array: add fetchMembers, remove eslint-disable comment
- Keyboard accessibility: tabIndex={0} + onKeyDown for Enter/Space on rows
- Guild context: handleRowClick depends on guildId
- Search total: display filteredTotal when available

Addresses review comments #1-7, #16-19 on PR #119.

* feat(dashboard): add Next.js API proxy routes for member endpoints

Create proxy routes following the existing bot-api-proxy pattern:
- GET /api/guilds/:guildId/members — enriched member list
- GET /api/guilds/:guildId/members/:userId — member detail
- POST /api/guilds/:guildId/members/:userId/xp — XP adjustment
- GET /api/guilds/:guildId/members/:userId/cases — mod case history
- GET /api/guilds/:guildId/members/export — CSV export (streamed)

All routes include guild admin authorization and proper error handling.
CSV export uses 30s timeout and streams the response body.

Addresses review comment #5 on PR #119.

* test(members): update tests for API changes

- XP tests: mock pool.connect() + client for transaction flow
- Add XP bounds test (±1,000,000 limit)
- Verify BEGIN/COMMIT/release called in transaction
- Search test: assert filteredTotal in response

* fix: lint and formatting fixes across all changed files

- Use template literals instead of string concatenation (biome)
- Use const for non-reassigned variables
- Add button type='button' for a11y
- Remove unused displayTotal variable
- Use Number.isNaN over global isNaN
- Format proxy routes to match biome standards

* fix(members-api): add global + per-route rate limiting to satisfy CodeQL

* 📝 Add docstrings to `feat/member-management`

Docstrings generation was requested by @BillChirico.

The following files were modified:

* `src/api/routes/guilds.js`
* `src/api/routes/members.js`
* `web/src/app/api/guilds/[guildId]/members/[userId]/cases/route.ts`
* `web/src/app/api/guilds/[guildId]/members/[userId]/route.ts`
* `web/src/app/api/guilds/[guildId]/members/[userId]/xp/route.ts`
* `web/src/app/api/guilds/[guildId]/members/export/route.ts`
* `web/src/app/api/guilds/[guildId]/members/route.ts`
* `web/src/app/dashboard/members/[userId]/page.tsx`
* `web/src/app/dashboard/members/page.tsx`
* `web/src/components/dashboard/member-table.tsx`

These files were ignored:
* `tests/api/routes/guilds.test.js`
* `tests/api/routes/members.test.js`

* fix: correct CSV formula-injection bug in escapeCsv

The escapeCsv function was discarding the original string value when
prefixing with a single quote to neutralize formula injection. Now
correctly preserves the value: str = `'${str}` instead of str = `'`.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: include guildId in member row click navigation

Include guildId as a query parameter when navigating to member detail page
to ensure guild context is preserved across navigation.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: use safeGetPool in all member endpoints

The GET /:id/members endpoint was using raw getPool() instead of
safeGetPool() with the 503 guard used by all other member endpoints.
Now consistently returns 503 when database is unavailable.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix: log CSV export errors instead of silent failure

Add console.error logging when CSV export fails, in addition to
setting the error state for user display.

Co-Authored-By: Claude Opus 4.6 <[email protected]>

* fix(members): dedupe rate limiting, add 401 handling, fix loading state, remove console.error

* fix(members): reject fractional XP amounts (column is INTEGER)

* test: boost branch coverage to 85% with targeted tests

Raise branch coverage from 83.32% to 85.24% across 4 key files:

- sentry.js: beforeSend filter, tracesSampleRate parsing, environment resolution
- events.js: review/showcase/challenge button handlers, partial fetch, rate limit/link filter branches
- rateLimit.js: repeat offender edge cases, permission checks, alert channel, cleanup
- members.js: safeGetPool null paths (503), transaction rollback, escapeCsv edge cases

New files: tests/modules/events-extra.test.js
Modified: tests/modules/rateLimit.test.js, tests/sentry.init.test.js
Removed unused import: src/api/index.js

* fix(members): reject non-integer XP amounts with 400

The reputation.xp column is INTEGER. Fractional values like 1.5 pass
the existing finite/non-zero check but get silently truncated by
PostgreSQL (admin adds 1.5, only 1 is stored).

Add explicit Number.isInteger check after the existing guards, returning
400 with 'amount must be an integer'.

* test(members): add test for fractional XP amount returning 400

Covers the new Number.isInteger guard — sending amount: 1.5 must return
400 with error 'amount must be an integer'.

* fix(members): scope membersRateLimit to member routes only

The global router.use(membersRateLimit) was applied to every request
hitting this router, which is mounted at /api/v1/guilds before the
guilds router. This accidentally rate-limited non-member guild endpoints
(e.g. /guilds/:id/analytics).

Remove the global router.use and add membersRateLimit explicitly on
each of the five member route definitions (export, list, detail, cases,
xp) so rate limiting is scoped correctly.

* fix(members-ui): fix roleColorStyle fallback for hex alpha concatenation

The caller appends '40' and '15' to the result for borderColor and
backgroundColor. When the fallback was 'hsl(var(--muted-foreground))'
this produced 'hsl(var(--muted-foreground))40' — not valid CSS — so
roles with Discord's default color (#000000) got no border/background.

Replace the HSL CSS-variable fallback with a plain hex grey (#6b7280)
so appending hex alpha digits produces valid 8-digit hex colours.

* fix: biome formatting in members.js

* fix(members): add AbortController and request sequencing to prevent stale responses

Add AbortController to cancel in-flight fetch requests when a new
request is triggered (e.g., from search/sort changes). Also add a
monotonic request ID counter to guard against out-of-order responses
overwriting newer state.

- Abort previous request on each new fetchMembers call
- Pass AbortSignal to fetch()
- Silently ignore AbortError exceptions
- Discard stale responses/errors via requestId guard
- Only clear loading state for the current (non-superseded) request
- Abort in-flight request on component unmount

* fix(members): use Discord server-side search instead of page-local filtering

Replace client-side substring filtering (which only searched within the
current page of Discord results) with Discord's guild.members.search()
API for server-side search across all guild members.

- search param now triggers guild.members.search({ query, limit })
- Cursor pagination (nextAfter) is null during search (Discord search
  does not support cursor-based paging)
- filteredTotal reflects actual server-side search result count
- Sort still applies to the returned page (documented limitation —
  global sort would require DB-driven member indexing)
- Updated tests to verify search() is called and filteredTotal semantics

* fix(members): stream CSV export in batches to reduce memory pressure

Replace the pattern of accumulating all guild members in one in-memory
Map before writing CSV.  Now each batch of 1000 members is fetched from
Discord, enriched from the DB, written to the response, and released
for GC — keeping peak memory at O(batch_size) instead of O(total_members).

- Move CSV header write before the batch loop
- Process and write each batch inline instead of collecting all first
- Remove userIds.length > 0 guards (batch loop guarantees non-empty)
- Track exportedCount incrementally
- Added streaming export test

* fix: button types, useCallback deps, array keys, remove duplicate tests and eslint comments

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <[email protected]>
BillChirico added a commit that referenced this pull request Feb 28, 2026
Replaces two hardcoded 25s with a single PAGE_SIZE = 25 constant.
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