Skip to content

feat: multi-project support, real-data APIs, backlog & PRD panels#3

Open
RBKunnela wants to merge 5 commits intoSynkraAI:mainfrom
RBKunnela:feat/multi-project-real-data-apis
Open

feat: multi-project support, real-data APIs, backlog & PRD panels#3
RBKunnela wants to merge 5 commits intoSynkraAI:mainfrom
RBKunnela:feat/multi-project-real-data-apis

Conversation

@RBKunnela
Copy link
Copy Markdown

@RBKunnela RBKunnela commented Mar 8, 2026

Summary

This PR brings the dashboard from static mock-data to a fully functional, real-data-driven platform:

  • Multi-project support — project registry, consolidated API routes, dynamic base path via env vars
  • New panels — Backlog (epic grouping, filtering, progress tracking), PRD (markdown + Mermaid rendering, architecture docs), Plans (categorized task templates)
  • Real-data API routes — stories, roadmap, PRDs, plans, insights, context, QA metrics, and logs all read from the filesystem (docs/ directory) with mock-data fallback
  • Enhanced existing panels — QA Metrics wired into page router (was showing "Coming Soon"), Insights with real calculations, Context panel improvements, StatusBar with live connection status
  • GitHub integration — GitHubPanel improvements, middleware for cache control
  • CodeRabbit config — automated PR review rules for PRD/architecture compliance
  • Terminal streaming — new use-terminal-stream hook for real-time output

Changes: 66 files (+3,294 / -1,155)

Security

  • No credentials, API keys, tokens, or secrets included
  • .env* properly gitignored
  • All sensitive config read from environment variables at runtime
  • Full diff audited for hardcoded credentials — clean

Test plan

  • Verify dashboard starts with pnpm dev
  • Confirm all 15 page routes return 200
  • Confirm all API endpoints return valid JSON (with mock fallback if no docs/ content)
  • Verify Backlog panel renders with epic grouping and filters
  • Verify PRD panel renders markdown with Mermaid diagrams
  • Verify QA Metrics panel loads (no longer "Coming Soon")
  • Check no credentials or tokens in committed files

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added six new dashboard sections: Context, Plans, PRDs, QA Metrics, Roadmap, and Insights for comprehensive project management and visibility.
    • Implemented multi-project support with a project registry, enabling seamless switching between different projects.
    • Introduced real-time data fetching and live updates across all panels, replacing static mock data by default.
    • Added connection status indicators to display real-time system connectivity and event activity.
  • Documentation

    • Redesigned README with API endpoint reference, architecture overview, and system setup guidance.
  • Chores

    • Updated deployment configuration with base path support for flexible hosting environments.

root and others added 5 commits March 8, 2026 01:15
- Add project-registry with async resolveProjectRoot() supporting
  ?project= query param, projects.json registry, and env var fallback
- Refactor 13 API routes to eliminate duplicate getProjectRoot() functions
- Add GET /api/projects and POST /api/projects/register endpoints
- Make GitHub API route project-aware (scoped to resolved project dir)
- Add new dashboard pages: context, insights, plans, prds, qa, roadmap
- Add backlog, plans, and PRD panel components
- Remove CODEOWNERS (not needed for private fork)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Private fork — no external contributor approval needed.
Simplified README to focus on features and API docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…hitecture docs

