Add verdict-to-incident handoff#6
Conversation
📝 WalkthroughWalkthroughAdds incident-case types, verdict-to-incident conversion, JSON persistence, server actions, promotion UI, responsive styling, and smoke validation for incident data and tracked secret files. ChangesIncident promotion
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant IncidentBoard
participant promoteVerdictToIncident
participant saveIncidentCaseFromVerdict
participant IncidentCasesJson
User->>IncidentBoard: click Promote
IncidentBoard->>promoteVerdictToIncident: submit board verdict
promoteVerdictToIncident->>saveIncidentCaseFromVerdict: save board
saveIncidentCaseFromVerdict->>IncidentCasesJson: write incident case
IncidentCasesJson-->>saveIncidentCaseFromVerdict: created incident
saveIncidentCaseFromVerdict-->>promoteVerdictToIncident: return incident
promoteVerdictToIncident-->>IncidentBoard: update incident list and message
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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.
Actionable comments posted: 6
🧹 Nitpick comments (1)
scripts/smoke.ts (1)
83-90: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winStrengthen the incident-case contract assertion.
Lines 83-90 validate only a subset of the generated record. A regression in
id, timestamps,source, confidence,recommendedAction, or most analytics evidence IDs would still pass. Assert the deterministic timestamp-derived ID, timestamp equality, verdict/action mapping, and every expected evidence task ID fromboard.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/smoke.ts` around lines 83 - 90, Strengthen the assertion for the record returned by incidentCaseFromVerdict by validating its deterministic timestamp-derived id, exact timestamp, source, confidence, recommendedAction, and verdict-to-action mappings against board. Expand linkedAnalytics.evidenceTaskIds validation to require every expected evidence task ID from board, while preserving the existing status, assignee, root-cause, action-item count, and top-suspect checks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/actions.ts`:
- Around line 29-30: Update promoteVerdictToIncident to stop persisting the
browser-provided IncidentBoard as trusted data. Reconstruct the board from
trusted server-side records, or validate the submitted fields and verify all
evidence IDs against the authoritative server-side source before calling
saveIncidentCaseFromVerdict.
In `@app/page.tsx`:
- Line 490: Update the date rendering in the Created field near latest.createdAt
to use a deterministic representation across server rendering and hydration by
supplying an explicit locale and timezone to the formatter, or rendering an
ISO-formatted value. Keep the existing createdAt value and display context
unchanged.
- Around line 47-57: Update the startPromoteTransition callback around
promoteVerdictToIncident to be async and await the promotion Promise, while
preserving the existing success and error handling so the transition remains
active until persistence completes.
In `@src/lib/incident-cases.ts`:
- Line 12: Update the incident-case action text around the
`board.verdict.evidence.taskId` reference so it never produces “attach evidence
from undefined.” Reuse the existing optional-task handling from the filtering
logic and omit the evidence clause or provide an appropriate fallback when no
verdict task exists, while preserving the current wording when `taskId` is
available.
- Line 17: Update incident creation in the function containing the generated id
and JSON persistence to use collision-resistant IDs that remain unique for
concurrent promotions, and make the read-modify-write operation atomic across
processes using transactional storage or a cross-process write lock. Do not rely
on an in-memory mutex alone; ensure concurrent requests preserve all updates
without overwriting each other.
- Around line 67-80: Update isIncidentCase to accept every declared incident
status, including acknowledged, mitigating, and resolved, while validating the
complete persisted IncidentCase shape. Require createdAt, assignee, and
confidence, and validate the expected nested fields within linkedAnalytics
before narrowing the value to IncidentCase.
---
Nitpick comments:
In `@scripts/smoke.ts`:
- Around line 83-90: Strengthen the assertion for the record returned by
incidentCaseFromVerdict by validating its deterministic timestamp-derived id,
exact timestamp, source, confidence, recommendedAction, and verdict-to-action
mappings against board. Expand linkedAnalytics.evidenceTaskIds validation to
require every expected evidence task ID from board, while preserving the
existing status, assignee, root-cause, action-item count, and top-suspect
checks.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 9ed69923-edc8-4fc4-b325-575c182e7bf3
📒 Files selected for processing (7)
.gitignoreapp/actions.tsapp/page.tsxapp/styles.cssscripts/smoke.tssrc/lib/incident-cases.tssrc/lib/types.ts
| export async function promoteVerdictToIncident(board: IncidentBoard) { | ||
| return saveIncidentCaseFromVerdict(board); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not persist client-claimed verdict evidence as trusted incident data.
This action accepts the complete board from the browser, so callers can forge root cause, confidence, service, and evidence IDs before persistence. Derive the board from trusted server-side data, or validate the payload and verify its evidence against the server-side source before saving.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/actions.ts` around lines 29 - 30, Update promoteVerdictToIncident to stop
persisting the browser-provided IncidentBoard as trusted data. Reconstruct the
board from trusted server-side records, or validate the submitted fields and
verify all evidence IDs against the authoritative server-side source before
calling saveIncidentCaseFromVerdict.
| startPromoteTransition(() => { | ||
| void promoteVerdictToIncident(board) | ||
| .then((incident) => { | ||
| setIncidentCases((existing) => [incident, ...existing].slice(0, 20)); | ||
| setPromoteMessage(`${incident.id} opened and linked to the proof board.`); | ||
| }) | ||
| .catch((error: unknown) => { | ||
| const message = error instanceof Error ? error.message : "unknown persistence error"; | ||
| setPromoteMessage(`Could not promote verdict: ${message}`); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate app/page.tsx and relevant files =="
fd -a 'page\.tsx$' . | sed 's#^\./##' | head -50
echo
if [ -f app/page.tsx ]; then
echo "== app/page.tsx outline =="
ast-grep outline app/page.tsx --view expanded || true
echo
echo "== app/page.tsx lines 1-140 =="
sed -n '1,140p' app/page.tsx | nl -ba
fi
echo
echo "== search for startPromoteTransition / promoteVerdictToIncident =="
rg -n "startPromoteTransition|promoteVerdictToIncident|setPromoting|isPromoting|startTransition" -S .Repository: knarayanareddy/Clickfuse
Length of output: 1080
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,140p' app/page.tsx
echo
sed -n '420,514p' app/page.tsx
echo
printf '\n== search relevant symbols ==\n'
rg -n "startPromoteTransition|promoteVerdictToIncident|setPromoting|isPromoting|startTransition|async function|action=" -S .Repository: knarayanareddy/Clickfuse
Length of output: 11190
🌐 Web query:
React 19 useTransition callback async function startTransition promise behavior
💡 Result:
In React 19, you can pass an asynchronous function (known as an "Action") to the startTransition callback [1][2]. This enables React to automatically track the pending state of asynchronous operations, which is particularly useful for handling forms, errors, and data updates without blocking the UI [1][3]. Key behaviors for async startTransition in React 19: 1. Pending State Tracking: When an async function is passed to the startTransition function (provided by useTransition), the isPending boolean is set to true immediately [1][4]. It remains true until the asynchronous operation completes and all associated state updates have been processed [1][4]. 2. Automatic grouping: React includes the asynchronous function execution and subsequent state updates within the Transition [5][6]. This keeps the UI responsive while the background work (such as fetch requests) is ongoing [1][2]. 3. Crucial Distinction between useTransition and standalone startTransition: - When using useTransition, state updates occurring after an await within the async action are automatically included in the transition [7]. - When using the standalone startTransition function (outside of components or where isPending is not needed), state updates performed after an await are not automatically included in the transition [6]. In this case, you must wrap those subsequent state updates in an additional startTransition call to ensure they remain part of the transition [5][6][7]. 4. Limitation: The current implementation of async transitions relies on grouping updates that occur while the async action is executing [4]. Because of the lack of a browser-native Async Context API, there is a known limitation where updates occurring after an await in a standalone startTransition require explicit re-wrapping [5][6][4]. This is documented as a known behavior that may be improved in future versions [5][6].
Citations:
- 1: https://react.dev/blog/2024/12/05/react-19
- 2: https://github.com/facebook/react/blob/ee0855f427832e899767f7659c5289364218ab9e/CHANGELOG.md
- 3: https://blog.logrocket.com/getting-started-react-19-starttransition/
- 4: Initial (client-only) async actions support react/react#26621
- 5: https://react.dev/reference/react/useTransition
- 6: https://react.dev/reference/react/startTransition
- 7: docs: update startTransition async guidance for React 19 reactjs/react.dev#8496
Return the promotion Promise from the transition.
The void call detaches the request, so the transition can finish before promoteVerdictToIncident(board) waits on persistence. Make the transition callback async and await the server action so isPromoting stays true for the whole promotion.
Proposed fix
- startPromoteTransition(() => {
- void promoteVerdictToIncident(board)
- .then((incident) => {
+ startPromoteTransition(async () => {
+ try {
+ const incident = await promoteVerdictToIncident(board);
setIncidentCases((existing) => [incident, ...existing].slice(0, 20));
setPromoteMessage(`${incident.id} opened and linked to the proof board.`);
- })
- .catch((error: unknown) => {
- const message = error instanceof Error ? error.message : "unknown persistence error";
- setPromoteMessage(`Could not promote verdict: ${message}`);
- });
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "unknown persistence error";
+ setPromoteMessage(`Could not promote verdict: ${message}`);
+ }
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| startPromoteTransition(() => { | |
| void promoteVerdictToIncident(board) | |
| .then((incident) => { | |
| setIncidentCases((existing) => [incident, ...existing].slice(0, 20)); | |
| setPromoteMessage(`${incident.id} opened and linked to the proof board.`); | |
| }) | |
| .catch((error: unknown) => { | |
| const message = error instanceof Error ? error.message : "unknown persistence error"; | |
| setPromoteMessage(`Could not promote verdict: ${message}`); | |
| }); | |
| }); | |
| startPromoteTransition(async () => { | |
| try { | |
| const incident = await promoteVerdictToIncident(board); | |
| setIncidentCases((existing) => [incident, ...existing].slice(0, 20)); | |
| setPromoteMessage(`${incident.id} opened and linked to the proof board.`); | |
| } catch (error: unknown) { | |
| const message = error instanceof Error ? error.message : "unknown persistence error"; | |
| setPromoteMessage(`Could not promote verdict: ${message}`); | |
| } | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/page.tsx` around lines 47 - 57, Update the startPromoteTransition
callback around promoteVerdictToIncident to be async and await the promotion
Promise, while preserving the existing success and error handling so the
transition remains active until persistence completes.
| <h3>{latest.title}</h3> | ||
| <dl> | ||
| <div><dt>Assignee</dt><dd>{latest.assignee}</dd></div> | ||
| <div><dt>Created</dt><dd>{new Date(latest.createdAt).toLocaleString()}</dd></div> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Tracked files around app/page.tsx:"
git ls-files | rg '(^|/)app/page\.tsx$|next\.config|package\.json'
echo
echo "Relevant lines in app/page.tsx:"
if [ -f app/page.tsx ]; then
wc -l app/page.tsx
sed -n '450,510p' app/page.tsx | nl -ba -v450
fi
echo
echo "Search for other createdAt/date formatting usages:"
rg -n "toLocaleString\(|Date\(|createdAt|new Date" app package.json next.config.* 2>/dev/null || true
echo
echo "Package framework hints:"
if [ -f package.json ]; then
node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json','utf8'));
console.log(JSON.stringify({dependencies: pkg.dependencies || {}, devDependencies: pkg.devDependencies || {}, scripts: pkg.scripts || {}}, null, 2));
JS
fi
echo
echo "Relevant Next config snippets if present:"
for f in $(git ls-files | rg '(^|/)next\.config\.(js|mjs|ts|cjs)$'); do
echo "--- $f"
sed -n '1,240p' "$f"
doneRepository: knarayanareddy/Clickfuse
Length of output: 329
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Relevant app/page.tsx block:"
sed -n '450,510p' app/page.tsx | cat -n
echo
echo "Search for date formatting/usages:"
rg -n "toLocaleString\(|toLocaleDateString\(|toLocaleTimeString\(|Date\(|createdAt|new Date" app
echo
echo "Framework/config/package hints:"
for f in package.json next.config.ts next.config.js; do
if [ -f "$f" ]; then
echo "--- $f"
sed -n '1,220p' "$f"
fi
done
echo
echo "App structure/client hints:"
rg -n '"use client"|React\.useEffect|useEffect|useState|Suspense|hydrate' app || trueRepository: knarayanareddy/Clickfuse
Length of output: 5243
🌐 Web query:
Next.js Next.js Server Components client components use "use client" hydration client side rendering
💡 Result:
In Next.js, Server Components and Client Components are used together to balance server-side rendering performance with client-side interactivity [1][2][3]. Server Components (the default) are rendered on the server, resulting in zero client-side JavaScript for that component's logic, which reduces bundle size and improves performance [1][4][3]. They are ideal for data fetching, accessing databases, and rendering static content [2][3][5]. Client Components are used when you need interactivity or browser-specific capabilities [2][3]. You opt into Client Components by adding the "use client" directive at the very top of a file [1][2][6]. This directive does not mean the component renders only on the client; rather, it defines a boundary [2][7]. Client Components are still pre-rendered to HTML on the server to provide a fast initial page load [8][9]. Once the HTML reaches the browser, the component undergoes hydration [8][10][9]. Hydration is the process by which React attaches event listeners and state to the server-rendered HTML, transforming it into an interactive interface [8][9]. Key best practices include: - Start with Server Components by default [6][11][5]. - Keep the "use client" boundary as deep as possible to minimize the JavaScript sent to the client [3][11][5]. - Use Client Components only when necessary, such as for React hooks (e.g., useState, useEffect), event handlers (e.g., onClick), or browser APIs (e.g., window, localStorage) [2][7][5]. - Pass Server Components as children or props to Client Components to maintain server-side rendering for non-interactive parts of your UI [1][6][11]. By strategically using these components, you ensure that only the necessary parts of your application are hydrated in the browser, keeping the overall application fast and responsive [3][10][5].
Citations:
- 1: https://nextjs.org/docs/app/getting-started/server-and-client-components
- 2: https://nextjs.org/docs/app/api-reference/directives/use-client
- 3: https://examples.vercel.com/academy/client-server-boundaries
- 4: https://nextjs.org/learn/react-foundations/server-and-client-components
- 5: https://www.pean.dev/blog/nextjs-server-vs-client-components-article
- 6: https://vercel.com/academy/nextjs-foundations/server-and-client-components
- 7: https://vercel-next-js.mintlify.app/app/core-concepts/rendering
- 8: https://dev.to/devjordan/nextjs-15-app-router-complete-guide-to-server-and-client-components-5h6k
- 9: https://docs.w3cub.com/nextjs/app/building-your-application/rendering/client-components
- 10: https://pearpages.com/blog/2026/07/03/from-url-bar-to-interactive-what-really-happens-when-a-next-js-app-boots
- 11: https://www.stripesys.com/blog/server-components-vs-client-components-nextjs
Use a deterministic date format in the rendered HTML.
app/page.tsx is a Client Component, so this date is rendered both during server-side rendering and during hydration. toLocaleString() can use different locale/timezone defaults on the server and browser; pass a fixed locale/timezone, or store/display an ISO-formatted value instead.
🧰 Tools
🪛 React Doctor (0.7.6)
[error] 490-490: This can cause a hydration mismatch because toLocaleString() formats with the server's locale and timezone during server rendering but the user's in the browser. Format it in a post-mount useEffect, or pass an explicit locale and timeZone.
Format locale/timezone-dependent values in a post-mount useEffect + state, or pass an explicit locale and timeZone so the server and the browser render the same text. Only runs on SSR-capable projects.
(no-locale-format-in-render)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/page.tsx` at line 490, Update the date rendering in the Created field
near latest.createdAt to use a deterministic representation across server
rendering and hydration by supplying an explicit locale and timezone to the
formatter, or rendering an ISO-formatted value. Keep the existing createdAt
value and display context unchanged.
Source: Linters/SAST tools
| const topSuspect = board.suspects.suspects[0]?.service ?? board.deploy.service; | ||
| const actionItems = [ | ||
| board.verdict.recommendedAction, | ||
| `Page ${board.deploy.service} owner and attach evidence from ${board.verdict.evidence.taskId}.`, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid generating an action item with undefined evidence.
taskId is optional here (as shown by the filtering on lines 35-42), so this can persist “attach evidence from undefined.” Omit that clause or provide a fallback when no verdict task exists.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/incident-cases.ts` at line 12, Update the incident-case action text
around the `board.verdict.evidence.taskId` reference so it never produces
“attach evidence from undefined.” Reuse the existing optional-task handling from
the filtering logic and omit the evidence clause or provide an appropriate
fallback when no verdict task exists, while preserving the current wording when
`taskId` is available.
| ]; | ||
|
|
||
| return { | ||
| id: `INC-${createdAt.slice(0, 10).replaceAll("-", "")}-${createdAt.slice(11, 19).replaceAll(":", "")}`, |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Make incident creation concurrency-safe.
IDs collide for promotions in the same second, and concurrent requests can both read the same JSON then overwrite each other’s updates. Use collision-resistant IDs plus transactional storage or a cross-process write lock; an in-memory mutex alone is insufficient for multiple server instances.
Also applies to: 58-63
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/incident-cases.ts` at line 17, Update incident creation in the
function containing the generated id and JSON persistence to use
collision-resistant IDs that remain unique for concurrent promotions, and make
the read-modify-write operation atomic across processes using transactional
storage or a cross-process write lock. Do not rely on an in-memory mutex alone;
ensure concurrent requests preserve all updates without overwriting each other.
| function isIncidentCase(value: unknown): value is IncidentCase { | ||
| return Boolean( | ||
| value && | ||
| typeof value === "object" && | ||
| "id" in value && | ||
| typeof value.id === "string" && | ||
| "status" in value && | ||
| value.status === "open" && | ||
| "rootCause" in value && | ||
| typeof value.rootCause === "string" && | ||
| "linkedAnalytics" in value && | ||
| value.linkedAnalytics && | ||
| typeof value.linkedAnalytics === "object" | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Validate the full persisted schema and all valid statuses.
The guard rejects valid acknowledged, mitigating, and resolved records despite the declared contract, while accepting records missing fields the UI renders (createdAt, assignee, confidence, and nested analytics fields). This makes valid incident updates vanish and permits malformed JSON records into typed UI state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/lib/incident-cases.ts` around lines 67 - 80, Update isIncidentCase to
accept every declared incident status, including acknowledged, mitigating, and
resolved, while validating the complete persisted IncidentCase shape. Require
createdAt, assignee, and confidence, and validate the expected nested fields
within linkedAnalytics before narrowing the value to IncidentCase.
Summary
Verification
Notes: demo-capture artifacts remain local and untracked.
Summary by CodeRabbit
New Features
Bug Fixes