-
Notifications
You must be signed in to change notification settings - Fork 0
Add verdict-to-incident handoff #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,3 +9,4 @@ out | |
| coverage | ||
| *.tsbuildinfo | ||
| clickfuse-fixture.json | ||
| .clickfuse-data/ | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,10 +2,10 @@ | |||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import { useChat } from "@ai-sdk/react"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { useTriggerChatTransport } from "@trigger.dev/sdk/chat/react"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { FormEvent, useMemo, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { mintChatAccessToken, startChatSession } from "./actions"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { FormEvent, useEffect, useMemo, useState, useTransition } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { listPromotedIncidents, mintChatAccessToken, promoteVerdictToIncident, startChatSession } from "./actions"; | ||||||||||||||||||||||||||||||||||||||||||||
| import { buildIncidentBoard } from "../src/lib/investigation"; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { IncidentBoard } from "../src/lib/types"; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { IncidentBoard, IncidentCase } from "../src/lib/types"; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { ServiceName } from "../src/lib/types"; | ||||||||||||||||||||||||||||||||||||||||||||
| import type { incidentAgent } from "../trigger/incident-agent"; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -14,6 +14,9 @@ export default function Home() { | |||||||||||||||||||||||||||||||||||||||||||
| const [question, setQuestion] = useState("Why did checkout latency spike after the 14:32 deploy?"); | ||||||||||||||||||||||||||||||||||||||||||||
| const [selectedService, setSelectedService] = useState<ServiceName | "all">("all"); | ||||||||||||||||||||||||||||||||||||||||||||
| const [openEvidence, setOpenEvidence] = useState<string | null>("timeline"); | ||||||||||||||||||||||||||||||||||||||||||||
| const [incidentCases, setIncidentCases] = useState<IncidentCase[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||
| const [promoteMessage, setPromoteMessage] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||
| const [isPromoting, startPromoteTransition] = useTransition(); | ||||||||||||||||||||||||||||||||||||||||||||
| const fixtureBoard = useMemo(() => buildIncidentBoard(), []); | ||||||||||||||||||||||||||||||||||||||||||||
| const liveMode = process.env.NEXT_PUBLIC_TRIGGER_CHAT_ENABLED === "true"; | ||||||||||||||||||||||||||||||||||||||||||||
| const transport = useTriggerChatTransport<typeof incidentAgent>({ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -27,6 +30,10 @@ export default function Home() { | |||||||||||||||||||||||||||||||||||||||||||
| [fixtureBoard, liveMode, messages] | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| void listPromotedIncidents().then(setIncidentCases).catch(() => setIncidentCases([])); | ||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function submit(event: FormEvent<HTMLFormElement>) { | ||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||
| setSubmitted(true); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -35,6 +42,22 @@ export default function Home() { | |||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function promoteCurrentVerdict() { | ||||||||||||||||||||||||||||||||||||||||||||
| setPromoteMessage(null); | ||||||||||||||||||||||||||||||||||||||||||||
| 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}`); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+57
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
💡 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 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <main className="shell"> | ||||||||||||||||||||||||||||||||||||||||||||
| <section className="hero"> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -72,6 +95,10 @@ export default function Home() { | |||||||||||||||||||||||||||||||||||||||||||
| setSelectedService={setSelectedService} | ||||||||||||||||||||||||||||||||||||||||||||
| openEvidence={openEvidence} | ||||||||||||||||||||||||||||||||||||||||||||
| setOpenEvidence={setOpenEvidence} | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases={incidentCases} | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage={promoteMessage} | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting={isPromoting} | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote={promoteCurrentVerdict} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| </main> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -131,13 +158,21 @@ function IncidentBoard({ | |||||||||||||||||||||||||||||||||||||||||||
| selectedService, | ||||||||||||||||||||||||||||||||||||||||||||
| setSelectedService, | ||||||||||||||||||||||||||||||||||||||||||||
| openEvidence, | ||||||||||||||||||||||||||||||||||||||||||||
| setOpenEvidence | ||||||||||||||||||||||||||||||||||||||||||||
| setOpenEvidence, | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases, | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage, | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting, | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote | ||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||
| board: ReturnType<typeof buildIncidentBoard>; | ||||||||||||||||||||||||||||||||||||||||||||
| selectedService: ServiceName | "all"; | ||||||||||||||||||||||||||||||||||||||||||||
| setSelectedService: (service: ServiceName | "all") => void; | ||||||||||||||||||||||||||||||||||||||||||||
| openEvidence: string | null; | ||||||||||||||||||||||||||||||||||||||||||||
| setOpenEvidence: (id: string | null) => void; | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases: IncidentCase[]; | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage: string | null; | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting: boolean; | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote: () => void; | ||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||
| const selected = selectedService === "all" ? null : selectedService; | ||||||||||||||||||||||||||||||||||||||||||||
| const timeline = selected ? board.timeline.points.filter((p) => p.service === selected) : board.timeline.points; | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -181,6 +216,13 @@ function IncidentBoard({ | |||||||||||||||||||||||||||||||||||||||||||
| open={openEvidence === "verdict"} | ||||||||||||||||||||||||||||||||||||||||||||
| onToggle={() => setOpenEvidence(openEvidence === "verdict" ? null : "verdict")} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| <IncidentCaseCard | ||||||||||||||||||||||||||||||||||||||||||||
| board={board} | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases={incidentCases} | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage={promoteMessage} | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting={isPromoting} | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote={onPromote} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| </section> | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -401,6 +443,75 @@ function VerdictCard({ | |||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function IncidentCaseCard({ | ||||||||||||||||||||||||||||||||||||||||||||
| board, | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases, | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage, | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting, | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote | ||||||||||||||||||||||||||||||||||||||||||||
| }: { | ||||||||||||||||||||||||||||||||||||||||||||
| board: ReturnType<typeof buildIncidentBoard>; | ||||||||||||||||||||||||||||||||||||||||||||
| incidentCases: IncidentCase[]; | ||||||||||||||||||||||||||||||||||||||||||||
| promoteMessage: string | null; | ||||||||||||||||||||||||||||||||||||||||||||
| isPromoting: boolean; | ||||||||||||||||||||||||||||||||||||||||||||
| onPromote: () => void; | ||||||||||||||||||||||||||||||||||||||||||||
| }) { | ||||||||||||||||||||||||||||||||||||||||||||
| const latest = incidentCases[0]; | ||||||||||||||||||||||||||||||||||||||||||||
| const evidenceIds = latest?.linkedAnalytics.evidenceTaskIds ?? [ | ||||||||||||||||||||||||||||||||||||||||||||
| board.timeline.evidence.taskId, | ||||||||||||||||||||||||||||||||||||||||||||
| board.diff.evidence.taskId, | ||||||||||||||||||||||||||||||||||||||||||||
| board.verdict.evidence.taskId | ||||||||||||||||||||||||||||||||||||||||||||
| ].filter((taskId): taskId is string => Boolean(taskId)); | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <article className="card wide incident-case-card"> | ||||||||||||||||||||||||||||||||||||||||||||
| <CardHeader title="Operational incident case" label="OLTP + OLAP handoff" tone="green" /> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="incident-case-grid"> | ||||||||||||||||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||||||||||||||||
| <p className="caption"> | ||||||||||||||||||||||||||||||||||||||||||||
| Promote the analytical verdict into a mutable incident record: status, owner, action items, and links back to | ||||||||||||||||||||||||||||||||||||||||||||
| the ClickHouse evidence that produced the answer. | ||||||||||||||||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||||||||||||||||
| <button className="promote-button" type="button" onClick={onPromote} disabled={isPromoting}> | ||||||||||||||||||||||||||||||||||||||||||||
| {isPromoting ? "Opening incident…" : "Promote verdict to incident"} | ||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||
| {promoteMessage && <p className="promote-message">{promoteMessage}</p>} | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="incident-case"> | ||||||||||||||||||||||||||||||||||||||||||||
| {latest ? ( | ||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="case-topline"> | ||||||||||||||||||||||||||||||||||||||||||||
| <strong>{latest.id}</strong> | ||||||||||||||||||||||||||||||||||||||||||||
| <span>{latest.status}</span> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <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. Choose a reason for hiding this commentThe 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"
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:
💡 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.
🧰 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 AgentsSource: Linters/SAST tools |
||||||||||||||||||||||||||||||||||||||||||||
| <div><dt>Confidence</dt><dd>{Math.round(latest.confidence * 100)}%</dd></div> | ||||||||||||||||||||||||||||||||||||||||||||
| <div><dt>Burn rate</dt><dd>{latest.linkedAnalytics.burnRate}x</dd></div> | ||||||||||||||||||||||||||||||||||||||||||||
| </dl> | ||||||||||||||||||||||||||||||||||||||||||||
| <ol> | ||||||||||||||||||||||||||||||||||||||||||||
| {latest.actionItems.map((item) => <li key={item}>{item}</li>)} | ||||||||||||||||||||||||||||||||||||||||||||
| </ol> | ||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="case-topline"> | ||||||||||||||||||||||||||||||||||||||||||||
| <strong>Draft from current verdict</strong> | ||||||||||||||||||||||||||||||||||||||||||||
| <span>not opened</span> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <h3>{board.verdict.rootCause}</h3> | ||||||||||||||||||||||||||||||||||||||||||||
| <p className="caption">{board.verdict.recommendedAction}</p> | ||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||||||||
| <p className="case-links">Linked analytics: {evidenceIds.join(" · ")}</p> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| </article> | ||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function CardHeader({ title, label, tone }: { title: string; label: string; tone: "red" | "amber" | "green" }) { | ||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <header className="card-header"> | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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