Skip to content

feat(backend): top.gg vote webhook endpoint - #729

Merged
LucasSantana-Dev merged 6 commits into
mainfrom
feature/topgg-vote-webhook
Apr 22, 2026
Merged

LucasSantana-Dev merged 6 commits into
mainfrom
feature/topgg-vote-webhook

Conversation

@LucasSantana-Dev

@LucasSantana-Dev LucasSantana-Dev commented Apr 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds `POST /webhooks/topgg-votes` to persist vote events for the upcoming top.gg listing (Phase 0 of the next-phases ultraplan).

Locked behind `TOPGG_AUTH_TOKEN` — returns 503 when unset, so the endpoint is inert until the listing actually goes live.

Behaviour

  • Verifies the `authorization` header against `TOPGG_AUTH_TOKEN` (top.gg's webhook auth scheme).
  • For `type: "test"` payloads → returns `{ ok: true, test: true }` (lets top.gg's "Send test webhook" button pass).
  • For `type: "upvote"`:
    • `votes:` — 12h TTL (matches top.gg's 12h vote window)
    • `votes:streak:` — incremented, 36h TTL (12h grace around the 24h cycle so a daily voter doesn't lose their streak to timezone drift)
    • Uses a single Redis pipeline so either all three ops succeed or we return 500.

Wiring

  • Registered in `routes/index.ts` alongside the other non-API endpoints (health, stats, internal notify).
  • No `/api/` prefix — matches top.gg's expected webhook URL shape and bypasses the per-origin `apiLimiter`. Still rate-limited via `writeLimiter`.

Env

  • `TOPGG_AUTH_TOKEN` added to `.env.example` (commented out).

Test plan

  • `npm run type:check --workspace=packages/backend` → clean
  • 6 unit tests cover: missing token (503), missing/wrong auth (401), test-type payload, missing user id (400), successful upvote (verifies all 3 pipeline ops + correct TTLs).
  • After merge + deploy: run top.gg's "Send test webhook" button; expect 200 with `{ ok: true, test: true }`
  • Real upvote writes two Redis keys with correct TTLs (manually verifiable via `redis-cli`)

Follow-up

Next PR will add `/voterewards` slash command in `packages/bot` that reads `votes:streak:` and grants tier benefits. See `docs/TOP_GG_SUBMISSION.md` §6 for the broader wiring plan.

Summary by CodeRabbit

  • New Features

    • Added top.gg vote webhook and public/internal endpoints to record and report votes
    • Added /voterewards Discord command showing vote status, streak, current/next tier, and vote link
    • Exposed vote tiers and vote URL used by UI and commands
  • Tests

    • Added unit tests covering webhook endpoints and the voterewards command
  • Chores

    • Added optional TOPGG_AUTH_TOKEN entry and docs to environment example

@LucasSantana-Dev
LucasSantana-Dev enabled auto-merge (squash) April 20, 2026 01:50
@vercel

vercel Bot commented Apr 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lucky Ready Ready Preview, Comment Apr 22, 2026 0:32am

Request Review

@coderabbitai

coderabbitai Bot commented Apr 20, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@LucasSantana-Dev has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 6 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 34 minutes and 6 seconds.

⌛ 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 645cc651-d380-4fc2-854b-2cd37feee9b6

📥 Commits

Reviewing files that changed from the base of the PR and between af85002 and 959d450.

📒 Files selected for processing (9)
  • .env.example
  • packages/backend/src/routes/index.ts
  • packages/backend/src/routes/webhooks.ts
  • packages/backend/tests/unit/routes/index.test.ts
  • packages/backend/tests/unit/routes/webhooks.test.ts
  • packages/bot/src/functions/general/commands/voterewards.spec.ts
  • packages/bot/src/functions/general/commands/voterewards.ts
  • packages/shared/src/constants/index.ts
  • packages/shared/src/constants/topgg.ts
📝 Walkthrough

Walkthrough

Adds Top.gg vote tracking: new shared constants, backend webhook and query routes with Redis-backed vote/streak persistence and auth checks, bot command to display vote rewards, and accompanying unit tests and .env example entry.

Changes

Cohort / File(s) Summary
Configuration
/.env.example
Added optional TOPGG_AUTH_TOKEN entry and documentation for Top.gg vote webhook usage.
Backend route registration
packages/backend/src/routes/index.ts
Now imports and registers webhook route groups: setupWebhookPublicRoutes(app) is registered before the /api/ rate limiter and setupWebhookApiRoutes(app) after the limiter.
Webhook implementation
packages/backend/src/routes/webhooks.ts
New module adding setupWebhookPublicRoutes, setupWebhookApiRoutes, and setupWebhookRoutes. Implements POST /webhooks/topgg-votes (Top.gg webhook with auth, idempotent SET NX, streak increment/expiry), GET /api/internal/votes/:userId (internal key auth), and GET /api/me/vote-status (authenticated user vote status with tier/next-tier computation). Includes lazy Redis client factory and error logging.
Backend tests
packages/backend/tests/unit/routes/webhooks.test.ts
New Jest/Supertest tests that mock ioredis pipeline and requireAuth; exercise webhook POST behavior (test/upvote/duplicate), internal vote lookup, and authenticated vote-status responses.
Routes index tests
packages/backend/tests/unit/routes/index.test.ts
Updated tests to expect the two webhook setup functions to be called and assert their registration order relative to the API limiter.
Shared constants
packages/shared/src/constants/topgg.ts, packages/shared/src/constants/index.ts
New Top.gg constants and tier definitions: TOP_GG_BOT_ID, TOP_GG_VOTE_URL, TOP_GG_VOTE_TIERS, tierForVoteStreak, and re-exports added to the constants index.
Discord bot command
packages/bot/src/functions/general/commands/voterewards.ts
New /voterewards slash command: fetches vote status from backend (using WEBAPP_BACKEND_URL + x-notify-key), computes tier/next-tier and formats an embed with voting CTA and countdown.
Bot command tests
packages/bot/src/functions/general/commands/voterewards.spec.ts
New unit tests mocking fetch, logging, and reply utilities to validate metadata, offline behavior, network requests, tier rendering, and error fallback.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant TopGG as Top.gg
    participant Backend as Backend Server
    participant Redis as Redis

    User->>TopGG: Cast vote
    TopGG->>Backend: POST /webhooks/topgg-votes (auth header)
    Backend->>Backend: verify TOPGG_AUTH_TOKEN
    Backend->>Redis: SET votes:{userId} NX EX 12h
    alt first vote
        Backend->>Redis: INCR votes:streak:{userId}
        Backend->>Redis: EXPIRE votes:streak:{userId} 36h
    else duplicate
        Backend-->Backend: return duplicate response
    end
    Backend->>TopGG: { ok: true }
Loading
sequenceDiagram
    participant Bot as Discord Bot
    participant Backend as Backend Server
    participant Redis as Redis

    Bot->>Backend: GET /api/me/vote-status (auth)
    Backend->>Redis: GET votes:{userId}, TTL votes:{userId}, GET votes:streak:{userId}
    Backend->>Backend: compute tier via tierForVoteStreak
    Backend->>Bot: { hasVoted, streak, nextVoteInSeconds, tier, nextTier, voteUrl }
    Bot->>User: display /voterewards embed
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

shared, enhancement

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main change: adding a top.gg vote webhook endpoint to the backend. It is concise, specific, and directly reflects the primary feature introduced.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/topgg-vote-webhook

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.

LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.
@github-actions github-actions Bot added the bot label Apr 20, 2026
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
LucasSantana-Dev added a commit that referenced this pull request Apr 20, 2026
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
@github-actions github-actions Bot added size/xl and removed size/l labels Apr 20, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (4)
packages/backend/src/routes/webhooks.ts (2)

145-154: Consider requiring type === 'upvote' explicitly before persisting.

After the type: 'test' branch, any payload with a user field is persisted regardless of type (unknown/missing types, future event types added by top.gg, etc.). Since the request is already auth-gated this is low-risk, but an explicit allowlist is more defensive and makes intent obvious:

     if (payload.type === 'test') {
         debugLog({ message: 'top.gg webhook test received' })
         res.status(200).json({ ok: true, test: true })
         return
     }
+    if (payload.type !== 'upvote') {
+        throw AppError.badRequest('unsupported webhook type')
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/routes/webhooks.ts` around lines 145 - 154, After the
'test' branch, require payload.type === 'upvote' before proceeding: check
payload.type (e.g., if (payload.type !== 'upvote')) and throw
AppError.badRequest('unsupported webhook type') or simply return; only then
validate userId and persist. Update the logic around the existing payload,
payload.type and userId checks so unknown/missing/future types are rejected
instead of allowing any request with a user field to be persisted.

10-22: VOTE_TIERS and tierFor are duplicated across backend and bot.

The identical table (thresholds 30/14/7/1, labels) and lookup function live in packages/bot/src/functions/general/commands/voterewards.ts:22-46. Any future tier tweak requires editing two files in lockstep or the backend-computed tier/nextTier will diverge from the bot's rendered tier. Consider moving the tier table + tierFor to @lucky/shared and importing it in both packages (the bot version can extend it with a perk field locally, or move perks there too).

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

In `@packages/backend/src/routes/webhooks.ts` around lines 10 - 22, The VOTE_TIERS
constant and tierFor function are duplicated; extract VOTE_TIERS and tierFor
into a single shared module (e.g. export them from `@lucky/shared`) and update
both packages to import those symbols instead of declaring them locally; keep
the bot's local file free of the duplicate and, if needed, extend the imported
VOTE_TIERS with a local `perk` field or keep perks in the bot code while using
the shared thresholds/lookup to compute tier/nextTier consistently across
backend and bot.
packages/bot/src/functions/general/commands/voterewards.ts (1)

7-8: Hard-coded bot ID duplicated with backend voteUrl.

TOP_GG_BOT_ID = 962198089161134131 is also hard-coded in packages/backend/src/routes/webhooks.ts:132. Same with the tier table (see the recommended-refactor comment on webhooks.ts:10-22). Moving both to @lucky/shared/constants (alongside COLOR that's already imported here) would prevent drift.

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

In `@packages/bot/src/functions/general/commands/voterewards.ts` around lines 7 -
8, Replace the duplicated hard-coded TOP_GG_BOT_ID and derived TOP_GG_VOTE_URL
by moving the constant(s) into the shared constants module (e.g., add
TOP_GG_BOT_ID and TOP_GG_VOTE_URL to `@lucky/shared/constants` alongside COLOR)
and update references in this file (TOP_GG_BOT_ID, TOP_GG_VOTE_URL) and the
backend's voteUrl usage to import and use those shared constants so the value is
defined in one place.
packages/backend/tests/unit/routes/webhooks.test.ts (1)

81-213: Nitpick: test grouping — GET blocks are nested inside the POST /webhooks/topgg-votes describe.

describe('GET /api/me/vote-status', ...) (lines 155–213) and the GET /api/internal/votes/:userId it blocks (lines 124–153), as well as the successful upvote test (lines 215–234), all live under the describe('POST /webhooks/topgg-votes', ...) opened at line 81. Test output will read misleadingly. Consider lifting them to siblings:

-describe('POST /webhooks/topgg-votes', () => {
+describe('POST /webhooks/topgg-votes', () => {
     // ...existing POST-only cases...
+})
+
+describe('GET /api/internal/votes/:userId', () => {
+    // moved
+})
+
+describe('GET /api/me/vote-status', () => {
+    // moved
+})

Also, once idempotency is fixed on the route (see comment on webhooks.ts:137-183), please add a test that POSTs the same user twice and asserts incr is called exactly once.

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

In `@packages/backend/tests/unit/routes/webhooks.test.ts` around lines 81 - 213,
The GET tests are incorrectly nested inside the describe('POST
/webhooks/topgg-votes') block—move the describe('GET /api/me/vote-status', ...)
block and the GET /api/internal/votes/:userId it tests out to be sibling
describes (i.e., close the POST describe before those GET tests) so output reads
correctly; then add a new test under the POST describe that POSTs to
'/webhooks/topgg-votes' twice with the same { user } payload and asserts the
idempotency behavior by checking the mocked incr function is called exactly once
(reference the test titles and the mocked incr symbol used in the file).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/backend/src/routes/index.ts`:
- Around line 85-86: The two API routes registered inside setupWebhookRoutes
(the endpoints /api/internal/votes/:userId and /api/me/vote-status) are being
added before app.use('/api/', apiLimiter) and thus bypass the rate limiter; move
the registration of those two routes so they are added after apiLimiter is
applied (either by splitting setupWebhookRoutes into setupWebhookPublicRoutes()
called before the limiter and setupWebhookApiRoutes() called after, or by
relocating the specific handlers for /api/internal/votes/:userId and
/api/me/vote-status to be registered after app.use('/api/', apiLimiter)); ensure
you only keep /webhooks/topgg-votes registered before the limiter if
intentional.

In `@packages/backend/src/routes/webhooks.ts`:
- Around line 57-74: Both verifyTopggAuth and verifyInternalKey use direct
string comparison which can leak timing info; replace those comparisons with a
constant-time comparison using crypto.timingSafeEqual: load the expected value
from process.env and the provided header, convert both to Buffers (or return
unauthorized if either is missing), check lengths first and if lengths differ
return unauthorized, then call crypto.timingSafeEqual on the buffers and
continue/throw the same AppError on mismatch; update verifyTopggAuth (uses
'authorization' header and TOPGG_AUTH_TOKEN) and verifyInternalKey (uses
'x-notify-key' and LUCKY_NOTIFY_API_KEY) accordingly.
- Around line 137-183: The handler currently always INCRs votes:streak:<userId>
and ignores per-command pipeline errors, causing duplicate streaks and lost
TTLs; modify the logic in the POST '/webhooks/topgg-votes' handler (inside
verifyTopggAuth block using getRedis(), voteKey and streakKey) to perform the
vote-key SET with NX (only set if not exists) and then conditionally INCR/EXPIRE
the streak only when the SET returned a successful OK, and after pipeline.exec()
inspect the pipeline replies for per-op errors (handle or log and rollback if
needed); ensure you return 200 for duplicate deliveries without incrementing the
streak and explicitly set/verify the streakKey TTL when incrementing,
throwing/logging and responding appropriately on pipeline-level failures.
- Around line 31-47: The getRedis function currently throws new AppError(500,
...) and returns a Redis client without an 'error' listener; update the
REDIS_HOST check to throw AppError.serviceUnavailable('REDIS_HOST is not
configured') and, after creating redisClient in getRedis, attach a no-op/logging
error handler (e.g., redisClient.on('error', (err) => /* log or ignore */)) so
ioredis errors don't become uncaught exceptions; refer to symbols getRedis,
redisClient, Redis, and AppError when making the changes.

In `@packages/bot/src/functions/general/commands/voterewards.ts`:
- Around line 62-80: The fetchVoteState function makes a fetch without a timeout
which can block Discord's 3s interaction window; update the fetch call inside
fetchVoteState to pass an AbortSignal (e.g., signal: AbortSignal.timeout(2500))
so the request is aborted if it exceeds ~2.5s, and handle the abort like other
errors (the existing catch will log and return null). Modify the fetch
invocation in fetchVoteState to include the signal option while keeping headers
the same; choose a timeout under 3000ms to allow interaction deferral to
proceed.

---

Nitpick comments:
In `@packages/backend/src/routes/webhooks.ts`:
- Around line 145-154: After the 'test' branch, require payload.type ===
'upvote' before proceeding: check payload.type (e.g., if (payload.type !==
'upvote')) and throw AppError.badRequest('unsupported webhook type') or simply
return; only then validate userId and persist. Update the logic around the
existing payload, payload.type and userId checks so unknown/missing/future types
are rejected instead of allowing any request with a user field to be persisted.
- Around line 10-22: The VOTE_TIERS constant and tierFor function are
duplicated; extract VOTE_TIERS and tierFor into a single shared module (e.g.
export them from `@lucky/shared`) and update both packages to import those symbols
instead of declaring them locally; keep the bot's local file free of the
duplicate and, if needed, extend the imported VOTE_TIERS with a local `perk`
field or keep perks in the bot code while using the shared thresholds/lookup to
compute tier/nextTier consistently across backend and bot.

In `@packages/backend/tests/unit/routes/webhooks.test.ts`:
- Around line 81-213: The GET tests are incorrectly nested inside the
describe('POST /webhooks/topgg-votes') block—move the describe('GET
/api/me/vote-status', ...) block and the GET /api/internal/votes/:userId it
tests out to be sibling describes (i.e., close the POST describe before those
GET tests) so output reads correctly; then add a new test under the POST
describe that POSTs to '/webhooks/topgg-votes' twice with the same { user }
payload and asserts the idempotency behavior by checking the mocked incr
function is called exactly once (reference the test titles and the mocked incr
symbol used in the file).

In `@packages/bot/src/functions/general/commands/voterewards.ts`:
- Around line 7-8: Replace the duplicated hard-coded TOP_GG_BOT_ID and derived
TOP_GG_VOTE_URL by moving the constant(s) into the shared constants module
(e.g., add TOP_GG_BOT_ID and TOP_GG_VOTE_URL to `@lucky/shared/constants`
alongside COLOR) and update references in this file (TOP_GG_BOT_ID,
TOP_GG_VOTE_URL) and the backend's voteUrl usage to import and use those shared
constants so the value is defined in one place.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 20fb7007-2cf2-4a41-9dc0-b772941eeb78

📥 Commits

Reviewing files that changed from the base of the PR and between 9f77e39 and 52bc07a.

📒 Files selected for processing (5)
  • .env.example
  • packages/backend/src/routes/index.ts
  • packages/backend/src/routes/webhooks.ts
  • packages/backend/tests/unit/routes/webhooks.test.ts
  • packages/bot/src/functions/general/commands/voterewards.ts
📜 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: Security
  • GitHub Check: Quality Gates
  • GitHub Check: SonarCloud Scan
🔇 Additional comments (1)
.env.example (1)

202-205: LGTM.

Documentation matches the backend behavior (commented-out optional var; endpoint returns 503 when unset).

Comment thread packages/backend/src/routes/index.ts Outdated
Comment thread packages/backend/src/routes/webhooks.ts
Comment thread packages/backend/src/routes/webhooks.ts
Comment thread packages/backend/src/routes/webhooks.ts
Comment thread packages/bot/src/functions/general/commands/voterewards.ts
LucasSantana-Dev added a commit that referenced this pull request Apr 21, 2026
…+ redis error listener

Addresses CodeRabbit feedback on #729:

- **Idempotency**: top.gg retries webhook deliveries on handler failure/timeout.
  The previous pipeline INCR'd votes:streak:<userId> on every successful POST,
  double-counting the streak on retries. Switched to SET NX as the idempotency
  gate — only the first write within the 12h vote window increments the streak;
  subsequent retries return { ok: true, duplicate: true } without touching the
  counter.
- **Timing-safe compare**: replaced raw !== on TOPGG_AUTH_TOKEN and
  LUCKY_NOTIFY_API_KEY with crypto.timingSafeEqual via a length-checked
  safeEqualString helper. Prevents length/prefix timing leaks over public
  internet.
- **Redis error listener**: attach .on('error', ...) to the cached ioredis
  client. Without this, connection failures during retry raise
  uncaughtException; we log and swallow since maxRetriesPerRequest=1 already
  surfaces errors per-request.
- **Explicit vote type check**: reject payloads where type is neither 'test'
  nor 'upvote'. Prevents future top.gg event types (e.g. downvotes) from
  silently incrementing streaks.

Tests: added 2 new cases (duplicate vote, unsupported type) + updated the
existing "records upvote" assertion to check SET NX. 15/15 passing.
@LucasSantana-Dev

Copy link
Copy Markdown
Owner Author

Addressed all three substantive CodeRabbit findings in 6604adc6:

  • ✅ Idempotent upvote handler — SET NX gates the streak increment; retries return { ok: true, duplicate: true } without touching the counter
  • ✅ Timing-safe token compare — both TOPGG_AUTH_TOKEN and LUCKY_NOTIFY_API_KEY now use crypto.timingSafeEqual via a length-checked helper
  • ✅ Redis error listener — attached to the cached client so connection failures don't raise uncaughtException
  • ✅ Explicit type === 'upvote' guard — unknown/future event types now 400 instead of silently persisting

Tests: added 2 new cases (duplicate vote idempotency, unsupported type rejection) + updated the existing "records upvote" assertion to check SET NX. 15/15 passing.

The interactionReply defer and rate-limit middleware queries were investigative, not actionable — left as-is.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/backend/src/routes/webhooks.ts (1)

187-208: ⚠️ Potential issue | 🟠 Major

Make the idempotency gate and streak update one atomic Redis operation.

SET NX succeeds before the streak pipeline runs. If incr/expire fails or returns a command error, the retry is treated as a duplicate and the streak is permanently skipped. A plain pipeline also does not provide rollback/atomicity. This is the unresolved Redis failure mode from the earlier idempotency review.

🛠️ Proposed fix using one Redis script

Add the script near the TTL constants:

 const STREAK_TTL_SECONDS = 60 * 60 * 36
+
+const RECORD_VOTE_SCRIPT = `
+if redis.call('EXISTS', KEYS[1]) == 1 then
+  return 0
+end
+redis.call('INCR', KEYS[2])
+redis.call('EXPIRE', KEYS[2], tonumber(ARGV[3]))
+redis.call('SET', KEYS[1], ARGV[1], 'EX', tonumber(ARGV[2]))
+return 1
+`

Then replace the split write:

-                const firstWrite = await redis.set(
+                const recordResult = await redis.eval(
+                    RECORD_VOTE_SCRIPT,
+                    2,
                     voteKey,
+                    streakKey,
                     Date.now().toString(),
-                    'EX',
-                    VOTE_TTL_SECONDS,
-                    'NX',
+                    String(VOTE_TTL_SECONDS),
+                    String(STREAK_TTL_SECONDS),
                 )
 
-                if (firstWrite !== 'OK') {
+                if (Number(recordResult) !== 1) {
                     debugLog({
                         message:
                             'top.gg vote already recorded — skipping streak increment',
@@
                     return
                 }
-
-                const pipeline = redis.pipeline()
-                pipeline.incr(streakKey)
-                pipeline.expire(streakKey, STREAK_TTL_SECONDS)
-                await pipeline.exec()
 
                 debugLog({
Does Redis pipeline provide atomic execution or rollback? For ioredis pipeline().exec(), are per-command errors returned in [err, result] tuples? Is Redis EVAL executed atomically with respect to other clients?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/backend/src/routes/webhooks.ts` around lines 187 - 208, The current
flow uses SET NX (voteKey) and then a separate pipeline.incr/expire (streakKey),
which is not atomic and can leave the vote recorded but streak not incremented;
replace this with a single Redis EVAL'd Lua script (or SCRIPT LOAD + EVALSHA)
that atomically: checks/sets voteKey with EX and NX (using KEYS[1],
VOTE_TTL_SECONDS), and if that set succeeded performs INCR on streakKey and
EXPIRE on streakKey (using KEYS[2], STREAK_TTL_SECONDS), then returns a clear
result indicating duplicate vs success; update the call sites that currently use
redis.set(... 'NX') and pipeline.exec() (referencing voteKey, streakKey,
VOTE_TTL_SECONDS, STREAK_TTL_SECONDS, redis.pipeline(), pipeline.exec()) to call
the script and handle the script’s return to send duplicate vs ok responses and
surface Redis errors instead of treating failures as duplicates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/backend/src/routes/webhooks.ts`:
- Around line 173-180: The code accepts any string for payload.user and then
builds Redis keys voteKey and streakKey which can collide (e.g., "streak:123");
validate payload.user is a valid Discord snowflake (numeric string of expected
length/format) before assigning userId, and throw AppError.badRequest if it
fails; update the validation logic near payload.user / userId (the
AppError.badRequest check) to use a helper or regex that enforces numeric
snowflake form, then only call getRedis() and build voteKey and streakKey when
the validated userId is safe.
- Around line 89-103: The pipeline exec currently discards per-command errors
via the pattern [[, voteTs], [, streakRaw], [, ttl]] which hides Redis command
errors; change the destructuring to capture each exec tuple (e.g., const
[voteRes, streakRes, ttlRes] = await redis.pipeline(...).exec()), check
voteRes[0], streakRes[0], ttlRes[0] for non-null Error and throw or return that
error with context (include voteKey/streakKey and command name), then read
results from voteRes[1], streakRes[1], ttlRes[1] and parse them (convert
streakRaw to Number and normalize ttl to non-negative) before returning
hasVoted, streak and nextVoteInSeconds; ensure you reference the existing
voteKey, streakKey, pipeline and exec usage when making the changes so
per-command errors are surfaced instead of silently returning defaults.

In `@packages/bot/src/functions/general/commands/voterewards.ts`:
- Around line 74-80: formatNextVoteIn currently shows "in 0m" for 1–59 seconds;
update the function to round up remaining seconds to whole minutes so sub-minute
values display "in 1m" and to handle minute-rollover into hours. Specifically,
in formatNextVoteIn(seconds) keep the early return for seconds <= 0, compute
hours = Math.floor(seconds / 3600), compute remaining = seconds - hours * 3600,
set minutes = Math.ceil(remaining / 60), if minutes === 60 then hours += 1 and
minutes = 0, then return `in ${hours}h ${minutes}m` when hours > 0 (omitting
minutes if 0 if you prefer) otherwise return `in ${minutes}m`; this ensures
1–59s become "in 1m" and hour boundaries are handled.

---

Duplicate comments:
In `@packages/backend/src/routes/webhooks.ts`:
- Around line 187-208: The current flow uses SET NX (voteKey) and then a
separate pipeline.incr/expire (streakKey), which is not atomic and can leave the
vote recorded but streak not incremented; replace this with a single Redis
EVAL'd Lua script (or SCRIPT LOAD + EVALSHA) that atomically: checks/sets
voteKey with EX and NX (using KEYS[1], VOTE_TTL_SECONDS), and if that set
succeeded performs INCR on streakKey and EXPIRE on streakKey (using KEYS[2],
STREAK_TTL_SECONDS), then returns a clear result indicating duplicate vs
success; update the call sites that currently use redis.set(... 'NX') and
pipeline.exec() (referencing voteKey, streakKey, VOTE_TTL_SECONDS,
STREAK_TTL_SECONDS, redis.pipeline(), pipeline.exec()) to call the script and
handle the script’s return to send duplicate vs ok responses and surface Redis
errors instead of treating failures as duplicates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acab5539-2d44-478e-a4ef-35c01a1373a7

📥 Commits

Reviewing files that changed from the base of the PR and between 52bc07a and af85002.

📒 Files selected for processing (8)
  • packages/backend/src/routes/index.ts
  • packages/backend/src/routes/webhooks.ts
  • packages/backend/tests/unit/routes/index.test.ts
  • packages/backend/tests/unit/routes/webhooks.test.ts
  • packages/bot/src/functions/general/commands/voterewards.spec.ts
  • packages/bot/src/functions/general/commands/voterewards.ts
  • packages/shared/src/constants/index.ts
  • packages/shared/src/constants/topgg.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/shared/src/constants/index.ts
  • packages/bot/src/functions/general/commands/voterewards.spec.ts
  • packages/shared/src/constants/topgg.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/backend/src/routes/index.ts
📜 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: Quality Gates
  • GitHub Check: SonarCloud Scan
🔇 Additional comments (3)
packages/backend/tests/unit/routes/index.test.ts (1)

25-26: Good coverage for the split webhook route registration.

The new mocks and call-order assertions protect the intended public-before-apiLimiter / API-after-apiLimiter wiring.

Also applies to: 117-120, 161-214

packages/bot/src/functions/general/commands/voterewards.ts (1)

51-72: Fetch path looks resilient.

The command handles missing config, non-OK responses, JSON fetch failures, and uses a sub-3s abort timeout before replying.

packages/backend/tests/unit/routes/webhooks.test.ts (1)

87-263: Solid route-level coverage for the new webhook contract.

The suite isolates Redis and covers auth, test payloads, unsupported types, internal reads, authenticated status, and duplicate vote behavior.

Comment thread packages/backend/src/routes/webhooks.ts Outdated
Comment thread packages/backend/src/routes/webhooks.ts Outdated
Comment thread packages/bot/src/functions/general/commands/voterewards.ts
LucasSantana-Dev and others added 6 commits April 21, 2026 21:31
Adds POST /webhooks/topgg-votes to persist vote events for the upcoming
top.gg listing. Locked behind TOPGG_AUTH_TOKEN — returns 503 when unset
so the endpoint is inert until the listing goes live.

On each upvote:
- votes:<userId>            — 12h TTL (matches top.gg's 12h vote window)
- votes:streak:<userId>     — incremented, 36h TTL (12h grace around the
                              24h cycle so daily voters don't lose streaks)

Returns { ok: true, test: true } for top.gg's test pings.

Wiring: registered in routes/index.ts alongside the other non-API
endpoints (health, stats, internal notify). No /api/ prefix — matches
top.gg's expected webhook URL shape.

Env: TOPGG_AUTH_TOKEN added to .env.example (commented out).

Tests: 6 unit tests covering missing/wrong auth, test-type payload,
missing user id, and a successful upvote (verifies pipeline ops).

Follow-up (next PR): /voterewards slash command in packages/bot that
reads votes:streak:<user> and grants tier benefits. See
docs/TOP_GG_SUBMISSION.md §6 for the broader wiring plan.
Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.
Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null
…+ redis error listener

Addresses CodeRabbit feedback on #729:

- **Idempotency**: top.gg retries webhook deliveries on handler failure/timeout.
  The previous pipeline INCR'd votes:streak:<userId> on every successful POST,
  double-counting the streak on retries. Switched to SET NX as the idempotency
  gate — only the first write within the 12h vote window increments the streak;
  subsequent retries return { ok: true, duplicate: true } without touching the
  counter.
- **Timing-safe compare**: replaced raw !== on TOPGG_AUTH_TOKEN and
  LUCKY_NOTIFY_API_KEY with crypto.timingSafeEqual via a length-checked
  safeEqualString helper. Prevents length/prefix timing leaks over public
  internet.
- **Redis error listener**: attach .on('error', ...) to the cached ioredis
  client. Without this, connection failures during retry raise
  uncaughtException; we log and swallow since maxRetriesPerRequest=1 already
  surfaces errors per-request.
- **Explicit vote type check**: reject payloads where type is neither 'test'
  nor 'upvote'. Prevents future top.gg event types (e.g. downvotes) from
  silently incrementing streaks.

Tests: added 2 new cases (duplicate vote, unsupported type) + updated the
existing "records upvote" assertion to check SET NX. 15/15 passing.
@sonarqubecloud

Copy link
Copy Markdown

@LucasSantana-Dev
LucasSantana-Dev merged commit ef06904 into main Apr 22, 2026
12 checks passed
@LucasSantana-Dev
LucasSantana-Dev deleted the feature/topgg-vote-webhook branch April 22, 2026 00:38
LucasSantana-Dev added a commit that referenced this pull request Apr 22, 2026
…ce (#775)

* docs(backlog): mark completed 2026-04-21 backlog items with PR evidence

10 of the 16 tasks in docs/specs/2026-04-21-backlog-map/tasks.md
already shipped — the unchecked state was sending future agents
down dead ends. Splits the list into Open / Completed, cites the
landing PR for each done item, and promotes the truly-remaining
work to the top.

Completed:
- A-R2 token aliases (#763)
- A-R3 SectionHeader eyebrowIcon + statusBand (#765)
- PR #729 webhook coverage (ef06904)
- tar Dependabot alerts (#766, replacing #755 which was closed)
- PR queue rebase sweep — 14 PRs landed 2026-04-22
- Shared Top.gg constants (already in packages/shared/src/constants/topgg.ts)
- validateBody + unskipped tests (#769)
- Frontend token/font decision (#763)
- Sonar coverage on #737 and #740 (both merged)

Still open: follow-redirects/hono lock refresh, release/tag drift
(production-visible), autoplay diversity dedup, queueManipulation
refactor, /integrations page collapse (B-R4), BRANDING_GUIDE update
(D-R2), per-page ports (13 pages).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(backlog): normalize version prefix (v2.6.148) per CodeRabbit

Use `v` prefix consistently alongside `v2.6.132`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
* feat(backend): top.gg vote webhook endpoint

Adds POST /webhooks/topgg-votes to persist vote events for the upcoming
top.gg listing. Locked behind TOPGG_AUTH_TOKEN — returns 503 when unset
so the endpoint is inert until the listing goes live.

On each upvote:
- votes:<userId>            — 12h TTL (matches top.gg's 12h vote window)
- votes:streak:<userId>     — incremented, 36h TTL (12h grace around the
                              24h cycle so daily voters don't lose streaks)

Returns { ok: true, test: true } for top.gg's test pings.

Wiring: registered in routes/index.ts alongside the other non-API
endpoints (health, stats, internal notify). No /api/ prefix — matches
top.gg's expected webhook URL shape.

Env: TOPGG_AUTH_TOKEN added to .env.example (commented out).

Tests: 6 unit tests covering missing/wrong auth, test-type payload,
missing user id, and a successful upvote (verifies pipeline ops).

Follow-up (next PR): /voterewards slash command in packages/bot that
reads votes:streak:<user> and grants tier benefits. See
docs/TOP_GG_SUBMISSION.md §6 for the broader wiring plan.

* feat(bot): /voterewards command + GET /api/internal/votes/:userId (#730)

Closes the top.gg vote loop:
- backend: adds GET /api/internal/votes/:userId behind LUCKY_NOTIFY_API_KEY
  returning { hasVoted, streak, nextVoteInSeconds }. Keeps all vote-related
  routes in the same file (webhooks.ts) but separates the internal read
  (internal-key auth) from the external webhook (top.gg auth).
- bot: /voterewards slash command fetches the user's state and replies
  with tier + streak + next-vote countdown. Graceful fallback when the
  backend is unreachable or env vars are missing — surfaces the top.gg
  vote link unconditionally so the command is always useful.

Tier table (matches docs/TOP_GG_SUBMISSION.md §6):
  1+ streak → Lucky Supporter
  7+ streak → Lucky Fan (early access to new commands)
  14+ streak → Lucky Regular (autoplay weighting + early access)
  30+ streak → Lucky Legend (dashboard badge + weighting + priority support)

Stacked on feature/topgg-vote-webhook (#729) — merge #729 first, then this
rebases cleanly.

Tests: 9 total in webhooks.test.ts (+ 3 new covering the GET endpoint).
Type-check: backend + bot both clean.

* feat(backend): GET /api/me/vote-status for the dashboard (#736)

Extends the top.gg flywheel to the authenticated web dashboard.

Authenticated user reads their own vote state + tier:

  GET /api/me/vote-status  (requireAuth)
  →  {
       hasVoted, streak, nextVoteInSeconds,
       tier: { label, threshold } | null,
       nextTier: { label, threshold } | null,
       voteUrl: "https://top.gg/bot/.../vote"
     }

Tier thresholds match the /voterewards slash command exactly:
  1+  → Lucky Supporter
  7+  → Lucky Fan
  14+ → Lucky Regular
  30+ → Lucky Legend

Reuses the existing readVoteState() helper — same Redis keys the webhook
writes to, so the three endpoints (POST webhook, GET internal, GET me)
stay in perfect sync. No schema change, no new env vars.

Stacked on feature/topgg-vote-webhook (builds on #729 + #730).

Tests: 3 new cases added to webhooks.test.ts (13 total now):
- 401 when not authenticated
- Streak-14 voter → tier=Regular, nextTier=Legend
- 0-streak user → tier=null, nextTier=Supporter
- 30+ streak → tier=Legend, nextTier=null

* fix(webhooks): idempotent top.gg upvotes + timing-safe token compare + redis error listener

Addresses CodeRabbit feedback on #729:

- **Idempotency**: top.gg retries webhook deliveries on handler failure/timeout.
  The previous pipeline INCR'd votes:streak:<userId> on every successful POST,
  double-counting the streak on retries. Switched to SET NX as the idempotency
  gate — only the first write within the 12h vote window increments the streak;
  subsequent retries return { ok: true, duplicate: true } without touching the
  counter.
- **Timing-safe compare**: replaced raw !== on TOPGG_AUTH_TOKEN and
  LUCKY_NOTIFY_API_KEY with crypto.timingSafeEqual via a length-checked
  safeEqualString helper. Prevents length/prefix timing leaks over public
  internet.
- **Redis error listener**: attach .on('error', ...) to the cached ioredis
  client. Without this, connection failures during retry raise
  uncaughtException; we log and swallow since maxRetriesPerRequest=1 already
  surfaces errors per-request.
- **Explicit vote type check**: reject payloads where type is neither 'test'
  nor 'upvote'. Prevents future top.gg event types (e.g. downvotes) from
  silently incrementing streaks.

Tests: added 2 new cases (duplicate vote, unsupported type) + updated the
existing "records upvote" assertion to check SET NX. 15/15 passing.

* fix: gate vote state routes behind api limiter

* fix: make topgg vote writes atomic
LucasSantana-Dev added a commit that referenced this pull request May 13, 2026
…ce (#775)

* docs(backlog): mark completed 2026-04-21 backlog items with PR evidence

10 of the 16 tasks in docs/specs/2026-04-21-backlog-map/tasks.md
already shipped — the unchecked state was sending future agents
down dead ends. Splits the list into Open / Completed, cites the
landing PR for each done item, and promotes the truly-remaining
work to the top.

Completed:
- A-R2 token aliases (#763)
- A-R3 SectionHeader eyebrowIcon + statusBand (#765)
- PR #729 webhook coverage (a8edbe8)
- tar Dependabot alerts (#766, replacing #755 which was closed)
- PR queue rebase sweep — 14 PRs landed 2026-04-22
- Shared Top.gg constants (already in packages/shared/src/constants/topgg.ts)
- validateBody + unskipped tests (#769)
- Frontend token/font decision (#763)
- Sonar coverage on #737 and #740 (both merged)

Still open: follow-redirects/hono lock refresh, release/tag drift
(production-visible), autoplay diversity dedup, queueManipulation
refactor, /integrations page collapse (B-R4), BRANDING_GUIDE update
(D-R2), per-page ports (13 pages).

* docs(backlog): normalize version prefix (v2.6.148) per CodeRabbit

Use `v` prefix consistently alongside `v2.6.132`.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 959d450f Deployed Apr 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant