Skip to content

feat: enable passwordless public overview dashboard as landing page#228

Merged
seanhanca merged 3 commits into
mainfrom
feat/enable-overview-to-be-pwdless
Apr 2, 2026
Merged

feat: enable passwordless public overview dashboard as landing page#228
seanhanca merged 3 commits into
mainfrom
feat/enable-overview-to-be-pwdless

Conversation

@seanhanca
Copy link
Copy Markdown
Contributor

@seanhanca seanhanca commented Apr 2, 2026

Summary

  • Transform the root path (/) into a live public overview dashboard visible to unauthenticated users, removing the login gate for first-time visitors
  • Extract ~1,400 lines of dashboard rendering into a shared OverviewContent component used by both the public landing page (via REST APIs) and the authenticated dashboard (via GraphQL plugin event bus)
  • Add PublicTopBar with Sign In / Get Started CTAs, a dismissible AuthCTABanner, and "Back to Overview" links on login/register pages for a seamless user journey

Changes

File What
middleware.ts Allow unauthenticated /; redirect authed users to /dashboard
page.tsx (root) Replace static hero with live PublicOverviewPage using usePublicDashboard
dashboard/page.tsx Extract rendering to OverviewContent, keep GraphQL data fetching
overview-content.tsx New — shared dashboard rendering (~1,400 lines)
usePublicDashboard.ts New — hook fetching all dashboard data from public REST APIs
public-top-bar.tsx New — minimal top bar with auth CTAs
auth-cta-banner.tsx New — dismissible banner prompting sign-in
login-form.tsx Add "Back to Overview" link
register-form.tsx Add "Back to Overview" link
auth-context.tsx Logout redirects to / instead of /login

User Journey

Unauthenticated visitor → / (live overview dashboard + CTAs)
                        → /login or /register (with "Back to Overview" escape)
                        → /dashboard (authenticated, full features)
                        → Logout → / (back to public overview)

Returning authenticated user → / → auto-redirect → /dashboard

Test plan

  • Unauthenticated user at / sees live public overview dashboard
  • PublicTopBar shows "Sign In" and "Get Started" buttons
  • AuthCTABanner appears and can be dismissed (persists in sessionStorage)
  • Login and register pages have "Back to Overview" link
  • Authenticated user at / is redirected to /dashboard
  • /dashboard still requires authentication (no regression)
  • Logout redirects to / not /login
  • Protected routes (/settings, /wallet) still require auth
  • Vercel build succeeds with zero errors

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Public-facing Network Overview dashboard with KPIs, pipelines, fees, GPU capacity, orchestrators, and live job feed.
    • Authentication CTA banner on public pages.
    • Back-to-Overview links added to login and registration pages.
    • New public top navigation bar with Sign In / Get Started links.
  • Refactor

    • Dashboard UI consolidated into a reusable overview component shared by public and authenticated pages.
    • Root path now serves a public overview for unauthenticated visitors.
    • Logout now redirects to the public home page.
  • Tests

    • Page tests updated to cover public overview rendering and auth links.

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 2, 2026

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

Project Deployment Actions Updated (UTC)
naap-platform Ready Ready Preview, Comment Apr 2, 2026 7:09pm

Request Review

@github-actions github-actions Bot added scope/shell Shell app changes size/XL Extra large PR (500+ lines) labels Apr 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 2, 2026

⚠️ This PR is very large (3363 lines changed). Please split it into smaller, focused PRs if possible.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 2, 2026

Warning

Rate limit exceeded

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

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ff1396d3-2452-41cd-858d-6a00d12ea0ae

📥 Commits

Reviewing files that changed from the base of the PR and between 675a6e0 and 637de1e.

📒 Files selected for processing (11)
  • apps/web-next/src/__tests__/page.test.tsx
  • apps/web-next/src/app/(auth)/login/login-form.tsx
  • apps/web-next/src/app/(auth)/register/register-form.tsx
  • apps/web-next/src/app/(dashboard)/dashboard/page.tsx
  • apps/web-next/src/app/page.tsx
  • apps/web-next/src/components/dashboard/auth-cta-banner.tsx
  • apps/web-next/src/components/dashboard/overview-content.tsx
  • apps/web-next/src/components/layout/public-top-bar.tsx
  • apps/web-next/src/contexts/auth-context.tsx
  • apps/web-next/src/hooks/usePublicDashboard.ts
  • apps/web-next/src/middleware.ts
📝 Walkthrough

Walkthrough

Adds a public network overview page and shared OverviewContent component, introduces public data-fetching hook and UI (PublicTopBar, AuthCTABanner), refactors dashboard pages to delegate rendering, adjusts auth logout behavior, and permits unauthenticated access to the root path.

Changes

