Skip to content

feat: Web Dashboard Shell (Next.js 16, Tailwind v4, Discord OAuth2)#60

Merged
BillChirico merged 83 commits intomainfrom
feat/web-dashboard-shell
Feb 17, 2026
Merged

feat: Web Dashboard Shell (Next.js 16, Tailwind v4, Discord OAuth2)#60
BillChirico merged 83 commits intomainfrom
feat/web-dashboard-shell

Conversation

@BillChirico
Copy link
Collaborator

Summary

Adds the foundation for the bills-bot web dashboard — a Next.js app with Discord OAuth2 authentication, responsive layout, and server management capabilities.

Closes #28

What's Included

Stack

  • Next.js 16.1.6 with React 19, TypeScript, Turbopack
  • Tailwind CSS v4 with CSS-based config (@theme block, no tailwind.config)
  • Shadcn UI components (button, card, avatar, dropdown, sheet, separator)
  • NextAuth.js 4 with Discord OAuth2 provider

Features

  • Discord OAuth2 — JWT sessions with access token, user ID, refresh token
  • Protected routes — Dashboard routes protected via Next.js proxy (middleware)
  • Server selector — Dropdown filtering to guilds where both user and bot are present
  • Responsive layout — Sidebar for desktop, hamburger sheet menu for mobile
  • Landing page — Bot features showcase + "Add to Server" button
  • Login page — Discord sign-in with auto-redirect if already authenticated
  • Dashboard overview — Placeholder stats cards + getting started guide
  • API route/api/guilds fetches mutual guilds (user ∩ bot)

Infrastructure

  • pnpm workspace setup (web/ as workspace member)
  • Dockerfile with multi-stage build (deps → builder → runner with standalone output)
  • Railway config (railway.toml with health check)
  • Environment config.env.example and .env.local.example

Testing

  • 56 tests across 13 test files
  • Coverage: 92% statements, 81% branches, 87% functions, 92% lines
  • Tests cover: auth, discord utils, API routes, all pages, all layout components, proxy config

Key Files

Path Description
web/package.json Dependencies and scripts
web/src/app/page.tsx Landing page
web/src/app/login/page.tsx Discord login
web/src/app/dashboard/ Protected dashboard pages
web/src/components/layout/ Shell, sidebar, header, server selector
web/src/lib/auth.ts NextAuth config with Discord provider
web/src/lib/discord.ts Discord API helpers (guilds, avatars)
web/src/proxy.ts Route protection (Next.js 16 proxy)
pnpm-workspace.yaml Monorepo workspace config

Environment Variables

DISCORD_CLIENT_ID        # Discord OAuth2 app client ID
DISCORD_CLIENT_SECRET    # Discord OAuth2 app client secret  
NEXTAUTH_SECRET          # JWT signing secret
NEXTAUTH_URL             # Canonical site URL
BOT_API_URL              # Bot API endpoint for guild list
NEXT_PUBLIC_DISCORD_CLIENT_ID  # Public client ID for invite links

How to Test

cd web
pnpm install
pnpm build    # Verify production build
pnpm lint     # TypeScript check
pnpm test     # Run all 56 tests
pnpm dev      # Start dev server (needs .env.local)

@BillChirico BillChirico force-pushed the feat/web-dashboard-shell branch from 67487a6 to 2daf4bf Compare February 16, 2026 01:27
@claude
Copy link

claude bot commented Feb 16, 2026

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


Claude Code Review: PR #60

Status: ✅ Approved

Review Summary

Completed a thorough review of this Web Dashboard Shell PR (83 commits, 66 files, +7063/-86 lines).

Key Files Reviewed

File Status
web/src/lib/auth.ts ✅ Clean
web/src/lib/discord.server.ts ✅ Clean
web/src/proxy.ts ✅ Clean
web/src/app/api/guilds/route.ts ✅ Clean
web/src/components/layout/server-selector.tsx ✅ Clean
web/src/components/layout/header.tsx ✅ Clean
web/src/app/login/page.tsx ✅ Clean
web/next.config.ts ✅ Clean
web/Dockerfile ✅ Clean
web/vitest.config.ts ✅ Clean
All test files ✅ Clean

Security Assessment

  • ✅ Access tokens stored only in server-side JWT, not exposed to client session
  • ✅ Token refresh errors properly propagated and handled
  • ✅ Open redirect prevention in login callback URL validation
  • ✅ Runtime validation of NEXTAUTH_SECRET (length, placeholder patterns)
  • ✅ CSRF protection via NextAuth's built-in mechanisms
  • ✅ Security headers configured (X-Frame-Options, HSTS, etc.)

Architecture Assessment

  • ✅ Proper server/client component separation
  • ✅ Defense-in-depth with both proxy and server-side auth checks
  • ✅ Discriminated union pattern for bot API availability
  • ✅ Rate limiting with abort-aware retry logic
  • ✅ Cursor-based pagination for users with 200+ guilds
  • ✅ Error boundaries at multiple levels (root, dashboard)

Testing Assessment

  • ✅ 80% coverage threshold configured
  • ✅ Comprehensive test coverage across 14 test files
  • ✅ CI passing

Verdict

LGTM - This PR implements a solid foundation for the web dashboard with proper security practices, good architecture patterns, and comprehensive testing.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 16, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Web admin dashboard: Discord login, pages (landing, login, dashboard), server selector, guild list, invite flow, and health/guild APIs; UI components and global/dashboard error screens; auth middleware protecting dashboard routes.
  • Documentation

    • README updates and example environment files for dashboard setup.
  • Chores

    • Package manager/dependency bumps, PNPM workspace, Docker configuration, and updated .gitignore.
  • Tests

    • Extensive unit and integration tests covering UI, auth, APIs, and utilities.

Walkthrough

