<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anish Kumar</title>
    <description>The latest articles on DEV Community by Anish Kumar (@anishisbusy).</description>
    <link>https://dev.to/anishisbusy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4138281%2F44a24cdb-56fb-4285-9173-5b3251c2a63d.jpg</url>
      <title>DEV Community: Anish Kumar</title>
      <link>https://dev.to/anishisbusy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anishisbusy"/>
    <language>en</language>
    <item>
      <title>Incident War Room: an incident-response tool where the approval gate and the AI can't be faked out</title>
      <dc:creator>Anish Kumar</dc:creator>
      <pubDate>Sat, 26 Sep 2026 18:05:38 +0000</pubDate>
      <link>https://dev.to/anishisbusy/incident-war-room-an-incident-response-tool-where-the-approval-gate-and-the-ai-cant-be-faked-out-5ee0</link>
      <guid>https://dev.to/anishisbusy/incident-war-room-an-incident-response-tool-where-the-approval-gate-and-the-ai-cant-be-faked-out-5ee0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Incident War Room&lt;/strong&gt; is an incident-response app for on-call teams. When&lt;br&gt;
something breaks at 2 AM, everything about the incident lives on one page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;live timeline&lt;/strong&gt; that every responder sees update in real time, no refresh&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;SEV1 escalation gate&lt;/strong&gt;: raising an incident to SEV1 needs sign-off from
&lt;strong&gt;two different on-call leads&lt;/strong&gt; before anything changes, and it's enforced
on the server, not just hidden in the UI&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;status page entry&lt;/strong&gt; published automatically once the escalation is approved&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;runbooks&lt;/strong&gt; for the incident's severity shown right on the page&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;AI-drafted postmortem&lt;/strong&gt;: resolving freezes the timeline and an LLM
drafts the root cause and action items from it&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;Ops Dashboard&lt;/strong&gt; inside Sanity Studio, built with the App SDK, that
can approve escalations too, through the same gate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's built with Next.js 16 (App Router) and Sanity as the whole backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6p776cs526umh6m9pdv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz6p776cs526umh6m9pdv.png" alt="Incidents list" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Live app:&lt;/strong&gt; &lt;a href="https://incident-war-room-nine.vercel.app" rel="noopener noreferrer"&gt;https://incident-war-room-nine.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/5JVUoHkKi0c" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try the gate yourself&lt;/strong&gt; (all passwords are &lt;code&gt;demo1234&lt;/code&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log in as &lt;code&gt;alice@example.com&lt;/code&gt; (prefilled), open the sample incident and click &lt;strong&gt;Raise to SEV1&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Approve as Alice. It stays at &lt;strong&gt;1 / 2&lt;/strong&gt; and the incident is still SEV3.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Log out&lt;/strong&gt;, log in as &lt;code&gt;dana@example.com&lt;/code&gt;, open the same incident and approve. It flips to SEV1 / escalated and a status page entry appears.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Mark resolved&lt;/strong&gt;. The timeline is frozen into a postmortem with an AI-drafted root cause and action items.&lt;/li&gt;
&lt;li&gt;Log in as &lt;code&gt;bob@example.com&lt;/code&gt; (an engineer): you can't approve at all.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03i97sh6w4qghwlnezw4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F03i97sh6w4qghwlnezw4.png" alt="One of two approvals: the incident is still SEV3" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flvte898c9ipbbculacq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flvte898c9ipbbculacq8.png" alt="Escalated, published to the status page, resolved, and the AI-drafted postmortem" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/Sarcastic-Soul" rel="noopener noreferrer"&gt;
        Sarcastic-Soul
      &lt;/a&gt; / &lt;a href="https://github.com/Sarcastic-Soul/incident-war-room" rel="noopener noreferrer"&gt;
        incident-war-room
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Incident War Room — Next.js + Sanity incident management app (dev.to x Sanity Path Two hackathon)
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Incident War Room&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A real-time incident-management app built for the dev.to × Sanity
&lt;a href="https://dev.to/challenges/sanity-2026-09-16" rel="nofollow"&gt;"Path Two" hackathon&lt;/a&gt;. Next.js
16 on the front end, Sanity as the backend — using &lt;strong&gt;Sanity Workflows&lt;/strong&gt; and
the &lt;strong&gt;Sanity App SDK&lt;/strong&gt; for real functionality, not as checkbox features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://incident-war-room-nine.vercel.app" rel="nofollow noopener noreferrer"&gt;https://incident-war-room-nine.vercel.app&lt;/a&gt;
&lt;strong&gt;Sanity project:&lt;/strong&gt; &lt;code&gt;am9ihg1w&lt;/code&gt; / dataset &lt;code&gt;production&lt;/code&gt;
&lt;strong&gt;Login:&lt;/strong&gt; seeded test accounts in &lt;a href="https://github.com/Sarcastic-Soul/incident-war-room/docs/testing-credentials.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/testing-credentials.md&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live incident timeline&lt;/strong&gt; — updates via Sanity's &lt;code&gt;listen()&lt;/code&gt; API, no polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEV1 escalation gate&lt;/strong&gt; — raising an incident to SEV1 opens an
&lt;code&gt;escalationApproval&lt;/code&gt; request needing sign-off from &lt;strong&gt;two&lt;/strong&gt; on-call leads
before severity actually changes. Enforced server-side
(&lt;code&gt;src/app/incidents/[id]/actions.ts&lt;/code&gt;), not just hidden in the UI: the
approver is always the logged-in responder, must be an on-call lead, and
can't approve twice, so one person can't push a SEV1 through alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sanity Workflow&lt;/strong&gt; — &lt;code&gt;escalationApproval&lt;/code&gt; is tracked &lt;code&gt;pending → approved/rejected&lt;/code&gt;
as a Kanban board in Studio (&lt;code&gt;sanity-plugin-workflow&lt;/code&gt;). Once…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Sarcastic-Soul/incident-war-room" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The schema is documented in &lt;code&gt;docs/schema.md&lt;/code&gt; and the design in&lt;br&gt;
&lt;code&gt;docs/architecture.md&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;p&gt;I built the whole thing with &lt;strong&gt;Claude Code&lt;/strong&gt;. Before any code, I had it&lt;br&gt;
write planning docs (&lt;code&gt;docs/overview.md&lt;/code&gt;, &lt;code&gt;docs/architecture.md&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;docs/schema.md&lt;/code&gt;) so each later prompt had a fixed target: a schema, the&lt;br&gt;
rules for the escalation gate, and what "real" use of Workflows and the&lt;br&gt;
App SDK meant. My rule for both bonus features was that a judge should be&lt;br&gt;
able to try to break them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where the model got stuck, and how I corrected it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16 is newer than the model.&lt;/strong&gt; The repo has an &lt;code&gt;AGENTS.md&lt;/code&gt; that
tells the agent to read the docs bundled in &lt;code&gt;node_modules/next&lt;/code&gt; before
writing code, because APIs changed. That's how it got things like
&lt;code&gt;middleware&lt;/code&gt; being renamed to &lt;code&gt;proxy&lt;/code&gt; right, instead of writing
last year's Next.js from memory.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Workflow plugin doesn't enforce anything.&lt;/strong&gt; The plan was to make
&lt;code&gt;sanity-plugin-workflow&lt;/code&gt; the gate. Its transitions only run in the
Studio UI, so anything with a write token can set &lt;code&gt;status: "approved"&lt;/code&gt;
directly. The Kanban board stayed, to track where each escalation stands,
but the real rule moved into a Server Action (&lt;code&gt;approveEscalation&lt;/code&gt;) that
is the only code path allowed to finish the transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A gate that could never open.&lt;/strong&gt; The first seed data had only one
on-call lead, so the two-approver gate sat at 1 / 2 forever. It looked
like it worked, but it could never finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Features that were only there on paper.&lt;/strong&gt; A &lt;code&gt;runbook&lt;/code&gt; schema existed
in Studio, but nothing in the app read it, and the page title was still
"Create Next App". Asking the agent "what's unused or unfinished?" found
more than asking it to "add a feature".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My first gate could be faked.&lt;/strong&gt; Near the end I asked Claude Code for a
status check and then to "do all the things required for this project".
Before recording the demo, it read &lt;code&gt;approveEscalation&lt;/code&gt; and pointed out
that the approver id came from a hidden form field, with an
"Approve as …" button for every lead. Alice could approve as herself
&lt;em&gt;and&lt;/em&gt; as Dana and push a SEV1 through alone, which is exactly what the
gate is for. Now the approver is whoever the signed session says is
logged in, must be an on-call lead, can't approve twice, and the write
uses &lt;code&gt;ifRevisionId&lt;/code&gt; so two approvals at the same moment can't both count
as "the first".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs you only see by clicking through.&lt;/strong&gt; Recording the demo also turned
up: &lt;code&gt;listen()&lt;/code&gt; ignores GROQ projections, so live posts arrived with
&lt;code&gt;author&lt;/code&gt; as a bare reference and showed "Unknown responder"; events the
server added (escalation, resolution) didn't show up because the client
kept its first list; and the page showed two different clocks (UTC from
the server on Vercel, local time from the browser). All fixed, all found
by driving the live site with Playwright, not by reading code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reaching past the Studio:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Workflows.&lt;/strong&gt; &lt;code&gt;escalationApproval&lt;/code&gt; is a Sanity Workflow
(&lt;code&gt;pending → approved / rejected&lt;/code&gt;), shown as a Kanban board in Studio.
Once the Server Action counts two approvals, it flips the incident to
SEV1 / escalated, creates a &lt;code&gt;statusPageEntry&lt;/code&gt;, adds a timeline event and
can POST to an external status page webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;App SDK.&lt;/strong&gt; The Ops Dashboard is a custom Studio tool built with
&lt;code&gt;@sanity/sdk-react&lt;/code&gt;. One GROQ query gives live mean time to resolution,
open incidents by severity and pending approvals. It started read-only,
which felt like decoration, so it can now approve escalations too, using
the &lt;em&gt;same&lt;/em&gt; Server Action as the app. There's no second, weaker path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI inside the content workflow.&lt;/strong&gt; Resolving an incident sends the
frozen timeline to Groq (&lt;code&gt;openai/gpt-oss-120b&lt;/code&gt;) for a root cause and up
to three action items, told to say "unknown" instead of guessing. A
custom Studio input (&lt;code&gt;RootCauseInput.tsx&lt;/code&gt;) adds a "Regenerate with AI"
button, so an editor can ask for another draft at any time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Project ID:&lt;/strong&gt; &lt;code&gt;am9ihg1w&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dataset:&lt;/strong&gt; &lt;code&gt;production&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Schema types: &lt;code&gt;incident&lt;/code&gt;, &lt;code&gt;timelineEvent&lt;/code&gt;, &lt;code&gt;responder&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;escalationApproval&lt;/code&gt;, &lt;code&gt;statusPageEntry&lt;/code&gt;, &lt;code&gt;postmortem&lt;/code&gt;, &lt;code&gt;runbook&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two modeling decisions I'd defend:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Timeline events are their own documents, not an array on the
incident.&lt;/strong&gt; An array means every responder posting at once edits the
same document. As separate &lt;code&gt;timelineEvent&lt;/code&gt; documents that reference the
incident, two posts in the same second never conflict, and the app
subscribes to them with &lt;code&gt;listen()&lt;/code&gt; filtered by &lt;code&gt;incident._ref&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postmortems copy the timeline instead of referencing it.&lt;/strong&gt; On
resolution, every event is copied into &lt;code&gt;postmortem.timelineSnapshot&lt;/code&gt; as
plain objects, so later edits to the live timeline can't quietly change
what the postmortem says happened.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