Cohort / File(s) Summary
Auth Form Navigation
apps/web-next/src/app/(auth)/login/login-form.tsx, apps/web-next/src/app/(auth)/register/register-form.tsx
Added a top “Back to Overview” Link with ArrowLeft icon; updated lucide-react imports. UI-only change.
Dashboard Page Refactor
apps/web-next/src/app/(dashboard)/dashboard/page.tsx
Replaced large inline dashboard rendering with delegation to OverviewContent; retained prefs init and GraphQL queries; removed inline REST capacity/perf fetches and related state.
Public Overview Page
apps/web-next/src/app/page.tsx
Replaced static HomePage with client PublicOverviewPage; reads/persists prefs from localStorage, gates fetching with prefsReady, and composes PublicTopBar + OverviewContent via usePublicDashboard.
OverviewContent Component
apps/web-next/src/components/dashboard/overview-content.tsx
New client component and OverviewContentProps implementing the shared dashboard UI for public/auth pages; runs additional REST enrichment fetches (capacity, live-video, perf-by-model), aggregates errors, and renders KPI/fees/GPU/pipelines/jobs/orchestrators.
Public UI Components
apps/web-next/src/components/layout/public-top-bar.tsx, apps/web-next/src/components/dashboard/auth-cta-banner.tsx
Added PublicTopBar (header with login/register links) and AuthCTABanner (sessionStorage-dismissable banner linking to login/register).
Public Dashboard Hook
apps/web-next/src/hooks/usePublicDashboard.ts
New usePublicDashboard hook fetching multiple /api/v1/dashboard/... endpoints via Promise.allSettled, returns {data,loading,refreshing,error,refetch}, and optionally polls only the job-feed.
Auth & Middleware
apps/web-next/src/contexts/auth-context.tsx, apps/web-next/src/middleware.ts
Logout now clears client auth storage and hard-navigates to / (no React state clear); middleware allows unauthenticated requests to / to fall through while authenticated requests still redirect to /dashboard.
Tests
apps/web-next/src/__tests__/page.test.tsx
Updated tests to mock usePublicDashboard, OverviewContent, and PublicTopBar; replaced previous HomePage assertions with checks for public top bar, auth links, overview-content render, and localStorage mock setup.

Sequence Diagram(s)

sequenceDiagram
    participant Browser
    participant PublicPage as PublicOverviewPage
    participant LocalStorage as localStorage
    participant UseHook as usePublicDashboard
    participant RESTApi as REST API (/api/v1/dashboard/...)
    participant JobFeed as JobFeedPolling
    Browser->>PublicPage: GET /
    PublicPage->>LocalStorage: read prefs (timeframe, pollInterval)
    LocalStorage-->>PublicPage: prefs or defaults
    PublicPage->>UseHook: call usePublicDashboard(timeframe, pollInterval)
    UseHook->>RESTApi: fetch KPI, pipelines, catalog, orchestrators, protocol, GPU, pricing, fees, job-feed
    RESTApi-->>UseHook: return aggregated data
    UseHook-->>PublicPage: data, loading, error, refetch
    PublicPage->>Browser: render PublicTopBar + OverviewContent
    alt jobFeed polling enabled
        UseHook->>JobFeed: start poll at interval
        loop every interval
            JobFeed->>RESTApi: fetch job-feed
            RESTApi-->>JobFeed: jobs
            JobFeed-->>UseHook: update jobs state
            UseHook-->>OverviewContent: update jobs
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • eliteprox
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.36% 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 'feat: enable passwordless public overview dashboard as landing page' directly describes the main change: making the root landing page a public, unauthenticated-accessible overview dashboard.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enable-overview-to-be-pwdless

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR makes / a public, unauthenticated “Network Overview” landing dashboard, while keeping /dashboard as the authenticated experience, by extracting the bulk of dashboard rendering into a shared OverviewContent component and adding public-facing UI elements (top bar + auth CTA banner).

Changes:

  • Update middleware behavior for / to allow unauthenticated access (while redirecting authenticated users to /dashboard).
  • Introduce OverviewContent shared dashboard renderer and refactor authenticated /dashboard page to use it.
  • Add a public landing page implementation that fetches dashboard data via REST (usePublicDashboard) plus public UI components (PublicTopBar, AuthCTABanner) and “Back to Overview” links from auth pages.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
apps/web-next/src/middleware.ts Allows unauthenticated access to / while redirecting authed users to /dashboard.
apps/web-next/src/app/page.tsx Replaces the static homepage with a public overview dashboard page.
apps/web-next/src/hooks/usePublicDashboard.ts Adds a REST-based data hook for the public dashboard experience.
apps/web-next/src/components/dashboard/overview-content.tsx New shared dashboard rendering component used by both public and authed pages.
apps/web-next/src/app/(dashboard)/dashboard/page.tsx Refactors authenticated dashboard page to delegate rendering to OverviewContent.
apps/web-next/src/components/layout/public-top-bar.tsx Adds a public top bar with Sign In / Get Started CTAs.
apps/web-next/src/components/dashboard/auth-cta-banner.tsx Adds a dismissible CTA banner on the public overview.
apps/web-next/src/contexts/auth-context.tsx Changes logout hard-navigation destination from /login to /.
apps/web-next/src/app/(auth)/login/login-form.tsx Adds “Back to Overview” link.
apps/web-next/src/app/(auth)/register/register-form.tsx Adds “Back to Overview” link.

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

