Blocks

Incident Response Block

Operations UI

A copyable incident command center for production incidents, on-call handoffs, customer communications, and post-incident accountability.

Reliability

Incident command center

Copy this into SaaS admin consoles, internal tools, infrastructure products, support operations, or status-page workflows where teams need one place to coordinate an active incident.

1200px

<script setup>
import { computed, ref, watch } from 'vue';
import { DomButton, DomNativeSelect, DomTabs, DomTextareaInput, DomToggle } from '@getdom/studio/vue';

const severityOptions = [
	{ label: 'SEV-1 Critical', value: 'SEV-1' },
	{ label: 'SEV-2 Major', value: 'SEV-2' },
	{ label: 'SEV-3 Minor', value: 'SEV-3' },
];

const audienceOptions = [
	{ label: 'Internal only', value: 'Internal only' },
	{ label: 'Customer email', value: 'Customer email' },
	{ label: 'Public status page', value: 'Public status page' },
];

const updateCadenceOptions = [
	{ label: '15 minutes', value: 15 },
	{ label: '30 minutes', value: 30 },
	{ label: '60 minutes', value: 60 },
];

const tabs = [
	{ key: 'tasks', label: 'Tasks' },
	{ key: 'timeline', label: 'Timeline' },
];

const incidents = [
	{
		id: 'checkout-errors',
		title: 'Elevated checkout API errors',
		status: 'Mitigating',
		severity: 'SEV-2',
		commander: 'Maya Chen',
		deputy: 'Sam Patel',
		started: '42 min',
		nextUpdate: '12 min',
		impact: '128 accounts',
		region: 'US + EU',
		summary: 'Checkout creation intermittently returns 502 responses after the edge gateway deploy.',
		services: [
			{ name: 'Checkout API', state: 'Degraded', metric: '7.8% errors', trend: 'down' },
			{ name: 'Edge gateway', state: 'Degraded', metric: 'P95 2.4s', trend: 'down' },
			{ name: 'Billing worker', state: 'Operational', metric: '0.2% errors', trend: 'flat' },
		],
		tasks: [
			{ label: 'Roll back edge gateway release', owner: 'API on-call', status: 'Done', due: 'Now' },
			{ label: 'Verify checkout creation from EU test tenant', owner: 'QA lead', status: 'In progress', due: '8 min' },
			{ label: 'Prepare enterprise customer note', owner: 'Support lead', status: 'Waiting', due: '15 min' },
			{ label: 'Open postmortem draft', owner: 'Incident deputy', status: 'Waiting', due: 'After mitigation' },
		],
		timeline: [
			{ label: 'Rollback started for edge gateway v2026.06.10', actor: 'API on-call', time: 'Today 16:18' },
			{ label: 'Public status page updated', actor: 'Maya Chen', time: 'Today 16:07' },
			{ label: 'Incident promoted from SEV-3 to SEV-2', actor: 'SRE bot', time: 'Today 15:58' },
			{ label: 'Alert opened from checkout error budget burn', actor: 'Monitoring', time: 'Today 15:42' },
		],
	},
	{
		id: 'webhook-lag',
		title: 'Webhook delivery backlog',
		status: 'Investigating',
		severity: 'SEV-3',
		commander: 'Ivy Tan',
		deputy: 'Jon Bell',
		started: '1h 12m',
		nextUpdate: '24 min',
		impact: '42 accounts',
		region: 'US only',
		summary: 'Delivery attempts are delayed for large batch customers while workers drain a retry queue.',
		services: [
			{ name: 'Webhook worker', state: 'Degraded', metric: '18k queued', trend: 'down' },
			{ name: 'Events API', state: 'Operational', metric: 'P95 180ms', trend: 'flat' },
			{ name: 'Retry store', state: 'Watch', metric: '72% capacity', trend: 'up' },
		],
		tasks: [
			{ label: 'Scale webhook workers to 60 replicas', owner: 'Platform on-call', status: 'Done', due: 'Done' },
			{ label: 'Identify tenants above retry threshold', owner: 'Data ops', status: 'In progress', due: '20 min' },
			{ label: 'Draft delayed delivery customer notice', owner: 'Success ops', status: 'Waiting', due: '30 min' },
		],
		timeline: [
			{ label: 'Retry queue dropped below 20k events', actor: 'Monitoring', time: 'Today 15:54' },
			{ label: 'Workers scaled from 24 to 60 replicas', actor: 'Platform on-call', time: 'Today 15:31' },
			{ label: 'Incident opened from delivery lag alert', actor: 'SRE bot', time: 'Today 15:12' },
		],
	},
	{
		id: 'login-timeouts',
		title: 'Login timeouts for SSO tenants',
		status: 'Monitoring',
		severity: 'SEV-2',
		commander: 'Nora Lee',
		deputy: 'Ari Grant',
		started: '2h 04m',
		nextUpdate: '18 min',
		impact: '21 tenants',
		region: 'EU only',
		summary: 'SAML login latency recovered after cache failover. Team is monitoring error budget and support volume.',
		services: [
			{ name: 'Identity API', state: 'Watch', metric: 'P95 640ms', trend: 'down' },
			{ name: 'SAML broker', state: 'Operational', metric: '0.1% errors', trend: 'flat' },
			{ name: 'Session cache', state: 'Operational', metric: 'Primary healthy', trend: 'flat' },
		],
		tasks: [
			{ label: 'Confirm SAML login from affected tenants', owner: 'Support lead', status: 'In progress', due: '10 min' },
			{ label: 'Keep failover cache as primary', owner: 'Identity on-call', status: 'Done', due: 'Done' },
			{ label: 'Schedule post-incident review', owner: 'Incident deputy', status: 'Waiting', due: 'Tomorrow' },
		],
		timeline: [
			{ label: 'Customer reports slowed to baseline', actor: 'Support lead', time: 'Today 14:51' },
			{ label: 'Session cache failover completed', actor: 'Identity on-call', time: 'Today 14:29' },
			{ label: 'Incident opened by support escalation', actor: 'Ari Grant', time: 'Today 13:58' },
		],
	},
];

const selectedIncidentId = ref(incidents[0].id);
const selectedSeverity = ref(incidents[0].severity);
const selectedAudience = ref('Public status page');
const updateCadence = ref(30);
const activeTab = ref('tasks');
const notifyEnterprise = ref(true);
const requireApproval = ref(true);
const includeMetrics = ref(false);
const updateDraft = ref('We are mitigating elevated checkout API errors. Checkout creation may fail intermittently for some customers while rollback and verification continue.');
const publishedUpdate = ref('');

const selectedIncident = computed(() => incidents.find((incident) => incident.id === selectedIncidentId.value) || incidents[0]);
const completedTasks = computed(() => selectedIncident.value.tasks.filter((task) => task.status === 'Done').length);
const readinessPercent = computed(() => Math.round((completedTasks.value / selectedIncident.value.tasks.length) * 100));
const affectedServices = computed(() => selectedIncident.value.services.filter((service) => service.state !== 'Operational').length);
const canPublish = computed(() => updateDraft.value.trim().length >= 40 && (!requireApproval.value || selectedAudience.value === 'Internal only' || selectedIncident.value.status !== 'Investigating'));
const updateSummary = computed(() => `${selectedAudience.value} update queued for ${selectedIncident.value.title}`);

watch(selectedIncident, (incident) => {
	selectedSeverity.value = incident.severity;
	activeTab.value = 'tasks';
	publishedUpdate.value = '';
	updateDraft.value = `We are ${incident.status.toLowerCase()} ${incident.title.toLowerCase()}. ${incident.summary}`;
});

function selectIncident(incident) {
	selectedIncidentId.value = incident.id;
}

function publishUpdate() {
	if (!canPublish.value) return;
	publishedUpdate.value = updateSummary.value;
}

function severityClasses(severity) {
	return {
		'SEV-1': 'bg-destructive/15 text-destructive',
		'SEV-2': 'bg-warning/15 text-warning',
		'SEV-3': 'bg-primary/10 text-primary',
	}[severity] || 'bg-secondary text-muted-fg';
}

function statusClasses(status) {
	return {
		Mitigating: 'bg-warning/15 text-warning',
		Investigating: 'bg-primary/10 text-primary',
		Monitoring: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
		Done: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
		'In progress': 'bg-primary/10 text-primary',
		Waiting: 'bg-secondary text-muted-fg',
		Operational: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
		Degraded: 'bg-warning/15 text-warning',
		Watch: 'bg-primary/10 text-primary',
	}[status] || 'bg-secondary text-muted-fg';
}
</script>

<template>
	<div class="w-full overflow-hidden rounded-3xl border border-border bg-background text-fg shadow-2xl shadow-black/10">
		<header class="border-b border-border skin-raised px-4 py-4 sm:px-6">
			<div class="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
				<div>
					<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Reliability operations</p>
					<h3 class="mt-1 text-2xl font-semibold tracking-tight">Incident command center</h3>
					<p class="mt-2 max-w-2xl text-sm leading-6 text-muted-fg">
						Coordinate active incidents with service health, owners, customer impact, communication review, and timeline evidence in one workspace.
					</p>
				</div>
				<div class="flex flex-wrap items-center gap-2">
					<DomNativeSelect v-model="selectedSeverity" :options="severityOptions" class="w-40" />
					<DomButton size="sm" variant="secondary">
						<svg class="size-4" viewBox="0 0 24 24" fill="none" aria-hidden="true">
							<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
						</svg>
						New incident
					</DomButton>
				</div>
			</div>
		</header>

		<div class="grid min-h-[46rem] lg:grid-cols-[19rem_minmax(0,1fr)_22rem]">
			<aside class="border-b border-border skin-raised lg:border-b-0 lg:border-r">
				<div class="grid grid-cols-3 border-b border-border text-center text-sm">
					<div class="px-3 py-3">
						<p class="text-xs text-muted-fg">Open</p>
						<p class="mt-1 text-lg font-semibold">{{ incidents.length }}</p>
					</div>
					<div class="border-l border-border px-3 py-3">
						<p class="text-xs text-muted-fg">SEV-2+</p>
						<p class="mt-1 text-lg font-semibold">2</p>
					</div>
					<div class="border-l border-border px-3 py-3">
						<p class="text-xs text-muted-fg">On call</p>
						<p class="mt-1 text-lg font-semibold">7</p>
					</div>
				</div>

				<div class="p-3">
					<p class="px-2 pb-2 text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Active incidents</p>
					<div class="space-y-2">
						<button
							v-for="incident in incidents"
							:key="incident.id"
							type="button"
							class="w-full rounded-xl border p-3 text-left transition hover:border-primary/50"
							:class="incident.id === selectedIncident.id ? 'border-primary/60 bg-primary/5' : 'border-border bg-background'"
							@click="selectIncident(incident)"
						>
							<div class="flex items-start justify-between gap-3">
								<div class="min-w-0">
									<p class="truncate text-sm font-semibold">{{ incident.title }}</p>
									<p class="mt-1 text-xs text-muted-fg">{{ incident.impact }} affected</p>
								</div>
								<span class="rounded-full px-2 py-0.5 text-[11px] font-semibold" :class="severityClasses(incident.severity)">
									{{ incident.severity }}
								</span>
							</div>
							<div class="mt-3 flex items-center justify-between text-xs">
								<span class="rounded-full px-2 py-0.5 font-semibold" :class="statusClasses(incident.status)">
									{{ incident.status }}
								</span>
								<span class="text-muted-fg">Next {{ incident.nextUpdate }}</span>
							</div>
						</button>
					</div>
				</div>
			</aside>

			<main class="min-w-0">
				<section class="border-b border-border px-4 py-5 sm:px-6">
					<div class="flex flex-col gap-4 xl:flex-row xl:items-start xl:justify-between">
						<div>
							<div class="flex flex-wrap items-center gap-2">
								<span class="rounded-full px-2.5 py-1 text-xs font-semibold" :class="severityClasses(selectedSeverity)">
									{{ selectedSeverity }}
								</span>
								<span class="rounded-full px-2.5 py-1 text-xs font-semibold" :class="statusClasses(selectedIncident.status)">
									{{ selectedIncident.status }}
								</span>
							</div>
							<h4 class="mt-3 text-xl font-semibold tracking-tight">{{ selectedIncident.title }}</h4>
							<p class="mt-2 max-w-2xl text-sm leading-6 text-muted-fg">{{ selectedIncident.summary }}</p>
						</div>
						<div class="grid grid-cols-3 overflow-hidden rounded-xl border border-border text-center text-sm">
							<div class="px-3 py-3">
								<p class="text-xs text-muted-fg">Duration</p>
								<p class="mt-1 font-semibold">{{ selectedIncident.started }}</p>
							</div>
							<div class="border-l border-border px-3 py-3">
								<p class="text-xs text-muted-fg">Region</p>
								<p class="mt-1 font-semibold">{{ selectedIncident.region }}</p>
							</div>
							<div class="border-l border-border px-3 py-3">
								<p class="text-xs text-muted-fg">Services</p>
								<p class="mt-1 font-semibold">{{ affectedServices }}</p>
							</div>
						</div>
					</div>
				</section>

				<section class="grid border-b border-border md:grid-cols-3">
					<div class="border-b border-border px-4 py-4 sm:px-6 md:border-b-0 md:border-r">
						<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Commander</p>
						<p class="mt-2 font-semibold">{{ selectedIncident.commander }}</p>
						<p class="mt-1 text-sm text-muted-fg">Deputy: {{ selectedIncident.deputy }}</p>
					</div>
					<div class="border-b border-border px-4 py-4 sm:px-6 md:border-b-0 md:border-r">
						<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Customer impact</p>
						<p class="mt-2 font-semibold">{{ selectedIncident.impact }}</p>
						<p class="mt-1 text-sm text-muted-fg">Impacted region: {{ selectedIncident.region }}</p>
					</div>
					<div class="px-4 py-4 sm:px-6">
						<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Task readiness</p>
						<div class="mt-3 h-2 overflow-hidden rounded-full bg-secondary">
							<div class="h-full rounded-full bg-primary" :style="{ width: `${readinessPercent}%` }"></div>
						</div>
						<p class="mt-2 text-sm text-muted-fg">{{ completedTasks }} of {{ selectedIncident.tasks.length }} tasks complete</p>
					</div>
				</section>

				<section class="border-b border-border px-4 py-5 sm:px-6">
					<div class="flex items-center justify-between gap-4">
						<div>
							<h5 class="font-semibold tracking-tight">Affected services</h5>
							<p class="mt-1 text-sm text-muted-fg">Current component health from monitoring and incident annotations.</p>
						</div>
						<DomButton size="sm" variant="secondary">Open metrics</DomButton>
					</div>
					<div class="mt-4 divide-y divide-border rounded-xl border border-border">
						<div
							v-for="service in selectedIncident.services"
							:key="service.name"
							class="grid gap-3 px-3 py-3 text-sm sm:grid-cols-[minmax(0,1fr)_7rem_7rem_4rem] sm:items-center"
						>
							<div class="min-w-0">
								<p class="truncate font-medium">{{ service.name }}</p>
								<p class="mt-1 text-xs text-muted-fg">Linked to monitor and runbook</p>
							</div>
							<span class="w-fit rounded-full px-2 py-0.5 text-xs font-semibold" :class="statusClasses(service.state)">
								{{ service.state }}
							</span>
							<span class="text-muted-fg">{{ service.metric }}</span>
							<span class="text-xs font-semibold" :class="service.trend === 'up' ? 'text-warning' : 'text-success'">
								{{ service.trend }}
							</span>
						</div>
					</div>
				</section>

				<section class="px-4 py-5 sm:px-6">
					<DomTabs v-model="activeTab" :tabs="tabs" />
					<div v-if="activeTab === 'tasks'" class="mt-4 divide-y divide-border rounded-xl border border-border">
						<div
							v-for="task in selectedIncident.tasks"
							:key="task.label"
							class="grid gap-3 px-3 py-3 text-sm sm:grid-cols-[minmax(0,1fr)_7rem_6rem] sm:items-center"
						>
							<div>
								<p class="font-medium">{{ task.label }}</p>
								<p class="mt-1 text-xs text-muted-fg">{{ task.owner }}</p>
							</div>
							<span class="w-fit rounded-full px-2 py-0.5 text-xs font-semibold" :class="statusClasses(task.status)">
								{{ task.status }}
							</span>
							<span class="text-muted-fg">{{ task.due }}</span>
						</div>
					</div>
					<div v-else class="mt-4 divide-y divide-border rounded-xl border border-border">
						<div v-for="event in selectedIncident.timeline" :key="`${event.label}-${event.time}`" class="px-3 py-3 text-sm">
							<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
								<p class="font-medium">{{ event.label }}</p>
								<span class="text-xs text-muted-fg">{{ event.time }}</span>
							</div>
							<p class="mt-1 text-xs text-muted-fg">{{ event.actor }}</p>
						</div>
					</div>
				</section>
			</main>

			<aside class="border-t border-border skin-raised lg:border-l lg:border-t-0">
				<section class="border-b border-border px-4 py-5">
					<h5 class="font-semibold tracking-tight">Status update</h5>
					<p class="mt-2 text-sm leading-6 text-muted-fg">Prepare the next incident update with audience, cadence, and approval controls.</p>
					<div class="mt-4 space-y-3">
						<DomNativeSelect v-model="selectedAudience" :options="audienceOptions" />
						<DomNativeSelect v-model="updateCadence" :options="updateCadenceOptions" />
						<DomTextareaInput v-model="updateDraft" label="Update draft" rows="6" />
					</div>
					<div class="mt-4 divide-y divide-border rounded-xl border border-border">
						<label class="flex items-center justify-between gap-4 px-3 py-3">
							<span>
								<span class="block text-sm font-medium">Notify enterprise accounts</span>
								<span class="block text-xs text-muted-fg">Send account-scoped email updates.</span>
							</span>
							<DomToggle v-model="notifyEnterprise" aria-label="Notify enterprise accounts" />
						</label>
						<label class="flex items-center justify-between gap-4 px-3 py-3">
							<span>
								<span class="block text-sm font-medium">Require comms approval</span>
								<span class="block text-xs text-muted-fg">Block public drafts until reviewed.</span>
							</span>
							<DomToggle v-model="requireApproval" aria-label="Require communications approval" />
						</label>
						<label class="flex items-center justify-between gap-4 px-3 py-3">
							<span>
								<span class="block text-sm font-medium">Include live metrics</span>
								<span class="block text-xs text-muted-fg">Attach current error and latency snapshot.</span>
							</span>
							<DomToggle v-model="includeMetrics" aria-label="Include live metrics" />
						</label>
					</div>
					<DomButton class="mt-4 w-full" :disabled="!canPublish" @click="publishUpdate">
						Publish update
					</DomButton>
					<p v-if="publishedUpdate" class="mt-3 rounded-xl bg-success/10 px-3 py-2 text-sm font-medium text-success">
						{{ publishedUpdate }}
					</p>
					<p v-else-if="!canPublish" class="mt-3 rounded-xl bg-warning/10 px-3 py-2 text-sm font-medium text-warning">
						Add a detailed update or switch to internal-only before publishing.
					</p>
				</section>

				<section class="border-b border-border px-4 py-5">
					<h5 class="font-semibold tracking-tight">Response checks</h5>
					<div class="mt-4 space-y-3 text-sm">
						<div class="flex items-start gap-3">
							<span class="mt-1 size-2 rounded-full bg-success"></span>
							<div>
								<p class="font-medium">Commander assigned</p>
								<p class="text-xs text-muted-fg">{{ selectedIncident.commander }} owns coordination.</p>
							</div>
						</div>
						<div class="flex items-start gap-3">
							<span class="mt-1 size-2 rounded-full bg-success"></span>
							<div>
								<p class="font-medium">Customer impact captured</p>
								<p class="text-xs text-muted-fg">{{ selectedIncident.impact }} across {{ selectedIncident.region }}.</p>
							</div>
						</div>
						<div class="flex items-start gap-3">
							<span class="mt-1 size-2 rounded-full" :class="readinessPercent > 60 ? 'bg-success' : 'bg-warning'"></span>
							<div>
								<p class="font-medium">Mitigation tasks moving</p>
								<p class="text-xs text-muted-fg">{{ readinessPercent }}% of response tasks complete.</p>
							</div>
						</div>
					</div>
				</section>

				<section class="px-4 py-5">
					<h5 class="font-semibold tracking-tight">Activity history</h5>
					<div class="mt-4 space-y-4">
						<div v-for="event in selectedIncident.timeline.slice(0, 3)" :key="event.label" class="border-l border-border pl-3 text-sm">
							<p class="font-medium">{{ event.label }}</p>
							<p class="mt-1 text-xs text-muted-fg">{{ event.actor }} / {{ event.time }}</p>
						</div>
					</div>
				</section>
			</aside>
		</div>
	</div>
</template>

Integration

How to use this block

Use this block when your product needs a practical incident workspace instead of scattered chat messages, tickets, and status-page drafts. It keeps severity, ownership, impacted services, customer communications, mitigation tasks, and timeline events visible for both engineering and customer-facing teams.

  • Load incident records from your incident API with severity, status, commander, affected services, customer impact, task ownership, and timeline events.
  • Connect service health to your monitoring provider so affected components and current metrics update independently from the written incident narrative.
  • Persist each status update with audience, author, timestamp, delivery channels, and acknowledgement state before sending it to a public status page or customer email list.
  • Keep severity transitions, task completions, and customer communications as immutable audit events so postmortems can reconstruct what happened.
  • Gate public updates, severity downgrades, and resolution behind permission checks when the incident affects enterprise customers or regulated systems.

Data

Recommended incident payload

{
	id: 'inc_2048',
	title: 'Elevated checkout API errors',
	severity: 'SEV-2',
	status: 'Mitigating',
	commander: 'Maya Chen',
	startedAt: '2026-06-10T15:42:00Z',
	customerImpact: {
		accountsAffected: 128,
		regions: ['us-east-1', 'eu-west-1'],
		summary: 'Checkout creation intermittently returns 502 responses.'
	},
	services: [
		{ key: 'checkout_api', name: 'Checkout API', state: 'Degraded', errorRate: 7.8 },
		{ key: 'billing_worker', name: 'Billing worker', state: 'Operational', errorRate: 0.2 }
	],
	tasks: [
		{ id: 'task_1', owner: 'API on-call', label: 'Roll back gateway release', status: 'Done' },
		{ id: 'task_2', owner: 'Support lead', label: 'Notify enterprise accounts', status: 'In progress' }
	],
	updateDraft: {
		audience: 'Public status page',
		message: 'We are mitigating elevated checkout API errors and will update again in 20 minutes.'
	}
}

Customization

Implementation notes

Incident model

Separate incident state from service health. Monitoring updates can change service rows while commanders keep narrative updates deliberate and reviewed.

Communication safety

Require audience, next-update timing, and approval status before publishing public updates or broad customer notifications.

Future updates

Good follow-ups include status-page channel pickers, PagerDuty/Opsgenie sync, postmortem templates, SLA impact calculators, and runbook launchers.