Skip to content

feat: public community page — leaderboards, showcases, and stats#120

Merged
BillChirico merged 24 commits intomainfrom
feat/community-page
Feb 28, 2026
Merged

feat: public community page — leaderboards, showcases, and stats#120
BillChirico merged 24 commits intomainfrom
feat/community-page

Conversation

@BillChirico
Copy link
Collaborator

Summary

Public-facing community page (no login required) showing server highlights.

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

  • GET /community/:guildId/leaderboard — Top 25 members by XP (public profiles only)
  • GET /community/:guildId/showcases — Project showcase gallery with upvotes
  • GET /community/:guildId/stats — Community stats (members, messages, projects, challenges)
  • GET /community/:guildId/profile/:userId — Public user profile with stats + badges
  • Rate limited: 30 req/min, no auth required
  • Privacy: only shows users who opted into public profiles

Frontend

  • /community/[guildId] — SSR community page with stats banner, XP leaderboard, showcase gallery
  • /community/[guildId]/[userId] — SSR public profile with avatar, level, XP bar, stats, badges, projects
  • Full SEO meta tags + Open Graph images

Migration

  • 013_public_profiles.cjs — adds public_profile boolean to user_stats with partial index

Tests

  • 27 new tests covering all endpoints, privacy enforcement, rate limiting
  • All 2350 tests passing

Closes #36

Copilot AI review requested due to automatic review settings February 28, 2026 04:12
@claude
Copy link

claude bot commented Feb 28, 2026

Claude finished @BillChirico's task in 3m 1s —— View job


Review Complete — 1 warning, 1 documentation issue, 1 nitpick

  • Read AGENTS.md and CLAUDE.md for project conventions
  • Read all changed files
  • Analyze code quality, architecture, security, testing
  • Post inline comments for issues found
  • Post top-level summary and verdict

Most prior review feedback has been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces, activeProjects filter, test mock). Three issues remain:

🟡 totalMessagesSent query is misleading — sums ALL-TIME messages_sent but only for users active in last 7 days. Remove the 7-day filter to match the field name.

🟡 Duplicated getApiBase() — both community pages re-implement getBotApiBaseUrl() from web/src/lib/bot-api.ts. Use the shared helper.

🟡 AGENTS.md not updated — Key Files table missing new files per project conventions.

🔵 Badge name inconsistencyActive Voice is message-based, not voice-based. Acknowledged for follow-up.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 14 minutes and 3 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between a46aa85 and c658ba9.

📒 Files selected for processing (4)
  • src/api/routes/community.js
  • tests/api/routes/community.test.js
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
📝 Walkthrough

Walkthrough

Adds public community features including a database migration enabling opt-in public profiles, new backend API routes for leaderboards, showcases, community statistics, and user profiles with rate limiting, frontend community hub and user profile pages, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Database Schema
migrations/013_public_profiles.cjs
Introduces migration adding public_profile BOOLEAN column to user_stats table with DEFAULT FALSE, and creates a filtered index on (guild_id, public_profile) for efficient public profile queries.
API Infrastructure
src/api/index.js
Mounts the new communityRouter at /community endpoint, exposing public community data without authentication requirement.
Community API Routes
src/api/routes/community.js
Implements public endpoints: GET /:guildId/leaderboard, GET /:guildId/showcases, GET /:guildId/stats, and GET /:guildId/profile/:userId. Includes per-IP rate limiting (30 req/min), helper utilities for level badges and rep config, Discord user resolution, and public_profile privacy enforcement. Returns 500/503 errors on failures.
API Route Tests
tests/api/routes/community.test.js
Adds 600+ lines of test coverage for community endpoints including public_profile filtering, pagination, rate limiting, error handling, badge validation, and privacy enforcement across leaderboard, showcases, stats, and profile routes.
Frontend Community Hub
web/src/app/community/[guildId]/page.tsx
Creates Next.js page rendering community overview with leaderboard (ranked members with avatars and XP bars), showcase gallery (project cards with tech tags and upvotes), community stats, and top contributors sidebar. Includes server-side data fetching, SEO metadata generation, and privacy notice.
Frontend User Profile
web/src/app/community/[guildId]/[userId]/page.tsx
Implements Next.js page for individual public user profiles displaying stats, XP progress, badge list, projects, and user info with avatar. Fetches profile server-side, generates dynamic metadata for share previews, and handles not-found state for non-public profiles.

Possibly related PRs

  • PR #111: Schema creation for user_stats table that this PR extends with the public_profile column and related index for public profile functionality.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: a public community page with leaderboards, showcases, and stats. It is concise and clearly conveys the primary feature addition.
Description check ✅ Passed The description comprehensively covers all aspects of the PR changes including backend API endpoints, frontend SSR pages, migration, and tests. It is well-organized and directly related to the changeset.
Linked Issues check ✅ Passed The PR successfully implements all coding-related objectives from issue #36: public leaderboard [#36], showcase gallery [#36], community stats [#36], public profiles [#36], SEO meta tags [#36], and privacy enforcement [#36]. Rate limiting and database indexing complete the implementation.
Out of Scope Changes check ✅ Passed All changes are scoped to the objectives in issue #36. The migration, API endpoints, frontend pages, and tests directly support public community features, privacy controls, and SEO requirements specified in the linked issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/community-page

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.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

Note

Docstrings generation - SUCCESS
Generated docstrings and committed to branch feat/community-page (commit: 684c08ba25b1b4192c7f131688fc66fab0120000)

Docstrings generation was requested by @BillChirico.

The following files were modified:

* `src/api/routes/community.js`
* `web/src/app/community/[guildId]/[userId]/page.tsx`
* `web/src/app/community/[guildId]/page.tsx`

These files were ignored:
* `tests/api/routes/community.test.js`
@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.

@greptile-apps
Copy link

greptile-apps bot commented Feb 28, 2026

Greptile Summary

Adds public community pages (leaderboards, showcases, stats, profiles) with proper privacy controls and SSR. All previously-raised issues have been addressed.

Key changes:

  • Migration adds public_profile opt-in flag with partial index for efficient queries
  • Four new public API endpoints (no auth required, rate-limited at 30 req/min)
  • Privacy enforced at query level using public_profile = TRUE filters
  • Backend uses parameterized queries, Winston logging, and proper error handling
  • Frontend uses SSR with Next.js App Router, full SEO metadata, and type-safe interfaces
  • 27 new tests achieve comprehensive coverage of happy paths, privacy enforcement, rate limiting, and error cases

Note: All previous thread issues (missing userId field, bulk fetch optimization) have been resolved.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk — one trivial locale fix needed
  • Excellent implementation with proper security (privacy enforcement, rate limiting, parameterized queries), comprehensive test coverage (27 tests, all passing), correct use of Winston logging, and all previous issues addressed. Single minor syntax fix needed for hardcoded locale.
  • No files require special attention — the locale fix at profile page:222 is trivial

Important Files Changed

Filename Overview
src/api/routes/community.js Public API endpoints with proper rate limiting, parameterized queries, privacy enforcement, and Winston logging
tests/api/routes/community.test.js Comprehensive test coverage (27 tests) covering all endpoints, privacy, rate limiting, and error cases
web/src/app/community/[guildId]/[userId]/page.tsx SSR profile page with proper types and SEO; one hardcoded locale needs fixing

Sequence Diagram

sequenceDiagram
    participant User as Browser
    participant Next as Next.js SSR
    participant API as Express API
    participant DB as PostgreSQL
    participant Discord as Discord.js Client

    User->>Next: GET /community/[guildId]
    Next->>API: GET /community/:guildId/stats
    Next->>API: GET /community/:guildId/leaderboard
    Next->>API: GET /community/:guildId/showcases
    
    par Fetch Stats
        API->>DB: Query user_stats, reputation<br/>(public_profile = TRUE)
        DB-->>API: Community stats
        API->>Discord: Bulk fetch top 3 members
        Discord-->>API: Member metadata
    and Fetch Leaderboard
        API->>DB: Query top 25 by XP<br/>(public_profile = TRUE)
        DB-->>API: User rankings
        API->>Discord: Bulk fetch members
        Discord-->>API: Avatars, usernames
    and Fetch Showcases
        API->>DB: Query showcases + JOIN user_stats<br/>(public_profile = TRUE)
        DB-->>API: Projects
        API->>Discord: Bulk fetch authors
        Discord-->>API: Author metadata
    end
    
    API-->>Next: JSON responses (rate limited 30/min)
    Next-->>User: Rendered community page

    User->>Next: Click member profile
    Next->>API: GET /community/:guildId/profile/:userId
    API->>DB: Check public_profile = TRUE
    alt Profile is public
        API->>DB: Fetch stats, reputation, showcases
        DB-->>API: User data
        API->>Discord: Fetch member info
        Discord-->>API: Avatar, username
        API-->>Next: Profile JSON
        Next-->>User: Rendered profile page
    else Profile is private
        API-->>Next: 404 Profile not found
        Next-->>User: 404 page
    end
Loading

Last reviewed commit: c658ba9

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.

6 files reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

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

2 critical issues, 1 nitpick found.


🔴 Critical: Leaderboard → Profile links are completely broken

The leaderboard API (src/api/routes/community.js:119-128) does not return user_id in its response. The frontend community page (web/src/app/community/[guildId]/page.tsx:338) builds profile links using member.username (e.g. /community/guild123/alice). However, the profile page route is /community/[guildId]/[userId] and the profile API endpoint queries by Discord user ID (snowflake). Since a username like "alice" will never match a snowflake like "123456789012345678", every profile link from the leaderboard will 404.

Fix: Add userId: row.user_id to the leaderboard response, update the LeaderboardMember TypeScript interface, and change the frontend link to use member.userId.

🟡 Warning: AGENTS.md not updated

Per the project's own documentation requirements, the Key Files table in AGENTS.md must be updated when adding new files/modules. The following are missing:

  • src/api/routes/community.js
  • web/src/app/community/[guildId]/page.tsx
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • migrations/013_public_profiles.cjs

🔵 Nitpick: Duplicated code across frontend pages

getApiBase(), API_BASE, XpBar, and StatCard are duplicated between the community page and the profile page. Could be extracted to a shared module.


📋 Prompt to fix all issues
Fix the following issues in the volvox-bot repository on branch feat/community-page:

1. FILE: src/api/routes/community.js, LINES 119-128
   ISSUE: The leaderboard response object is missing `user_id`. Add `userId: row.user_id` as the first property in the returned object (before `username`).

2. FILE: web/src/app/community/[guildId]/page.tsx, LINE 9-17
   ISSUE: The LeaderboardMember interface is missing `userId`. Add `userId: string;` as the first property.

3. FILE: web/src/app/community/[guildId]/page.tsx, LINE 338
   ISSUE: Profile link uses `member.username` but needs to use `member.userId`. Change:
     href={`/community/${guildId}/${member.username}`}
   to:
     href={`/community/${guildId}/${member.userId}`}

4. FILE: AGENTS.md
   ISSUE: Key Files table is missing new files. Add these rows to the Key Files table:
   - `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
   - `web/src/app/community/[guildId]/page.tsx` | Community hub page — SSR stats banner, leaderboard, showcase gallery
   - `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile page — SSR avatar, level, XP, stats, badges, projects
   - `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats

5. OPTIONAL (nitpick): Extract duplicated `getApiBase()`, `API_BASE`, `XpBar`, and `StatCard` from the two community page files into a shared module like `web/src/lib/community-utils.ts` or `web/src/app/community/shared.tsx`.

After changes, run `npx biome check --write` and `pnpm test` to verify.

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

Implements a public-facing “Community Hub” feature (no login) by adding new community API endpoints, SSR community/profile pages in the web app, and a DB opt-in flag to ensure only users with public profiles are exposed.

Changes:

  • Added public /api/v1/community/* endpoints (leaderboard, showcases, stats, public profile) with aggressive rate limiting.
  • Added SSR community hub and public profile pages under /community/[guildId] and /community/[guildId]/[userId] with SEO metadata.
  • Added a migration introducing user_stats.public_profile (default false) and tests covering endpoints, privacy enforcement, and rate limiting.

Reviewed changes

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

Show a summary per file
File Description
web/src/app/community/[guildId]/page.tsx SSR community hub page rendering stats, leaderboard, and showcase gallery.
web/src/app/community/[guildId]/[userId]/page.tsx SSR public profile page with stats, badges, and project list + metadata.
src/api/routes/community.js New public community router with rate limiting and privacy gating for leaderboard/profile.
src/api/index.js Mounts the new community router as a public route group.
migrations/013_public_profiles.cjs Adds public_profile opt-in flag + partial index for public queries.
tests/api/routes/community.test.js Adds route tests for correctness, privacy behavior, error handling, and rate limiting.

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

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/community.js`:
- Around line 119-127: The leaderboard response is missing a stable user
identifier; update the returned object in the leaderboard mapping (the block
that currently returns username, displayName, avatar, xp, level, badge:
getLevelBadge(level), rank: offset + idx + 1) to include the persistent user id
(e.g., userId or user_id) coming from the query row (e.g., row.user_id or
row.userId) so downstream profile routing that resolves by user_id works
reliably; ensure the property name matches what the frontend expects (userId or
user_id) and use the same row field used in the SQL SELECT.
- Around line 164-171: The showcases query currently returns all rows for a
guild regardless of users' privacy settings; update the SQL used in the route
handler that builds the two queries (the COUNT query and the SELECT query using
pool.query/ORDER BY ${orderBy}) to only include showcases whose authors have
opted into public profiles by joining the user_stats (or users) table and adding
a WHERE clause like user_stats.public_profile = true (or equivalent) for both
the COUNT query and the SELECT query; ensure the JOIN targets the same author_id
used in the SELECT (e.g., JOIN user_stats us ON s.author_id = us.user_id) so
counts and paging remain consistent with the filtered result set.
- Around line 99-113: The current Promise.all loop maps membersResult.rows and
calls guild.members.fetch(row.user_id) per member (in the block that computes
level via computeLevel and builds username/displayName/avatar), causing many
individual API requests; change this to batch-fetch all user IDs once using
guild.members.fetch({ user: userIdsArray }) (as done in
src/commands/leaderboard.js), store the returned Map, then inside the
membersResult.rows.map use map.get(row.user_id) to populate username,
displayName, and avatar (falling back to row.user_id when missing) so you
replace per-row fetches with a single bulk fetch and efficient lookups.

In `@tests/api/routes/community.test.js`:
- Around line 159-220: Add a new test in the GET
/api/v1/community/:guildId/showcases suite that verifies privacy filtering:
mockPool.query should first return total rows and then a projects array
containing one project by PUBLIC_USER (opted-in) and one by PRIVATE_USER (not
opted-in); call request(app).get(`/api/v1/community/${GUILD_ID}/showcases`) and
assert the response only includes the opted-in project (res.body.projects length
is 1, contains the PUBLIC_USER project fields and authorName 'Alice') and that
the PRIVATE_USER project is excluded; use the existing constants PUBLIC_USER and
PRIVATE_USER and inspect mockPool.query.mock.calls to ensure the DB query was
executed for the showcases endpoint.

In `@web/src/app/community/`[guildId]/[userId]/page.tsx:
- Around line 240-244: Replace the raw <img> used to render the avatar with
Next.js's Image component: import Image from 'next/image' at the top of the
file, then swap the <img src={profile.avatar} alt={profile.displayName}
className="h-24 w-24 rounded-full object-cover ring-2 ring-primary/20" /> usage
to an <Image> that uses profile.avatar and profile.displayName and supplies
either explicit width/height (e.g., 96x96 matching h-24/w-24) or uses
fill/layout with appropriate objectFit to preserve the rounded/full styling;
ensure the same classes (rounded-full, ring-2, ring-primary/20) remain applied
and remove any unsupported props for Image.

In `@web/src/app/community/`[guildId]/page.tsx:
- Around line 201-209: XpBar currently computes progress from a hardcoded
thresholds array causing wrong/negative widths; update XpBar({ xp, level }) to
instead accept and use API-provided values (e.g., currentThreshold,
nextThreshold or a precomputed progressPercent) and compute progress from those
fields rather than the local thresholds constant; modify the call sites that
render XpBar to pass the API's threshold/progress fields returned by the
community endpoints (the same change must also be applied to the XpBar usage in
the component in the [userId] page) so both UI and API use the same level
thresholds and avoid negative/incorrect widths.
- Around line 246-247: Replace the raw <img> in the MemberAvatar component with
Next.js's Image component: import Image from 'next/image', use <Image
src={avatar} alt={name} width={sizePx} height={sizePx}
className={`${sizeClasses[size]} rounded-full object-cover`} /> (or provide
appropriate numeric width/height derived from your size mapping) and remove the
plain img; ensure src is a valid string or handled with next/image compatible
loader/config and preserve the same className/alt semantics so Next's
optimization and repo lint rules are satisfied.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 512dbd7 and 684c08b.

📒 Files selected for processing (6)
  • migrations/013_public_profiles.cjs
  • src/api/index.js
  • src/api/routes/community.js
  • tests/api/routes/community.test.js
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
📜 Review details
⏰ 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). (2)
  • GitHub Check: claude-review
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')

Files:

  • src/api/index.js
  • src/api/routes/community.js
  • tests/api/routes/community.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome

Files:

  • src/api/index.js
  • src/api/routes/community.js
  • tests/api/routes/community.test.js
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Files:

  • src/api/index.js
  • src/api/routes/community.js
web/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/image Image component with appropriate layout and sizing props in Next.js components

Files:

  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores

Files:

  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
🧬 Code graph analysis (2)
tests/api/routes/community.test.js (1)
src/api/server.js (1)
  • createApp (27-87)
web/src/app/community/[guildId]/[userId]/page.tsx (4)
web/src/components/ui/card.tsx (5)
  • Card (55-55)
  • CardContent (55-55)
  • CardHeader (55-55)
  • CardTitle (55-55)
  • CardDescription (55-55)
src/commands/rank.js (1)
  • currentThreshold (57-57)
src/api/routes/community.js (1)
  • level (352-352)
web/src/components/ui/badge.tsx (1)
  • Badge (46-46)
🔇 Additional comments (2)
migrations/013_public_profiles.cjs (1)

10-27: Migration structure looks solid and rollback-safe.

up/down are symmetric, idempotent (IF NOT EXISTS / IF EXISTS), and aligned with the feature’s privacy model.

src/api/index.js (1)

22-24: Public community routing placement is correct.

Mounting /community as a public route here is consistent with the feature goals and keeps auth-protected groups intact.

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 — 1 warning, 1 documentation issue

Most issues from prior reviews have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, test mock). Two issues remain:

🟡 Warning

  1. Stats activeProjects count inconsistent with showcases endpoint (community.js:251): The /showcases endpoint correctly filters by public_profile = TRUE, but the /stats project count queries all showcases without the filter. The stats banner may show a higher project count than what actually appears on the showcases page. See inline comment for fix.

🟡 Documentation

  1. AGENTS.md Key Files table not updated: Per project conventions (AGENTS.md line 177, 183–184), new files must be added to the Key Files table. Missing entries:
    • src/api/routes/community.js — Public community API (leaderboard, showcases, stats, profile; no auth, rate-limited)
    • web/src/app/community/[guildId]/page.tsx — Community hub SSR page
    • web/src/app/community/[guildId]/[userId]/page.tsx — Public profile SSR page
    • migrations/013_public_profiles.cjs — Adds public_profile boolean to user_stats

Prompt to fix all issues
Fix the following issues on branch feat/community-page:

1. FILE: src/api/routes/community.js, LINE 251
   ISSUE: Stats activeProjects count doesn't filter by public_profile, inconsistent with showcases endpoint.
   FIX: Replace the plain showcases count query with one that joins user_stats and filters by public_profile = TRUE:
   pool.query(
     `SELECT COUNT(*)::int AS count
      FROM showcases s
      INNER JOIN user_stats us ON us.guild_id = s.guild_id AND us.user_id = s.author_id
      WHERE s.guild_id = $1 AND us.public_profile = TRUE`,
     [guildId],
   ),

2. FILE: AGENTS.md
   ISSUE: Key Files table is missing new files added in this PR.
   FIX: Add these rows to the Key Files table (in the appropriate position alphabetically):
   - `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
   - `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
   - `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
   - `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index

After changes, run: pnpm test

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@web/src/app/community/`[guildId]/[userId]/page.tsx:
- Around line 170-177: The XpBar component currently computes progress using a
hardcoded thresholds array in the XpBar function (variable thresholds), which
can disagree with guild-specific level thresholds defined in
repConfig.levelThresholds; update the component to consume threshold values
computed by the API instead of local constants — have the API include
currentThreshold and nextThreshold (or a precomputed progressPercent) with the
returned xp/level, then change XpBar to accept those props (e.g.,
currentThreshold/nextThreshold or progressPercent) and compute progress from
them rather than the local thresholds array.

In `@web/src/app/community/`[guildId]/page.tsx:
- Around line 390-391: Replace the unstable array index key used in the top
contributors list with a stable unique id: in the map over stats.topContributors
(the expression using contributor and idx) change the element key from idx to
the contributor's unique identifier (contributor.userId) so React can reconcile
items reliably.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 512dbd7 and a46aa85.

📒 Files selected for processing (6)
  • migrations/013_public_profiles.cjs
  • src/api/index.js
  • src/api/routes/community.js
  • tests/api/routes/community.test.js
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
📜 Review details
⏰ 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: Greptile Review
  • GitHub Check: Agent
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (5)
**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

**/*.js: Use ESM modules only — import/export, never require()
Always use node: protocol for Node.js builtin imports (e.g., import { readFileSync } from 'node:fs')

Files:

  • src/api/routes/community.js
  • tests/api/routes/community.test.js
  • src/api/index.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,tsx}: Always use semicolons
Use single quotes — enforced by Biome
Use 2-space indentation — enforced by Biome

Files:

  • src/api/routes/community.js
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • tests/api/routes/community.test.js
  • src/api/index.js
  • web/src/app/community/[guildId]/page.tsx
src/**/*.js

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.js: NEVER use console.log, console.warn, console.error, or any console.* method in src/ files — always use Winston logger instead: import { info, warn, error } from '../logger.js'
Pass structured metadata to Winston logger: info('Message processed', { userId, channelId })
Use custom error classes from src/utils/errors.js and always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read config; use setConfigValue(path, value, guildId?) to update at runtime
Use safeSend() utility for outgoing Discord messages to sanitize mentions and enforce allowedMentions
Use splitMessage() utility for messages exceeding Discord's 2000-character limit
Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Files:

  • src/api/routes/community.js
  • src/api/index.js
web/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use next/image Image component with appropriate layout and sizing props in Next.js components

Files:

  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
web/src/**/*.{tsx,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use Zustand store (zustand) for state management in React components; implement fetch-on-demand pattern in stores

Files:

  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
🧠 Learnings (2)
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to web/**/*.{tsx,ts} : Use next/image Image component with appropriate layout and sizing props in Next.js components

Applied to files:

  • web/src/app/community/[guildId]/[userId]/page.tsx
  • web/src/app/community/[guildId]/page.tsx
📚 Learning: 2026-02-27T16:24:15.054Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-27T16:24:15.054Z
Learning: Applies to src/**/*.js : Add tests for all new code with mandatory 80% coverage threshold on statements, branches, functions, and lines; run pnpm test before every commit

Applied to files:

  • tests/api/routes/community.test.js
🧬 Code graph analysis (4)
src/api/routes/community.js (3)
src/api/middleware/rateLimit.js (1)
  • rateLimit (15-56)
src/modules/config.js (2)
  • getConfig (282-313)
  • err (94-94)
src/modules/reputation.js (1)
  • computeLevel (48-58)
web/src/app/community/[guildId]/[userId]/page.tsx (5)
web/src/components/ui/card.tsx (5)
  • Card (55-55)
  • CardContent (55-55)
  • CardHeader (55-55)
  • CardTitle (55-55)
  • CardDescription (55-55)
src/commands/rank.js (1)
  • currentThreshold (57-57)
src/api/routes/community.js (1)
  • level (361-361)
web/src/components/ui/badge.tsx (1)
  • Badge (46-46)
src/commands/profile.js (1)
  • badge (83-83)
tests/api/routes/community.test.js (1)
src/api/server.js (1)
  • createApp (27-87)
src/api/index.js (1)
src/api/routes/community.js (1)
  • router (16-16)
🪛 GitHub Actions: CI
web/src/app/community/[guildId]/page.tsx

[error] 391-391: lint/suspicious/noArrayIndexKey: Avoid using the index of an array as key property in an element.


[warning] 396-399: Formatter would have printed content changes (formatting differences detected).

🔇 Additional comments (11)
src/api/index.js (1)

9-9: LGTM!

The community router is correctly mounted as a public route before authenticated routes. The placement after /health and before /auth is appropriate for an unauthenticated, rate-limited endpoint group.

Also applies to: 22-24

migrations/013_public_profiles.cjs (1)

1-27: LGTM!

The migration is well-designed:

  • Privacy-by-default with DEFAULT FALSE
  • Idempotent with IF NOT EXISTS / IF EXISTS guards
  • Partial index efficiently supports queries filtering public_profile = TRUE
  • Down migration correctly drops index before column
web/src/app/community/[guildId]/[userId]/page.tsx (1)

1-84: LGTM!

Data fetching logic is well-structured with proper error handling, revalidation, and the Image component from next/image is correctly used for avatar rendering (addressing the previous review comment).

web/src/app/community/[guildId]/page.tsx (2)

205-213: Hardcoded XP thresholds may mismatch guild-specific configuration.

Same issue as in [userId]/page.tsx: the XpBar component uses fixed thresholds while the API uses guild-configurable values.


236-271: LGTM!

MemberAvatar correctly uses next/image with appropriate width and height props derived from the size mapping, addressing the previous review comment.

tests/api/routes/community.test.js (2)

397-434: LGTM!

The showcases privacy regression test properly validates that:

  1. SQL queries include public_profile = TRUE filtering
  2. Only public users' projects appear in responses
  3. Private users' projects are excluded

This addresses the previous review comment about missing privacy coverage for showcases.


582-603: LGTM!

Rate limiting test correctly validates the 30 req/min limit by sending 31 requests and asserting at least one receives a 429 response. Placement at the end of the suite prevents rate limit state from affecting other tests.

src/api/routes/community.js (4)

61-137: LGTM!

The leaderboard endpoint correctly:

  • Filters by public_profile = TRUE in both count and data queries
  • Uses batch member fetching (guild.members.fetch({ user: userIds }))
  • Includes userId in the response for stable profile routing
  • Applies sensible pagination limits (1-100)

145-222: LGTM!

The showcases endpoint properly enforces privacy by joining user_stats and filtering us.public_profile = TRUE in both count and data queries. Batch member fetching is correctly implemented.


229-312: LGTM!

The stats endpoint appropriately:

  • Filters memberCount and topContributors by public_profile = TRUE
  • Uses aggregate counts for messagesThisWeek, activeProjects, and challengesCompleted (no per-user privacy concern)
  • Implements batch fetching for top contributors

320-438: LGTM!

The profile endpoint correctly validates public_profile opt-in before returning any user data, returns 404 for non-opted-in users, and assembles a comprehensive response with badges, projects, and stats.

The stats endpoint counted ALL showcases for the guild, but the showcases
endpoint only returns entries from users with public_profile=TRUE. This
caused the stats banner to show a higher project count than the gallery
actually displayed.

Added INNER JOIN on user_stats with public_profile=TRUE filter so the
count stays consistent with what the showcases endpoint exposes.

Resolves review thread PRRT_kwDORICdSM5xTk2J
Expose currentLevelXp/nextLevelXp in /community/:guildId/profile/:userId and
update profile XpBar to consume these values instead of hardcoded defaults.
This keeps profile progress bars consistent with guild-specific reputation
thresholds.
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 28, 2026
coderabbitai[bot]
coderabbitai bot previously approved these changes Feb 28, 2026
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 6 out of 6 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 — 1 warning, 1 documentation issue

Most issues from prior review rounds have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces). Two issues remain:

🟡 Warning: messagesThisWeek stat is semantically incorrect (community.js:252-256)

messages_sent is a running lifetime total (confirmed in engagement.js:35: messages_sent = user_stats.messages_sent + 1). The query sums ALL-TIME messages_sent for users with last_active >= NOW() - '7 days'. The UI labels this "Messages This Week" which is inaccurate — it's actually "total lifetime messages from recently active users." See inline comment for fix options.

🟡 Documentation: AGENTS.md Key Files table not updated

Per project conventions (AGENTS.md lines 183-184), new files must be added to the Key Files table. Missing entries:

  • src/api/routes/community.js
  • web/src/app/community/[guildId]/page.tsx
  • web/src/app/community/[guildId]/[userId]/page.tsx
  • migrations/013_public_profiles.cjs

Prompt to fix all issues
Fix the following issues on branch feat/community-page:

1. FILE: src/api/routes/community.js, LINES 252-256
   ISSUE: messagesThisWeek stat sums ALL-TIME messages_sent for recently active users, not messages sent this week. The label "Messages This Week" is misleading.
   FIX: Either rename the API field and frontend label to accurately describe what it measures (e.g., "Messages by Active Members"), OR if there's a messages/events table with timestamps, query actual weekly message counts instead.

2. FILE: AGENTS.md
   ISSUE: Key Files table is missing new files added in this PR.
   FIX: Add these rows to the Key Files table:
   - `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
   - `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
   - `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
   - `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index

After changes, run: npx biome check --write && pnpm test

The messages_sent column is a lifetime running total, not a weekly
count. Summing it for recently-active users is misleading when labeled
'Messages This Week'.

Rename the API response field to totalMessagesSent and update the
frontend CommunityStats interface and display label accordingly.
@BillChirico BillChirico merged commit 6f6a80a into main Feb 28, 2026
9 of 10 checks passed
@BillChirico BillChirico deleted the feat/community-page branch February 28, 2026 05:23
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 — 1 warning, 1 documentation issue, 1 nitpick

Most issues from prior review rounds have been addressed (userId in leaderboard, showcases privacy filter, batch fetching, next/image, XP thresholds, stable keys, TS interfaces, activeProjects filter, test mock). Three issues remain:

🟡 Warning: totalMessagesSent query is misleading (community.js:252-256)

The query sums ALL-TIME messages_sent for users with last_active >= NOW() - '7 days'. The field name and UI label both say "Total Messages Sent" implying a community-wide total, but users inactive for 8+ days are excluded. Remove the 7-day filter to match the semantics. See inline comment.

🟡 Documentation: Duplicated getApiBase() / API_BASE (page.tsx:54-66, [userId]/page.tsx:54-67)

Both community pages re-implement API URL normalization that already exists as getBotApiBaseUrl() in web/src/lib/bot-api.ts. The shared helper also returns null when BOT_API_URL is unset (safer for production) rather than silently falling back to http://localhost:3001.

🟡 Documentation: AGENTS.md Key Files table not updated

Per project conventions (AGENTS.md lines 183-184), new files must be added. Missing entries: community.js, both page.tsx files, and the migration.

🔵 Nitpick: Badge name inconsistency (community.js:418)

🗣️ Active Voice with description 100+ messages — name implies voice activity but it's message-based. PR author acknowledged for follow-up.


Prompt to fix all issues
Fix the following issues on branch feat/community-page:

1. FILE: src/api/routes/community.js, LINES 252-256
   ISSUE: totalMessagesSent query sums ALL-TIME messages_sent but only for users active in last 7 days. The field name and UI label imply a community-wide total.
   FIX: Remove the 7-day filter from the messages query:
   pool.query(
     `SELECT COALESCE(SUM(messages_sent), 0)::int AS total
      FROM user_stats
      WHERE guild_id = $1`,
     [guildId],
   ),

2. FILE: web/src/app/community/[guildId]/page.tsx, LINES 54-66
   FILE: web/src/app/community/[guildId]/[userId]/page.tsx, LINES 54-67
   ISSUE: Duplicated getApiBase() / API_BASE instead of using shared getBotApiBaseUrl() from web/src/lib/bot-api.ts.
   FIX: In both files, replace the local API_BASE constant and getApiBase() function with:
   import { getBotApiBaseUrl } from '@/lib/bot-api';
   const API_BASE = getBotApiBaseUrl() ?? 'http://localhost:3001/api/v1';
   Then use API_BASE directly in fetch calls (it already ends with /api/v1).

3. FILE: AGENTS.md
   ISSUE: Key Files table is missing new files added in this PR.
   FIX: Add these rows to the Key Files table:
   - `src/api/routes/community.js` | Public community API — leaderboard, showcases, stats, profile (no auth, rate-limited)
   - `web/src/app/community/[guildId]/page.tsx` | Community hub SSR page — stats banner, leaderboard, showcase gallery
   - `web/src/app/community/[guildId]/[userId]/page.tsx` | Public profile SSR page — avatar, level, XP, stats, badges, projects
   - `migrations/013_public_profiles.cjs` | Migration — adds public_profile boolean to user_stats with partial index

After changes, run: npx biome check --write && pnpm test

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: public community page — leaderboards, showcases, and stats

2 participants