Skip to content

fix(dashboard): path boundary guards, title sync safety, AGENTS docs#263

Merged
BillChirico merged 1 commit intomainfrom
fix/dashboard-title-followup
Mar 8, 2026
Merged

fix(dashboard): path boundary guards, title sync safety, AGENTS docs#263
BillChirico merged 1 commit intomainfrom
fix/dashboard-title-followup

Conversation

@BillChirico
Copy link
Collaborator

Summary

Follow-up to #262 based on Claude's code review.

Changes

1. Leaf-route path boundary fix (page-titles.ts)
All leaf matchers now use pathname === '/dashboard/x' || pathname.startsWith('/dashboard/x/') instead of bare startsWith. Prevents false-positive matches on future sibling routes (e.g. /dashboard/ai would have matched /dashboard/airline).

2. DashboardTitleSync title overwrite guard (dashboard-title-sync.tsx)
Added a guard to avoid overwriting more-specific page-level metadata titles set by Next.js. If the current title already ends with APP_TITLE and differs from what the component would set, it leaves the page-level title intact.

3. AGENTS.md docs (AGENTS.md)
Documented the dashboardTitleMatchers pattern — new routes must add an entry, with instructions on the correct matcher format.

4. Tests (page-titles.test.ts)
Added path-boundary and subtree match test cases.

- page-titles.ts: leaf matchers now use exact equality OR subtree startsWith
  to prevent false-positive matches (e.g. /dashboard/ai != /dashboard/airline)
- dashboard-title-sync.tsx: guard against overwriting more-specific page-level
  metadata titles set by Next.js — only update if title is missing or generic
- page-titles.test.ts: add path-boundary and subtree match test cases
- AGENTS.md: document dashboardTitleMatchers pattern for new routes
Copilot AI review requested due to automatic review settings March 8, 2026 02:19
@BillChirico BillChirico added this to the v0.1.0 - "Big Boy MVP" milestone Mar 8, 2026
@BillChirico BillChirico added priority: low Low priority scope: dashboard Web dashboard type: bug Bug fix work labels Mar 8, 2026
@BillChirico BillChirico added priority: low Low priority scope: dashboard Web dashboard type: bug Bug fix work labels Mar 8, 2026
@github-project-automation github-project-automation bot moved this to Backlog in Volvox.Bot Mar 8, 2026
@railway-app railway-app bot temporarily deployed to volvox-bot / volvox-bot-pr-263 March 8, 2026 02:19 Destroyed
@railway-app
Copy link

railway-app bot commented Mar 8, 2026

🚅 Deployed to the volvox-bot-pr-263 environment in volvox-bot

Service Status Web Updated (UTC)
bot 🚨 Crashed (View Logs) Web Mar 8, 2026 at 2:20 am
web ✅ Success (View Logs) Mar 8, 2026 at 2:20 am
docs ✅ Success (View Logs) Web Mar 8, 2026 at 2:19 am

@claude
Copy link

claude bot commented Mar 8, 2026

Claude finished @BillChirico's task in 6m 17s —— View job


Code Review — PR #263

  • Gather context
  • Read changed files
  • Analyze for issues
  • Post review findings

Issues Found

1. Guard in DashboardTitleSync blocks cross-section client navigation

web/src/components/layout/dashboard-title-sync.tsx:24

if (current.endsWith(APP_TITLE) && current !== computed && current !== APP_TITLE) {
  return;
}

This guard cannot distinguish between:

  • A title set by Next.js server-rendered metadata (should be preserved)
  • A title the component itself set on a previous client navigation (should be overwritten)

Broken scenario — navigating from Overview → Members → Performance:

Step pathname computed current (before effect) Guard fires? Result
1. Initial load /dashboard Overview - APP_TITLE '' No (endsWith = false) Sets "Overview" ✓
2. Nav to members /dashboard/members Members - APP_TITLE Overview - APP_TITLE Yes (all 3 conditions true) Stays "Overview" ✗

After step 1, every subsequent navigation will trigger all three guard conditions: current.endsWith(APP_TITLE) ✓, current !== computed ✓, current !== APP_TITLE ✓ — and the title never updates again.

The existing test 'updates the title when the route changes' (dashboard-title-sync.test.tsx:29) should be broken by this guard: it navigates from /dashboard/dashboard/tickets/42 after the first render sets the title, hitting exactly the broken scenario above. This is only undetected because web tests are not run by any CI job (the Test (Vitest Coverage) job runs root Vitest only; the Web — Lint, Typecheck & Build job does typecheck/lint/build but no pnpm --prefix web test step).

Fix — track what the component last set via a useRef to distinguish its own title from a server-set one: Fix this →

// Correct approach
const lastSetTitle = useRef<string | null>(null);

useEffect(() => {
  const computed = getDashboardDocumentTitle(pathname);
  const current = document.title;

  if (
    current !== lastSetTitle.current &&  // not set by us
    current.endsWith(APP_TITLE) &&
    current !== computed &&
    current !== APP_TITLE
  ) {
    return;
  }

  lastSetTitle.current = computed;
  document.title = computed;
}, [pathname]);

2. No tests for the guard's intended behavior

web/tests/components/layout/dashboard-title-sync.test.tsx was not modified. There is no test verifying that a server-set metadata title (e.g. 'Member Profile - APP_TITLE' set before the component mounts) is correctly preserved, nor any test that cross-section navigation still works after an initial component-driven nav. Without these, the guard has no regression coverage regardless of correctness.


Pre-existing CI Blocker (not caused by this PR)

The Web — Lint, Typecheck & Build job is failing due to a Biome formatting error in web/src/components/dashboard/performance-dashboard.tsx (introduced in a6e76d0 / f0ba92a on main). The <Tooltip> elements on lines 363–365 and 405–407 need to be inlined. This is unblocked from this PR but will need resolving before merge.


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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coveralls
Copy link

Coverage Status

coverage: 87.561% (-0.005%) from 87.566%
when pulling 0eca857 on fix/dashboard-title-followup
into febddc7 on main.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 8, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 21 minutes and 53 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.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 2977d5b2-3ecf-47c0-b1f8-10ae48e9abeb

📥 Commits

Reviewing files that changed from the base of the PR and between febddc7 and 0eca857.

📒 Files selected for processing (4)
  • AGENTS.md
  • web/src/components/layout/dashboard-title-sync.tsx
  • web/src/lib/page-titles.ts
  • web/tests/lib/page-titles.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/dashboard-title-followup
  • 🛠️ Publish Changes: Commit on current branch
  • 🛠️ Publish Changes: Create PR

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.

@greptile-apps
Copy link

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR is a follow-up to #262 that hardens the dashboard title system: all leaf-route matchers in page-titles.ts are updated to use exact equality plus a startsWith('/x/') subtree check (preventing false-positive matches on sibling routes), AGENTS.md documents the matcher convention for future contributors, and page-titles.test.ts includes comprehensive path-boundary test cases.

However, the guard added to DashboardTitleSync contains a critical logic flaw that breaks its primary function: the component will skip every title update after the first page visit during client-side navigation. Since DashboardTitleSync itself produces titles of the form "Section - APP_TITLE" on every navigation, the guard condition will always be satisfied on the next navigation, causing the title to freeze on whichever section was first visited and never update again.

Files requiring fixes:

  • web/src/components/layout/dashboard-title-sync.tsx — the guard condition must be redesigned to track previously-set titles and distinguish them from externally-set page metadata.

Confidence Score: 2/5

  • Not safe to merge — the guard in DashboardTitleSync prevents title updates on every client-side navigation after the first page visit.
  • The path-boundary fix in page-titles.ts and the accompanying tests are solid (would be a 5 on their own), but the guard added to DashboardTitleSync introduces a regression that defeats the component's core responsibility on every client-side navigation. The guard condition will always be satisfied after the first page visit, causing the browser tab title to freeze and never update again during dashboard navigation.
  • web/src/components/layout/dashboard-title-sync.tsx requires a fix to the guard condition before merging.

Sequence Diagram

sequenceDiagram
    participant User
    participant NextJS as Next.js Router
    participant Sync as DashboardTitleSync
    participant DOM as document.title

    User->>NextJS: Navigate to /dashboard/config
    NextJS->>DOM: Set "Bot Config" (from metadata export)
    Sync->>Sync: useEffect fires (pathname=/dashboard/config)
    Note over Sync: computed = "Bot Config - APP_TITLE"<br/>current = "Bot Config"<br/>endsWith(APP_TITLE)? NO → guard skips
    Sync->>DOM: Set "Bot Config - APP_TITLE" ✅

    User->>NextJS: Navigate to /dashboard/ai
    Note over NextJS: No metadata export — title unchanged
    Sync->>Sync: useEffect fires (pathname=/dashboard/ai)
    Note over Sync: computed = "AI Chat - APP_TITLE"<br/>current = "Bot Config - APP_TITLE" (stale!)<br/>endsWith(APP_TITLE)? YES<br/>current ≠ computed? YES<br/>current ≠ APP_TITLE? YES → GUARD FIRES ❌
    Sync-->>DOM: Early return — title stuck at "Bot Config - APP_TITLE"
Loading

Last reviewed commit: 0eca857

Comment on lines +24 to +26
if (current.endsWith(APP_TITLE) && current !== computed && current !== APP_TITLE) {
return;
}
Copy link

Choose a reason for hiding this comment

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

Guard fires on every client-side navigation, stalling the title