Comment on lines +141 to +158
// Job feed polling (only polls job-feed, not everything)
useEffect(() => {
if (skip || !jobFeedPollInterval || jobFeedPollInterval <= 0) return;
if (!hasFetchedRef.current) return;

const id = setInterval(async () => {
const result = await fetchJson<{ streams: JobFeedEntry[]; queryFailed?: boolean }>(`${API}/job-feed`);
if (mountedRef.current && result) {
setData(prev => ({
...prev,
jobs: result.streams ?? [],
jobFeedConnected: !result.queryFailed,
}));
}
}, jobFeedPollInterval);

return () => clearInterval(id);
}, [skip, jobFeedPollInterval]);
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

The job-feed polling effect will never start: it bails out when hasFetchedRef.current is false on mount, and later setting hasFetchedRef.current = true does not trigger a re-render/effect re-run (refs aren’t reactive). As a result, the interval is never created and the public job feed won’t update.

Consider deriving a reactive “hasFetched” state (or using loading/refreshing/skip as dependencies) so the polling effect can start after the initial fetch completes, or remove the hasFetchedRef guard and allow polling immediately.

Copilot uses AI. Check for mistakes.
Comment on lines +53 to +60
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url);
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

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

error is effectively never set for network/HTTP failures because fetchJson swallows exceptions and returns null on non-2xx responses. fetchAll then treats these as successful results (and even calls setError(null)), so consumers can’t distinguish “data unavailable due to an error” from “API returned empty data”.

If error is intended to be meaningful, consider having fetchJson throw (or return a {data, error} shape) on non-OK responses so fetchAll can set an error state when one or more endpoints fail.

Suggested change
async function fetchJson<T>(url: string): Promise<T | null> {
try {
const res = await fetch(url);
if (!res.ok) return null;
return await res.json();
} catch {
return null;
}
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Request to ${url} failed with status ${res.status}`);
}
return (await res.json()) as T;

Copilot uses AI. Check for mistakes.
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

Caution

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

⚠️ Outside diff range comments (1)
apps/web-next/src/contexts/auth-context.tsx (1)

303-320: ⚠️ Potential issue | 🟠 Major

Make logout best-effort on the client too.

await fetch(...) makes sign-out depend on the network, and this path only calls clearAllAuthStorage(), which does not clear the client-readable naap_auth_token cookie written by setTokenStorage(). If that request never reaches the server, middleware can still treat / as authenticated and bounce the user right back to /dashboard; window.location.href also keeps the protected page in history. Clear the local token first, fire the revoke in the background, and use window.location.replace('/').

🛠️ One way to keep the client teardown immediate
   const logout = useCallback(async () => {
     const token = getToken();
+    setTokenStorage(null);
+    clearAllAuthStorage();
+
     if (token) {
-      try {
-        await fetch(`${API_BASE}/v1/auth/logout`, {
-          method: 'POST',
-          headers: { Authorization: `Bearer ${token}` },
-          credentials: 'include',
-        });
-      } catch (error) {
-        console.error('Logout error:', error);
-      }
+      void fetch(`${API_BASE}/v1/auth/logout`, {
+        method: 'POST',
+        headers: { Authorization: `Bearer ${token}` },
+        credentials: 'include',
+        keepalive: true,
+      }).catch((error) => {
+        console.error('Logout error:', error);
+      });
     }
-    clearAllAuthStorage();
-    // Navigate BEFORE updating React state to prevent RequireAuth from
-    // racing us to /login when isAuthenticated flips to false.
-    window.location.href = '/';
+    window.location.replace('/');
   }, [getToken]);

Based on learnings, logout follows a best-effort revoke / guaranteed clear contract.

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

In `@apps/web-next/src/contexts/auth-context.tsx` around lines 303 - 320, The
logout function currently awaits the network revoke which can delay client
teardown and doesn't clear the client-readable cookie set by setTokenStorage;
change logout to first clear local auth (call clearAllAuthStorage and explicitly
remove/overwrite the client token cookie set by setTokenStorage so
naap_auth_token is gone), then fire the POST to `${API_BASE}/v1/auth/logout`
without awaiting (best-effort background revoke) and finally navigate with
window.location.replace('/') to avoid keeping the protected page in history;
keep getToken usage only for the background call and ensure any network errors
are caught/logged but do not block the clear-and-redirect flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web-next/src/components/dashboard/overview-content.tsx`:
- Around line 1093-1154: When an enrichment fetch returns a non-OK or otherwise
invalid response, clear the stale state and reset dedupe refs so old values
don't persist: in the net-capacity effect clear setNetCapacity({}) and set
lastFetchedNetCapacityKeyRef.current = null when body is null/invalid or in the
catch (unless cancelled); in the live-video effect clear
setLiveVideoCapacity({}) and reset liveVideoCapacityModelsRef.current = '' on
invalid body or in the catch (unless cancelled); in the perf-by-model effect
clear setModelFpsByPipelineModel({}) on invalid body or in the catch (unless
cancelled). Update the promise chains and catch handlers that call
fetch('/api/v1/network/capacity'),
fetch(`/api/v1/network/live-video-capacity?...`) and
fetch(`/api/v1/network/perf-by-model?...`) accordingly, referencing
lastFetchedNetCapacityKeyRef, liveVideoCapacityModelsRef, setNetCapacity,
setLiveVideoCapacity, and setModelFpsByPipelineModel.
- Around line 1183-1244: The dashboard grids use fixed two-column layouts and a
hard 600px row height which break on small screens; update the grid markup and
styles so sections stack to a single column below desktop widths and
remove/relax the fixed row height on mobile. Specifically, replace the inline
gridTemplateColumns and gridAutoRows usage with responsive classes (e.g., use
grid-cols-1 for small screens and md:grid-cols-2 for desktop) on the containers
that render KPIGroupCard, ProtocolFeesCard, GPUCapacityCard, PipelinesCard,
JobFeedCard and OrchestratorTableCard, and change the fixed gridAutoRows:'600px'
to a responsive rule (e.g., no fixed height on small screens and apply a
min-height or fixed height only at md+), ensuring RefreshWrap and child cards
inherit flexible heights so no horizontal overflow or squeezed content occurs.

In `@apps/web-next/src/hooks/usePublicDashboard.ts`:
- Around line 91-132: fetchAll can return out-of-order results and overwrite
newer state; add a request-order guard by introducing a fetchIdRef (e.g.,
useRef<number>) that you increment at start of fetchAll, capture into a local
const currentId, and only call setData, setError, setLoading(false), and update
hasFetchedRef.current if currentId === fetchIdRef.current; keep existing
mountedRef checks. Reference fetchAll, mountedRef, hasFetchedRef, setData,
setError, and setLoading when applying the guard so older responses are ignored
and loading is only cleared for the latest request.

---

Outside diff comments:
In `@apps/web-next/src/contexts/auth-context.tsx`:
- Around line 303-320: The logout function currently awaits the network revoke
which can delay client teardown and doesn't clear the client-readable cookie set
by setTokenStorage; change logout to first clear local auth (call
clearAllAuthStorage and explicitly remove/overwrite the client token cookie set
by setTokenStorage so naap_auth_token is gone), then fire the POST to
`${API_BASE}/v1/auth/logout` without awaiting (best-effort background revoke)
and finally navigate with window.location.replace('/') to avoid keeping the
protected page in history; keep getToken usage only for the background call and
ensure any network errors are caught/logged but do not block the
clear-and-redirect flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f9869040-13c9-4a9e-8aa3-9e5e0105d1c3

📥 Commits

Reviewing files that changed from the base of the PR and between 6b877ff and dcd37e4.

📒 Files selected for processing (10)
  • apps/web-next/src/app/(auth)/login/login-form.tsx
  • apps/web-next/src/app/(auth)/register/register-form.tsx
  • apps/web-next/src/app/(dashboard)/dashboard/page.tsx
  • apps/web-next/src/app/page.tsx
  • apps/web-next/src/components/dashboard/auth-cta-banner.tsx
  • apps/web-next/src/components/dashboard/overview-content.tsx
  • apps/web-next/src/components/layout/public-top-bar.tsx
  • apps/web-next/src/contexts/auth-context.tsx
  • apps/web-next/src/hooks/usePublicDashboard.ts
  • apps/web-next/src/middleware.ts

Comment on lines +1093 to +1154
useEffect(() => {
if (!prefsReady || rtLoading || lbLoading) return;
if (!pipelineCatalog?.length) return;

const catalogKeyPart = [...pipelineCatalog]
.map((e) => ({ id: e.id, models: [...e.models].sort() }))
.sort((a, b) => a.id.localeCompare(b.id));
const pricingKeyPart = [...pricing]
.map((p) => ({ pipeline: p.pipeline, model: p.model ?? '', capacity: p.capacity ?? null }))
.sort((a, b) => a.pipeline.localeCompare(b.pipeline) || a.model.localeCompare(b.model));
const key = JSON.stringify({ catalog: catalogKeyPart, pricing: pricingKeyPart });

if (!catalogNeedsNetCapacityFetch(pipelineCatalog, pricing)) {
lastFetchedNetCapacityKeyRef.current = null;
setNetCapacity({});
return;
}
if (lastFetchedNetCapacityKeyRef.current === key) return;

let cancelled = false;
fetch('/api/v1/network/capacity')
.then((res) => (res.ok ? res.json() : null))
.then((body: { capacityByPipelineModel?: Record<string, number> } | null) => {
if (cancelled || !body?.capacityByPipelineModel || typeof body.capacityByPipelineModel !== 'object') return;
setNetCapacity(body.capacityByPipelineModel);
lastFetchedNetCapacityKeyRef.current = key;
})
.catch(() => {});
return () => { cancelled = true; };
}, [prefsReady, rtLoading, lbLoading, pipelineCatalog, pricing]);

useEffect(() => {
if (!prefsReady || !pipelineCatalog?.length) return;
const liveVideoEntry = pipelineCatalog.find((e) => e.id === LIVE_VIDEO_PIPELINE_ID);
if (!liveVideoEntry?.models.length) return;
const modelsKey = [...liveVideoEntry.models].sort().join(',');
if (liveVideoCapacityModelsRef.current === modelsKey) return;
liveVideoCapacityModelsRef.current = modelsKey;
let cancelled = false;
fetch(`/api/v1/network/live-video-capacity?models=${encodeURIComponent(liveVideoEntry.models.join(','))}`)
.then((res) => (res.ok ? res.json() : null))
.then((body: { capacityByModel?: Record<string, number> } | null) => {
if (cancelled || !body?.capacityByModel || typeof body.capacityByModel !== 'object') return;
setLiveVideoCapacity(body.capacityByModel);
})
.catch(() => { if (!cancelled) liveVideoCapacityModelsRef.current = ''; });
return () => { cancelled = true; };
}, [prefsReady, pipelineCatalog]);

useEffect(() => {
if (!prefsReady) return;
const { start, end } = getTimeframeRangeIso(timeframe);
let cancelled = false;
fetch(`/api/v1/network/perf-by-model?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`)
.then((res) => (res.ok ? res.json() : null))
.then((body: { fpsByPipelineModel?: Record<string, number> } | null) => {
if (cancelled || !body?.fpsByPipelineModel || typeof body.fpsByPipelineModel !== 'object') return;
setModelFpsByPipelineModel(body.fpsByPipelineModel);
})
.catch(() => {});
return () => { cancelled = true; };
}, [prefsReady, timeframe]);
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 | 🟠 Major

Reset stale enrichment state when these fetches miss.

These effects only update state on success. If one of the enrichment calls returns non-OK after the timeframe or model set changes, PipelinesCard keeps showing the previous FPS/capacity values under the new labels. On the live-video branch, liveVideoCapacityModelsRef.current is also filled before the request, so a 503 for the same model set suppresses every retry until the catalog changes. Clear the relevant maps, and reset the live-video dedupe ref, on invalid responses so stale numbers do not linger indefinitely.

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

In `@apps/web-next/src/components/dashboard/overview-content.tsx` around lines
1093 - 1154, When an enrichment fetch returns a non-OK or otherwise invalid
response, clear the stale state and reset dedupe refs so old values don't
persist: in the net-capacity effect clear setNetCapacity({}) and set
lastFetchedNetCapacityKeyRef.current = null when body is null/invalid or in the
catch (unless cancelled); in the live-video effect clear
setLiveVideoCapacity({}) and reset liveVideoCapacityModelsRef.current = '' on
invalid body or in the catch (unless cancelled); in the perf-by-model effect
clear setModelFpsByPipelineModel({}) on invalid body or in the catch (unless
cancelled). Update the promise chains and catch handlers that call
fetch('/api/v1/network/capacity'),
fetch(`/api/v1/network/live-video-capacity?...`) and
fetch(`/api/v1/network/perf-by-model?...`) accordingly, referencing
lastFetchedNetCapacityKeyRef, liveVideoCapacityModelsRef, setNetCapacity,
setLiveVideoCapacity, and setModelFpsByPipelineModel.

Comment on lines +1183 to +1244
{/* Row 1: [KPI 2×2 box] [Protocol + Fees box] */}
<section>
<div className="grid gap-3 items-stretch [&>*]:h-full [&>*]:min-h-0" style={{ gridTemplateColumns: '3fr 2fr' }}>
{kpi ? (
<RefreshWrap refreshing={lbRefreshing} className="h-full"><KPIGroupCard data={kpi} /></RefreshWrap>
) : uiLbLoading ? <WidgetSkeleton /> : <WidgetUnavailable label="KPI" />}

{protocol && fees ? (
<RefreshWrap refreshing={rtRefreshing || feesRefreshing} className="h-full min-h-0 flex flex-col">
<ProtocolFeesCard protocol={protocol} fees={fees} />
</RefreshWrap>
) : (uiRtLoading || uiFeesLoading) ? <WidgetSkeleton /> : (
<div className="flex flex-col gap-3">
{protocol ? <ProtocolCard data={protocol} /> : uiRtLoading ? <WidgetSkeleton /> : <WidgetUnavailable label="Protocol" />}
{fees ? <FeesCard data={fees} /> : uiFeesLoading ? <WidgetSkeleton /> : <WidgetUnavailable label="Fees" />}
</div>
)}
</div>
</section>

{/* Row 2: [Network GPUs] [Pipelines] */}
<section>
<div className="grid grid-cols-2 gap-3 items-stretch [&>*]:h-full [&>*]:min-h-0">
{gpuCapacity ? (
<RefreshWrap refreshing={rtRefreshing} className="h-full min-h-0 flex flex-col">
<GPUCapacityCard data={gpuCapacity} timeframeHours={kpi?.timeframeHours ?? 12} />
</RefreshWrap>
) : uiRtLoading ? <WidgetSkeleton /> : <WidgetUnavailable label="GPU Capacity" />}
{pipelineCatalog != null && pipelineCatalog.length > 0 ? (
<RefreshWrap refreshing={lbRefreshing || rtRefreshing} className="h-full min-h-0 flex flex-col">
<PipelinesCard
data={pipelines}
catalog={pipelineCatalog}
pricing={pricing}
netCapacity={netCapacity}
liveVideoCapacity={liveVideoCapacity}
modelFpsByPipelineModel={modelFpsByPipelineModel}
timeframeHours={kpi?.timeframeHours ?? 12}
/>
</RefreshWrap>
) : uiLbLoading ? <WidgetSkeleton /> : <WidgetUnavailable label="Pipelines" />}
</div>
</section>

{/* Row 3: [Live Job Feed] [Orchestrators table] */}
<section>
<div className="grid gap-3 items-stretch [&>*]:h-full [&>*]:min-h-0" style={{ gridTemplateColumns: '2fr 3fr', gridAutoRows: '600px' }}>
<JobFeedCard
jobs={jobs}
connected={jobFeedConnected}
pollInterval={jobFeedPollInterval}
onPollIntervalChange={onJobFeedPollIntervalChange}
feedMeta={jobFeedMeta}
feedError={jobFeedError}
/>
{orchestrators.length > 0 ? (
<RefreshWrap refreshing={lbRefreshing} className="h-full min-h-0 flex flex-col">
<OrchestratorTableCard data={orchestrators} catalog={pipelineCatalog} />
</RefreshWrap>
) : uiLbLoading ? <WidgetSkeleton className="h-full" /> : <WidgetUnavailable label="Orchestrators" />}
</div>
</section>
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 | 🟠 Major

Collapse these dashboard grids below desktop widths.

The new landing page stays in fixed two-column desktop grids, and the job/orchestrator section keeps a hard 600px row height at all widths. On smaller screens that squeezes tables/cards into unreadable columns or forces horizontal overflow. Add responsive breakpoints that stack these sections and relax the fixed height on mobile.

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

In `@apps/web-next/src/components/dashboard/overview-content.tsx` around lines
1183 - 1244, The dashboard grids use fixed two-column layouts and a hard 600px
row height which break on small screens; update the grid markup and styles so
sections stack to a single column below desktop widths and remove/relax the
fixed row height on mobile. Specifically, replace the inline gridTemplateColumns
and gridAutoRows usage with responsive classes (e.g., use grid-cols-1 for small
screens and md:grid-cols-2 for desktop) on the containers that render
KPIGroupCard, ProtocolFeesCard, GPUCapacityCard, PipelinesCard, JobFeedCard and
OrchestratorTableCard, and change the fixed gridAutoRows:'600px' to a responsive
rule (e.g., no fixed height on small screens and apply a min-height or fixed
height only at md+), ensuring RefreshWrap and child cards inherit flexible
heights so no horizontal overflow or squeezed content occurs.

Comment on lines +91 to +132
const fetchAll = useCallback(async () => {
if (!mountedRef.current) return;
setLoading(true);

try {
const period = timeframeToPeriod(timeframe);
const [kpi, pipelines, catalog, orchestrators, protocol, gpuCap, pricing, fees, jobFeedRaw] =
await Promise.all([
fetchJson<DashboardKPI>(`${API}/kpi?timeframe=${timeframe}`),
fetchJson<DashboardPipelineUsage[]>(`${API}/pipelines?timeframe=${timeframe}&limit=50`),
fetchJson<DashboardPipelineCatalogEntry[]>(`${API}/pipeline-catalog`),
fetchJson<DashboardOrchestrator[]>(`${API}/orchestrators?period=${period}`),
fetchJson<DashboardProtocol>(`${API}/protocol`),
fetchJson<DashboardGPUCapacity>(`${API}/gpu-capacity?timeframe=${timeframe}`),
fetchJson<DashboardPipelinePricing[]>(`${API}/pricing`),
fetchJson<DashboardFeesInfo>(`${API}/fees?days=180`),
fetchJson<{ streams: JobFeedEntry[]; queryFailed?: boolean }>(`${API}/job-feed`),
]);

if (!mountedRef.current) return;

setData({
kpi: kpi,
pipelines: pipelines ?? [],
pipelineCatalog: catalog ?? [],
orchestrators: orchestrators ?? [],
protocol: protocol,
gpuCapacity: gpuCap,
pricing: pricing ?? [],
fees: fees,
jobs: jobFeedRaw?.streams ?? [],
jobFeedConnected: !!(jobFeedRaw && !jobFeedRaw.queryFailed),
});
setError(null);
hasFetchedRef.current = true;
} catch (err) {
if (!mountedRef.current) return;
setError((err as Error)?.message ?? 'Failed to fetch dashboard data');
} finally {
if (mountedRef.current) setLoading(false);
}
}, [timeframe]);
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 | 🟠 Major

Sequence fetchAll() results so old responses can't win.

fetchAll() can be in flight from the initial load, a timeframe change, and refetch(). Without a request token or abort signal, the slowest call wins and can overwrite newer dashboard data or clear loading while a newer request is still running.

🛠️ A lightweight request-order guard
   const [error, setError] = useState<string | null>(null);
   const mountedRef = useRef(true);
+  const requestIdRef = useRef(0);
@@
   const fetchAll = useCallback(async () => {
+    const requestId = ++requestIdRef.current;
     if (!mountedRef.current) return;
     setLoading(true);
@@
-      if (!mountedRef.current) return;
+      if (!mountedRef.current || requestId !== requestIdRef.current) return;
@@
-      if (!mountedRef.current) return;
+      if (!mountedRef.current || requestId !== requestIdRef.current) return;
       setError((err as Error)?.message ?? 'Failed to fetch dashboard data');
     } finally {
-      if (mountedRef.current) setLoading(false);
+      if (mountedRef.current && requestId === requestIdRef.current) {
+        setLoading(false);
+      }
     }
   }, [timeframe]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web-next/src/hooks/usePublicDashboard.ts` around lines 91 - 132,
fetchAll can return out-of-order results and overwrite newer state; add a
request-order guard by introducing a fetchIdRef (e.g., useRef<number>) that you
increment at start of fetchAll, capture into a local const currentId, and only
call setData, setError, setLoading(false), and update hasFetchedRef.current if
currentId === fetchIdRef.current; keep existing mountedRef checks. Reference
fetchAll, mountedRef, hasFetchedRef, setData, setError, and setLoading when
applying the guard so older responses are ignored and loading is only cleared
for the latest request.

eliteprox
eliteprox previously approved these changes Apr 2, 2026
Copy link
Copy Markdown
Contributor

@eliteprox eliteprox left a comment

Choose a reason for hiding this comment

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

This looks good to me! We could improve the verbiage a little bit. I suggest the title could be "Network Platform" for the site header and "Overview" as the subheading.

Image

All of the review comments are related to the metrics overview feature itself and will be addressed in other PRs. LGTM

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.

♻️ Duplicate comments (2)
apps/web-next/src/components/dashboard/overview-content.tsx (2)

1113-1120: ⚠️ Potential issue | 🟠 Major

Clear stale enrichment state on non-OK responses.

The handlers in apps/web-next/src/app/api/v1/network/capacity/route.ts Lines 1-18, apps/web-next/src/app/api/v1/network/live-video-capacity/route.ts Lines 1-27, and apps/web-next/src/app/api/v1/network/perf-by-model/route.ts Lines 1-54 return 503 { error: ... } bodies, so these branches just early-return on outages. That leaves the previous capacity/FPS values rendered under the new catalog/timeframe, and the live-video path also keeps liveVideoCapacityModelsRef.current set, which suppresses retries for the same model set after a non-OK response.

Also applies to: 1128-1138, 1146-1152

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

In `@apps/web-next/src/components/dashboard/overview-content.tsx` around lines
1113 - 1120, The fetch handlers currently early-return on non-OK responses and
errors, leaving previous capacity state/stale refs; update each fetch
(.then/res.ok branches and the .catch) to explicitly clear stale enrichment
state on failures by calling the relevant state resetters (e.g., call
setNetCapacity(null) and reset lastFetchedNetCapacityKeyRef.current = undefined
in the capacity fetch branch), and similarly clear live-video state refs (reset
liveVideoCapacityModelsRef.current = undefined and call
setLiveVideoCapacity(null)) and any perf-by-model state when their responses are
non-OK or on catch so retries and rendering don't retain old values.

1185-1200: ⚠️ Potential issue | 🟠 Major

Make these dashboard grids responsive before exposing them publicly.

These rows still force desktop two-column layouts at every breakpoint, and the last row is pinned to 600px. On the new public landing page that squeezes the cards/tables on phones and can force overflow instead of stacking cleanly.

Also applies to: 1205-1223, 1229-1242

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

In `@apps/web-next/src/components/dashboard/overview-content.tsx` around lines
1185 - 1200, The grid forces a two-column desktop layout and uses inline fixed
gridTemplateColumns which prevents responsive stacking; update the container and
affected rows (the div using style={{ gridTemplateColumns: '3fr 2fr' }} and the
similar blocks at the other ranges) to use responsive Tailwind utilities instead
of the inline style and any fixed min-height classes: replace the inline style
with className="grid grid-cols-1 md:[grid-template-columns:3fr_2fr] gap-3
items-stretch [&>*]:h-full [&>*]:min-h-0" (or equivalent responsive Tailwind
utilities), and remove/replace any hard-coded min-h-600px or fixed heights in
the nearby components (e.g., the blocks wrapping
RefreshWrap/ProtocolFeesCard/ProtocolCard/FeesCard) with responsive min-h
utilities like md:min-h-[600px] only if needed so cards stack to one column on
small screens and expand on md+. Ensure references to RefreshWrap, KPIGroupCard,
ProtocolFeesCard, ProtocolCard and FeesCard keep their existing layout classes
but drop fixed heights that force overflow.
🧹 Nitpick comments (1)
apps/web-next/src/__tests__/page.test.tsx (1)

26-27: Assert the OverviewContent prop contract, not just its presence.

The stub throws away every prop, so this suite still passes if apps/web-next/src/app/page.tsx Lines 29-79 stops forwarding isPublic or any of the new dashboard fields into OverviewContent. Capture the props in the mock and assert a minimal objectContaining(...) contract so the page wiring is actually covered.

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

In `@apps/web-next/src/__tests__/page.test.tsx` around lines 26 - 27, The test
currently stubs OverviewContent but discards all props; change the mock in
page.test.tsx to capture and expose the props so the test can assert the prop
contract: create a jest.fn named mockOverviewProps and implement the mock as
OverviewContent = (props) => { mockOverviewProps(props); return <div
data-testid="overview-content">Overview Content</div>; }; then update the
assertions to verify mockOverviewProps was called with
expect.objectContaining({...}) including isPublic and the expected dashboard
fields forwarded from page.tsx (use expect.any(...) for complex subfields as
needed).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web-next/src/components/dashboard/overview-content.tsx`:
- Around line 1113-1120: The fetch handlers currently early-return on non-OK
responses and errors, leaving previous capacity state/stale refs; update each
fetch (.then/res.ok branches and the .catch) to explicitly clear stale
enrichment state on failures by calling the relevant state resetters (e.g., call
setNetCapacity(null) and reset lastFetchedNetCapacityKeyRef.current = undefined
in the capacity fetch branch), and similarly clear live-video state refs (reset
liveVideoCapacityModelsRef.current = undefined and call
setLiveVideoCapacity(null)) and any perf-by-model state when their responses are
non-OK or on catch so retries and rendering don't retain old values.
- Around line 1185-1200: The grid forces a two-column desktop layout and uses
inline fixed gridTemplateColumns which prevents responsive stacking; update the
container and affected rows (the div using style={{ gridTemplateColumns: '3fr
2fr' }} and the similar blocks at the other ranges) to use responsive Tailwind
utilities instead of the inline style and any fixed min-height classes: replace
the inline style with className="grid grid-cols-1
md:[grid-template-columns:3fr_2fr] gap-3 items-stretch [&>*]:h-full
[&>*]:min-h-0" (or equivalent responsive Tailwind utilities), and remove/replace
any hard-coded min-h-600px or fixed heights in the nearby components (e.g., the
blocks wrapping RefreshWrap/ProtocolFeesCard/ProtocolCard/FeesCard) with
responsive min-h utilities like md:min-h-[600px] only if needed so cards stack
to one column on small screens and expand on md+. Ensure references to
RefreshWrap, KPIGroupCard, ProtocolFeesCard, ProtocolCard and FeesCard keep
their existing layout classes but drop fixed heights that force overflow.

---

Nitpick comments:
In `@apps/web-next/src/__tests__/page.test.tsx`:
- Around line 26-27: The test currently stubs OverviewContent but discards all
props; change the mock in page.test.tsx to capture and expose the props so the
test can assert the prop contract: create a jest.fn named mockOverviewProps and
implement the mock as OverviewContent = (props) => { mockOverviewProps(props);
return <div data-testid="overview-content">Overview Content</div>; }; then
update the assertions to verify mockOverviewProps was called with
expect.objectContaining({...}) including isPublic and the expected dashboard
fields forwarded from page.tsx (use expect.any(...) for complex subfields as
needed).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e50ad2e5-b379-463d-9a66-2c7669b3bb1d

📥 Commits

Reviewing files that changed from the base of the PR and between dcd37e4 and 675a6e0.

📒 Files selected for processing (3)
  • apps/web-next/src/__tests__/page.test.tsx
  • apps/web-next/src/components/dashboard/overview-content.tsx
  • apps/web-next/src/hooks/usePublicDashboard.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/web-next/src/hooks/usePublicDashboard.ts

@seanhanca seanhanca enabled auto-merge (squash) April 2, 2026 19:03
seanhanca added a commit that referenced this pull request Apr 2, 2026
…ding

Address eliteprox review feedback on PR #228.

Made-with: Cursor
Transform the root path (/) into a live public overview dashboard that
unauthenticated users see immediately, removing the login gate for
first-time visitors. Authenticated users at / are redirected to /dashboard.

Key changes:
- Middleware: allow unauthenticated access to /; redirect authed users to /dashboard
- New PublicTopBar with Sign In / Get Started CTAs
- New OverviewContent shared component extracted from dashboard page (~1400 lines)
- New usePublicDashboard hook fetching data from public REST APIs
- New AuthCTABanner (dismissible) for unauthenticated users
- Login/register forms: add "Back to Overview" escape hatch
- Logout redirects to / instead of /login (avoids dead end)

Made-with: Cursor
- usePublicDashboard: replace non-reactive hasFetchedRef with reactive
  hasFetched state so job-feed polling actually starts after initial fetch
- usePublicDashboard: fetchJson now throws on non-OK responses instead
  of silently returning null, making error state meaningful; fetchAll
  uses Promise.allSettled for partial-failure resilience
- overview-content: add optional chaining on fetch() calls to guard
  against undefined returns in test environments
- page.test.tsx: rewrite stale homepage tests for the new public
  overview page, mocking OverviewContent and usePublicDashboard

Made-with: Cursor
…ding

Address eliteprox review feedback on PR #228.

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope/shell Shell app changes size/XL Extra large PR (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants