feat: enable passwordless public overview dashboard as landing page#228
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (11)
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
OverviewContentshared dashboard renderer and refactor authenticated/dashboardpage 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.
| // 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]); |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
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 | 🟠 MajorMake logout best-effort on the client too.
await fetch(...)makes sign-out depend on the network, and this path only callsclearAllAuthStorage(), which does not clear the client-readablenaap_auth_tokencookie written bysetTokenStorage(). If that request never reaches the server, middleware can still treat/as authenticated and bounce the user right back to/dashboard;window.location.hrefalso keeps the protected page in history. Clear the local token first, fire the revoke in the background, and usewindow.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
📒 Files selected for processing (10)
apps/web-next/src/app/(auth)/login/login-form.tsxapps/web-next/src/app/(auth)/register/register-form.tsxapps/web-next/src/app/(dashboard)/dashboard/page.tsxapps/web-next/src/app/page.tsxapps/web-next/src/components/dashboard/auth-cta-banner.tsxapps/web-next/src/components/dashboard/overview-content.tsxapps/web-next/src/components/layout/public-top-bar.tsxapps/web-next/src/contexts/auth-context.tsxapps/web-next/src/hooks/usePublicDashboard.tsapps/web-next/src/middleware.ts
| 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]); |
There was a problem hiding this comment.
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.
| {/* 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> |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
♻️ Duplicate comments (2)
apps/web-next/src/components/dashboard/overview-content.tsx (2)
1113-1120:⚠️ Potential issue | 🟠 MajorClear stale enrichment state on non-OK responses.
The handlers in
apps/web-next/src/app/api/v1/network/capacity/route.tsLines 1-18,apps/web-next/src/app/api/v1/network/live-video-capacity/route.tsLines 1-27, andapps/web-next/src/app/api/v1/network/perf-by-model/route.tsLines 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 keepsliveVideoCapacityModelsRef.currentset, 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 | 🟠 MajorMake 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 theOverviewContentprop contract, not just its presence.The stub throws away every prop, so this suite still passes if
apps/web-next/src/app/page.tsxLines 29-79 stops forwardingisPublicor any of the new dashboard fields intoOverviewContent. Capture the props in the mock and assert a minimalobjectContaining(...)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
📒 Files selected for processing (3)
apps/web-next/src/__tests__/page.test.tsxapps/web-next/src/components/dashboard/overview-content.tsxapps/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
…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
4a14fe6 to
637de1e
Compare

Summary
/) into a live public overview dashboard visible to unauthenticated users, removing the login gate for first-time visitorsOverviewContentcomponent used by both the public landing page (via REST APIs) and the authenticated dashboard (via GraphQL plugin event bus)PublicTopBarwith Sign In / Get Started CTAs, a dismissibleAuthCTABanner, and "Back to Overview" links on login/register pages for a seamless user journeyChanges
middleware.ts/; redirect authed users to/dashboardpage.tsx(root)PublicOverviewPageusingusePublicDashboarddashboard/page.tsxOverviewContent, keep GraphQL data fetchingoverview-content.tsxusePublicDashboard.tspublic-top-bar.tsxauth-cta-banner.tsxlogin-form.tsxregister-form.tsxauth-context.tsx/instead of/loginUser Journey
Test plan
/sees live public overview dashboardPublicTopBarshows "Sign In" and "Get Started" buttonsAuthCTABannerappears and can be dismissed (persists in sessionStorage)/is redirected to/dashboard/dashboardstill requires authentication (no regression)/not/login/settings,/wallet) still require authMade with Cursor
Summary by CodeRabbit
New Features
Refactor
Tests