The condition current.endsWith(APP_TITLE) will match titles that DashboardTitleSync itself previously set (e.g. "Bot Config - Volvox.Bot - AI Powered Discord Bot"). Because the component runs in the persistent dashboard shell and sets the title on every navigation, the next navigation will always see a current that ends with APP_TITLE (set by the previous run) but differs from the new computed. All three guard conditions will be true, the early return fires, and the title is never updated after the first page visit.

Concrete trace:

  1. /dashboard/configDashboardTitleSync sets title to "Bot Config - Volvox.Bot - AI Powered Discord Bot"
  2. Navigate to /dashboard/aiDashboardTitleSync fires:
    • computed = "AI Chat - Volvox.Bot - AI Powered Discord Bot"
    • current = "Bot Config - Volvox.Bot - AI Powered Discord Bot" ← set by step 1
    • current.endsWith(APP_TITLE)true
    • current !== computedtrue
    • current !== APP_TITLEtrue
    • Guard returns early → title stuck as "Bot Config - ..."

Furthermore, the guard is designed to protect titles set by a page's metadata export. However, createPageMetadata() returns bare titles (e.g. { title: 'Bot Config' }) without the APP_TITLE suffix, so current.endsWith(APP_TITLE) would be false for those titles anyway — meaning the guard never fires for its intended use case.

A reliable fix is to track what DashboardTitleSync last set via a ref so the guard can distinguish "title set externally by page metadata" from "stale title set by this component":

import { useRef } from 'react';

const prevComputedRef = useRef<string | null>(null);

useEffect(() => {
  const computed = getDashboardDocumentTitle(pathname);
  const current = document.title;

  // Only respect a page-level title if it wasn't set by this component on the
  // previous navigation.
  if (
    current.endsWith(APP_TITLE) &&
    current !== computed &&
    current !== APP_TITLE &&
    current !== prevComputedRef.current
  ) {
    prevComputedRef.current = null;
    return;
  }

  document.title = computed;
  prevComputedRef.current = computed;
}, [pathname]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: web/src/components/layout/dashboard-title-sync.tsx
Line: 24-26

Comment:
Guard fires on every client-side navigation, stalling the title

The condition `current.endsWith(APP_TITLE)` will match titles that `DashboardTitleSync` itself previously set (e.g. `"Bot Config - Volvox.Bot - AI Powered Discord Bot"`). Because the component runs in the persistent dashboard shell and sets the title on every navigation, the *next* navigation will always see a `current` that ends with `APP_TITLE` (set by the previous run) but differs from the new `computed`. All three guard conditions will be true, the early return fires, and the title is **never updated** after the first page visit.

Concrete trace:
1. `/dashboard/config``DashboardTitleSync` sets title to `"Bot Config - Volvox.Bot - AI Powered Discord Bot"`
2. Navigate to `/dashboard/ai``DashboardTitleSync` fires:
   - `computed = "AI Chat - Volvox.Bot - AI Powered Discord Bot"`
   - `current = "Bot Config - Volvox.Bot - AI Powered Discord Bot"` ← set by step 1
   - `current.endsWith(APP_TITLE)`**true**
   - `current !== computed`**true**
   - `current !== APP_TITLE`**true**
   - Guard returns early → title stuck as `"Bot Config - ..."` ❌

Furthermore, the guard is designed to protect titles set by a page's `metadata` export. However, `createPageMetadata()` returns bare titles (e.g. `{ title: 'Bot Config' }`) without the `APP_TITLE` suffix, so `current.endsWith(APP_TITLE)` would be **false** for those titles anyway — meaning the guard never fires for its intended use case.

A reliable fix is to track what `DashboardTitleSync` last set via a ref so the guard can distinguish "title set externally by page metadata" from "stale title set by this component":

```javascript
import { useRef } from 'react';
…
const prevComputedRef = useRef<string | null>(null);

useEffect(() => {
  const computed = getDashboardDocumentTitle(pathname);
  const current = document.title;

  // Only respect a page-level title if it wasn't set by this component on the
  // previous navigation.
  if (
    current.endsWith(APP_TITLE) &&
    current !== computed &&
    current !== APP_TITLE &&
    current !== prevComputedRef.current
  ) {
    prevComputedRef.current = null;
    return;
  }

  document.title = computed;
  prevComputedRef.current = computed;
}, [pathname]);
```

How can I resolve this? If you propose a fix, please make it concise.

@BillChirico BillChirico merged commit f7d7dac into main Mar 8, 2026
19 of 25 checks passed
@BillChirico BillChirico deleted the fix/dashboard-title-followup branch March 8, 2026 04:37
@github-project-automation github-project-automation bot moved this from Backlog to Done in Volvox.Bot Mar 8, 2026
@github-actions
Copy link
Contributor

github-actions bot commented Mar 8, 2026

🧹 Preview Environment Cleaned Up

The Railway preview environment for this PR has been removed.

Environment: pr-263

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: low Low priority scope: dashboard Web dashboard type: bug Bug fix work

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants