Skip to content

Add verdict-to-incident handoff#6

Merged
knarayanareddy merged 1 commit into
mainfrom
codex/oltp-incident-cases
Jul 23, 2026
Merged

Add verdict-to-incident handoff#6
knarayanareddy merged 1 commit into
mainfrom
codex/oltp-incident-cases

Conversation

@knarayanareddy

@knarayanareddy knarayanareddy commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

  • add verdict-to-incident promotion so the proof board can create an operational incident record
  • persist incident case status, assignee, timestamps, verdict, action items, and linked analytics evidence
  • expose the incident handoff in the UI and smoke coverage

Verification

  • pnpm build
  • pnpm smoke

Notes: demo-capture artifacts remain local and untracked.

Summary by CodeRabbit

  • New Features

    • Added the ability to promote root-cause verdicts into operational incident records.
    • Added an incident board section displaying incident details, recommended actions, analytics links, and evidence task IDs.
    • Added support for viewing previously promoted incidents.
    • Added responsive incident card layouts for smaller screens.
  • Bug Fixes

    • Improved smoke checks for incident data and tracked secret-bearing files.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Incident promotion

Layer / File(s) Summary
Incident case contract and persistence
src/lib/types.ts, src/lib/incident-cases.ts, .gitignore
Defines IncidentCase, derives incident records from verdicts, validates stored entries, persists up to 20 cases, and ignores local incident data.
Server action wiring
app/actions.ts
Adds server actions for listing promoted incidents and saving a verdict as an incident case.
Promotion UI flow
app/page.tsx, app/styles.css
Loads promoted incidents, provides transition-backed promotion, renders incident details or a draft, and adds responsive incident-case styling.
Smoke validation
scripts/smoke.ts
Validates derived incident fields and detects secret-bearing files using Git-tracked file checks with a filesystem fallback.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: promoting a verdict into an incident handoff.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/oltp-incident-cases

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.

@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: 6

🧹 Nitpick comments (1)
scripts/smoke.ts (1)

83-90: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Strengthen 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 from board.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6dbd6d6 and 38e8a3d.

📒 Files selected for processing (7)
  • .gitignore
  • app/actions.ts
  • app/page.tsx
  • app/styles.css
  • scripts/smoke.ts
  • src/lib/incident-cases.ts
  • src/lib/types.ts

Comment thread app/actions.ts
Comment on lines +29 to +30
export async function promoteVerdictToIncident(board: IncidentBoard) {
return saveIncidentCaseFromVerdict(board);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment thread app/page.tsx
Comment on lines +47 to +57
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}`);
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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:


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.

Suggested change
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.

Comment thread app/page.tsx
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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"
done

Repository: 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 || true

Repository: 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:


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

Comment thread src/lib/incident-cases.ts
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}.`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 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.

Comment thread src/lib/incident-cases.ts
];

return {
id: `INC-${createdAt.slice(0, 10).replaceAll("-", "")}-${createdAt.slice(11, 19).replaceAll(":", "")}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

Comment thread src/lib/incident-cases.ts
Comment on lines +67 to +80
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"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ 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.

@knarayanareddy
knarayanareddy merged commit de3cbf0 into main Jul 23, 2026
1 check passed
@knarayanareddy
knarayanareddy deleted the codex/oltp-incident-cases branch July 23, 2026 12:28
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