- Enhanced PRD API to scan architecture docs (core-architecture.md, docs/architecture/*.md)
- Converted PRD detail route to catch-all [...slug] for nested paths
- Replaced plain <pre> text with MarkdownRenderer + Mermaid diagram support
- Added category badges (PRD vs Architecture) with distinct icons and colors
- Updated CodeRabbit config with PRD and architecture doc review rules
- Fixed systemd ExecStartPre fuser command (wrap in bash -c for || true)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The QA view had no case statement in the page switch, falling through to
the default PlaceholderView. Added import and case 'qa' to render the
existing QAMetricsPanel component which was fully built but never connected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 8, 2026

Warning

Ignoring CodeRabbit configuration file changes. For security, only the configuration from the base branch is applied for open source repositories.

📝 Walkthrough

Walkthrough

Introduces multi-project support with a project registry, new dashboard pages (roadmap, plans, PRDs, QA metrics, insights, context), comprehensive API routes for data aggregation and content management, centralized API URL routing via basePath, a new terminal streaming hook for real-time logs, and updates caching strategies via middleware. Refactors project root resolution across existing routes to use a shared resolver.

Changes

Cohort / File(s) Summary
Configuration & Build Setup
.coderabbit.yaml, .github/CODEOWNERS, .gitignore, tsconfig.json, next.config.ts, package.json
Added CodeRabbit config; removed CODEOWNERS; updated Git ignore for server artifacts; added basePath and caching headers to Next.js config; added ansi-to-html dependency.
Documentation
README.md
Replaced Portuguese content with English; reorganized from tutorial-style to API-centric documentation; added API endpoints table, multi-project support section, and architecture overview.
Core Library & Multi-Project Support
src/lib/project-registry.ts, src/lib/api.ts, src/lib/squad-api-utils.ts
New project registry system with filesystem persistence and in-memory caching; new apiUrl helper for basePath routing; consolidated project root resolution logic.
Caching & Middleware
src/middleware.ts
Added Next.js middleware enforcing no-cache headers for non-static assets to ensure fresh content.
New Dashboard Pages
src/app/(dashboard)/{context,insights,plans,prds,qa,roadmap}/page.tsx
Six new client-side page components rendering corresponding panel components.
API Routes - Project Management
src/app/api/projects/route.ts, src/app/api/projects/register/route.ts
New endpoints for listing and registering projects via the project registry.
API Routes - Data Aggregation
src/app/api/context/route.ts, src/app/api/insights/route.ts, src/app/api/roadmap/route.ts
Complex routes aggregating project context (rules, agents, configs, MCPs), calculating insights (velocity, cycle time, bottlenecks), and parsing roadmap files with priority classification.
API Routes - Content Management
src/app/api/prds/route.ts, src/app/api/prds/[...slug]/route.ts, src/app/api/plans/route.ts
Routes for listing/retrieving PRDs, architecture docs, and task plans from project directories; includes YAML frontmatter parsing and safe path traversal guards.
API Routes - Metrics & Monitoring
src/app/api/qa/metrics/route.ts, src/app/api/bob/{status,events}/route.ts, src/app/api/events/route.ts, src/app/api/logs/route.ts
Refactored metric collection, Bob integration, and added real-time log streaming via SSE with agent filtering and reconnection logic.
API Routes - Refactored Project Root
src/app/api/stories/route.ts, src/app/api/stories/[id]/route.ts, src/app/api/status/route.ts, src/app/api/github/route.ts
Replaced local project root helpers with centralized async resolveProjectRoot resolver; updated function signatures and call sites.
New Components
src/components/backlog/, src/components/plans/, src/components/prd/, src/components/qa/
New BacklogPanel (Kanban-style with epic grouping), PlansPanel (category browsing), PRDPanel (list and detail views), and refactored QAMetricsPanel (now uses settings store, updated UI components).
Updated Components
src/components/{context,insights,roadmap}/..., src/components/{github,stories,bob,monitor,terminals}/...
Replaced static/mock data with live API fetching; added loading/error states; centralized API URLs via apiUrl helper; improved refresh controls and status indicators.
UI & Markdown Rendering
src/components/ui/markdown-renderer.tsx
Added Mermaid diagram rendering support for language-mermaid code blocks.
Hooks - Centralized API Routing
src/hooks/use-{agents,aios-status,realtime-status,squads,stories,terminal-stream}.ts
Updated all data-fetching hooks to use apiUrl for basePath support; added new useTerminalStream hook for SSE-based real-time log streaming with reconnection logic.
Type Definitions & Store
src/types/index.ts, src/stores/settings-store.ts, src/app/layout.tsx
Added new sidebar view types (backlog, plans, prds, qa); changed default mock data setting to false (production mode); added dynamic rendering export to layout.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Dashboard as Dashboard UI
    participant Middleware as Middleware
    participant API as API Routes
    participant Registry as Project Registry
    participant FS as File System

    User->>Dashboard: Request data (e.g., /api/context)
    Dashboard->>Middleware: HTTP Request
    Middleware->>Middleware: Check cache headers<br/>(skip for _next/static)
    Middleware->>API: Apply no-cache headers
    API->>Registry: resolveProjectRoot(request)
    Registry->>Registry: Check ?project param
    Registry->>FS: Load projects.json (cached)
    FS-->>Registry: Registry data
    Registry-->>API: Project root path
    API->>FS: Scan project files<br/>(rules, agents, configs, etc.)
    FS-->>API: File content
    API->>API: Aggregate & transform data
    API-->>Dashboard: JSON response
    Dashboard-->>User: Rendered content
Loading
sequenceDiagram
    participant Client as Client
    participant SSE as SSE Endpoint<br/>(/api/logs)
    participant Agent as Agent Process
    participant FileSystem as Log File

    Client->>SSE: Connect EventSource<br/>(agentId, since params)
    SSE->>FileSystem: Read agent log file
    FileSystem-->>SSE: Recent log lines
    SSE->>Client: Send initial lines
    Agent->>FileSystem: Write new log output
    FileSystem-->>SSE: File update detected
    SSE->>Client: Stream new line events
    Client->>Client: Update terminal display
    loop Reconnection Logic
        alt Connection drops
            Client->>SSE: Attempt reconnect<br/>(max 5 attempts)
        else Max attempts exceeded
            SSE-->>Client: Final error event
        end
    end
    Client->>SSE: Disconnect/close
    SSE->>SSE: Cleanup resources
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes


🐰 Hop, hop—new routes bloom,
Registry maps each project's room,
Context and insights now stream bright,
Logs flow real-time through the night!
Dashboard expands with pride so tall,
Multi-project dreams answer the call.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the three main changes: multi-project support, real-data APIs, and new panels (backlog & PRD), matching the core objectives of the PR.

✏️ 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

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

@RBKunnela
Copy link
Copy Markdown
Author

@pedroavalerio @PedroValerio — Hi Pedro, this PR is ready for your review.

It includes all the dashboard improvements we've been working on: multi-project support, real-data API routes, Backlog panel, PRD panel with Mermaid rendering, QA Metrics fix, and CodeRabbit config.

No credentials, tokens, or secrets are included — fully audited. All sensitive config is read from environment variables at runtime.

Please take a look when you get a chance. Thanks!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/components/ui/markdown-renderer.tsx (1)

64-99: ⚠️ Potential issue | 🟠 Major

Mermaid components are incorrectly wrapped in <pre> tags.

When the code renderer returns a <Mermaid /> component for language-mermaid blocks, the pre renderer receives this component as its child and attempts to match it by checking child?.props?.className === 'language-mermaid'. However, the Mermaid component does not have a className prop matching that value, so the check fails and the diagram gets wrapped in a <pre> tag anyway. Additionally, the exact equality check on line 67 is brittle since rehype-highlight can add extra classes.

Use isValidElement() to safely check if the child is a React element, then check if its type is the Mermaid component directly. Also use .includes('language-mermaid') on line 67 to handle potential extra classes added by rehype-highlight.

Suggested fix
-import ReactMarkdown from 'react-markdown';
+import { isValidElement } from 'react';
+import ReactMarkdown from 'react-markdown';
 import remarkGfm from 'remark-gfm';
 import rehypeHighlight from 'rehype-highlight';
 import type { Components } from 'react-markdown';
 import { Mermaid } from '@/components/mermaidcn/mermaid';
@@
   code: ({ className, children }) => {
     const isBlock = className?.includes('language-');
-    if (className === 'language-mermaid') {
+    if (className?.includes('language-mermaid')) {
       const chartText = String(children).replace(/\n$/, '');
       return (
         <Mermaid
@@
   },
   pre: ({ children }) => {
-    const child = children as React.ReactElement<{ className?: string }>;
-    if (child?.props?.className === 'language-mermaid') {
+    const child = isValidElement(children) ? children : null;
+    if (child?.type === Mermaid) {
       return <>{children}</>;
     }
     return (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/markdown-renderer.tsx` around lines 64 - 99, The pre
renderer is incorrectly wrapping Mermaid diagrams because it checks
child?.props?.className === 'language-mermaid' and doesn't verify the child is a
React element; update the pre renderer to first use React.isValidElement(child)
and then either check the element type === Mermaid to catch components returned
by the code renderer or check
child.props.className?.includes('language-mermaid') to handle extra classes from
rehype-highlight; adjust the conditional in pre (and keep the code renderer that
returns <Mermaid />) so Mermaid components are returned unwrapped (return
<>children</>) when the child is a Mermaid element.
src/components/qa/QAMetricsPanel.tsx (1)

157-163: ⚠️ Potential issue | 🟠 Major

Don't advance lastUpdated on a failed fetch.

When /api/qa/metrics returns non-2xx, this path keeps the old metrics value but still updates lastUpdated, so the footer looks like a successful refresh. Throw on !response.ok or gate setLastUpdated() behind the successful parse.

🕒 Suggested fix
       } else {
         const response = await fetch(apiUrl('/api/qa/metrics'));
-        if (response.ok) {
-          const data = await response.json();
-          setMetrics(data);
-        }
+        if (!response.ok) {
+          throw new Error(`QA metrics request failed with ${response.status}`);
+        }
+        const data = await response.json();
+        setMetrics(data);
       }
       setLastUpdated(new Date());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/qa/QAMetricsPanel.tsx` around lines 157 - 163, The fetch
handler in QAMetricsPanel.tsx updates setLastUpdated even when the fetch failed;
modify the fetch logic (e.g., inside your fetchMetrics function) to only advance
setLastUpdated after a successful response parse — either throw when response.ok
is false (before parsing) or move setLastUpdated(new Date()) into the success
branch immediately after setMetrics(data); ensure you reference the existing
fetch call to apiUrl('/api/qa/metrics'), response.ok check, setMetrics, and
setLastUpdated when applying the change.
src/app/api/stories/[id]/route.ts (1)

149-156: ⚠️ Potential issue | 🔴 Critical

These story handlers can hit the wrong repository.

GET/PUT/DELETE all ignore the incoming request when resolving the project root. Reads will come from the default project, and the write paths can update or archive a story in the wrong repo.

Suggested fix
 export async function GET(
-  _request: NextRequest,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> }
 ) {
   try {
     const { id } = await params;
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);
     const storiesDir = path.join(projectRoot, 'docs', 'stories');
 ...
 export async function PUT(
   request: NextRequest,
   { params }: { params: Promise<{ id: string }> }
 ) {
   try {
     const { id } = await params;
     const body = await request.json() as UpdateStoryRequest;
 
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);
     const storiesDir = path.join(projectRoot, 'docs', 'stories');
 ...
 export async function DELETE(
-  _request: NextRequest,
+  request: NextRequest,
   { params }: { params: Promise<{ id: string }> }
 ) {
   try {
     const { id } = await params;
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);
     const storiesDir = path.join(projectRoot, 'docs', 'stories');

Also applies to: 190-198, 309-316

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

In `@src/app/api/stories/`[id]/route.ts around lines 149 - 156, The GET/PUT/DELETE
handlers for stories call resolveProjectRoot() without using the incoming
request, so they may resolve the default repo instead of the repo implied by the
request; update each handler (GET, PUT, DELETE in
src/app/api/stories/[id]/route.ts) to derive the project root from the request
(use the NextRequest argument instead of ignoring it) before reading/writing
stories, e.g., extract repo/project-identifying info from the request (headers,
host, or path) and pass that into resolveProjectRoot or a new
resolveProjectRootFromRequest helper, then use that projectRoot for path.join
when locating the 'docs/stories' directory and for any archive/update operations
so reads/writes target the correct repository.
src/components/insights/InsightsPanel.tsx (1)

131-174: ⚠️ Potential issue | 🟡 Minor

Guard trend math for zero and unchanged baselines.

If the previous velocity is 0, this renders Infinity%/NaN%. And when the error rate is unchanged, the badge still shows an upward trend. Both edge cases need explicit handling.

Suggested fix
-  const velocityChange = ((data.velocity.current - data.velocity.previous) / data.velocity.previous * 100).toFixed(0);
+  const velocityChange =
+    data.velocity.previous > 0
+      ? (((data.velocity.current - data.velocity.previous) / data.velocity.previous) * 100).toFixed(0)
+      : null;
   const errorChange = data.errorRate.previous - data.errorRate.current;
@@
-              trendValue={`${velocityChange}% vs last week`}
+              trendValue={velocityChange ? `${velocityChange}% vs last week` : 'No prior-week baseline'}
@@
-              trend={errorChange > 0 ? 'down' : 'up'}
-              trendValue={`${Math.abs(errorChange)}% ${errorChange > 0 ? 'decrease' : 'increase'}`}
+              trend={errorChange === 0 ? 'stable' : errorChange > 0 ? 'down' : 'up'}
+              trendValue={errorChange === 0 ? 'No change' : `${Math.abs(errorChange)}% ${errorChange > 0 ? 'decrease' : 'increase'}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/insights/InsightsPanel.tsx` around lines 131 - 174, The
velocity and error trend math needs guards: update the velocityChange
computation (variable velocityChange) to check if data.velocity.previous is 0 or
falsy and handle that case (e.g., set velocityChange to null or a safe string
like '—' and set a corresponding trend prop to 'flat' or omit percent) so you
don't divide by zero; likewise change errorChange logic (variable errorChange)
to detect zero change and pass trend='flat' and trendValue='no change' to the
MetricCard when data.errorRate.current === data.errorRate.previous; make these
checks where velocityChange and errorChange are computed and when you build the
MetricCard props (title "Velocity" and "Error Rate") so the UI shows sensible
values instead of Infinity/NaN or an incorrect up arrow.
🟠 Major comments (30)
.gitignore-42-43 (1)

42-43: ⚠️ Potential issue | 🟠 Major

Remove server/bun.lock from .gitignore to maintain reproducible installs.

The server/ directory is a standalone Bun-managed package with its own package.json and dev dependencies (@types/bun). The README documents running bun install to set up the server. Ignoring the Bun lockfile will allow dependency versions to drift between dev environments, CI, and production deploys. Keep server/bun.lock tracked.

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

In @.gitignore around lines 42 - 43, Remove the server/bun.lock entry from
.gitignore so the Bun lockfile is version-controlled; edit the .gitignore file
and delete the line "server/bun.lock" (leave other entries like
"server/node_modules/" intact) to ensure reproducible installs for the server
package.
src/components/stories/StoryCreateModal.tsx-121-121 (1)

121-121: ⚠️ Potential issue | 🟠 Major

Preserve the active project when posting a new story.

This request drops the dashboard's ?project= selection. On a non-default project page, creates will fall back to the resolver's default project unless the current project id is appended here.

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

In `@src/components/stories/StoryCreateModal.tsx` at line 121, The POST to
apiUrl('/api/stories') currently drops the dashboard ?project selection; update
the request to preserve the active project by including the current project id
when creating a story. Either append the project query param to the fetch URL
(e.g. apiUrl(`/api/stories?project=${encodeURIComponent(activeProjectId)}`)
using the component's activeProject/activeProjectId prop/state) or include a
project field in the JSON body (e.g. body: JSON.stringify({ ..., project:
activeProjectId })). Make this change inside StoryCreateModal where response =
await fetch(apiUrl('/api/stories'), ...) is called and ensure you reference the
existing active project variable (activeProject, activeProjectId, or
currentProject) available in the component.
next.config.ts-6-6 (1)

6-6: ⚠️ Potential issue | 🟠 Major

Drive basePath from the same env source as the client helper.

The codebase introduces env-driven base-path support in src/lib/api.ts (line 8), but next.config.ts (line 6) hard-codes basePath: '/aiox-dashboard'. When a deployment overrides NEXT_PUBLIC_BASE_PATH, the API helper generates URLs with the custom prefix while Next.js routing remains fixed at /aiox-dashboard, causing a routing mismatch.

Suggested fix
 import type { NextConfig } from "next";
 
+const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? '/aiox-dashboard';
+
 const nextConfig: NextConfig = {
   // Externalize native modules that can't be bundled
   serverExternalPackages: ['chokidar'],
-  basePath: '/aiox-dashboard',
+  basePath,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.ts` at line 6, The hard-coded basePath in next.config.ts should
be driven from the same env var used by the client helper; replace the literal
'/aiox-dashboard' with a value derived from process.env.NEXT_PUBLIC_BASE_PATH
(fallbacking to '' or undefined if not set) so Next.js routing matches the URLs
generated by src/lib/api.ts; update the basePath assignment in next.config.ts to
read NEXT_PUBLIC_BASE_PATH and ensure it normalizes empty/undefined values
consistently with the client helper.
src/lib/api.ts-8-15 (1)

8-15: ⚠️ Potential issue | 🟠 Major

Use nullish coalescing to allow root-path deployments.

The || operator treats an explicit empty string as falsy, preventing NEXT_PUBLIC_BASE_PATH='' from configuring a root deployment. Switch to ?? (nullish coalescing) to distinguish between "not set" and "set to empty string".

Suggested fix
-const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH || '/aiox-dashboard';
+const BASE_PATH = (process.env.NEXT_PUBLIC_BASE_PATH ?? '/aiox-dashboard').replace(/\/$/, '');
@@
 export function apiUrl(path: string): string {
-  return `${BASE_PATH}${path}`;
+  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
+  return `${BASE_PATH}${normalizedPath}`;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/api.ts` around lines 8 - 15, The BASE_PATH constant uses the ||
operator which treats an explicit empty string as unset; change its
initialization to use the nullish coalescing operator (??) so
NEXT_PUBLIC_BASE_PATH='' is respected for root-path deployments (update the
BASE_PATH declaration where it's defined and leave apiUrl(path: string)
unchanged so that apiUrl uses the corrected BASE_PATH).
src/middleware.ts-21-25 (1)

21-25: ⚠️ Potential issue | 🟠 Major

Narrow matcher to pages and APIs only; exclude Next.js optimized assets and public files.

The current matcher /((?!_next/static|favicon.ico).*) is too broad. Verification confirms it catches:

  • /_next/image (Next.js image optimization)
  • /manifest.json, /robots.txt (static metadata)
  • All files in public/ (SVGs, icons, etc.)

All these paths receive Cache-Control: no-cache, no-store, must-revalidate, defeating browser/CDN caching for immutable assets and degrading performance.

The matcher should exclude at minimum: _next/static, _next/image, favicon.ico, public/, and metadata files (manifest.json, robots.txt, sitemap.xml). Alternatively, use an allowlist approach matching only /api/**, /(.*\.)?html, and /page patterns instead.

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

In `@src/middleware.ts` around lines 21 - 25, The middleware config.matcher is too
broad (export const config -> matcher) and is matching Next.js optimized assets
and public metadata; narrow it to only target pages and API routes by replacing
the current negative-match regex with either an allowlist style matcher that
explicitly includes "/api/:path*", page routes (e.g. root and page paths) and
HTML routes, or by updating the regex to exclude "_next/static", "_next/image",
"favicon.ico", "manifest.json", "robots.txt", "sitemap.xml" and any public asset
extensions; update the matcher value accordingly so middleware runs only for API
and page requests, not for static or public files.
src/components/backlog/BacklogPanel.tsx-101-105 (1)

101-105: ⚠️ Potential issue | 🟠 Major

Keep filtered stories attached to a stable epic group.

Filtering epics separately from stories means a story can still match the filters while its epic gets dropped. In that path you create epic: null groups and then key/collapse them with group-${idx}, so epic-owned stories end up rendered as “Unassigned Stories” and can inherit the wrong expansion state after a sort/filter change. Carry the referenced epicId (or the resolved epic from the full epic list) through EpicGroup and use that as the stable render/state key.

Also applies to: 137-149, 335-341

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

In `@src/components/backlog/BacklogPanel.tsx` around lines 101 - 105,
filteredEpics is created by filtering epics separately from stories which allows
stories to reference epics that were dropped, leading to epic: null groups and
unstable keys like group-${idx}; fix this by carrying the referenced epicId (or
resolving the epic object from the full epics list) into the EpicGroup props and
use that epicId/resolved epic as the stable React key and collapse/expansion
identifier instead of relying on group-${idx} or null; update the code paths
that build EpicGroup (the mapping that uses filteredEpics and the story grouping
logic around EpicGroup) so they resolve epic via epicId (or lookup in the
original epics array) before rendering and pass that stable id/object through
for keys and expansion state (also apply same change to the other instances
noted at the lines around 137-149 and 335-341).
src/components/backlog/BacklogPanel.tsx-624-640 (1)

624-640: ⚠️ Potential issue | 🟠 Major

Label the filter comboboxes.

These selects only have adjacent text, so assistive tech won't announce which control is “Status”, “Priority”, or “Agent”. Please wire an accessible name with <label htmlFor> or at least aria-label.

♿ Minimal fix
 function FilterSelect({ label, value, onChange, options }: FilterSelectProps) {
   return (
     <div className="flex items-center gap-1.5">
       <span className="text-detail text-text-muted">{label}:</span>
       <select
+        aria-label={label}
         value={value}
         onChange={(e) => onChange(e.target.value)}
         className="text-sm bg-transparent border px-2 py-1 text-text-secondary focus:outline-none focus:border-gold"
         style={{ borderColor: 'var(--border-subtle)' }}
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/backlog/BacklogPanel.tsx` around lines 624 - 640, The select
elements in FilterSelect lack an accessible name; update the FilterSelect
component to associate a label with the select by generating a stable id (e.g.,
derive from the label prop or accept an id prop) and using a <label
htmlFor={id}> wrapping or adjacent to the visible label text, and set the
select's id to that id; alternatively, if you prefer not to add a <label>, add
an aria-label or aria-labelledby on the select using the label text (ensure
uniqueness), so Screen Readers can announce controls like "Status", "Priority",
or "Agent" — modify the FilterSelect function to create/use this id and wire it
to both the label and the select (or add aria-label) accordingly.
src/components/qa/QAMetricsPanel.tsx-100-112 (1)

100-112: ⚠️ Potential issue | 🟠 Major

Fix the stacked-bar math.

Line 110 already scales each column by total / maxVal. Lines 106-107 divide by maxVal again, so any non-max day only fills part of its own column and the pass/fail split is understated. Compute the inner segment heights from total instead.

📊 Suggested fix
 function TrendBar({ data }: { data: Array<{ date: string; passed: number; failed: number }> }) {
   const maxVal = Math.max(...data.map(d => d.passed + d.failed), 1);
   return (
     <div className="flex items-end gap-1 h-20">
       {data.map((d) => {
         const total = d.passed + d.failed;
-        const passHeight = (d.passed / maxVal) * 100;
-        const failHeight = (d.failed / maxVal) * 100;
+        const passHeight = total === 0 ? 0 : (d.passed / total) * 100;
+        const failHeight = total === 0 ? 0 : (d.failed / total) * 100;
         return (
           <div key={d.date} className="flex-1 flex flex-col items-center gap-0.5">
             <div className="w-full flex flex-col-reverse" style={{ height: `${(total / maxVal) * 100}%`, minHeight: '2px' }}>
               <div className="w-full bg-green-500 rounded-t-sm" style={{ height: `${passHeight}%` }} />
               {failHeight > 0 && <div className="w-full bg-red-500 rounded-t-sm" style={{ height: `${failHeight}%` }} />}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/qa/QAMetricsPanel.tsx` around lines 100 - 112, In TrendBar,
the inner segment heights are incorrectly divided by maxVal; change passHeight
and failHeight to be relative to the day's total (e.g., passHeight = total ?
(d.passed / total) * 100 : 0 and failHeight = total ? (d.failed / total) * 100 :
0) while keeping the outer column height as (total / maxVal) * 100%; update the
computations in the TrendBar function where passHeight and failHeight are
calculated and ensure you guard against total === 0 to avoid NaN.
src/app/api/status/route.ts-120-123 (1)

120-123: ⚠️ Potential issue | 🟠 Major

Accept request parameter to support ?project= scoping.

The resolveProjectRoot() function accepts an optional request parameter to check the ?project= query parameter, but the /api/status GET handler doesn't accept a request, so it cannot resolve projects dynamically. Add the request parameter to the handler and pass it to resolveProjectRoot():

export async function GET(request: NextRequest) {
  try {
    const statusFilePath = path.join(await resolveProjectRoot(request), STATUS_FILE_NAME);

This aligns with /api/github/route.ts, which correctly passes request to enable project scoping.

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

In `@src/app/api/status/route.ts` around lines 120 - 123, The GET handler
currently lacks a request parameter, so update the function signature export
async function GET(request: NextRequest) to accept the incoming request and pass
that request into resolveProjectRoot by calling await
resolveProjectRoot(request) when building statusFilePath (keeping
STATUS_FILE_NAME as before); ensure NextRequest is imported or available in the
module if not already and adjust any callers/tests accordingly.
src/app/api/bob/events/route.ts-22-24 (1)

22-24: ⚠️ Potential issue | 🟠 Major

Pass the incoming request into project resolution.

This endpoint receives request: NextRequest but calls resolveProjectRoot() without it. The resolveProjectRoot function accepts an optional request parameter to check the ?project= query parameter first before falling back to the default project. Without passing the request, SSE subscribers for non-default projects will receive the wrong Bob stream.

Fix
 export async function GET(request: NextRequest) {
-  const projectRoot = await resolveProjectRoot();
+  const projectRoot = await resolveProjectRoot(request);
   const statusFilePath = path.join(projectRoot, BOB_STATUS_FILE_NAME);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/bob/events/route.ts` around lines 22 - 24, The GET handler calls
resolveProjectRoot() without the incoming NextRequest so query-based project
selection is ignored; update the GET function to pass the request into
resolveProjectRoot(request) (so resolveProjectRoot can read the ?project= query)
before computing statusFilePath (statusFilePath remains path.join(projectRoot,
BOB_STATUS_FILE_NAME)), ensuring SSE subscribers receive the correct project
stream.
src/app/api/projects/register/route.ts-9-16 (1)

9-16: ⚠️ Potential issue | 🟠 Major

Add path validation in registerProject to prevent arbitrary filesystem access.

The route correctly validates projectPath as a non-empty string, but registerProject stores the path directly without validation. This allows registering any arbitrary path—including absolute paths, relative paths with traversal sequences, or symlinks—which are then used unsanitized in filesystem operations across the application. Add checks to ensure the path:

  • Is within allowed boundaries (e.g., under a specific parent directory)
  • Is not absolute (or if absolute, verified against a whitelist)
  • Does not contain traversal sequences or resolve outside intended scope
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/projects/register/route.ts` around lines 9 - 16, registerProject
currently stores projectPath without sanitization; update
registerProject(projectPath, name?) to validate and canonicalize paths before
persisting: reject absolute paths unless they match a configured whitelist,
normalize and resolve projectPath via path.resolve + path.normalize (or
fs.realpath) and ensure the resolved path startsWith a configured allowedBaseDir
(compare resolved strings), detect and reject traversal by comparing resolved vs
joined baseDir, and optionally lstat the resolved path to disallow symlinks
(fs.lstat(...).isSymbolicLink()). On any validation failure return/throw a
400-like error so callers (route.ts) don’t register arbitrary or escaped
filesystem locations. Ensure error messages reference registerProject and
projectPath for easy tracing.
src/app/api/qa/metrics/route.ts-73-75 (1)

73-75: ⚠️ Potential issue | 🟠 Major

This metrics route still ignores ?project=.

collectQAMetrics() resolves the default root because GET() never passes the incoming request through. In a multi-project session, /api/qa/metrics?project=... will still read .aios from the default project.

Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
 ...
-async function collectQAMetrics(): Promise<QAMetrics> {
-  const projectRoot = await resolveProjectRoot();
+async function collectQAMetrics(request: NextRequest): Promise<QAMetrics> {
+  const projectRoot = await resolveProjectRoot(request);
   const aiosDir = path.join(projectRoot, '.aios');
   ...
 }
 ...
-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    const metrics = await collectQAMetrics();
+    const metrics = await collectQAMetrics(request);
     return NextResponse.json(metrics);

Also applies to: 219-221

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

In `@src/app/api/qa/metrics/route.ts` around lines 73 - 75, collectQAMetrics
currently always calls resolveProjectRoot() with no context, so GET requests
with ?project= are ignored; update collectQAMetrics to accept an optional
project identifier/path (e.g., collectQAMetrics(project?: string)) and use that
to call resolveProjectRoot(project) (or a new resolveProjectRootFromParam
helper) instead of always resolving the default, then update the GET handler to
parse req.nextUrl.searchParams.get('project') (or equivalent) and pass that
value into collectQAMetrics; apply the same change for the other call sites
referenced around the 219-221 region so all metrics collection honors ?project=.
src/lib/squad-api-utils.ts-6-13 (1)

6-13: 🛠️ Refactor suggestion | 🟠 Major

Avoid keeping a second project-root resolver here.

getProjectRoot() only consults AIOS_PROJECT_ROOT and a hardcoded fallback, so any caller that stays on this helper bypasses both the registry and request-scoped ?project= selection. That makes it easy for squad routes to drift from the centralized resolver the rest of the API now uses.

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

In `@src/lib/squad-api-utils.ts` around lines 6 - 13, getProjectRoot() duplicates
project root resolution and bypasses the centralized resolver; remove this
ad-hoc resolver and update callers to use the centralized async
resolveProjectRoot from the project-registry (import and await
resolveProjectRoot where getProjectRoot() was used). If a synchronous shim is
absolutely required, make getProjectRoot() throw a clear deprecation/error
telling callers to switch to await resolveProjectRoot(), rather than returning a
hardcoded fallback; reference getProjectRoot() and resolveProjectRoot() to
locate and change the call sites.
src/app/api/prds/route.ts-54-57 (1)

54-57: ⚠️ Potential issue | 🟠 Major

Pass the request into resolveProjectRoot() here too.

This endpoint lists project-scoped docs, but GET() resolves the root without the incoming request. That makes /api/prds?project=... return the default project's PRDs regardless of the current selection.

Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
 ...
-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);
     const docsDir = path.join(projectRoot, 'docs');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/prds/route.ts` around lines 54 - 57, The GET handler is calling
resolveProjectRoot() without the incoming Request, so project-scoped queries
(e.g., ?project=...) are ignored; update the export async function GET to accept
the Request (e.g., GET(request: Request)) and pass that request into
resolveProjectRoot(request), keeping the rest of the logic (e.g., docsDir =
path.join(projectRoot, 'docs')) the same so the resolved project root reflects
the current request selection.
src/app/api/prds/[...slug]/route.ts-6-14 (1)

6-14: ⚠️ Potential issue | 🟠 Major

This PRD route still ignores the selected project.

The handler receives the request but throws it away before resolving the root, so ?project= always falls back to the default repo. PRDPanel will show the wrong document set when users switch projects.

Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
 ...
 export async function GET(
-  _request: Request,
+  request: NextRequest,
   { params }: { params: Promise<{ slug: string[] }> }
 ) {
   try {
     const { slug } = await params;
     const slugPath = slug.join('/');
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/prds/`[...slug]/route.ts around lines 6 - 14, The GET handler is
ignoring the request's ?project= query so it always uses the default repo;
extract the project param from the incoming request (e.g., const project = new
URL(_request.url).searchParams.get('project')), await params to get slug as
before, and pass the project into resolveProjectRoot (call
resolveProjectRoot(project) or resolveProjectRoot(undefined) only when project
is missing) so filePath is resolved for the selected project rather than always
the default.
src/components/prd/PRDPanel.tsx-52-59 (1)

52-59: ⚠️ Potential issue | 🟠 Major

Clear and gate PRD content fetches by the current selection.

handleSelect() keeps the previous prdContent, and the detail view only switches once prdContent !== null. Clicking document B right after document A can therefore show A's body under B's header until the second fetch finishes, and an older response can still overwrite the newer selection. Reset the content before navigation and ignore or abort responses that no longer match the latest selectedSlug.

♻️ Suggested fix
   const handleSelect = (slug: string) => {
     setSelectedSlug(slug);
-    fetchPRDContent(slug);
+    setPrdContent(null);
+    void fetchPRDContent(slug);
   };

-  if (selectedSlug && prdContent !== null) {
+  if (selectedSlug) {

You'll still want an AbortController or request token inside fetchPRDContent() so a slower older response can't overwrite the latest selection.

Also applies to: 71-74, 104-137

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

In `@src/components/prd/PRDPanel.tsx` around lines 52 - 59, Reset the displayed
PRD body immediately when selection changes and guard fetchPRDContent against
stale responses: in handleSelect() set prdContent to null (or empty) and update
selectedSlug before calling fetchPRDContent; inside fetchPRDContent() create an
AbortController (or attach a request token) and store it per-call so you can
abort previous requests, and when a response arrives verify that its slug
matches the current selectedSlug (or that the controller has not been aborted)
before calling setPrdContent; ensure you cancel the previous controller when
starting a new fetch so slower earlier responses cannot overwrite the latest
selection.
src/components/context/ContextPanel.tsx-134-151 (1)

134-151: ⚠️ Potential issue | 🟠 Major

Don't relabel stale mock data as live context.

If useMockData flips to false and the live request fails, data keeps the old MOCK_CONTEXT, the error state stays hidden because data is still set, and the footer switches to "Live project context". That makes demo payloads look like real project data. Clear data before a live load, or track the active source separately and render the footer/error state from that source instead.

Also applies to: 168-181, 309-311

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

In `@src/components/context/ContextPanel.tsx` around lines 134 - 151, The
fetchContext function currently leaves MOCK_CONTEXT in state when
settings.useMockData flips to false and a live fetch fails, causing mock data to
be labeled as live; update fetchContext (and the analogous handlers at lines
referenced) to clear prior mock data before attempting a live load (e.g., call
setData(null) or setData(undefined)) and/or set an explicit source flag (e.g.,
setContextSource('live'|'mock'|'error')) so the UI renders footer/error based on
that source rather than the presence of data; ensure you still set
setLoading(true) and setError(null) before the fetch and only setData(json) on
successful fetch, and setContextSource('error') on failure.
src/hooks/use-terminal-stream.ts-46-49 (1)

46-49: ⚠️ Potential issue | 🟠 Major

Use apiUrl() for the EventSource endpoint too.

This hardcodes /aiox-dashboard, so log streaming breaks whenever the app runs under a different base path or no base path at all. Build the SSE URL through the shared helper here as well so it follows the same routing config as the rest of the dashboard.

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

In `@src/hooks/use-terminal-stream.ts` around lines 46 - 49, The SSE endpoint URL
is being built with a hardcoded base (`/aiox-dashboard/api/logs`) which breaks
when the app base path changes; update the URL construction in
use-terminal-stream (the `url` variable that uses `agentId` and
`lastTimestampRef.current`) to call the shared apiUrl() helper instead of
hardcoding `/aiox-dashboard/api/...`, ensuring query params (agent and since)
are encoded via encodeURIComponent and passed to apiUrl so the EventSource will
follow the same routing/base-path configuration as the rest of the dashboard.
src/app/api/logs/route.ts-117-175 (1)

117-175: ⚠️ Potential issue | 🟠 Major

Serialize readNewLines() before updating fileOffset.

fs.watch() can fire again while a previous readNewLines() call is still awaiting stat/open/read, and both paths mutate fileOffset and partialLine. Overlap here can re-read or skip bytes, which will show up as duplicated or missing terminal lines. Guard the reader with an in-flight promise or lock.

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

In `@src/app/api/logs/route.ts` around lines 117 - 175, readNewLines is not
serialized and concurrent invocations can race on shared state (fileOffset,
partialLine), causing duplicated or missing bytes; protect the reader by
introducing an in-flight lock/promise (e.g., a boolean like isReading or a
Promise currentRead) inside the same scope as readNewLines and early-return if a
read is already running, then perform stat/open/read/parse/update while holding
the lock and release it when done (also ensure errors release the lock); update
references to fileOffset and partialLine only while the lock is held and keep
watcher/pollInterval behavior unchanged so multiple watch events simply return
when a read is in-flight.
src/app/api/logs/route.ts-84-95 (1)

84-95: ⚠️ Potential issue | 🟠 Major

Use one resume cursor end-to-end.

since is filtered against timestamps parsed from the raw log text, but every streamed TerminalLine.timestamp is generated with new Date().toISOString(). On reconnect the client sends back server receipt time, not the line's log time, so resume can skip or duplicate output depending on the log format. Reuse the same cursor source for both filtering and emitted events, or switch the resume token to a byte offset.

Also applies to: 100-105, 147-154

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

In `@src/app/api/logs/route.ts` around lines 84 - 95, The code filters allLines
using parsed timestamps from the raw text (allLines and linesToSend) while
emitted TerminalLine.timestamp values are generated at receipt, causing resume
inconsistencies; change the resume logic to use a single cursor source
end-to-end: either (A) switch filtering to use the same timestamp/token carried
on emitted TerminalLine objects (use the line.timestamp/TerminalLine.timestamp
field instead of parsing the raw string) so reconnect compares the same
server-side receipt timestamp, or (B) change the resume token to a stable
byte-offset/stream-offset and filter by that instead; update the filtering code
around since/linesToSend and any emit logic that sets TerminalLine.timestamp to
use the chosen single cursor source consistently (also apply the same change
where similar timestamp parsing occurs at the other affected blocks).
src/app/api/insights/route.ts-46-52 (1)

46-52: ⚠️ Potential issue | 🟠 Major

Populate weeklyActivity.stories before returning it.

getWeeklyActivity() initializes every day with stories: 0, and nothing ever increments that field. The API will therefore report zero story activity even when story files exist, which makes the chart misleading. Derive the daily counts from the story timestamps before serializing weeklyActivity.

Also applies to: 76-82, 91-93, 171-175

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

In `@src/app/api/insights/route.ts` around lines 46 - 52, getWeeklyActivity
currently initializes each day's object with stories: 0 but never increments it;
update the function to collect story timestamps, map each story date to the
corresponding day in the last 7 days, and increment the stories counter for that
day before returning result. Specifically, inside getWeeklyActivity (using the
days, activity, result and today variables) parse your story metadata/file
timestamps, convert them to Date objects, determine which of the last 7 day
buckets each timestamp falls into, and increment either activity[dayKey] or
result[i].stories accordingly so weeklyActivity.stories reflects actual story
counts.
src/app/api/insights/route.ts-61-65 (1)

61-65: ⚠️ Potential issue | 🟠 Major

Keep git log off the synchronous request path.

execSync() blocks the handler thread for up to five seconds on every /api/insights request. Under concurrent dashboard traffic this can stall the whole Node worker. Use an async subprocess plus caching, or precompute this metric outside the request.

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

In `@src/app/api/insights/route.ts` around lines 61 - 65, The code is blocking
request handling by calling execSync in the insights route handler; replace the
synchronous call with an async subprocess and add short-lived caching: use
util.promisify(require('child_process').exec) (or child_process.spawn wrapped in
a Promise) to run the git log command asynchronously (refer to execSync -> exec,
and the variable output/projectRoot) and implement an in-memory cache (e.g.,
gitDatesCache + gitDatesCacheTs) to return cached results for a configurable TTL
(e.g., 30–60s) to avoid running the command on every request; ensure you
preserve cwd and timeout options and handle errors by returning an empty result
or previous cached value.
src/hooks/use-terminal-stream.ts-145-158 (1)

145-158: ⚠️ Potential issue | 🟠 Major

Mark the terminal disconnected when enabled goes false.

The effect cleanup closes the socket, but it never clears the store state or status, so a disabled stream can still render as connected. Reuse disconnect() for the disable/cleanup path instead of duplicating a partial teardown.

♻️ Suggested fix
 useEffect(() => {
-  if (enabled) {
-    connect();
-  }
-
-  return () => {
-    if (reconnectTimeoutRef.current) {
-      clearTimeout(reconnectTimeoutRef.current);
-    }
-    if (eventSourceRef.current) {
-      eventSourceRef.current.close();
-    }
-  };
-}, [connect, enabled]);
+  if (!enabled) {
+    disconnect();
+    return;
+  }
+
+  connect();
+  return disconnect;
+}, [connect, disconnect, enabled]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/use-terminal-stream.ts` around lines 145 - 158, The cleanup
currently closes sockets but doesn't update the terminal store/status when
enabled flips false; replace the manual teardown in the useEffect cleanup with a
call to the existing disconnect() function so the store and status are reset
too. Specifically, in the effect that depends on connect and enabled (in
use-terminal-stream.ts), call disconnect() in the return cleanup path (and when
disabling the stream) instead of only clearing reconnectTimeoutRef.current and
closing eventSourceRef.current so the terminal state is properly marked
disconnected.
src/app/api/plans/route.ts-14-17 (1)

14-17: ⚠️ Potential issue | 🟠 Major

Pass the request object to resolveProjectRoot() to enable project switching.

The route's GET() handler doesn't receive the request parameter needed for multi-project support. resolveProjectRoot() accepts an optional NextRequest to check ?project=<name> query parameters, but without passing it, the function falls back to the default project and ignores the query parameter.

Update src/app/api/plans/route.ts line 16 to pass the request:

-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);

(Note: src/app/api/github/route.ts line 31 correctly demonstrates this pattern.)

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

In `@src/app/api/plans/route.ts` around lines 14 - 17, The GET() route handler is
not passing the incoming request to resolveProjectRoot(), so project switching
via ?project= is ignored; update the GET() signature to accept the request
parameter and call resolveProjectRoot(request) (matching the pattern used in
github route), ensuring resolveProjectRoot receives the NextRequest so it can
read query params and return the correct projectRoot.
src/app/api/context/route.ts-1-5 (1)

1-5: ⚠️ Potential issue | 🟠 Major

This endpoint never honors ?project=.

resolveProjectRoot() only reads the query string when you pass it the current request. Because this GET() drops that argument, /api/context?project=foo still serves the default project.

Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
@@
-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);

Also applies to: 223-225

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

In `@src/app/api/context/route.ts` around lines 1 - 5, The GET route drops the
request when calling resolveProjectRoot so query params like ?project=foo are
ignored; update the handler(s) that call resolveProjectRoot (e.g., the exported
GET function and the other handler around the same file that also calls
resolveProjectRoot) to pass the incoming Request object (the first parameter)
into resolveProjectRoot so it can read req.url/searchParams, i.e., change
resolveProjectRoot() -> resolveProjectRoot(request) wherever currently invoked
without arguments.
src/app/api/roadmap/route.ts-1-5 (1)

1-5: ⚠️ Potential issue | 🟠 Major

This route also drops the selected project.

Without forwarding the incoming request into resolveProjectRoot(), ?project= can never select a non-default repository here.

Suggested fix
-import { NextResponse } from 'next/server';
+import { NextRequest, NextResponse } from 'next/server';
@@
-export async function GET() {
+export async function GET(request: NextRequest) {
   try {
-    const projectRoot = await resolveProjectRoot();
+    const projectRoot = await resolveProjectRoot(request);

Also applies to: 156-158

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

In `@src/app/api/roadmap/route.ts` around lines 1 - 5, The route is currently
calling resolveProjectRoot without passing the incoming Request, so the query
param ?project cannot be handled; update calls to resolveProjectRoot(...) to
pass the Request object (the handler's req) so it can read the project query
(e.g., change resolveProjectRoot() to resolveProjectRoot(req)); make this change
for both the initial import usage and the other occurrence around the block
referenced (the calls that currently omit arguments), ensuring any variable
names (req/request) match the route handler signature.
src/app/api/roadmap/route.ts-27-43 (1)

27-43: ⚠️ Potential issue | 🟠 Major

Roadmap item IDs collide across files.

parseRoadmapFile() resets IDs for every markdown file, but GET() concatenates all items from docs/roadmap. Any client keyed by item.id will collide as soon as there is more than one file.

Suggested fix
 async function parseRoadmapFile(filePath: string): Promise<RoadmapItem[]> {
   const content = await fs.readFile(filePath, 'utf-8');
   const items: RoadmapItem[] = [];
+  const fileId = path.basename(filePath, path.extname(filePath));
@@
-      id: `roadmap-${i + 1}`,
+      id: `${fileId}-${i + 1}`,
@@
-            id: `roadmap-${itemIndex}`,
+            id: `${fileId}-${itemIndex}`,
@@
-            id: `roadmap-${itemIndex}`,
+            id: `${fileId}-${itemIndex}`,
@@
-        id: `roadmap-${itemIndex}`,
+        id: `${fileId}-${itemIndex}`,
@@
-            id: `roadmap-${itemIndex}`,
+            id: `${fileId}-${itemIndex}`,

Also applies to: 67-76, 90-99, 117-126, 138-147, 165-169

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

In `@src/app/api/roadmap/route.ts` around lines 27 - 43, parseRoadmapFile
currently generates ids starting at "roadmap-1" per file causing collisions when
GET() aggregates multiple files; change id generation in parseRoadmapFile to
include a file-unique prefix (e.g., derive slug from the filePath or filename
using path.basename or a provided fileSlug) and then append the per-file index
(e.g., `${slug}-${i+1}`) so every RoadmapItem id is globally unique; update all
similar id-generating blocks (the other ranges noted) to use the same
filename-based prefix convention so aggregated lists from GET() contain
non-colliding ids.
src/app/api/context/route.ts-31-35 (1)

31-35: ⚠️ Potential issue | 🟠 Major

Relative file paths are based on the wrong root.

scanFiles() currently relativizes against AIOS_PROJECT_ROOT / process.cwd() instead of the resolved projectRoot. For any registered project outside that fallback path, this API will return misleading path values.

Suggested fix
 async function scanFiles(
+  projectRoot: string,
   dir: string,
   type: ContextFile['type'],
   descFn: (name: string) => string,
 ): Promise<ContextFile[]> {
@@
-        path: path.relative(process.env.AIOS_PROJECT_ROOT || path.resolve(process.cwd(), '..', '..'), fullPath),
+        path: path.relative(projectRoot, fullPath),
@@
-  const ruleFiles = await scanFiles(rulesDir, 'rules', (name) => {
+  const ruleFiles = await scanFiles(projectRoot, rulesDir, 'rules', (name) => {
@@
-  const agentFiles = await scanFiles(agentsDir, 'agent', (name) => {
+  const agentFiles = await scanFiles(projectRoot, agentsDir, 'agent', (name) => {
@@
-      const squadAgentFiles = await scanFiles(squadAgentsDir, 'agent', (name) => {
+      const squadAgentFiles = await scanFiles(projectRoot, squadAgentsDir, 'agent', (name) => {

Also applies to: 47-48, 80-83, 94-97, 107-110

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

In `@src/app/api/context/route.ts` around lines 31 - 35, scanFiles() is computing
relative paths against AIOS_PROJECT_ROOT/process.cwd() instead of the actual
resolved projectRoot, producing incorrect ContextFile.path values for projects
outside that fallback; update scanFiles (and the other occurrences noted) to use
the resolved projectRoot variable when relativizing file paths (e.g. use
path.relative(projectRoot, absoluteFilePath) and normalize/ensure no leading
slashes if your code expects that) so ContextFile.path and any description logic
from descFn operate from the real projectRoot rather than the fallback; apply
the same replacement for all other places that call path.relative or compute a
relative path (lines referenced) so the projectRoot variable is the source of
truth.
src/lib/project-registry.ts-49-60 (1)

49-60: ⚠️ Potential issue | 🟠 Major

An explicit but unknown project should fail, not fall through.

If the caller asks for ?project=foo and foo is not registered, this silently serves the default project instead. That makes the UI look like it switched repos while actually showing another one.

Suggested fix
   if (request) {
     const projectName = request.nextUrl.searchParams.get('project');
     if (projectName) {
       const registry = await loadRegistry();
       const entry = registry.projects[projectName];
-      if (entry?.path) {
-        return entry.path;
+      if (!entry?.path) {
+        throw new Error(`Unknown project: ${projectName}`);
       }
+      return entry.path;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/project-registry.ts` around lines 49 - 60, The resolveProjectRoot
function currently falls back to the default when a ?project=<name> is provided
but not found; change the logic so that if
request.nextUrl.searchParams.get('project') yields a projectName but
loadRegistry() returns no entry or entry.path, the function does not fall
through but instead fails explicitly (e.g., throw an Error or reject) with a
clear message including the requested projectName; update resolveProjectRoot to
check registry.projects[projectName] and entry?.path and raise an error when
missing so callers can handle an unknown project rather than silently returning
the default.
src/lib/project-registry.ts-84-90 (1)

84-90: ⚠️ Potential issue | 🟠 Major

Validate and normalize project paths before saving them.

registerProject() persists any string as-is. A typo or relative path poisons the registry, and an arbitrary absolute path becomes trusted input for every filesystem-backed route later.

Suggested fix
 export async function registerProject(name: string, projectPath: string): Promise<ProjectRegistry> {
+  const normalizedPath = await fs.realpath(path.resolve(projectPath));
+  const stat = await fs.stat(normalizedPath);
+  if (!stat.isDirectory()) {
+    throw new Error(`Project path is not a directory: ${normalizedPath}`);
+  }
+
   const registry = await loadRegistry();
-  registry.projects[name] = { path: projectPath };
+  registry.projects[name] = { path: normalizedPath };
   if (!registry.defaultProject) {
     registry.defaultProject = name;
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/project-registry.ts` around lines 84 - 90, registerProject currently
saves projectPath verbatim; validate and normalize it before persisting by
resolving it to an absolute, normalized path and verifying it points to an
existing directory and is inside the allowed workspace root. In the
registerProject function, call path.resolve(projectPath) (or equivalent) to
normalize, check fs.stat/exists to ensure it's a directory, and ensure the
resolved path is a child of the workspace root (e.g., compare prefixes) to avoid
accepting arbitrary absolute paths; only after these checks update
registry.projects[name] and call saveRegistry. Keep references to
registerProject, loadRegistry, saveRegistry and ProjectRegistry when making
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0dd0f494-bba2-4c1c-a4d3-d9ef1eead669

📥 Commits

Reviewing files that changed from the base of the PR and between dc221f4 and 27bf65d.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (65)
  • .coderabbit.yaml
  • .github/CODEOWNERS
  • .gitignore
  • README.md
  • next.config.ts
  • package.json
  • src/app/(dashboard)/context/page.tsx
  • src/app/(dashboard)/insights/page.tsx
  • src/app/(dashboard)/plans/page.tsx
  • src/app/(dashboard)/prds/page.tsx
  • src/app/(dashboard)/qa/page.tsx
  • src/app/(dashboard)/roadmap/page.tsx
  • src/app/api/bob/events/route.ts
  • src/app/api/bob/status/route.ts
  • src/app/api/context/route.ts
  • src/app/api/events/route.ts
  • src/app/api/github/route.ts
  • src/app/api/insights/route.ts
  • src/app/api/logs/route.ts
  • src/app/api/plans/route.ts
  • src/app/api/prds/[...slug]/route.ts
  • src/app/api/prds/route.ts
  • src/app/api/projects/register/route.ts
  • src/app/api/projects/route.ts
  • src/app/api/qa/metrics/route.ts
  • src/app/api/roadmap/route.ts
  • src/app/api/status/route.ts
  • src/app/api/stories/[id]/route.ts
  • src/app/api/stories/route.ts
  • src/app/layout.tsx
  • src/app/page.tsx
  • src/components/backlog/BacklogPanel.tsx
  • src/components/backlog/index.ts
  • src/components/bob/BobOrchestrationView.tsx
  • src/components/context/ContextPanel.tsx
  • src/components/github/GitHubPanel.tsx
  • src/components/insights/InsightsPanel.tsx
  • src/components/kanban/KanbanBoard.tsx
  • src/components/layout/StatusBar.tsx
  • src/components/monitor/MonitorPanel.tsx
  • src/components/plans/PlansPanel.tsx
  • src/components/plans/index.ts
  • src/components/prd/PRDPanel.tsx
  • src/components/prd/index.ts
  • src/components/qa/QAMetricsPanel.tsx
  • src/components/qa/index.ts
  • src/components/roadmap/RoadmapView.tsx
  • src/components/stories/StoryCard.tsx
  • src/components/stories/StoryCreateModal.tsx
  • src/components/stories/StoryEditModal.tsx
  • src/components/terminals/TerminalStream.tsx
  • src/components/ui/markdown-renderer.tsx
  • src/hooks/use-agents.ts
  • src/hooks/use-aios-status.ts
  • src/hooks/use-realtime-status.ts
  • src/hooks/use-squads.ts
  • src/hooks/use-stories.ts
  • src/hooks/use-terminal-stream.ts
  • src/lib/api.ts
  • src/lib/project-registry.ts
  • src/lib/squad-api-utils.ts
  • src/middleware.ts
  • src/stores/settings-store.ts
  • src/types/index.ts
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • .github/CODEOWNERS

Comment on lines 33 to 35
// Check if gh CLI is authenticated
try {
await execFileAsync('gh', ['auth', 'status']);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

head -100 src/app/api/github/route.ts

Repository: SynkraAI/aiox-dashboard

Length of output: 2622


Add timeouts to prevent hung gh subprocess calls from blocking the request handler.

The gh auth status, gh issue list, gh pr list, and gh repo view calls all run on the request path without timeout protection. If any gh or git process hangs, it will pin the route worker indefinitely until the platform terminates the request. Move execOpts before the auth check and add a timeout value to protect all invocations consistently.

Suggested fix
-    // Check if gh CLI is authenticated
-    try {
-      await execFileAsync('gh', ['auth', 'status']);
+    const execOpts = { cwd: projectRoot, timeout: 10_000 };
+    // Check if gh CLI is authenticated
+    try {
+      await execFileAsync('gh', ['auth', 'status'], execOpts);
     } catch {
       return NextResponse.json(
         {
           error: 'GitHub CLI not authenticated',
           message: 'Run "gh auth login" to authenticate',
         },
         { status: 401 }
       );
     }
 
     // Fetch issues and PRs in parallel, scoped to the project directory
-    const execOpts = { cwd: projectRoot };
     const [issuesResult, prsResult] = await Promise.allSettled([

Also applies to: lines 46–78

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

In `@src/app/api/github/route.ts` around lines 33 - 35, Define shared exec options
with a timeout (e.g., create or move the const execOpts before the auth check)
and pass that execOpts to every execFileAsync call (the 'gh auth status', 'gh
issue list', 'gh pr list', and 'gh repo view' invocations) so all subprocess
calls use the same timeout protection; update calls that currently use
execFileAsync(cmd, args) to execFileAsync(cmd, args, execOpts) and choose a
reasonable timeout value to avoid hanging the request handler.

Comment on lines +16 to +23
// Prevent path traversal
const resolved = path.resolve(filePath);
const docsRoot = path.resolve(path.join(projectRoot, 'docs'));
if (!resolved.startsWith(docsRoot)) {
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
}

const content = await fs.readFile(filePath, 'utf-8');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Tighten the docs-root boundary check.

resolved.startsWith(docsRoot) also accepts sibling paths like .../docs-backup/..., so a slug such as ../docs-backup/secrets can escape the docs directory. Use a separator-safe containment check and read from the canonical resolved path.

Suggested fix
-    const resolved = path.resolve(filePath);
-    const docsRoot = path.resolve(path.join(projectRoot, 'docs'));
-    if (!resolved.startsWith(docsRoot)) {
+    const resolved = path.resolve(filePath);
+    const docsRoot = path.resolve(projectRoot, 'docs');
+    const relative = path.relative(docsRoot, resolved);
+    if (relative.startsWith('..') || path.isAbsolute(relative)) {
       return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
     }
 
-    const content = await fs.readFile(filePath, 'utf-8');
+    const content = await fs.readFile(resolved, 'utf-8');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/api/prds/`[...slug]/route.ts around lines 16 - 23, The current
startsWith check allows sibling directories like "docs-backup" to pass; instead
compute canonical resolved (path.resolve(filePath)) and canonical docsRoot
(path.resolve(path.join(projectRoot, 'docs'))), then use a separator-safe
containment test such as checking path.relative(docsRoot, resolved) — reject if
it begins with '..' or isAbsolute — and finally use the canonical resolved path
when calling fs.readFile; update the validation around resolved, docsRoot,
filePath, path.resolve, path.relative, and fs.readFile accordingly and return
the same NextResponse.json error on failure.

// SWR for API fetch (disabled when using mock data)
const { data, error, isLoading, mutate } = useSWR<StoriesResponse>(
useMockData ? null : '/api/stories', // null key disables fetching
useMockData ? null : apiUrl('/api/stories'), // null key disables fetching
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/lib/api.ts =="
sed -n '1,220p' src/lib/api.ts

echo
echo "== project-selection sources in client code =="
rg -n "selectedProject|currentProject|project\\b|searchParams|URLSearchParams|window\\.location" src/stores src/hooks src/components src/lib -g '!**/node_modules/**'

echo
echo "== sample apiUrl call sites touched by this PR =="
rg -n "apiUrl\\('/api/(stories|github|squads|events|status)'" src

Repository: SynkraAI/aiox-dashboard

Length of output: 4778


🏁 Script executed:

cat -n src/hooks/use-stories.ts

Repository: SynkraAI/aiox-dashboard

Length of output: 3802


🏁 Script executed:

rg -A 8 "const { data, error" src/hooks | head -60

Repository: SynkraAI/aiox-dashboard

Length of output: 176


🏁 Script executed:

rg -A 8 "useSWR" src/hooks | head -100

Repository: SynkraAI/aiox-dashboard

Length of output: 5160


🏁 Script executed:

rg "useProjectsStore|activeProjectId|projects-store" src/hooks

Repository: SynkraAI/aiox-dashboard

Length of output: 49


🏁 Script executed:

rg "useProjectsStore|activeProjectId" src/

Repository: SynkraAI/aiox-dashboard

Length of output: 1288


🏁 Script executed:

rg -B 3 -A 10 "export const useStories" src/hooks/use-stories.ts

Repository: SynkraAI/aiox-dashboard

Length of output: 49


🏁 Script executed:

grep -r "apiUrl.*?project" src/

Repository: SynkraAI/aiox-dashboard

Length of output: 49


🏁 Script executed:

find src -name "*.ts" -path "*/api/*" -o -path "*/app/*" | head -20

Repository: SynkraAI/aiox-dashboard

Length of output: 678


🏁 Script executed:

find src -path "*api/stories*" -type f

Repository: SynkraAI/aiox-dashboard

Length of output: 129


🏁 Script executed:

cat -n src/app/api/stories/route.ts

Repository: SynkraAI/aiox-dashboard

Length of output: 25938


🏁 Script executed:

cat -n src/lib/project-registry.ts | head -100

Repository: SynkraAI/aiox-dashboard

Length of output: 3214


Pass the active project to the API endpoint; otherwise switching projects will always fetch from the default project.

The apiUrl() helper only prepends the base path and does not include project selection. The useStories hook does not import or reference useProjectsStore, so the SWR key never includes the activeProjectId. Meanwhile, the server-side handler in /api/stories calls resolveProjectRoot() without passing the NextRequest object, which means it cannot extract the ?project= query param and always defaults to the default project.

To fix multi-project support: either add the project as a query parameter in the SWR key (e.g., apiUrl('/api/stories?project=${activeProjectId}')), import activeProjectId from useProjectsStore, or pass request to resolveProjectRoot() in the API handler so the server can resolve the correct project root based on the query parameter.

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

In `@src/hooks/use-stories.ts` at line 53, The SWR fetch key in useStories
currently uses apiUrl('/api/stories') and never includes the active project, so
switching projects still fetches the default; update the key to include the
activeProjectId (e.g., apiUrl(`/api/stories?project=${activeProjectId}`) or
similar) by importing activeProjectId from useProjectsStore inside useStories,
and/or update the server handler for /api/stories to call
resolveProjectRoot(request) (pass the NextRequest) so resolveProjectRoot can
read the ?project= query param; ensure the unique symbols involved are
useStories, apiUrl('/api/stories'), useProjectsStore / activeProjectId, the
/api/stories handler, and resolveProjectRoot.

@PedroValerio
Copy link
Copy Markdown

@pedroavalerio @PedroValerio — Hi Pedro, this PR is ready for your review.

It includes all the dashboard improvements we've been working on: multi-project support, real-data API routes, Backlog panel, PRD panel with Mermaid rendering, QA Metrics fix, and CodeRabbit config.

No credentials, tokens, or secrets are included — fully audited. All sensitive config is read from environment variables at runtime.

Please take a look when you get a chance. Thanks!

LGTM but I‘ll defer to @pedroavalerio

rafaelscosta added a commit that referenced this pull request Mar 10, 2026
Auto-Experiment #3: Bundle size optimization via code splitting.

Lazy loading (React.lazy + Suspense):
- page.tsx: 11 view panels lazy-loaded (only active view loads)
- AppLayout: AgentExplorer, GlobalSearch, KeyboardShortcuts lazy-loaded
  (modal/overlay components only load when opened)

Barrel export cleanup (export * → named exports):
- stories, kanban, terminals, terminal, roadmap, qa, github,
  context, insights, settings, agents, workflow index.ts files
- Enables better tree-shaking by making imports explicit

Build passes, 65/65 tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

2 participants