Skip to content

Refactor Lookahead dashboard into modular components#49

Merged
Ross1116 merged 7 commits into
mainfrom
staging
Mar 21, 2026
Merged

Refactor Lookahead dashboard into modular components#49
Ross1116 merged 7 commits into
mainfrom
staging

Conversation

@Ross1116

@Ross1116 Ross1116 commented Mar 19, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features

    • Rebuilt Lookahead dashboard UI: weekly demand heatmap, stat cards, planning alerts, empty-state guidance, window-size selector, upload banner with progress/polling, and version history with delete/confirm flows.
  • Bug Fixes / Reliability

    • Improved proxy handling for streaming uploads, added GET timeouts, refined retry behavior, and standardized error responses.
  • Performance

    • Binary responses now include cache-control; background data polling disabled (freshness via explicit updates).

@vercel

vercel Bot commented Mar 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
sitespace-app Ready Ready Preview, Comment Mar 27, 2026 9:45am

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

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

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 56a8a9ea-7bef-4ca5-a19c-e0b766c0dac0

📥 Commits

Reviewing files that changed from the base of the PR and between aaf102c and b7c4d12.

📒 Files selected for processing (4)
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
📝 Walkthrough

Walkthrough

Removes the legacy in-route LookaheadDashboard and replaces it with a modular dashboard and supporting components under src/components/lookahead/; updates the page import, enhances the API proxy (streaming, cache-control, timeouts, unified error wrapper), disables SWR polling, and extends an upload response type.

Changes

Cohort / File(s) Summary
Removed legacy dashboard
src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
Deleted the previous monolithic client-side LookaheadDashboard used by the app route.
Page import update
src/app/(dashboard)/lookahead/page.tsx
Switched import to the new dashboard implementation at @/components/lookahead/LookaheadDashboard.
New Lookahead components
src/components/lookahead/LookaheadDashboard.tsx, src/components/lookahead/DemandHeatmap.tsx, src/components/lookahead/PlanningAlerts.tsx, src/components/lookahead/StatCards.tsx, src/components/lookahead/UploadBanner.tsx, src/components/lookahead/VersionHistory.tsx, src/components/lookahead/WindowSelector.tsx, src/components/lookahead/EmptyForecastState.tsx
Adds a client-side orchestrating dashboard and multiple UI subcomponents handling project selection, streaming uploads + polling, alert dismissal, version deletion, heatmap rendering, stat cards, legends, empty states, and window selection.
Lookahead utilities
src/components/lookahead/utils.ts
New formatting helpers, PivotResult type, and pivotRows to produce a weeks×assets matrix from snapshot rows.
Proxy API streaming & error handling
src/app/api/proxy/route.ts
Always sets Cache-Control on binary responses (with fallback), forwards multipart/non-GET bodies as streaming request.body with duplex: "half", adds a 9s timeout for proxied GETs, suppresses 401-retry for multipart uploads, and wraps route handlers with an error boundary mapping Abort/Timeout → 504 and other errors → 502.
SWR config and types
src/lib/swr.ts, src/types/index.ts
Disables SWR periodic refresh (refreshInterval = 0) and adds optional ai_classification_fallback?: boolean to UploadStatusResponse.completeness_notes.
Repo housekeeping
.gitignore
Added /.vscode/ to ignore and ensured trailing newline.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Dashboard as LookaheadDashboard
  participant API as /api/proxy
  participant Backend
  participant Cache as SWR

  User->>Dashboard: select project / upload programme file
  Dashboard->>API: POST (streaming body, duplex)
  API->>Backend: forward streaming upload
  Backend-->>API: 202 Accepted (uploadId)
  API-->>Dashboard: uploadId (with Cache-Control)
  loop poll
    Dashboard->>API: GET /status (proxied, 9s timeout)
    API->>Backend: fetch status
    Backend-->>API: status update
    API-->>Dashboard: status
  end
  Dashboard->>Cache: mutate/invalidate snapshot/alerts/versions
  Dashboard->>API: GET snapshots/alerts/versions (proxied)
  API->>Backend: proxy fetch
  Backend-->>API: data
  API-->>Dashboard: response (errors mapped via withErrorBoundary)
  User->>Dashboard: dismiss alert / request delete version
  Dashboard->>API: DELETE version
  API->>Backend: forward delete
  Backend-->>API: delete result
  API-->>Dashboard: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through files and split the maze,
New widgets stitched in tidy arrays,
Streams hum softly, alerts take stage,
Heatmaps and stats now share the page,
A little rabbit cheers — code blooms today!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% 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 'Refactor Lookahead dashboard into modular components' accurately summarizes the main change: moving the monolithic LookaheadDashboard component into a collection of focused, reusable child components.

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

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.

@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Reviews resumed.

@coderabbitai coderabbitai Bot changed the title @coderabbitai Refactor Lookahead dashboard into modular components Mar 19, 2026
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (1)
src/components/lookahead/WindowSelector.tsx (1)

25-35: Expose selected state to assistive tech on window-size buttons.

At Line 25, selection is only conveyed by styling. Add aria-pressed (and explicit type="button") so keyboard/screen-reader users get state feedback.

Suggested fix
             <button
               key={w}
+              type="button"
+              aria-pressed={windowSize === w}
               onClick={() => onSetWindowSize(w)}
               className={`px-4 py-1.5 rounded-md text-sm font-semibold transition-all ${
                 windowSize === w
                   ? "bg-[var(--navy)] text-white shadow-sm"
                   : "text-slate-500 hover:text-slate-800"
               }`}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/WindowSelector.tsx` around lines 25 - 35, The
window-size toggle buttons in the WindowSelector component only use visual
styling to indicate selection; update the button elements used in the render
(the element created in the map that calls onSetWindowSize) to include an
explicit type="button" and an accessible pressed state by adding
aria-pressed={windowSize === w} so assistive tech and keyboard users receive the
selected state (keep existing onClick and className logic intact).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app/api/proxy/route.ts`:
- Around line 154-159: The code sets fetchOpts.body = request.body (a one-time
ReadableStream) so retries after a 401 will attempt to resend a consumed stream
and multipart uploads break; fix by detecting streaming/multipart uploads (e.g.,
inspect request.headers.get("content-type") for "multipart/form-data" or check
if request.body is a ReadableStream) and either (A) buffer the body for
multipart requests using await request.arrayBuffer() and assign that buffer to
fetchOpts.body (and still set (fetchOpts as RequestInit & { duplex: string
}).duplex = "half" only when using a stream), or (B) disable automatic retry for
streaming/multipart requests: do not attempt the retry path on 401 and instead
return the 401 to the client so the client can re-submit after token refresh;
implement the chosen behavior around where fetchOpts and request.body are set
and where the first fetch and retry logic occurs.

In `@src/components/lookahead/DemandHeatmap.tsx`:
- Around line 87-89: The barWidth calculation can divide by zero (maxDemand ===
0) producing Infinity and invalid CSS; update the computation for barWidth (and
any place that sets the width style) to guard against zero/NaN by returning 0
when maxDemand is falsy or the result is not finite (e.g. if (!maxDemand)
barWidth = 0 or compute then if (!Number.isFinite(barWidth)) barWidth = 0), and
ensure the width style uses a finite fallback (e.g. use '0%' when barWidth is
invalid) so variables row, maxDemand, barWidth and the width style assignment
are all protected.

In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 144-154: The response from uploadProgramme can arrive after the
user changes projects, causing stale upload state to be applied; in
handleFileSelected capture the current projectId (e.g., const currentProject =
projectId) or use an AbortController/token before calling uploadProgramme, then
after awaiting the result verify that projectId still equals the captured
currentProject (or that the request wasn't aborted) before calling
setUploadPhase({ kind: "polling", uploadId: ... }) and startPolling(...); if the
project changed or the request was aborted, discard the result and do not update
state or start polling.
- Around line 109-138: The polling loop using setInterval with an async callback
can start overlapping fetchUploadStatus calls; add an in-flight guard (e.g.,
isFetchingRef) checked at the top of the interval callback and set to true
before awaiting fetchUploadStatus and set to false in a finally block to ensure
only one request runs at a time, or replace setInterval with recursive
setTimeout chaining that awaits fetchUploadStatus before scheduling the next
poll; update references in this code path (pollingRef, pollingGenerationRef,
generation, POLL_MAX_ATTEMPTS, stopPolling, setUploadPhase, fetchUploadStatus)
to use the new guard/timing approach so concurrent requests are prevented and
cleanup (stopPolling) still clears pollingRef and resets the in-flight flag.

In `@src/components/lookahead/PlanningAlerts.tsx`:
- Around line 69-79: The dismiss button in PlanningAlerts.tsx currently omits an
explicit type so it defaults to "submit" and may trigger form submissions;
update the button element that calls onDismiss(alert.key) to include
type="button" to prevent unintended submits, keeping the existing onClick,
className, aria-label and conditional color logic unchanged.

In `@src/components/lookahead/StatCards.tsx`:
- Around line 74-77: The subtitle assignment in the StatCards component
currently sets sub to an empty string when heatmap is present but
heatmap.assets.length === 0; update the ternary for the sub field (the
expression using heatmap, heatmap.assets.slice(0,
3).map(formatAssetType).join(", ") and the ellipsis logic) so that when heatmap
exists but heatmap.assets.length is 0 it returns the fallback string "No assets"
(e.g., use heatmap.assets.length > 0 ? ... : "No assets"), ensuring the fallback
also applies when heatmap is null.

In `@src/components/lookahead/UploadBanner.tsx`:
- Around line 79-84: The dismiss button in the UploadBanner component lacks an
accessible name; update the button (the element with onClick={onDismiss} that
renders the X icon) to include an accessible label such as aria-label="Dismiss"
or add screen-reader-only text inside the button so screen readers announce its
purpose; ensure the label describes the action (e.g., "Close upload banner" or
"Dismiss") and keep the onDismiss handler and X icon unchanged.

In `@src/components/lookahead/VersionHistory.tsx`:
- Around line 22-25: The buttons in VersionHistory.tsx currently omit an
explicit type and thus default to type="submit", which can cause accidental form
submissions; update each <button> element in the VersionHistory component
(including the one that toggles setIsOpen and the other click-handler buttons)
to include type="button" so they behave as non-submit controls and avoid
submitting surrounding forms.
- Around line 108-110: The trash action currently uses "opacity-0
group-hover:opacity-100" which hides it from keyboard users; update the element
in VersionHistory.tsx (the delete/trash action JSX that has className="opacity-0
group-hover:opacity-100 ...", and title="Delete this version") to also respond
to keyboard focus by adding focus:opacity-100 and focus-visible:opacity-100 (or
equivalent) to the className and provide an accessible name via aria-label
(e.g., aria-label="Delete this version") or include a visually-hidden label
(sr-only) instead of relying only on title so screen readers and keyboard users
can access the control. Ensure the button remains reachable via keyboard
(tabIndex if needed) and keep the existing hover behavior.

---

Nitpick comments:
In `@src/components/lookahead/WindowSelector.tsx`:
- Around line 25-35: The window-size toggle buttons in the WindowSelector
component only use visual styling to indicate selection; update the button
elements used in the render (the element created in the map that calls
onSetWindowSize) to include an explicit type="button" and an accessible pressed
state by adding aria-pressed={windowSize === w} so assistive tech and keyboard
users receive the selected state (keep existing onClick and className logic
intact).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6f6f080f-7300-44ca-8f4a-7ca8e96028c1

📥 Commits

Reviewing files that changed from the base of the PR and between 865b939 and 2123779.

📒 Files selected for processing (14)
  • src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx
  • src/app/(dashboard)/lookahead/page.tsx
  • src/app/api/proxy/route.ts
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/EmptyForecastState.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/PlanningAlerts.tsx
  • src/components/lookahead/StatCards.tsx
  • src/components/lookahead/UploadBanner.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
  • src/components/lookahead/utils.ts
  • src/lib/swr.ts
  • src/types/index.ts
💤 Files with no reviewable changes (1)
  • src/app/(dashboard)/lookahead/_components/LookaheadDashboard.tsx

Comment thread src/app/api/proxy/route.ts
Comment thread src/components/lookahead/DemandHeatmap.tsx Outdated
Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/PlanningAlerts.tsx
Comment thread src/components/lookahead/StatCards.tsx Outdated
Comment thread src/components/lookahead/UploadBanner.tsx
Comment thread src/components/lookahead/VersionHistory.tsx
Comment thread src/components/lookahead/VersionHistory.tsx Outdated
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/components/lookahead/LookaheadDashboard.tsx (1)

159-175: ⚠️ Potential issue | 🟠 Major

The project-switch guard still cannot see the new project.

At Lines 167 and 171, projectId is the same value captured when handleFileSelected started, so an old uploadProgramme() response can still move the new project UI into polling or error after Lines 205-208 switch projects.

Suggested fix
   const pollingRef = useRef<ReturnType<typeof setInterval> | null>(null);
   const pollingGenerationRef = useRef(0);
+  const uploadGenerationRef = useRef(0);
@@
   const handleFileSelected = useCallback(
     async (file: File | null) => {
       if (!file || !projectId) return;
+      const generation = ++uploadGenerationRef.current;
       const targetProject = projectId;
       setUploadPhase({ kind: "uploading" });
       try {
         const result = await uploadProgramme(targetProject, file);
-        // Discard result if the user switched projects while uploading
-        if (pollingGenerationRef.current !== 0 && targetProject !== projectId) return;
+        if (uploadGenerationRef.current !== generation) return;
         setUploadPhase({ kind: "polling", uploadId: result.upload_id });
         startPolling(result.upload_id);
       } catch (err) {
-        if (targetProject !== projectId) return;
+        if (uploadGenerationRef.current !== generation) return;
         setUploadPhase({ kind: "error", message: getApiErrorMessage(err) });
       }
     },
     [projectId, startPolling],
   );
@@
   const handleProjectSelect = useCallback(
     (proj: ApiProject) => {
       if (!proj?.id) return;
+      uploadGenerationRef.current += 1;
       stopPolling();
       setShowProjectSelector(false);
       setProjectId(proj.id);

Also applies to: 202-210

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

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 159 - 175, The
guard comparing projectId after async work uses the stale captured projectId in
handleFileSelected, so responses can incorrectly update UI for a newly selected
project; fix by maintaining a live ref (e.g., projectIdRef) that you update
whenever projectId changes and replace comparisons of the captured projectId
with projectIdRef.current after the await(s) and before calling
setUploadPhase/startPolling (and in the catch block and the other similar block
around lines 202-210); update references to use projectIdRef.current (or an
equivalent getter) when validating targetProject !== current project before
mutating state or starting polling.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 518-550: StatCards is being rendered even when the forecast empty
states will be shown, causing a misleading zeroed dashboard; update the
rendering logic so StatCards is only rendered when not loading and there is a
valid forecast (i.e. projectId is present, heatmap exists, and
visibleWeeks.length > 0). Concretely, change the condition around the
StatCards/forecast block to check a single predicate (e.g. const hasForecast =
!!projectId && !!heatmap && visibleWeeks.length > 0) and render <StatCards
stats={stats} ... /> only when hasForecast && !isLoading && !snapshotLoading;
otherwise render the existing <EmptyForecastState reason="no-project" |
"no-data" /> branches. This ensures StatCards (function/component name
StatCards) does not show when EmptyForecastState will be shown.
- Around line 236-246: The code in LookaheadDashboard.tsx computes monday and
then uses monday.toISOString().split("T")[0], which converts to UTC and can
shift the date; replace that UTC conversion with a local YYYY-MM-DD formatter
(e.g., build currentWeekStr from monday.getFullYear(), monday.getMonth()+1 and
monday.getDate() with zero-padding, or use monday.toLocaleDateString('en-CA'))
so currentWeekStr reflects the local Monday; then use that local string when
computing startIdx against weeks.

---

Duplicate comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 159-175: The guard comparing projectId after async work uses the
stale captured projectId in handleFileSelected, so responses can incorrectly
update UI for a newly selected project; fix by maintaining a live ref (e.g.,
projectIdRef) that you update whenever projectId changes and replace comparisons
of the captured projectId with projectIdRef.current after the await(s) and
before calling setUploadPhase/startPolling (and in the catch block and the other
similar block around lines 202-210); update references to use
projectIdRef.current (or an equivalent getter) when validating targetProject !==
current project before mutating state or starting polling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 30c775c0-3a89-410e-a364-260b7abc7a99

📥 Commits

Reviewing files that changed from the base of the PR and between 2123779 and 1620541.

📒 Files selected for processing (8)
  • src/app/api/proxy/route.ts
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/PlanningAlerts.tsx
  • src/components/lookahead/StatCards.tsx
  • src/components/lookahead/UploadBanner.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/components/lookahead/WindowSelector.tsx
  • src/components/lookahead/PlanningAlerts.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/DemandHeatmap.tsx
  • src/app/api/proxy/route.ts
  • src/components/lookahead/UploadBanner.tsx

Comment thread src/components/lookahead/LookaheadDashboard.tsx
Comment thread src/components/lookahead/LookaheadDashboard.tsx
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/components/lookahead/WindowSelector.tsx (1)

6-6: Redundant type alias can be removed.

WindowSize is just an alias for LookaheadWindowSize with no added value. Consider using LookaheadWindowSize directly in the Props interface to reduce indirection.

♻️ Suggested simplification
-type WindowSize = LookaheadWindowSize;
-
 interface Props {
-  windowSize: WindowSize;
-  onSetWindowSize: (size: WindowSize) => void;
+  windowSize: LookaheadWindowSize;
+  onSetWindowSize: (size: LookaheadWindowSize) => void;
   lastUpdated: string | null;
 }

And update line 24:

-          {(["2W", "4W", "6W"] as WindowSize[]).map((w) => (
+          {(["2W", "4W", "6W"] as LookaheadWindowSize[]).map((w) => (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/WindowSelector.tsx` at line 6, Remove the redundant
type alias WindowSize and use LookaheadWindowSize directly in the Props
interface: delete the line declaring "type WindowSize = LookaheadWindowSize",
update the Props interface (and any other references in this file) to replace
WindowSize with LookaheadWindowSize, and run a quick grep/IDE search in
WindowSelector.tsx to ensure no remaining references to WindowSize remain.
src/components/lookahead/VersionHistory.tsx (1)

22-26: Expose accordion state to assistive tech.

Line 22 toggle is missing aria-expanded/aria-controls. Add these plus a panel id so screen readers get collapse state.

Accessibility tweak
       <button
         type="button"
         onClick={() => setIsOpen((v) => !v)}
+        aria-expanded={isOpen}
+        aria-controls="programme-history-panel"
         className="w-full flex items-center justify-between px-5 py-3.5 text-sm font-bold text-slate-600 hover:text-slate-900 transition-colors"
       >
@@
-        <div className="border-t border-slate-100 p-3 space-y-1 max-h-64 overflow-y-auto custom-scrollbar">
+        <div
+          id="programme-history-panel"
+          className="border-t border-slate-100 p-3 space-y-1 max-h-64 overflow-y-auto custom-scrollbar"
+        >

Also applies to: 41-41

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

In `@src/components/lookahead/VersionHistory.tsx` around lines 22 - 26, The toggle
button currently using onClick={() => setIsOpen((v) => !v)} should expose its
state to assistive tech: bind aria-expanded to the isOpen state
(aria-expanded={isOpen}) and add aria-controls pointing to the panel's id; give
the collapsible panel a unique id (e.g., versionHistoryPanel or similar) and
ensure the button's aria-controls value matches that id so screen readers can
detect collapse state; apply the same change for the second toggle instance
referenced by the component (the other button using setIsOpen) and ensure the
panel element has role="region" or appropriate semantic container if not already
present.
src/components/lookahead/DemandHeatmap.tsx (1)

154-188: Consider extracting status text logic for readability.

The nested ternary chain is correct but hard to follow. Extracting to a helper function could improve maintainability.

♻️ Optional: Extract status text to a helper
function StatusText({ row }: { row: LookaheadRow }) {
  if (row.demand_hours === 0) {
    return row.booked_hours > 0 ? (
      <span className="text-[10px] text-slate-400">
        {row.booked_hours}h booked · No forecast demand
      </span>
    ) : (
      <span className="text-[10px] text-slate-300">No activity forecast</span>
    );
  }

  return (
    <>
      <span className="text-[10px] text-slate-400">{row.demand_hours}h needed</span>
      {row.booked_hours > 0 && (
        <span className="text-[10px] text-teal font-medium">{row.booked_hours}h booked</span>
      )}
      {row.gap_hours > 0 ? (
        <span className="text-[10px] text-red-500 font-semibold">{row.gap_hours}h unbooked</span>
      ) : row.booked_hours > 0 ? (
        <span className="text-[10px] text-green-600 font-medium">All booked ✓</span>
      ) : (
        <span className="text-[10px] text-red-400">Nothing booked yet</span>
      )}
    </>
  );
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/DemandHeatmap.tsx` around lines 154 - 188, The
nested ternary rendering for the status text in DemandHeatmap.tsx is hard to
read—extract it into a small helper component or function (e.g., StatusText({
row }: { row: LookaheadRow })) that returns the same JSX branches based on
row.demand_hours, row.booked_hours and row.gap_hours, preserve all classNames
and text exactly, then replace the inline ternary block with a single
<StatusText row={row} /> invocation so the logic is isolated and more
maintainable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/lookahead/DemandHeatmap.tsx`:
- Around line 204-207: The legend in the DemandHeatmap component currently shows
a single swatch with class bg-red-500 for "Unbooked" while gap bars use multiple
colors by demand_level; update the legend JSX (the block that currently renders
the div with className "w-3 h-1.5 rounded-full bg-red-500" and the "Unbooked"
label) to either render multiple swatches matching the demand_level color
classes (bg-amber-300, bg-amber-400, bg-orange-500, bg-red-500) with a shared
"Unbooked" label, or replace the label with a short note like "Unbooked — color
intensity reflects urgency" so the legend accurately represents the varying
unbooked colors.

In `@src/components/lookahead/VersionHistory.tsx`:
- Around line 10-11: The component currently uses a single deletingId which
allows starting another delete before the first resolves; change the prop and
state to track multiple inflight deletes (e.g., deletingIds: Set<string> or
string[] and isDeleting(uploadId) checks) and update all usages: replace
deletingId with deletingIds in VersionHistory and any parent
(LookaheadDashboard), update the delete handler (e.g., onDelete /
handleDeleteVersion) to add the uploadId to deletingIds before firing the async
delete request and remove it when the promise resolves/rejects, and use
isDeleting(uploadId) to disable the specific row/button; update types and call
sites that referenced deletingId (lines noted) accordingly.

---

Nitpick comments:
In `@src/components/lookahead/DemandHeatmap.tsx`:
- Around line 154-188: The nested ternary rendering for the status text in
DemandHeatmap.tsx is hard to read—extract it into a small helper component or
function (e.g., StatusText({ row }: { row: LookaheadRow })) that returns the
same JSX branches based on row.demand_hours, row.booked_hours and row.gap_hours,
preserve all classNames and text exactly, then replace the inline ternary block
with a single <StatusText row={row} /> invocation so the logic is isolated and
more maintainable.

In `@src/components/lookahead/VersionHistory.tsx`:
- Around line 22-26: The toggle button currently using onClick={() =>
setIsOpen((v) => !v)} should expose its state to assistive tech: bind
aria-expanded to the isOpen state (aria-expanded={isOpen}) and add aria-controls
pointing to the panel's id; give the collapsible panel a unique id (e.g.,
versionHistoryPanel or similar) and ensure the button's aria-controls value
matches that id so screen readers can detect collapse state; apply the same
change for the second toggle instance referenced by the component (the other
button using setIsOpen) and ensure the panel element has role="region" or
appropriate semantic container if not already present.

In `@src/components/lookahead/WindowSelector.tsx`:
- Line 6: Remove the redundant type alias WindowSize and use LookaheadWindowSize
directly in the Props interface: delete the line declaring "type WindowSize =
LookaheadWindowSize", update the Props interface (and any other references in
this file) to replace WindowSize with LookaheadWindowSize, and run a quick
grep/IDE search in WindowSelector.tsx to ensure no remaining references to
WindowSize remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fbafb578-f80a-4e23-bc8d-bdeae455c6a6

📥 Commits

Reviewing files that changed from the base of the PR and between e937b9b and aaf102c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • .gitignore
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/PlanningAlerts.tsx
  • src/components/lookahead/StatCards.tsx
  • src/components/lookahead/UploadBanner.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/components/lookahead/PlanningAlerts.tsx
  • .gitignore
  • src/components/lookahead/LookaheadDashboard.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/components/lookahead/UploadBanner.tsx
  • src/components/lookahead/StatCards.tsx

Comment thread src/components/lookahead/DemandHeatmap.tsx
Comment thread src/components/lookahead/VersionHistory.tsx Outdated
- DemandHeatmap: replace single unbooked swatch with four colour-graded
  swatches (amber-300/400, orange-500, red-500) matching GAP_BAR_COLOR;
  extract nested status-text ternary into StatusText helper component
- VersionHistory: change deletingId (single) to deletingIds (Set<string>)
  so concurrent deletes each disable only their own row; add aria-expanded,
  aria-controls, id, and role="region" to toggle/panel for screen readers
- WindowSelector: remove redundant WindowSize alias; use LookaheadWindowSize
  directly throughout
- LookaheadDashboard: update delete handler and VersionHistory prop to use
  the new deletingIds Set
@Ross1116

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/components/lookahead/LookaheadDashboard.tsx (1)

411-420: Consider adding aria-expanded for screen reader accessibility.

The project selector button controls dropdown visibility but lacks aria-expanded. Adding it would improve the experience for assistive technology users.

♿ Suggested accessibility improvement
                   <Button
                     onClick={() => setShowProjectSelector((v) => !v)}
+                    aria-expanded={showProjectSelector}
+                    aria-haspopup="listbox"
                     className="bg-navy hover:bg-(--navy-hover) text-white rounded-lg px-5 py-5 h-auto text-sm font-bold shadow-md shadow-slate-900/10 flex items-center gap-2"
                   >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/lookahead/LookaheadDashboard.tsx` around lines 411 - 420, The
project-selector Button that toggles setShowProjectSelector is missing an
accessibility state; add an aria-expanded attribute to the Button tied to the
dropdown state (use showProjectSelector) so screen readers know when the
selector is open (e.g., aria-expanded={showProjectSelector}), and ensure the
Button element (component name Button) still receives the onClick handler and
accessible label (selectedProject?.name fallback) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/components/lookahead/LookaheadDashboard.tsx`:
- Around line 411-420: The project-selector Button that toggles
setShowProjectSelector is missing an accessibility state; add an aria-expanded
attribute to the Button tied to the dropdown state (use showProjectSelector) so
screen readers know when the selector is open (e.g.,
aria-expanded={showProjectSelector}), and ensure the Button element (component
name Button) still receives the onClick handler and accessible label
(selectedProject?.name fallback) unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35116262-28d3-4e54-9b78-7bd3bf4695f1

📥 Commits

Reviewing files that changed from the base of the PR and between aaf102c and b7c4d12.

📒 Files selected for processing (4)
  • src/components/lookahead/DemandHeatmap.tsx
  • src/components/lookahead/LookaheadDashboard.tsx
  • src/components/lookahead/VersionHistory.tsx
  • src/components/lookahead/WindowSelector.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/components/lookahead/VersionHistory.tsx

@Ross1116 Ross1116 merged commit 515eb38 into main Mar 21, 2026
3 checks passed
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.

1 participant