Adds a Next.js web dashboard under web/: app routes/pages/layouts, NextAuth Discord integration with token refresh, API routes (auth, guilds, health), Discord server utilities, UI components and providers, Docker/workspace configs, TypeScript tooling, env templates, and extensive Vitest tests and mocks.

Changes

Cohort / File(s) Summary
Root & workspace
/.gitignore, package.json, pnpm-workspace.yaml
Ignored web build and env artifacts; bumped pnpm and dotenv; added PNPM workspace referencing web.
Environment templates & Docker
web/.env.example, web/.env.local.example, web/Dockerfile
Added env templates for Discord/NextAuth/BOT values and a multi-stage Dockerfile for standalone Next.js runtime.
Project config & tooling
web/package.json, web/tsconfig.json, web/vitest.config.ts, web/postcss.config.mjs, web/components.json, web/next-env.d.ts, web/next.config.ts
New web app configs: Next.js/TS settings, PostCSS/Tailwind, shadcn UI config, Vitest config (coverage, aliases), Next env typings, and Next.js config with security headers and remote image patterns.
Global styles & root layouts/errors
web/src/app/globals.css, web/src/app/layout.tsx, web/src/app/global-error.tsx, web/src/app/error.tsx
Added Tailwind-based theming, Inter font, Providers wiring, app metadata, and app-level/global error boundaries.
Pages, dashboard & API routes
web/src/app/page.tsx, web/src/app/login/page.tsx, web/src/app/dashboard/page.tsx, web/src/app/dashboard/layout.tsx, web/src/app/dashboard/error.tsx, web/src/app/api/auth/[...nextauth]/route.ts, web/src/app/api/health/route.ts, web/src/app/api/guilds/route.ts
Added landing, login, dashboard pages/layouts; NextAuth route; health and authenticated guild-listing API endpoints (with auth checks and abort/timeouts).
Auth, proxy, types
web/src/lib/auth.ts, web/src/proxy.ts, web/src/types/next-auth.d.ts, web/src/types/discord.ts
Implemented NextAuth Discord provider with token refresh logic and exported authOptions/refreshDiscordToken; proxy middleware protecting /dashboard/*; added NextAuth and Discord type augmentations.
Discord server utilities
web/src/lib/discord.server.ts, web/src/lib/discord.ts
Added rate-limit-aware fetchWithRateLimit, paginated fetchUserGuilds, fetchBotGuilds (bot API optional), getMutualGuilds, getBotInviteUrl, and getGuildIconUrl. Note abort handling and BOT_API_URL/BOT_API_SECRET fallbacks.
Utilities & logger
web/src/lib/utils.ts, web/src/lib/logger.ts
Added cn utility (clsx + tailwind-merge) and a simple logger wrapper exposing debug/info/warn/error.
UI primitives (Radix/shadcn wrappers)
web/src/components/ui/avatar.tsx, web/src/components/ui/button.tsx, web/src/components/ui/card.tsx, web/src/components/ui/dropdown-menu.tsx, web/src/components/ui/separator.tsx, web/src/components/ui/sheet.tsx, web/src/components/ui/skeleton.tsx
Introduced reusable UI components with variants, forwarded refs, displayName settings, and consistent styling wrappers.
Layout & provider components
web/src/components/providers.tsx, web/src/components/error-card.tsx, web/src/components/layout/dashboard-shell.tsx, web/src/components/layout/header.tsx, web/src/components/layout/mobile-sidebar.tsx, web/src/components/layout/server-selector.tsx, web/src/components/layout/sidebar.tsx
Added Providers, ErrorCard, DashboardShell, session-aware Header (auto sign-out on RefreshTokenError), MobileSidebar, ServerSelector (fetch + localStorage persistence + retry), and Sidebar navigation.
Tests & test setup
web/tests/setup.ts, web/tests/__mocks__/server-only.ts, web/tests/**/*.test.*
Added Vitest setup, a server-only mock, and extensive tests covering auth, API routes, pages, components, Discord utilities, middleware, and utils (many network and env scenarios).
Docs & misc
web/next-env.d.ts, README.md
Added Next env typings (also listed above) and expanded README with Web Dashboard documentation and setup instructions.
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.39% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly describes the main change: adding a Web Dashboard foundation with Next.js 16, Tailwind v4, and Discord OAuth2 authentication. At 67 characters, it is concise and within reasonable limits.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, covering stack, features, infrastructure, testing, key files, and how to test.
Linked Issues check ✅ Passed The code changes fully implement the requirements from issue #28: Next.js app with Discord OAuth2, JWT sessions, protected routes, server selector, responsive layout, landing/login/dashboard pages, API route, pnpm workspace, Dockerfile, and environment configuration.
Out of Scope Changes check ✅ Passed All changes are in scope for the web dashboard foundation: configuration files, component implementations, auth logic, API routes, tests, and documentation. No unrelated changes detected outside the stated objectives.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ 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 feat/web-dashboard-shell

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.

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

This is a well-structured foundation for the web dashboard with good test coverage (92% statements). However, there are issues that should be addressed:

🟡 Warnings (Should Fix)

  1. Middleware not properly configured (web/src/proxy.ts)

    • The proxy.ts file exports middleware configuration, but there's no middleware.ts file at the project root. Dashboard routes (/dashboard/*) are not protected by authentication middleware. This is a security concern.
  2. Missing token refresh logic (web/src/lib/auth.ts:35)

    • Discord tokens expire after 7 days, but there's no refresh logic. Users will need to re-authenticate unnecessarily.

🔵 Nitpicks (Nice to Have)

  1. Silent error swallowing (web/src/components/layout/server-selector.tsx:38)
  2. Bot API lacks authentication (web/src/lib/discord.ts:41)
  3. Missing rate limit handling (web/src/lib/discord.ts:19)
  4. Optional lockfile glob (web/Dockerfile:8)

✅ What's Good

  • Clean TypeScript code with proper type definitions
  • Comprehensive test suite with 80% coverage thresholds
  • Proper auth flow with JWT sessions
  • Good Dockerfile with multi-stage build and non-root user
  • Well-documented env vars in .env.example
  • No hardcoded credentials or secrets
  • Responsive layout with mobile support

Required Action

Please fix the middleware configuration to ensure dashboard routes are protected. The current implementation allows unauthenticated access to /dashboard/* pages.

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

This is a solid foundation for the web dashboard with good architecture, comprehensive tests, and proper authentication flow. However, I've identified several issues that should be addressed:

🟡 Warnings (2)

  1. No token refresh logic (web/src/lib/auth.ts:25-35): The JWT callback stores refreshToken and accessTokenExpires but never uses them to refresh expired tokens. This will cause silent failures after the Discord access token expires.

  2. Dockerfile context documentation (web/Dockerfile:8): The Dockerfile assumes it's run from within the web/ directory, which may not be obvious in a monorepo setup.

🔵 Nitpicks (3)

  1. Missing bot API authentication (web/src/lib/discord.ts:32-51): The bot API endpoint call has no authentication, which could be a security concern if exposed.

  2. Guild selection not persisted (web/src/components/layout/server-selector.tsx:24): The selected guild is lost on page refresh. Consider URL state or localStorage.

  3. Proxy naming convention (web/src/proxy.ts:3): The file exports proxy instead of the conventional middleware for Next.js.

✅ What's Good

  • Clean TypeScript with proper type definitions
  • Good test coverage (56 tests across 13 files)
  • Proper NextAuth.js integration with Discord OAuth2
  • Well-structured component hierarchy
  • Responsive layout with mobile support
  • Secure session handling with JWT strategy
  • Good error handling in API routes
  • Proper environment variable documentation

Please address the warnings before merging. The nitpicks are optional but would improve the codebase.

coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 16, 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.

LGTM - Well-structured foundation for the web dashboard. Good test coverage, proper auth flow with token refresh, and clean architecture. Only minor nitpicks about console.* usage which can be addressed in a follow-up.

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.

LGTM

claude[bot]
claude bot previously approved these changes Feb 16, 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.

✅ Code Review: Approved

I've completed a comprehensive review of PR #60: Web Dashboard Shell.

Summary

This PR introduces a well-architected Next.js 16 web dashboard with Discord OAuth2 authentication. The implementation follows security best practices and includes thorough test coverage.

Security

  • ✅ Access tokens are properly kept server-side (JWT only, not exposed to client session)
  • ✅ Token refresh errors are handled gracefully with session invalidation
  • ✅ NEXTAUTH_SECRET validation rejects weak/placeholder values
  • ✅ Open redirect protection on login callback URLs
  • ✅ Security headers configured (X-Frame-Options, HSTS, nosniff, Referrer-Policy)
  • ✅ AbortController cleanup prevents memory leaks
  • ✅ Bot API authentication requires shared secret

Architecture

  • ✅ Clean separation of server/client code with server-only import
  • ✅ Proper TypeScript types for Discord API and NextAuth
  • ✅ Rate limit handling with abort-aware retry logic
  • ✅ Guild pagination for users in 200+ servers
  • ✅ Graceful degradation when bot API is unavailable
  • ✅ Error boundaries at root and dashboard levels

Testing

  • ✅ 80% coverage threshold enforced
  • ✅ Tests cover auth flows, token refresh, API routes, and components
  • ✅ Proper env var isolation to prevent test pollution

Documentation

  • ✅ README updated with web dashboard section
  • ✅ Environment variables documented
  • ✅ Setup instructions provided
  • ✅ .env.example includes all required variables

Minor Notes

  • 🔵 Left one nitpick comment on the Dockerfile build context documentation

The code is production-ready. Well done! 🎉

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
Verify each finding against the current code and only fix it if needed.


In `@web/next.config.ts`:
- Around line 26-34: The images.remotePatterns entry currently allows any path
on cdn.discordapp.com (images.remotePatterns -> pathname: "/**"); tighten this
by replacing the broad pathname with scoped patterns such as "/avatars/**",
"/icons/**", and "/embed/**" to limit allowed Discord assets, and add additional
remotePatterns entries for GitHub (hostname: "avatars.githubusercontent.com"
and/or "raw.githubusercontent.com") with appropriate pathnames to permit
GitHub-sourced images per project guidelines; update the images.remotePatterns
array (the same symbol) to include these narrowed Discord patterns and the new
GitHub patterns so Next.js Image only allows the intended remote image sources.

In `@web/src/lib/auth.ts`:
- Around line 136-141: The callback currently returns an expired token unchanged
when expiresAt has passed and token.refreshToken is falsy; update the logic in
the function handling token refresh (the branch that currently returns token and
calls refreshDiscordToken(token)) to detect expired tokens without a
refreshToken and set an error flag on the returned token (e.g., token.error =
"RefreshAccessTokenError" or similar) so downstream code can detect stale
credentials and redirect to re-auth; keep using refreshDiscordToken(token as
Record<string, unknown>) when a refreshToken exists but ensure the no-refresh
path mutates/returns the token with the error property set.
- Around line 48-50: The code unsafely casts token.refreshToken to string and
may send "undefined" to Discord; update the refresh flow (where
token.refreshToken is used—e.g., the function handling the refresh request that
builds grant_type: "refresh_token") to first assert the refresh token exists
(check token.refreshToken !== undefined/null/empty) and if missing throw or
return a clear error before constructing the request body, so you never cast or
send an undefined refresh token to Discord.

In `@web/tests/components/layout/header.test.tsx`:
- Around line 60-71: Test is currently asserting the Skeleton via a CSS class
(".animate-pulse"), which is brittle; add a stable test id to the Skeleton
component (e.g., data-testid="skeleton" on the Skeleton render) and update the
header test (the "loading state" spec in header.test.tsx that uses
mockUseSession and renders <Header />) to assert the skeleton via
screen.getByTestId("skeleton") or screen.queryByTestId("skeleton") instead of
container.querySelector(".animate-pulse"); this keeps the test resilient to
styling changes while keeping the same behavior.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c752959 and 0fe85b0.

📒 Files selected for processing (5)
  • web/next.config.ts
  • web/src/lib/auth.ts
  • web/src/lib/discord.server.ts
  • web/tests/api/guilds.test.ts
  • web/tests/components/layout/header.test.tsx
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
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/app/**/* : Use Next.js 16 App Router with `src/app` directory structure
📚 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 next.config.ts : Configure Next.js Image component in `next.config.ts` to allow remote patterns for GitHub images

Applied to files:

  • web/next.config.ts
📚 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/app/blog/**/*.{ts,tsx} : Use `next-mdx-remote/rsc` for server-side markdown rendering with syntax highlighting via `rehype-highlight`

Applied to files:

  • web/next.config.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/commands/*.js : Enforce Discord timeout maximum of 28 days and slowmode maximum of 6 hours (21600 seconds) in command logic

Applied to files:

  • web/src/lib/auth.ts
  • web/src/lib/discord.server.ts
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
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:

  • web/src/lib/auth.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/**/*.js : Always log errors with context before re-throwing

Applied to files:

  • web/src/lib/auth.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/**/*.js : Always use Winston for logging via `import { info, warn, error } from '../logger.js'`, never use `console.log()`, `console.warn()`, `console.error()`, or any `console.*` methods in src/ files

Applied to files:

  • web/src/lib/auth.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/index.js : Ensure Discord intents are enabled for MessageContent, GuildMembers, and GuildVoiceStates on the bot client

Applied to files:

  • web/src/lib/auth.ts
  • web/src/lib/discord.server.ts
📚 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/app/**/* : Use Next.js 16 App Router with `src/app` directory structure

Applied to files:

  • web/src/lib/auth.ts
🧬 Code graph analysis (3)
web/tests/components/layout/header.test.tsx (1)
web/src/components/layout/header.tsx (1)
  • Header (20-108)
web/tests/api/guilds.test.ts (1)
web/src/app/api/guilds/route.ts (1)
  • GET (12-38)
web/src/lib/auth.ts (2)
web/src/lib/logger.ts (1)
  • logger (19-24)
src/logger.js (1)
  • error (230-232)
⏰ 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). (3)
  • GitHub Check: claude-review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: claude-review
🔇 Additional comments (15)
web/next.config.ts (2)

3-22: Security headers look solid.

The TODO properly acknowledges the nonce-based CSP deferral. The four headers (clickjacking, MIME-sniffing, referrer, HSTS) are well-configured with appropriate values for a dashboard application.


35-46: Headers and export are correctly structured.

The async headers() properly applies security headers to all routes via the /(.*) source pattern, and the default export follows Next.js conventions.

web/tests/components/layout/header.test.tsx (5)

1-36: Well-structured mock setup.

Hoisted vi.fn() references with per-test overrides via mockReturnValue is the right pattern for testing components with different session states. The authenticated session fixture is clean and reusable.


38-58: LGTM — basic rendering assertions are correct.

clearAllMocks in beforeEach with a default authenticated session is a solid baseline.


73-83: LGTM — unauthenticated branch is well-covered.

Asserting both the link's presence and its href attribute is thorough.


85-104: Good coverage of the RefreshTokenError flow.

The mock places error on the session data object, matching the component's session?.error check. The guard test (line 100–103) ensuring signOut is not called in the normal case is a valuable negative assertion.


106-139: Good interaction tests distinguishing both sign-out paths.

The dropdown tests correctly verify callbackUrl: "/" (manual sign-out via dropdown) vs the RefreshTokenError test's callbackUrl: "/login", covering both code paths in the component. Using userEvent.setup() with waitFor is the right approach for async Radix dropdown rendering.

web/src/lib/auth.ts (2)

7-14: Solid runtime validation for secrets and credentials.

The module-level validation for NEXTAUTH_SECRET with length and placeholder checks, plus the Discord credential checks, ensures clear startup failures. The BOT_API_SECRET warning is also a good operational safeguard.


44-100: Well-structured token refresh with good error handling.

Network errors, non-JSON responses, and response shape validation are all covered. The refresh token rotation logic correctly falls back to the existing token.

web/src/lib/discord.server.ts (4)

19-63: Rate-limit retry with abort-aware sleep is well-implemented.

The loop bounds, retry-after parsing with sensible fallback, abort signal handling, and clean exit conditions all look correct.


70-128: Paginated guild fetch with proper safety checks.

JSON parse guard, Array.isArray validation, cursor-based pagination, and abort signal forwarding are all in place. Good implementation.


142-195: Discriminated BotGuildResult cleanly separates "unavailable" from "empty".

The available flag, env var guards, and comprehensive error handling (non-OK response, network error, JSON shape) are solid.


202-235: fetchUserGuilds rejection crashes the entire getMutualGuilds call.

fetchBotGuilds has a defensive .catch() wrapper inside Promise.all, but fetchUserGuilds does not. A transient Discord API failure (network blip, 5xx) will reject the Promise.all and propagate to the caller. This is arguably correct (no user guilds = nothing to show), but it's worth noting the asymmetry is intentional — the caller (GET /api/guilds) does wrap this in a try/catch returning 500.

No change needed; the error path is handled one level up.

web/tests/api/guilds.test.ts (2)

1-46: Clean test setup with proper mock hoisting and env var restoration.

The vi.mock() calls are correctly hoisted, the top-level import of GET is clean, and the afterEach properly restores NEXTAUTH_SECRET. Previous review concerns are addressed.


48-127: Good coverage of the main API route paths.

The five test cases cover the key scenarios: no token, missing access token, successful response, refresh token error, and Discord API failure. The RefreshTokenError test correctly asserts that getMutualGuilds is not called, matching the early-return guard in the route.

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 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

BillChirico and others added 6 commits February 16, 2026 18:32
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 16, 2026
- getGuildIconUrl now returns null when no icon hash is provided instead
  of incorrectly using /embed/avatars/ (which is for user avatars, not
  guilds). Discord has no default guild icon CDN path; callers already
  handle null by showing a placeholder Server icon.
- Rename local 'guilds' variable to 'fetchedGuilds' in server-selector
  to avoid shadowing the component-level useState variable.
- Update tests to match new null return behavior.
@BillChirico BillChirico merged commit c090199 into main Feb 17, 2026
5 of 6 checks passed
@BillChirico BillChirico deleted the feat/web-dashboard-shell branch February 17, 2026 00:13
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.

Code Review Summary

I reviewed the web dashboard implementation. CI is passing with tests at 92% coverage (above 80% threshold). Most patterns are solid, but I found one critical issue that needs fixing.

🔴 Critical

Docker Compose build context mismatch (docker-compose.yml:35-36, web/Dockerfile:8-12)

The docker-compose.yml sets context: ./web for the web service, but web/Dockerfile explicitly requires the monorepo root as context:

# docker-compose.yml
web:
  build:
    context: ./web  # ❌ Wrong - Dockerfile needs root context
    dockerfile: Dockerfile
# web/Dockerfile comment
# Build context: Must be the monorepo root (not web/). The Dockerfile expects
# pnpm-workspace.yaml, pnpm-lock.yaml, and package.json at the root level

The Dockerfile copies package.json, pnpm-lock.yaml, pnpm-workspace.yaml from root and web/package.json from a subdirectory. With context: ./web, these paths won't exist and the build will fail.

Fix: Change docker-compose.yml to:

web:
  build:
    context: .
    dockerfile: web/Dockerfile

🟡 Warnings

  1. Missing web/.dockerignore - The root .dockerignore excludes web/ entirely (line 50), but the web service needs its own .dockerignore when using a separate context. Since the fix above changes context to root, this becomes less critical, but you may still want a web-specific dockerignore for local builds.

  2. CSP not implemented (web/next.config.ts:3-4) - The TODO comment acknowledges this, but shipping without Content-Security-Policy leaves the app more vulnerable to XSS. Consider prioritizing the nonce-based CSP implementation mentioned in the comment.


🔵 Nitpicks

  1. Health endpoint could include version (web/src/app/api/health/route.ts) - For debugging deployed versions, consider adding version: process.env.npm_package_version or a build-time git SHA to the health response.

What's Good (not changing verdict)

  • Clean OAuth2 implementation with proper token refresh and error propagation
  • Rate limiting with abort signal support in Discord API calls
  • 80% coverage thresholds enforced in vitest.config.ts
  • Security headers properly configured (HSTS, X-Frame-Options, etc.)
  • Logger abstraction allows future migration to structured logging
  • Graceful degradation when bot API is unavailable

Please fix the Docker Compose context mismatch and this can be approved.

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.

LGTM

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 OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

ENV HOSTNAME="0.0.0.0"

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
Copy link

Choose a reason for hiding this comment

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

Dockerfile HEALTHCHECK references nonexistent /api/health route

High Severity

The Dockerfile HEALTHCHECK pings http://localhost:3000/api/health, but no corresponding Next.js API route exists at web/src/app/api/health/route.ts. The only API routes created by this PR are /api/auth/[...nextauth] and /api/guilds. This means the health check will always receive a 404, causing Docker (and Railway) to mark the container as unhealthy after three retries, potentially triggering restart loops in production.

Fix in Cursor Fix in Web

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: 3

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@web/next.config.ts`:
- Around line 1-22: Add a Permissions-Policy header to the existing
securityHeaders array in next.config.ts to explicitly disable unused features;
specifically add an object with key "Permissions-Policy" and value "camera=(),
microphone=(), geolocation=()" to the securityHeaders array so the header is
sent alongside X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and
Strict-Transport-Security.

In `@web/src/components/layout/server-selector.tsx`:
- Around line 167-174: getGuildIconUrl returns string | null but Image.src
expects string; update the two places in server-selector.tsx where
getGuildIconUrl is passed to the Image component (the branch using
selectedGuild?.icon and the later branch using guild.icon) to assert the value
is non-null (use a non-null assertion on the getGuildIconUrl call) so TypeScript
treats the return as string for the Image src; keep the existing runtime guards
(selectedGuild?.icon / guild.icon) and only add the non-null assertion to the
getGuildIconUrl invocation.

In `@web/tests/components/layout/header.test.tsx`:
- Around line 83-101: The RefreshTokenError test relies on synchronous flushing
of useEffect; make the "calls signOut when session has RefreshTokenError" spec
more robust by making the test async, rendering <Header /> after setting
mockUseSession to return the error, and wrapping the assertion that mockSignOut
was called with { callbackUrl: "/login" } inside Testing Library's waitFor so
the effect-triggered signOut has time to run; update the test that references
mockUseSession, mockSignOut, and Header accordingly.
📜 Review details

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fe85b0 and 326e610.

📒 Files selected for processing (11)
  • web/Dockerfile
  • web/next.config.ts
  • web/src/app/login/page.tsx
  • web/src/components/layout/header.tsx
  • web/src/components/layout/server-selector.tsx
  • web/src/lib/auth.ts
  • web/src/lib/discord.ts
  • web/src/proxy.ts
  • web/tests/app/login.test.tsx
  • web/tests/components/layout/header.test.tsx
  • web/tests/lib/discord.test.ts
🧰 Additional context used
🧠 Learnings (19)
📓 Common learnings
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/app/**/* : Use Next.js 16 App Router with `src/app` directory structure
📚 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/app/**/*.{ts,tsx} : Use Server Components by default in `src/app` for data fetching; mark interactive components with `"use client"` directive

Applied to files:

  • web/src/components/layout/server-selector.tsx
  • web/src/lib/discord.ts
📚 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/app/page.tsx : Separate data fetching (Server Components) from UI interactivity (Client Components), passing data as props from `src/app/page.tsx` to `homepage-client.tsx`

Applied to files:

  • web/src/components/layout/server-selector.tsx
  • web/src/app/login/page.tsx
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
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:

  • web/src/components/layout/server-selector.tsx
  • web/src/lib/auth.ts
  • web/src/lib/discord.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/**/*.js : Always use Winston for logging via `import { info, warn, error } from '../logger.js'`, never use `console.log()`, `console.warn()`, `console.error()`, or any `console.*` methods in src/ files

Applied to files:

  • web/src/components/layout/server-selector.tsx
  • web/src/lib/auth.ts
  • web/src/lib/discord.ts
📚 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/components/**/*.{ts,tsx} : Use `cn()` utility function combining `clsx` and `tailwind-merge` from `src/lib/utils.ts` for conditional class names

Applied to files:

  • web/src/components/layout/server-selector.tsx
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/index.js : Ensure Discord intents are enabled for MessageContent, GuildMembers, and GuildVoiceStates on the bot client

Applied to files:

  • web/tests/lib/discord.test.ts
  • web/src/lib/auth.ts
  • web/src/lib/discord.ts
📚 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/components/**/*.{ts,tsx} : Use `phosphor-icons/react` for Phosphor icons in navigation and social links; use `lucide-react` for Lucide icons in theme toggle and blog components

Applied to files:

  • web/src/app/login/page.tsx
📚 Learning: 2025-10-10T15:05:26.145Z
Learnt from: CR
Repo: BillChirico/LUA-Obfuscator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-10T15:05:26.145Z
Learning: Applies to **/*.{jsx,tsx} : Use React hooks for state management in the web UI

Applied to files:

  • web/src/app/login/page.tsx
📚 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 next.config.ts : Configure Next.js Image component in `next.config.ts` to allow remote patterns for GitHub images

Applied to files:

  • web/next.config.ts
📚 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/app/blog/**/*.{ts,tsx} : Use `next-mdx-remote/rsc` for server-side markdown rendering with syntax highlighting via `rehype-highlight`

Applied to files:

  • web/next.config.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/commands/*.js : Enforce Discord timeout maximum of 28 days and slowmode maximum of 6 hours (21600 seconds) in command logic

Applied to files:

  • web/src/lib/auth.ts
  • web/src/lib/discord.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/**/*.js : Always log errors with context before re-throwing

Applied to files:

  • web/src/lib/auth.ts
📚 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/app/**/* : Use Next.js 16 App Router with `src/app` directory structure

Applied to files:

  • web/src/lib/auth.ts
  • web/src/proxy.ts
📚 Learning: 2026-02-04T02:20:09.131Z
Learnt from: CR
Repo: VolvoxCommunity/Volvox.Website PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-02-04T02:20:09.131Z
Learning: Applies to {package.json,.npmrc,pnpm-lock.yaml} : Use pnpm package manager (pinned to v10.23.0) with `.npmrc` configured for strict peer dependencies and disabled shamefully-hoist

Applied to files:

  • web/Dockerfile
📚 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 **/*.{ts,tsx,js,jsx,json,css,md} : After changing or editing any files, run the complete validation workflow: `pnpm format && pnpm typecheck && pnpm lint && pnpm build` before committing

Applied to files:

  • web/Dockerfile
📚 Learning: 2025-10-10T15:05:26.145Z
Learnt from: CR
Repo: BillChirico/LUA-Obfuscator PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-10-10T15:05:26.145Z
Learning: Applies to package.json : Only add new packages when absolutely necessary or explicitly requested

Applied to files:

  • web/Dockerfile
📚 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/components/**/*.{ts,tsx} : Use `canvas-confetti` package to trigger celebratory confetti animations on Discord link click

Applied to files:

  • web/src/lib/discord.ts
📚 Learning: 2026-02-16T17:06:27.423Z
Learnt from: CR
Repo: BillChirico/bills-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-16T17:06:27.423Z
Learning: Applies to src/**/*.js : Use `splitMessage()` utility to split messages exceeding Discord's 2000-character limit

Applied to files:

  • web/src/lib/discord.ts
🧬 Code graph analysis (6)
web/src/components/layout/header.tsx (2)
web/src/lib/auth.ts (1)
  • session (155-168)
web/src/components/layout/mobile-sidebar.tsx (1)
  • MobileSidebar (14-42)
web/tests/lib/discord.test.ts (2)
web/src/lib/discord.ts (1)
  • getGuildIconUrl (33-41)
web/src/lib/discord.server.ts (4)
  • fetchWithRateLimit (19-63)
  • fetchUserGuilds (70-128)
  • fetchBotGuilds (142-195)
  • getMutualGuilds (202-235)
web/src/lib/auth.ts (1)
web/src/lib/logger.ts (1)
  • logger (19-24)
web/tests/app/login.test.tsx (1)
web/src/app/login/page.tsx (1)
  • LoginPage (87-99)
web/tests/components/layout/header.test.tsx (1)
web/src/components/layout/header.tsx (1)
  • Header (20-108)
web/src/lib/discord.ts (1)
src/deploy-commands.js (1)
  • clientId (25-25)
🪛 Trivy (0.69.1)
web/Dockerfile

[info] 1-1: No HEALTHCHECK defined

Add HEALTHCHECK instruction in your Dockerfile

Rule: DS-0026

Learn more

(IaC/Dockerfile)

⏰ 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). (4)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Cursor Bugbot
  • GitHub Check: claude-review
  • GitHub Check: claude-review
🔇 Additional comments (16)
web/Dockerfile (2)

1-14: LGTM — deps stage is well structured.

Proper use of cache mount for pnpm store, frozen lockfile for reproducibility, and root package.json is copied for corepack version pinning.


36-63: Runner stage looks solid.

Non-root user, proper --chown, standalone output layout, HEALTHCHECK targeting /api/health, and correct HOSTNAME=0.0.0.0 for container networking. The Trivy hint about missing HEALTHCHECK is a false positive — it's present at Lines 60–61.

web/next.config.ts (1)

24-46: LGTM — config is clean and well-scoped.

standalone output aligns with the Dockerfile's multi-stage build, and Discord CDN patterns are properly restricted to /{avatars,icons,embed}/**.

web/src/proxy.ts (1)

1-28: LGTM!

Clean route protection implementation. Good use of request.nextUrl.pathname + request.nextUrl.search for preserving the full callback URL, proper RefreshTokenError handling, and well-scoped matcher config.

web/src/lib/discord.ts (1)

1-41: LGTM!

Well-documented permissions breakdown with verification expression. Both utility functions handle edge cases cleanly — getBotInviteUrl returns null when unconfigured, and getGuildIconUrl correctly distinguishes animated vs. static icons and returns null for missing hashes.

web/src/components/layout/server-selector.tsx (2)

42-97: Well-structured abort and loading guard pattern.

The loadGuilds function properly aborts previous in-flight requests, guards against stale finally blocks via controller identity check, handles 401 with a redirect, and validates response shape. This addresses all prior review feedback cleanly.


131-157: Good empty-state UX with invite CTA and fallback messaging.

The empty state now clearly communicates "Bill Bot isn't in any of your Discord servers" with an invite button when the client ID is configured, and a helpful fallback message when it's not. This addresses the prior feedback about distinguishing empty states.

web/tests/lib/discord.test.ts (1)

1-533: Thorough and well-structured test suite.

Good coverage across all Discord utility functions. The env var cleanup pattern in beforeEach/afterEach is exception-safe, fake timers are used for rate-limit tests, and URL-based mock dispatching in getMutualGuilds tests is clean. The pagination test for fetchUserGuilds properly validates the after cursor parameter.

web/src/lib/auth.ts (2)

44-105: Solid token refresh implementation with comprehensive error handling.

All prior feedback has been addressed: network errors caught, JSON parsing guarded, response shape validated, missing refresh token handled, and token rotation supported. The guard on lines 45–48 makes the as string cast on line 54 redundant but harmless.


119-154: JWT callback handles all token lifecycle states correctly.

Good coverage of: initial sign-in with default expiry fallback, early return for non-expired tokens, refresh attempt when expired, and error flag when no refresh token is available. The security comment about token exposure (lines 121–126) is a helpful clarification.

web/src/app/login/page.tsx (1)

15-85: Clean login flow with good security and UX considerations.

The callbackUrl validation (lines 22–25) correctly prevents open redirects. The RefreshTokenError carve-out (lines 29–33) properly defers to the Header's centralized sign-out handler. The loading/spinner logic (line 41) avoids the flash-of-login-form for already-authenticated users.

web/tests/app/login.test.tsx (1)

1-107: Good test coverage for the login flow.

Tests cover all key paths: rendering, default and custom callbackUrl, privacy note, RefreshTokenError (verifying no signOut race), and authenticated redirect. Mock shapes correctly align with the session structure from auth callbacks.

web/src/components/layout/header.tsx (2)

20-35: Good centralized RefreshTokenError handler with duplicate-prevention guard.

The signingOut ref prevents multiple sign-out attempts when the session refetches. Note that the ref is never reset — if signOut were to fail without navigating away, subsequent errors would be silently ignored. In practice, signOut redirects the page so the component unmounts, making this a non-issue.


50-104: Clean three-state rendering for session status.

Skeleton for loading, sign-in button for unauthenticated, and avatar dropdown for authenticated — all prior feedback addressed. The Documentation link now points to the GitHub repo instead of the Discord Developer Portal, which is more appropriate for end users.

web/tests/components/layout/header.test.tsx (2)

1-42: Well-structured test setup with properly hoisted mocks.

The hoisted vi.fn() declarations before vi.mock, followed by the Header import, correctly handle vitest's module-mock hoisting semantics. The beforeEach reset and mutable mockUseSession pattern cleanly addresses all prior review feedback about static mocks.


104-137: Dropdown interaction tests are thorough and correctly structured.

Good use of userEvent.setup() and waitFor for the async dropdown rendering. Both menu visibility and the sign-out action (with distinct callbackUrl: "/" vs the error-driven "/login") are properly verified.

Comment on lines +1 to +22
import type { NextConfig } from "next";

// TODO: Implement nonce-based CSP as a separate task.
// script-src 'self' without 'unsafe-inline' breaks Next.js RSC streaming/hydration.
const securityHeaders = [
{
key: "X-Frame-Options",
value: "DENY",
},
{
key: "X-Content-Type-Options",
value: "nosniff",
},
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin",
},
{
key: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
];
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

Good security header baseline; CSP gap is acknowledged.

The four headers provide solid defense-in-depth. The TODO on Lines 3–4 correctly identifies that a nonce-based CSP is needed for Next.js App Router streaming/hydration and defers it as a separate task. This is the right approach — a broken CSP is worse than a missing one.

Consider also adding a Permissions-Policy header to disable unused browser features (camera, microphone, geolocation, etc.) as an additional hardening measure:

{
  key: "Permissions-Policy",
  value: "camera=(), microphone=(), geolocation=()",
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/next.config.ts` around lines 1 - 22, Add a Permissions-Policy header to
the existing securityHeaders array in next.config.ts to explicitly disable
unused features; specifically add an object with key "Permissions-Policy" and
value "camera=(), microphone=(), geolocation=()" to the securityHeaders array so
the header is sent alongside X-Frame-Options, X-Content-Type-Options,
Referrer-Policy, and Strict-Transport-Security.

