Seen as a design The generated document itself — its toggles and disclosures work here
Seen as a design The generated document itself — its toggles and disclosures work here
Copy and paste, or save a file and hand it over. For a machine with no network, one “Everything” file is enough.
Everything in this panel, including the contents behind each URL, in one Markdown file — Large, so it goes as a file (copy is for 32 KB and under)
Build an HTML document with "Disaster Recovery Runbook". This layout runs, so there is no composition to rebuild — what it needs is content. ## What this layout does Coordinate service recovery dependencies, RTO/RPO, owners, actions, and validation in one ordered runbook. ## What you have to supply - Define system, RTO/RPO, ordered steps, prerequisites, target time, owner, recovery action, and validation. ## The shape of the input - `title` — string — 復旧計画の結論 - `subtitle` — string? — 対象環境・演習日・承認版 - `system` — string — 対象システム - `rtoMinutes` — number — サービス復旧目標時間 - `rpoMinutes` — number — 許容データ損失時間 - `steps` — array (3–12) — 依存順序、目標時刻、操作、責任者、検証、準備状態を持つ復旧工程 schema.ts holds the machine-checkable validation. Ask rather than guess when an input is missing. ## When not to use it - Use an incident response policy to define severity-based response and escalation immediately after an outage. ## How to build it Put the input in a JSON file and run: ```bash node scripts/render.mjs disaster-recovery-runbook <input.json> --theme ink -o out.html ``` The output is one self-contained file and fetches nothing at render time. ## The source - https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/template.tsx - https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/schema.ts - https://design-vault.pages.dev/designs/disaster-recovery-runbook/source/sample-data.json - https://design-vault.pages.dev/designs/disaster-recovery-runbook/sample.html
Template, input schema, sample data and metadata. This document was generated from those four. The generated document
/designs/disaster-recovery-runbook/source/template.tsx
import type { Props } from "./schema";
const CSS = String.raw`
.asset-disaster-recovery-runbook{font-family:var(--font-sans);color:var(--color-primary);background:var(--color-surface);max-width:var(--container-slide);margin:auto;padding:3rem 2.5rem;line-height:1.55;font-size:var(--text-body-sm)}
.asset-disaster-recovery-runbook *{box-sizing:border-box}.asset-disaster-recovery-runbook header{margin-bottom:1.1rem}.asset-disaster-recovery-runbook .eyebrow{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary);margin:0 0 .6rem}.asset-disaster-recovery-runbook h1{font-size:var(--text-h1);line-height:1.25;max-width:28em;margin:0}.asset-disaster-recovery-runbook .subtitle{color:var(--color-secondary);margin:.65rem 0 0}.asset-disaster-recovery-runbook .objectives{display:flex;gap:1.5rem;flex-wrap:wrap;padding:.7rem 0;border-block:1px solid var(--color-outline);margin-bottom:1rem}.asset-disaster-recovery-runbook .objectives p{margin:0;color:var(--color-secondary);font-size:var(--text-label)}.asset-disaster-recovery-runbook .objectives strong{font-family:var(--font-mono);font-size:var(--text-h3);color:var(--color-primary);margin-right:.3rem}.asset-disaster-recovery-runbook .steps{border-left:2px solid var(--color-outline);margin-left:.65rem}.asset-disaster-recovery-runbook .step{position:relative;padding:0 0 1rem 1.4rem}.asset-disaster-recovery-runbook .step:before{content:"";position:absolute;left:-.37rem;top:.25rem;width:.58rem;height:.58rem;background:var(--color-tertiary);border-radius:50%}.asset-disaster-recovery-runbook .head{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap}.asset-disaster-recovery-runbook code{font-family:var(--font-mono);font-size:var(--text-label);color:var(--color-tertiary)}.asset-disaster-recovery-runbook .head h2{font-size:var(--text-body-md);margin:0;flex:1}.asset-disaster-recovery-runbook .badge{font-size:var(--text-label);border:1px solid var(--color-outline);padding:.03rem .4rem;color:var(--color-secondary)}.asset-disaster-recovery-runbook .summary{display:grid;grid-template-columns:minmax(0,.6fr) minmax(0,1.3fr) minmax(0,1.3fr);gap:.65rem;padding:.55rem .65rem;margin-top:.5rem;background:var(--color-surface-subtle)}.asset-disaster-recovery-runbook .field{min-width:0}.asset-disaster-recovery-runbook .field span{display:block;font-size:var(--text-label);color:var(--color-secondary)}.asset-disaster-recovery-runbook .field p{margin:.08rem 0 0;overflow-wrap:anywhere}.asset-disaster-recovery-runbook .details{display:grid;grid-template-columns:1fr 1fr;gap:.5rem 1.1rem;margin-top:.5rem}.asset-disaster-recovery-runbook .details p{margin:.08rem 0 0;overflow-wrap:anywhere}.asset-disaster-recovery-runbook .validation{padding-top:.35rem;border-top:1px dashed var(--color-outline)}.asset-disaster-recovery-runbook .deps{font-family:var(--font-mono);font-size:var(--text-label)}.asset-disaster-recovery-runbook .still-svg{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media(max-width:680px){.asset-disaster-recovery-runbook{padding:1.5rem 1rem}.asset-disaster-recovery-runbook h1{font-size:var(--text-h2)}.asset-disaster-recovery-runbook .summary{grid-template-columns:minmax(0,1fr)}.asset-disaster-recovery-runbook .details{grid-template-columns:minmax(0,1fr)}.asset-disaster-recovery-runbook .objectives{gap:.8rem}}
@media print{.asset-disaster-recovery-runbook{max-width:none;padding:1rem}.asset-disaster-recovery-runbook .step{break-inside:avoid}}
`;
const stateLabel = { planned: "予定", ready: "手順準備済", blocked: "阻害あり", exercised: "演習済" };
const compact = (value: string, max: number) => [...value].length > max ? `${[...value].slice(0, max - 1).join("")}…` : value;
function Still({ data }: { data: Props }) {
return <svg className="still-svg" data-layout-still="true" width="1280" height="720" viewBox="0 0 1280 720" aria-hidden="true">
<rect width="1280" height="720" fill="var(--color-surface)" />
<text x="48" y="43" fontSize="13" fontFamily="sans-serif" fill="var(--color-secondary)">{data.system} · RTO {data.rtoMinutes}m / RPO {data.rpoMinutes}m</text>
<text x="48" y="82" fontSize="25" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{compact(data.title, 68)}</text>
<text x="48" y="117" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">復旧順序 · 前提工程 · 目標時刻 · 責任者 · 正常性検証</text>
{data.steps.slice(0, 8).map((step, index) => {
const y = 164 + index * 62;
return <g key={step.id}>
<line x1="48" x2="1232" y1={y - 19} y2={y - 19} stroke="var(--color-outline)" />
<text x="52" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-tertiary)">{String(step.sequence).padStart(2, "0")} · {step.id}</text>
<text x="190" y={y + 3} fontSize="12" fontWeight="700" fontFamily="sans-serif" fill="var(--color-on-surface)">{compact(step.service, 31)}</text>
<text x="500" y={y + 3} fontSize="11" fontFamily="sans-serif" fill="var(--color-secondary)">T+{step.targetMinutes}m · {stateLabel[step.state]}</text>
<text x="730" y={y + 3} fontSize="11" fontFamily="monospace" fill="var(--color-on-surface)">{step.dependsOn.join(", ") || "開始"}</text>
</g>;
})}
<text x="48" y="704" fontSize="12" fontFamily="sans-serif" fill="var(--color-secondary)">{data.steps.length}復旧工程 · 依存順序と検証条件を記録</text>
</svg>;
}
export default function Template(data: Props) {
return <section className="asset asset-disaster-recovery-runbook">
<style>{CSS}</style>
<header>
<p className="eyebrow">{data.system} · 災害復旧手順</p>
<h1>{data.title}</h1>
{data.subtitle && <p className="subtitle">{data.subtitle}</p>}
</header>
<div className="objectives" aria-label="復旧目標"><p><strong>{data.rtoMinutes}</strong>分 RTO</p><p><strong>{data.rpoMinutes}</strong>分 RPO</p><p><strong>{data.steps.length}</strong>復旧工程</p></div>
<div className="steps">
{data.steps.slice().sort((a, b) => a.sequence - b.sequence).map((step) => <article className="step" key={step.id}>
<div className="head"><code>{String(step.sequence).padStart(2, "0")} · {step.id}</code><h2>{step.service}</h2><span className="badge" data-state={step.state}>{stateLabel[step.state]}</span></div>
<div className="summary">
<div className="field"><span>目標時刻</span><p>T+{step.targetMinutes}分</p></div>
<div className="field"><span>先行工程</span><p className="deps">{step.dependsOn.length ? step.dependsOn.join(" · ") : "初動工程"}</p></div>
<div className="field"><span>実施責任者</span><p>{step.owner}</p></div>
</div>
<div className="details"><div className="field"><span>復旧操作</span><p>{step.action}</p></div><div className="field validation"><span>復旧後の確認</span><p>{step.validation}</p></div></div>
</article>)}
</div>
<Still data={data} />
</section>;
}
/designs/disaster-recovery-runbook/source/schema.ts
import { z } from "zod";
const text = z.string().trim().min(1).max(160);
const short = z.string().trim().min(1).max(48);
const stepSchema = z.object({
id: short.describe("復旧工程の一意な識別子"),
sequence: z.number().int().min(1).max(12).describe("復旧順序"),
service: short.describe("復旧するサービスまたはデータ資産"),
dependsOn: z.array(short).max(4).describe("先に完了すべき復旧工程ID"),
targetMinutes: z.number().int().positive().describe("障害検知からの復旧目標時刻"),
action: text.describe("担当者が実施する復旧操作"),
owner: short.describe("復旧操作の担当者またはチーム"),
validation: text.describe("復旧後の正常性確認"),
state: z.enum(["planned", "ready", "blocked", "exercised"]).describe("手順準備・演習状態"),
}).strict();
export const schema = z.object({
title: text.describe("復旧計画の結論"),
subtitle: text.optional().describe("対象環境・演習日・承認版"),
system: short.describe("対象システムまたは業務サービス"),
rtoMinutes: z.number().int().positive().describe("サービス復旧目標時間(分)"),
rpoMinutes: z.number().int().nonnegative().describe("許容データ損失時間(分)"),
steps: z.array(stepSchema).min(3).max(12).describe("依存順序を持つ復旧手順"),
}).strict().superRefine((data, ctx) => {
const ids = data.steps.map(({ id }) => id);
const sequences = data.steps.map(({ sequence }) => sequence);
if (new Set(ids).size !== ids.length) {
ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧工程IDは一意にすること" });
}
if (new Set(sequences).size !== sequences.length) {
ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧順序は一意にすること" });
}
const byId = new Map(data.steps.map((step) => [step.id, step]));
data.steps.forEach((step, index) => {
if (new Set(step.dependsOn).size !== step.dependsOn.length) {
ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: "前提工程は重複できない" });
}
step.dependsOn.forEach((dependency) => {
const prerequisite = byId.get(dependency);
if (!prerequisite) {
ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: `未登録の前提工程: ${dependency}` });
} else if (prerequisite.sequence >= step.sequence) {
ctx.addIssue({ code: "custom", path: ["steps", index, "dependsOn"], message: "前提工程は依存先より前の順序にすること" });
}
});
if (step.targetMinutes > data.rtoMinutes) {
ctx.addIssue({ code: "custom", path: ["steps", index, "targetMinutes"], message: "各復旧目標は全体RTO以内にすること" });
}
});
const ordered = [...data.steps].sort((a, b) => a.sequence - b.sequence);
if (ordered.some((step, index) => index > 0 && step.targetMinutes <= ordered[index - 1].targetMinutes)) {
ctx.addIssue({ code: "custom", path: ["steps"], message: "復旧目標時刻は順序に沿って増加させること" });
}
});
export type Props = z.infer<typeof schema>;
/designs/disaster-recovery-runbook/source/meta.yaml
id: disaster-recovery-runbook
name:
ja: 災害復旧手順・依存順序台帳
en: Disaster Recovery Runbook
purpose:
ja: サービス復旧の依存順序、RTO/RPO、責任者、実施操作、復旧検証をひと続きの手順にまとめる。
en: Coordinate service recovery dependencies, RTO/RPO, owners, actions, and validation in one ordered runbook.
status: published
still: ./still.png
category: operations
catalogOrder: 870
jobs:
- plan-sequence
- assign-governance
useCases:
- process
- status
layoutKinds:
- timeline
- stack
tags:
- disaster-recovery
- business-continuity
- rto
- rpo
- recovery-exercise
whenToUse:
- ja: 複数サービスにまたがる停止から、依存順序と復旧目標時間を守って業務を戻すとき。
en: Use to restore a multi-service operation in dependency order against recovery targets.
avoidWhen:
- ja: 障害発生直後の重大度別応答と担当引き継ぎを定義する場合は、インシデント対応方針を使う。
en: Use an incident response policy to define severity-based response and escalation immediately after an outage.
requiredInputs:
- ja: システム、RTO/RPO、順序づけた工程、先行工程、目標時刻、責任者、復旧操作、検証条件を定義する。
en: Define system, RTO/RPO, ordered steps, prerequisites, target time, owner, recovery action, and validation.
relatedLayouts:
- incident-escalation-policy
- system-landscape-map
dataSchema:
title: string — 復旧計画の結論
subtitle: string? — 対象環境・演習日・承認版
system: string — 対象システム
rtoMinutes: number — サービス復旧目標時間
rpoMinutes: number — 許容データ損失時間
steps: array (3–12) — 依存順序、目標時刻、操作、責任者、検証、準備状態を持つ復旧工程
sources: []
/designs/disaster-recovery-runbook/source/sample-data.json
{
"title": "決済復旧は認証・キュー・台帳の順で行い、4時間以内に注文受付を再開する",
"subtitle": "Production · exercise 2026-09-19 · approved runbook v3.2",
"system": "Northstar Payments",
"rtoMinutes": 240,
"rpoMinutes": 15,
"steps": [
{ "id": "DR-01", "sequence": 1, "service": "Identity provider", "dependsOn": [], "targetMinutes": 30, "action": "DRリージョンの認証クラスタを昇格し、ルーティングを切り替える。", "owner": "Identity On-call", "validation": "合成ログインが成功し、セッション発行遅延がSLO内。", "state": "ready" },
{ "id": "DR-02", "sequence": 2, "service": "Payment queue", "dependsOn": ["DR-01"], "targetMinutes": 75, "action": "永続キューを最新スナップショットから復元し、未処理イベントを再投入する。", "owner": "Payments Platform", "validation": "キュー長が安定し、イベント重複が発生しない。", "state": "exercised" },
{ "id": "DR-03", "sequence": 3, "service": "Order ledger", "dependsOn": ["DR-02"], "targetMinutes": 180, "action": "台帳を復元し、決済プロバイダーとの未照合取引を再照合する。", "owner": "Finance Operations", "validation": "残高照合が許容差内で、テスト注文を完了できる。", "state": "planned" },
{ "id": "DR-04", "sequence": 4, "service": "Checkout entry point", "dependsOn": ["DR-01", "DR-03"], "targetMinutes": 220, "action": "ヘルス確認を通した後、注文受付を段階的に再開する。", "owner": "Commerce Incident Lead", "validation": "地域別成功率とエラー率が通常SLO内である。", "state": "planned" }
]
}