Comment on lines +167 to +174
{selectedGuild?.icon ? (
<Image
src={getGuildIconUrl(selectedGuild.id, selectedGuild.icon, 64)}
alt={selectedGuild.name}
width={20}
height={20}
className="rounded-full"
/>
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

🧩 Analysis chain

🏁 Script executed:

# Find the getGuildIconUrl function definition
rg "getGuildIconUrl" --type ts --type tsx -B 2 -A 10

Repository: BillChirico/bills-bot

Length of output: 92


🏁 Script executed:

# Check tsconfig.json for strict null checks
fd "tsconfig" --type f -x cat {}

Repository: BillChirico/bills-bot

Length of output: 766


🏁 Script executed:

# Read the server-selector.tsx file to see the actual code at lines 167-174 and 196
wc -l web/src/components/layout/server-selector.tsx

Repository: BillChirico/bills-bot

Length of output: 114


🏁 Script executed:

# Look for Image import and usage in server-selector.tsx
rg "from.*Image|import.*Image" web/src/components/layout/server-selector.tsx -A 2

Repository: BillChirico/bills-bot

Length of output: 216


🏁 Script executed:

# Find getGuildIconUrl function - try without type filter
rg "getGuildIconUrl" -B 2 -A 10

Repository: BillChirico/bills-bot

Length of output: 6768


🏁 Script executed:

# Read the server-selector.tsx file completely
cat -n web/src/components/layout/server-selector.tsx

Repository: BillChirico/bills-bot

Length of output: 8573


getGuildIconUrl returns string | null but Image.src requires string.

Although the selectedGuild?.icon and guild.icon guards ensure the return value is a string at runtime, TypeScript's strict mode does not narrow the return type of getGuildIconUrl. This causes a type error at line 169 and line 196. Add a non-null assertion to both calls.

Proposed fix
                src={getGuildIconUrl(selectedGuild.id, selectedGuild.icon, 64)!}

And similarly on Line 196:

                src={getGuildIconUrl(guild.id, guild.icon, 64)!}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/server-selector.tsx` around lines 167 - 174,
getGuildIconUrl returns string | null but Image.src expects string; update the
two places in server-selector.tsx where getGuildIconUrl is passed to the Image
component (the branch using selectedGuild?.icon and the later branch using
guild.icon) to assert the value is non-null (use a non-null assertion on the
getGuildIconUrl call) so TypeScript treats the return as string for the Image
src; keep the existing runtime guards (selectedGuild?.icon / guild.icon) and
only add the non-null assertion to the getGuildIconUrl invocation.

Comment on lines +83 to +101
describe("RefreshTokenError", () => {
it("calls signOut when session has RefreshTokenError", () => {
mockUseSession.mockReturnValue({
data: {
user: { id: "123", name: "TestUser" },
error: "RefreshTokenError",
},
status: "authenticated",
});

render(<Header />);

expect(mockSignOut).toHaveBeenCalledWith({ callbackUrl: "/login" });
});

it("does not call signOut when session has no error", () => {
render(<Header />);
expect(mockSignOut).not.toHaveBeenCalled();
});
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

RefreshTokenError assertion relies on useEffect flushing synchronously within render.

This works because Testing Library's render wraps in act, which flushes effects. However, if this ever becomes flaky, wrap the assertion in waitFor for resilience:

      render(<Header />);
-     expect(mockSignOut).toHaveBeenCalledWith({ callbackUrl: "/login" });
+     await waitFor(() => {
+       expect(mockSignOut).toHaveBeenCalledWith({ callbackUrl: "/login" });
+     });

The it callback would also need to be async. This is a minor robustness suggestion — the current form is valid.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/tests/components/layout/header.test.tsx` around lines 83 - 101, The
RefreshTokenError test relies on synchronous flushing of useEffect; make the
"calls signOut when session has RefreshTokenError" spec more robust by making
the test async, rendering <Header /> after setting mockUseSession to return the
error, and wrapping the assertion that mockSignOut was called with {
callbackUrl: "/login" } inside Testing Library's waitFor so the effect-triggered
signOut has time to run; update the test that references mockUseSession,
mockSignOut, and Header accordingly.

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 2 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

const response = await fetch("/api/guilds", { signal: controller.signal });
if (response.status === 401) {
// Auth failure — redirect to login instead of showing a misleading retry
window.location.href = "/login";
Copy link

Choose a reason for hiding this comment

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

Brief incorrect UI flash during 401 redirect

Low Severity

When the /api/guilds fetch returns a 401, loadGuilds sets window.location.href = "/login" and returns early from the try block. However, the finally block still runs and sets loading to false. Since setGuilds was never called, the component re-renders with loading=false, error=false, and guilds=[], briefly displaying the "No mutual servers" empty state (with the "Invite Bill Bot" button) before the browser completes the redirect to /login. The finally block doesn't account for the in-progress redirect scenario.

Additional Locations (1)

Fix in Cursor Fix in Web

href: "/dashboard/settings",
icon: Settings,
},
];
Copy link

Choose a reason for hiding this comment

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

Sidebar links to non-existent dashboard routes show 404

Low Severity

The navigation array defines links to /dashboard/moderation, /dashboard/ai, /dashboard/members, /dashboard/config, and /dashboard/settings, but none of these routes have corresponding page components. Only /dashboard has a page.tsx. Clicking any of these sidebar links results in a Next.js 404 page, which is a confusing experience for users exploring the dashboard. Disabling or visually marking unimplemented links as "coming soon" would avoid this.

Fix in Cursor Fix in Web

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: Discord OAuth2 login + Next.js app shell

2 participants