Download index.html from Hugdrigo/continuityos-resilience-command-center: direct link, hf CLI and curl.
- Browser
- Download file 54.2 kB
-
https://huggingface.co/spaces/Hugdrigo/continuityos-resilience-command-center/resolve/main/index.html
- Command line
-
hf download hf://spaces/Hugdrigo/continuityos-resilience-command-center/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Hugdrigo/continuityos-resilience-command-center/resolve/main/index.html
54.2 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <meta name="theme-color" content="#101c30"><meta name="description" content="ContinuityOS is an interactive business continuity planning and recovery simulation studio."> | |
| <title>ContinuityOS | Business Resilience Command Center</title> | |
| <style> | |
| :root{color-scheme:light;--ink:#14243a;--muted:#71809a;--nav:#102039;--nav2:#162b45;--navtext:#a9b9d2;--bg:#f4f7fb;--paper:#fff;--edge:#e4eaf2;--teal:#087e83;--cyan:#0b9ca1;--pale:#e2f6f3;--coral:#df685b;--orange:#d49a47;--violet:#746bce;--shadow:0 12px 40px rgba(31,51,75,.045);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} | |
| *{box-sizing:border-box}html{scroll-behavior:smooth}body{margin:0;background:var(--bg);color:var(--ink);font-size:13px;line-height:1.5}body.dark{color-scheme:dark;--ink:#e7edf6;--muted:#9eabc2;--bg:#121b2b;--paper:#1b293a;--edge:#35445a;--pale:#173f47;--shadow:none}button,input,select,textarea{font:inherit}button{cursor:pointer}button:disabled{opacity:.4;cursor:not-allowed}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #78cfcf;outline-offset:2px}a{color:var(--teal)}.shell{display:grid;grid-template-columns:242px minmax(0,1fr);min-height:100vh}.sidebar{position:sticky;top:0;height:100vh;padding:28px 16px 22px;background:radial-gradient(circle at 0% 4%,#1b4256,transparent 35%),linear-gradient(160deg,#12233d,#0e1a2d);color:#fff;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:10px;padding:0 10px 44px}.mark{width:43px;height:43px;border-radius:13px;display:grid;place-items:center;background:#00a8aa;color:#f0ffff;font-size:25px;font-weight:900;box-shadow:0 9px 23px #00aaa843}.brand strong{font-size:17px;letter-spacing:-.7px;line-height:1}.brand strong span{color:#4ed1c8}.brand small{display:block;font-size:8px;letter-spacing:1.7px;margin-top:6px;color:#819ab0;font-weight:800}.nav-label{color:#698098;letter-spacing:1.8px;font-size:9px;font-weight:850;padding:0 14px;margin-bottom:15px}.nav{display:grid;gap:7px}.nav button{border:1px solid transparent;background:transparent;color:var(--navtext);display:flex;align-items:center;text-align:left;padding:12px 13px;border-radius:11px;gap:12px;font-size:11px;font-weight:740}.nav button:hover{background:#273c53;color:#fff}.nav button.active{color:#f1ffff;background:#1b454e;border-color:#2b7370}.nav .ico{font-size:17px;width:20px;text-align:center}.nav small{margin-left:auto;color:#54c7c3;font-size:9px}.side-foot{margin-top:auto;padding:23px 10px 0;border-top:1px solid #294054}.side-foot .pill{display:inline-block;border:1px solid #24605e;background:#153b3e;color:#6ce2d3;padding:5px 9px;font-weight:850;border-radius:99px;font-size:9px}.side-foot p{font-size:10px;color:#8da0b7;line-height:1.6}.side-foot strong{color:#dceff0}.main{padding:29px clamp(18px,3.35vw,58px) 46px;max-width:1760px;width:100%;min-width:0;margin:auto}.topbar{display:flex;justify-content:space-between;gap:15px;align-items:center;margin-bottom:24px}.breadcrumb{text-transform:uppercase;font-size:10px;letter-spacing:1.3px;color:var(--muted);font-weight:820}.breadcrumb b{color:var(--ink)}.top-actions{display:flex;align-items:center;gap:9px}.live{font-size:10px;color:#087e83;background:var(--pale);padding:8px 11px;border-radius:100px;font-weight:800}.avatar{border-radius:50%;background:#243850;color:#b5eae4;display:grid;place-items:center;font-size:11px;font-weight:900;height:34px;width:34px}.icon-button{height:35px;width:35px;border-radius:9px;background:var(--paper);color:var(--ink);border:1px solid var(--edge);font-size:15px}.view{display:none}.view.active{display:block}.hero{background:linear-gradient(117deg,#113a49,#176368 62%,#18868c);border-radius:20px;padding:29px 36px;color:white;position:relative;overflow:hidden;min-height:183px;margin-bottom:18px;display:flex;align-items:center;justify-content:space-between;gap:25px}.hero::before{content:"";position:absolute;right:10px;top:-118px;width:380px;height:380px;border-radius:50%;border:1px solid #9ff9ed42;box-shadow:0 0 0 39px #b0ecee12,0 0 0 91px #b0ecee0d,0 0 0 138px #b0ecee06}.hero .eyebrow{color:#b2f0e9;text-transform:uppercase;letter-spacing:1.8px;font-size:9px;font-weight:870}.hero h1{letter-spacing:-1.5px;font-size:clamp(26px,2.65vw,37px);line-height:1.17;margin:9px 0 9px}.hero p{margin:0;color:#d1f2f0;font-size:12px;max-width:610px}.hero-stat{border-left:1px solid #bcf3ef7a;padding-left:23px;position:relative;z-index:1;text-align:center}.hero-stat b{display:block;font-size:39px;line-height:1;letter-spacing:-1.8px}.hero-stat small{display:block;color:#b9e7e5;font-weight:850;letter-spacing:.9px;text-transform:uppercase;font-size:9px;margin-top:9px}.disclaimer{padding:11px 14px;background:#fff9ef;color:#87653a;border:1px solid #f3e1bf;border-radius:10px;font-size:11px;margin-bottom:16px}body.dark .disclaimer{background:#3a3024;color:#ecd1a3;border-color:#5c4c37}.kpis{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:15px}.card{border:1px solid var(--edge);border-radius:16px;background:var(--paper);box-shadow:var(--shadow);padding:20px;min-width:0}.kpi{min-height:143px}.kpi-icon{height:33px;width:33px;border-radius:10px;background:var(--pale);color:var(--teal);display:grid;place-items:center;font-size:16px;margin-bottom:15px}.kpi-icon.orange{background:#fff1e1;color:#bb7c33}.kpi-icon.coral{background:#feeeeb;color:var(--coral)}.kpi-icon.purple{background:#efedfc;color:var(--violet)}.kpi-value{font-size:clamp(23px,2.3vw,32px);font-weight:850;letter-spacing:-1.4px;line-height:1.06;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.kpi-label{font-size:11px;font-weight:780;color:var(--muted);margin-top:9px}.kpi-detail{font-size:10px;color:var(--muted);margin-top:4px}.section-head{display:flex;align-items:end;justify-content:space-between;gap:14px;margin:23px 0 12px}.section-head h2{font-size:18px;letter-spacing:-.7px;margin:0}.section-head p{color:var(--muted);font-size:11px;margin:4px 0 0}.grid{display:grid;gap:15px}.grid.two{grid-template-columns:1.25fr 1fr}.grid.equal{grid-template-columns:1fr 1fr}.grid.three{grid-template-columns:repeat(3,minmax(0,1fr))}.card-head{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:17px}.card-head h3{margin:0;font-size:14px;letter-spacing:-.4px}.card-head p{margin:4px 0 0;font-size:10px;color:var(--muted)}.tag{font-size:9px;color:var(--teal);background:var(--pale);border-radius:99px;padding:6px 10px;font-weight:850;letter-spacing:.5px;white-space:nowrap}.button{background:var(--paper);border:1px solid var(--edge);color:var(--ink);border-radius:10px;font-size:11px;font-weight:760;padding:10px 13px}.button:hover{border-color:#77c8c5}.button.primary{background:var(--teal);color:white;border-color:var(--teal)}.button.primary:hover{background:#05696e}.button.warn{color:#ae5348;background:#fff4f2;border-color:#efd5d0}.button.small{padding:7px 10px;font-size:10px}.controls{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.legend{display:flex;gap:15px;flex-wrap:wrap;color:var(--muted);font-size:10px;margin-top:11px}.legend i{display:inline-block;border-radius:2px;height:9px;width:9px;background:var(--c);margin-right:5px}.chart{width:100%;min-height:250px;display:grid;place-items:center}.chart svg{width:100%;height:auto;max-height:330px}.bars{display:grid;gap:13px}.barrow{display:grid;grid-template-columns:minmax(90px,1.05fr) 2.2fr 55px;gap:10px;align-items:center;font-size:10px}.barrow strong{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:10px}.barrow b{text-align:right;font-variant-numeric:tabular-nums}.track{height:10px;border-radius:99px;background:#e8eef4;overflow:hidden}.track span{height:100%;display:block;border-radius:99px;background:var(--bar)}.insight{padding:13px 0;border-top:1px solid var(--edge);display:flex;gap:12px;align-items:flex-start}.insight:first-child{border-top:0}.insight .bubble{height:32px;width:32px;border-radius:9px;background:var(--pale);color:var(--teal);display:grid;place-items:center;flex:none;font-size:15px}.insight strong{display:block;font-size:11px}.insight p{font-size:10px;margin:3px 0 0;color:var(--muted)}.table-scroll{overflow:auto;width:100%}.data-table{border-collapse:collapse;width:100%;font-size:11px;min-width:700px}.data-table th{text-align:left;padding:12px 10px;font-size:9px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);border-bottom:1px solid var(--edge);white-space:nowrap}.data-table td{padding:12px 10px;border-bottom:1px solid var(--edge);vertical-align:middle}.data-table tr:last-child td{border-bottom:0}.data-table .name{font-weight:810}.data-table .sub{color:var(--muted);font-size:10px;margin-top:3px}.risk{border-radius:100px;display:inline-block;font-size:9px;font-weight:840;padding:5px 9px;background:var(--pale);color:var(--teal)}.risk.high{background:#ffe9e6;color:#bc4d44}.risk.medium{background:#fff2df;color:#a56c24}.risk.low{background:#e1f7ed;color:#168056}.mini{font-variant-numeric:tabular-nums}.progress{height:6px;background:#e6edf2;border-radius:10px;margin-top:7px;overflow:hidden}.progress span{display:block;height:100%;background:var(--teal)}.scenario-picks{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px}.scenario-pick{border:1px solid var(--edge);border-radius:12px;padding:14px 12px;background:var(--paper);color:var(--ink);text-align:left}.scenario-pick.active{background:var(--pale);border-color:#65c1ba;box-shadow:inset 0 0 0 1px #65c1ba}.scenario-pick b{display:block;font-size:11px}.scenario-pick small{font-size:10px;color:var(--muted);display:block;margin-top:6px}.scenario-pick .symbol{font-size:23px;display:block;margin-bottom:8px}.field label{display:block;font-size:10px;font-weight:800;color:var(--muted);margin-bottom:6px}.field input,.field select,.field textarea{width:100%;min-height:39px;background:var(--paper);color:var(--ink);padding:9px 10px;border-radius:9px;border:1px solid var(--edge)}.field textarea{min-height:68px;resize:vertical}.field input[type=range]{padding:0;min-height:27px;accent-color:var(--teal);cursor:pointer}.form-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.form-grid .wide{grid-column:1/-1}.range-label{display:flex;justify-content:space-between;gap:12px}.range-label output{font-weight:840;color:var(--teal)}.metricline{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:12px 0;border-bottom:1px solid var(--edge)}.metricline:last-child{border:0}.metricline span{color:var(--muted);font-size:11px}.metricline b{font-size:13px;font-variant-numeric:tabular-nums}.checklist{display:grid;gap:10px}.check-item{display:flex;gap:11px;align-items:flex-start;border:1px solid var(--edge);border-radius:11px;padding:13px;background:var(--paper)}.check-item input{margin-top:3px;accent-color:var(--teal)}.check-item strong{display:block;font-size:11px}.check-item small{color:var(--muted);display:block;margin-top:4px;font-size:10px}.check-item.checked{background:var(--pale);border-color:#75c1b9}.callout{background:var(--pale);border-radius:12px;border:1px solid #bfe7e1;color:var(--ink);padding:14px;font-size:11px}.callout strong{display:block;margin-bottom:5px}.empty{padding:31px 15px;text-align:center;color:var(--muted);font-size:11px}.empty b{display:block;color:var(--ink);font-size:14px;margin-bottom:5px}.footer{display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;font-size:9px;letter-spacing:1.05px;color:var(--muted);padding:30px 2px 0;font-weight:800}.footer span{letter-spacing:0;font-weight:650}.audit{max-height:255px;overflow:auto}.audit-line{display:flex;gap:10px;padding:11px 0;border-top:1px solid var(--edge)}.audit-line:first-child{border:0}.audit-line .dot{background:var(--teal);height:7px;width:7px;border-radius:100%;flex:none;margin-top:6px}.audit-line b{display:block;font-size:10px}.audit-line small{display:block;font-size:9px;color:var(--muted);margin-top:3px}.modal-back{position:fixed;inset:0;background:#071225a8;z-index:10;display:grid;place-items:center;padding:20px}.modal-back.hidden,.toast.hidden{display:none}.modal{width:min(610px,100%);max-height:92vh;overflow:auto;border-radius:17px;background:var(--paper);padding:25px;box-shadow:0 20px 90px #0004}.modal-head{display:flex;justify-content:space-between;align-items:center;gap:10px;margin-bottom:18px}.modal-head h2{margin:0;font-size:18px}.modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:20px}.dep-list{max-height:105px;overflow:auto;display:grid;grid-template-columns:1fr 1fr;gap:8px}.dep-list label{display:flex;gap:7px;align-items:center;font-size:11px;color:var(--ink)}.dep-list input{accent-color:var(--teal)}.toast{position:fixed;bottom:20px;right:22px;z-index:30;background:#102b35;color:#e1fffb;border-radius:12px;padding:13px 18px;box-shadow:0 10px 30px #0003;font-size:12px;max-width:90vw}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.small-note{font-size:10px;color:var(--muted);line-height:1.65}.method{font-size:11px;color:var(--muted);line-height:1.7;padding-left:20px}.method li{padding-bottom:7px}.mobile-menu{display:none}@media(max-width:1140px){.kpis{grid-template-columns:repeat(2,1fr)}.grid.two{grid-template-columns:1fr 1fr}.main{padding-left:24px;padding-right:24px}.scenario-picks{grid-template-columns:repeat(2,1fr)}}@media(max-width:860px){.shell{display:block}.sidebar{height:auto;position:relative;padding:14px 17px}.brand{padding:0}.nav-label,.side-foot{display:none}.sidebar .nav{margin-top:14px;display:flex;overflow:auto;gap:5px}.nav button{min-width:132px;padding:9px}.sidebar .nav small{display:none}.main{padding:19px 18px 36px}.grid.two,.grid.equal,.grid.three{grid-template-columns:1fr}.hero{padding:23px;min-height:auto}.hero-stat{min-width:100px}.section-head{margin-top:19px}.topbar{margin-bottom:19px}}@media(max-width:560px){.main{padding:14px 12px 30px}.sidebar{padding:13px}.brand strong{font-size:15px}.brand .mark{width:37px;height:37px}.sidebar .nav button{min-width:117px;font-size:10px}.breadcrumb{font-size:9px}.topbar{gap:6px}.live{font-size:9px;padding:7px}.avatar{display:none}.hero{display:block;padding:23px 21px}.hero h1{font-size:28px}.hero-stat{text-align:left;border:0;border-top:1px solid #b0e7df66;padding:13px 0 0;margin-top:17px}.hero-stat b{font-size:27px}.hero-stat small{display:inline;margin-left:10px}.kpis{gap:9px}.card{padding:15px;border-radius:13px}.kpi{min-height:131px}.kpi-value{font-size:24px}.section-head h2{font-size:16px}.scenario-picks{gap:7px}.scenario-pick{padding:11px}.form-grid{grid-template-columns:1fr}.form-grid .wide{grid-column:auto}.dep-list{grid-template-columns:1fr}.barrow{grid-template-columns:minmax(85px,1fr) 1.1fr 43px;gap:6px}.footer{display:block}.footer span{display:block;margin-top:7px}}@media print{.sidebar,.topbar,.controls,.button,.modal-back,.toast,.scenario-picks{display:none!important}.shell{display:block}.main{padding:0}.view{display:none!important}#recovery.view{display:block!important}.card{break-inside:avoid;box-shadow:none}.footer{font-size:8px}} | |
| </style> | |
| </head><body> | |
| <div class="shell"><aside class="sidebar"> | |
| <div class="brand"><div class="mark">◈</div><div><strong>Continuity<span>OS</span></strong><small>HUGDRIGO LABS / 085</small></div></div> | |
| <div class="nav-label">COMMAND CENTER</div><nav class="nav" aria-label="Main navigation"> | |
| <button class="active" data-tab="overview"><span class="ico">▦</span> Overview <small>01</small></button> | |
| <button data-tab="services"><span class="ico">▤</span> Services <small>02</small></button> | |
| <button data-tab="scenario"><span class="ico">◇</span> Scenario lab <small>03</small></button> | |
| <button data-tab="recovery"><span class="ico">☑</span> Recovery plan <small>04</small></button> | |
| </nav><div class="side-foot"><span class="pill">● LOCAL-FIRST WORKSPACE</span><p><strong>Business Resilience Studio</strong><br>Model recovery decisions before a disruption occurs.</p><small style="font-size:9px;color:#6a8398">EDUCATIONAL SIMULATION · v1.0</small></div></aside> | |
| <main class="main"> | |
| <div class="topbar"><div class="breadcrumb">HUGDRIGO / <b id="crumb">RESILIENCE OVERVIEW</b></div><div class="top-actions"><span class="live">◉ SIMULATED DATA</span><button id="themeToggle" class="icon-button" title="Toggle dark theme" aria-label="Toggle dark theme">☾</button><div class="avatar">HG</div></div></div> | |
| <section class="view active" id="overview"> | |
| <div class="hero"><div><span class="eyebrow">ORGANIZATIONAL RESILIENCE INTELLIGENCE</span><h1>Know what matters.<br>Recover what counts.</h1><p>Map critical services, test disruption scenarios, and turn recovery priorities into a measurable action plan.</p></div><div class="hero-stat"><b id="heroScore">—</b><small>Readiness index</small></div></div> | |
| <div class="disclaimer"><b>Illustrative planning sandbox:</b> all default services, losses, recovery timings and stress trials are hypothetical. No real-time systems, incidents, security tools or operational data are connected.</div> | |
| <div class="kpis"><article class="card kpi"><div class="kpi-icon">▦</div><div class="kpi-value" id="kActive">0</div><div class="kpi-label">Critical services modeled</div><div class="kpi-detail">Active service inventory</div></article><article class="card kpi"><div class="kpi-icon orange">◷</div><div class="kpi-value" id="kRto">0h</div><div class="kpi-label">Median recovery objective</div><div class="kpi-detail">RTO across active services</div></article><article class="card kpi"><div class="kpi-icon coral">◇</div><div class="kpi-value" id="kExposure">$0</div><div class="kpi-label">Modeled disruption impact</div><div class="kpi-detail">Current scenario, not forecast</div></article><article class="card kpi"><div class="kpi-icon purple">☑</div><div class="kpi-value" id="kTasks">0%</div><div class="kpi-label">Recovery steps complete</div><div class="kpi-detail">Browser-local checklist</div></article></div> | |
| <div class="section-head"><div><h2>Recovery readiness</h2><p>Prioritize weak points and dependencies across your modeled operations.</p></div><button class="button" data-tab="services">Manage services ↗</button></div> | |
| <div class="grid two"><article class="card"><div class="card-head"><div><h3>Service readiness profile</h3><p>Illustrative confidence scores entered by the user</p></div><span class="tag">0–100 INDEX</span></div><div id="readinessBars" class="bars"></div><div class="legend"><span><i style="--c:#0c9595"></i>Strong ≥ 80</span><span><i style="--c:#deac58"></i>Developing 55–79</span><span><i style="--c:#df685b"></i>Needs attention < 55</span></div></article> | |
| <article class="card"><div class="card-head"><div><h3>Executive insights</h3><p>Derived from editable assumptions</p></div><span class="tag">RULE-BASED</span></div><div id="insightList"></div></article></div> | |
| <div class="section-head"><div><h2>Impact concentration</h2><p>Which services contribute most to your modeled financial exposure?</p></div><button class="button" data-tab="scenario">Open scenario lab →</button></div> | |
| <article class="card"><div class="card-head"><div><h3>Estimated interruption loss by service</h3><p>Using selected scenario, assumed hourly loss and modeled interruption time</p></div><span class="tag" id="activeScenarioTag">CLOUD DISRUPTION</span></div><div id="impactChart" class="chart"></div></article> | |
| </section> | |
| <section class="view" id="services"><div class="section-head" style="margin-top:0"><div><h2>Service dependency registry</h2><p>Own your continuity inventory, assumptions and recovery targets.</p></div><div class="controls"><button class="button" id="exportServices">Export CSV ↓</button><button class="button primary" id="addService">+ Add service</button></div></div> | |
| <div class="disclaimer">Service readiness is a <b>user-entered planning estimate</b>. Dependency links are used for simplified, one-level knock-on scenario modeling, not a full dependency graph or production monitoring.</div> | |
| <article class="card"><div class="card-head"><div><h3>Active services</h3><p>Click edit to change assumptions</p></div><input aria-label="Search services" id="serviceSearch" placeholder="Search services…" style="width:190px;max-width:100%;border:1px solid var(--edge);border-radius:9px;padding:9px;color:var(--ink);background:var(--paper)"></div><div id="servicesTable" class="table-scroll"></div></article> | |
| <div class="section-head"><div><h2>Archived services</h2><p>Archived records are excluded from calculations but preserved locally.</p></div></div><article class="card" id="archivedList"></article></section> | |
| <section class="view" id="scenario"><div class="section-head" style="margin-top:0"><div><h2>Disruption scenario lab</h2><p>Explore hypothetical impacts, response levers and recovery constraints.</p></div><button id="downloadScenario" class="button">Export simulation ↓</button></div><div class="disclaimer">Scenario multipliers are <b>illustrative assumptions</b>, not historical incident rates, calibrated probabilities or verified monetary forecasts.</div> | |
| <div class="scenario-picks" id="scenarioPicks"></div> | |
| <div class="grid two" style="margin-top:15px"><article class="card"><div class="card-head"><div><h3>Response assumptions</h3><p>Update levers to instantly recalculate deterministic outputs</p></div><span class="tag">INTERACTIVE</span></div> | |
| <div class="field" style="margin-bottom:17px"><div class="range-label"><label for="durationSlider">Disruption duration</label><output id="durationOutput">12 hours</output></div><input id="durationSlider" type="range" min="1" max="72" step="1"></div> | |
| <div class="field" style="margin-bottom:17px"><div class="range-label"><label for="containmentSlider">Containment effectiveness</label><output id="containmentOutput">55%</output></div><input id="containmentSlider" type="range" min="0" max="100" step="5"></div> | |
| <div class="field" style="margin-bottom:17px"><div class="range-label"><label for="budgetSlider">Additional response budget</label><output id="budgetOutput">$25,000</output></div><input id="budgetSlider" type="range" min="0" max="100000" step="5000"></div> | |
| <div class="callout"><strong>Interpretation</strong>More effective containment and additional response capacity reduce modeled downtime. These controls do not guarantee an actual recovery outcome.</div></article> | |
| <article class="card"><div class="card-head"><div><h3>Scenario impact snapshot</h3><p>Calculated for your active services</p></div><span class="tag" id="simTag">MODEL OUTPUT</span></div><div class="metricline"><span>Total modeled interruption loss</span><b id="simLoss">$0</b></div><div class="metricline"><span>Aggregate modeled downtime</span><b id="simDowntime">0h</b></div><div class="metricline"><span>Services above recovery target</span><b id="simBreaches">0</b></div><div class="metricline"><span>Response budget (planning input)</span><b id="simBudget">$0</b></div><div class="metricline"><span>Potential data-loss window, maximum</span><b id="simDataWindow">0 min</b></div><p class="small-note">Modeled interruption loss excludes response budget. Data-loss windows are derived from entered RPO objectives, not measurements of lost records.</p></article></div> | |
| <div class="section-head"><div><h2>Uncertainty exercise</h2><p>Compare many random variations of the selected illustrative scenario.</p></div><button id="runStress" class="button primary">↻ Run 400 trials</button></div> | |
| <div class="grid two"><article class="card"><div class="card-head"><div><h3>Simulated impact distribution</h3><p>10th, 50th and 90th percentiles across 400 deterministic trials</p></div><span class="tag">SEEDED TEST</span></div><div id="stressChart" class="chart"></div><div id="stressStats" class="legend"></div></article> | |
| <article class="card"><div class="card-head"><div><h3>Exposure breakdown</h3><p>Prioritized operational services in the current scenario</p></div><span class="tag">MODELED</span></div><div id="scenarioTable" class="table-scroll"></div></article></div></section> | |
| <section class="view" id="recovery"><div class="section-head" style="margin-top:0"><div><h2>Actionable recovery plan</h2><p>Track preparedness tasks and review an ordered response sequence.</p></div><button id="printPlan" class="button">Print plan ↓</button></div> | |
| <div class="grid two"><article class="card"><div class="card-head"><div><h3>Priority recovery order</h3><p>Sorted by current modeled loss and criticality</p></div><span class="tag">SCENARIO-BASED</span></div><div id="recoveryPriorities"></div></article><article class="card"><div class="card-head"><div><h3>Preparedness checklist</h3><p>Completion saved only in this browser</p></div><span class="tag" id="taskBadge">0/0 DONE</span></div><div class="checklist" id="recoveryChecks"></div></article></div> | |
| <div class="section-head"><div><h2>Workspace and evidence</h2><p>Keep an offline record of changes and document your assumptions.</p></div></div><div class="grid equal"><article class="card"><div class="card-head"><div><h3>Data controls</h3><p>No login, backend, external data or paid services</p></div></div><div class="metricline"><div><b>JSON workspace backup</b><div class="small-note">Includes services, scenarios and checklist progress</div></div><button class="button small" id="exportJson">Download ↓</button></div><div class="metricline"><div><b>Restore workspace</b><div class="small-note">Input is validated before it replaces local state</div></div><button class="button small" id="importJson">Import ↑</button></div><div class="metricline"><div><b>Reset sample workspace</b><div class="small-note">Replaces local sample data after confirmation</div></div><button class="button small warn" id="resetDemo">Reset</button></div><p class="small-note">Nothing is sent to servers by this application. Browsers may clear local storage. Export a backup before resetting or switching devices.</p></article> | |
| <article class="card"><div class="card-head"><div><h3>Recent activity</h3><p>Local change history for this workspace</p></div><span class="tag">LATEST 25</span></div><div id="auditList" class="audit"></div></article></div> | |
| <div class="section-head"><div><h2>Model transparency</h2><p>What the numbers mean and what they do not prove.</p></div></div><div class="grid equal"><article class="card"><div class="card-head"><h3>Calculation method</h3></div><ol class="method"><li>A preset scenario assigns a hypothetical exposure factor to each functional domain. A severely affected direct dependency adds up to 0.25 exposure to a dependent service (one level).</li><li>Modeled downtime = minimum of scenario duration and [RTO × readiness factor × scenario recovery factor × containment factor × budget factor + 18% × scenario duration × exposure].</li><li>Modeled interruption loss = downtime × user-entered hourly loss × exposure. Total impact sums individual services; overlaps may be double counted.</li><li>Potential data-loss window uses RPO × scenario multiplier, capped by downtime and presented as an illustrative window only.</li><li>400 seeded stress trials change hypothetical duration and intensity; percentiles describe this exercise, not actual disruption probabilities.</li></ol></article><article class="card"><div class="card-head"><h3>Scope and limitations</h3></div><p class="small-note">ContinuityOS is a static, educational planning tool. It does not detect or respond to real incidents, measure true resilience, certify compliance with ISO 22301 or other standards, establish service-level guarantees, or predict actual losses or recovery times. Defaults are fictional and must be replaced with validated organizational inputs before any serious decision. Financial impact can overlap across dependencies; the model omits knock-on losses beyond one dependency level, contractual penalties, taxes, insurance and externalities.</p><div class="callout" style="margin-top:16px"><strong>Good practice</strong>Use this tool for workshops and structured discussions. Verify objectives, owners, dependencies, assumptions and response playbooks with responsible personnel.</div></article></div> | |
| </section> | |
| <footer class="footer">CONTINUITYOS · HUGDRIGO LABS · PROJECT #85 <span>◉ Local-first · Fictional demo data · No external APIs</span></footer> | |
| </main></div> | |
| <div class="modal-back hidden" id="modalBack"><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle"><div class="modal-head"><h2 id="modalTitle">Add critical service</h2><button class="button small" id="closeModal" aria-label="Close dialog">✕</button></div> | |
| <form id="serviceForm"><div class="form-grid"><div class="field wide"><label for="svcName">Service name</label><input id="svcName" maxlength="70" required placeholder="e.g. Customer identity platform"></div> | |
| <div class="field"><label for="svcOwner">Business owner</label><input id="svcOwner" maxlength="60" required placeholder="e.g. IT Operations"></div><div class="field"><label for="svcDomain">Function</label><select id="svcDomain"><option value="cloud">Cloud & IT</option><option value="operations">Operations</option><option value="customer">Customer experience</option><option value="finance">Finance & payments</option><option value="security">Security & identity</option><option value="analytics">Data & analytics</option></select></div> | |
| <div class="field"><label for="svcCritical">Business criticality (1–5)</label><input id="svcCritical" type="number" step="1" min="1" max="5" required></div><div class="field"><label for="svcReadiness">Readiness index (0–100)</label><input id="svcReadiness" type="number" step="1" min="0" max="100" required></div> | |
| <div class="field"><label for="svcRto">RTO target (hours)</label><input id="svcRto" type="number" step="0.25" min="0.25" max="720" required></div><div class="field"><label for="svcRpo">RPO target (minutes)</label><input id="svcRpo" type="number" step="1" min="0" max="43200" required></div> | |
| <div class="field wide"><label for="svcLoss">Assumed interruption cost (USD/hour)</label><input id="svcLoss" type="number" min="0" step="1" max="100000000" required></div> | |
| <div class="field wide"><label>Direct dependencies (optional)</label><div class="dep-list" id="depList"></div><div class="small-note">Dependency exposure is considered only for direct, active linked services.</div></div></div><div class="modal-actions"><button type="button" class="button" id="cancelModal">Cancel</button><button type="submit" class="button primary">Save service</button></div></form></div></div> | |
| <input id="fileInput" type="file" class="sr-only" accept=".json,application/json"><div id="toast" class="toast hidden" role="status" aria-live="polite"></div> | |
| <script> | |
| (()=>{'use strict'; | |
| const KEY='hugdrigo-continuityos-project85-v1'; | |
| const DEMO={version:1,theme:'light',scenario:{type:'cloud',hours:12,containment:55,budget:25000,seed:85},checks:{},audits:[],services:[ | |
| {id:'svc-01',name:'Cloud infrastructure',owner:'Platform engineering',domain:'cloud',criticality:5,rto:3,rpo:30,hourlyLoss:26500,readiness:72,deps:[],archived:false}, | |
| {id:'svc-02',name:'Payment processing',owner:'Finance operations',domain:'finance',criticality:5,rto:2,rpo:10,hourlyLoss:33000,readiness:87,deps:['svc-01'],archived:false}, | |
| {id:'svc-03',name:'Order fulfillment',owner:'Supply chain',domain:'operations',criticality:5,rto:8,rpo:120,hourlyLoss:17800,readiness:59,deps:['svc-01'],archived:false}, | |
| {id:'svc-04',name:'Customer support',owner:'Customer experience',domain:'customer',criticality:3,rto:12,rpo:240,hourlyLoss:8400,readiness:83,deps:['svc-01'],archived:false}, | |
| {id:'svc-05',name:'Identity & access',owner:'Security operations',domain:'security',criticality:5,rto:1,rpo:5,hourlyLoss:22500,readiness:65,deps:['svc-01'],archived:false}, | |
| {id:'svc-06',name:'Data warehouse',owner:'Data & insights',domain:'analytics',criticality:3,rto:24,rpo:360,hourlyLoss:6500,readiness:76,deps:['svc-01'],archived:false}]}; | |
| const PRESETS={cloud:{name:'Cloud outage',icon:'☁',subtitle:'Region-level service degradation',factor:1.12,loss:1.25,exposure:{cloud:1,operations:.38,customer:.34,finance:.45,security:.72,analytics:.63}},ransomware:{name:'Ransomware',icon:'▣',subtitle:'Defensive isolation exercise',factor:1.45,loss:2,exposure:{cloud:.8,operations:.9,customer:.65,finance:.84,security:1,analytics:.95}},supplier:{name:'Supplier failure',icon:'▧',subtitle:'Third-party delivery interruption',factor:1.28,loss:.5,exposure:{cloud:.12,operations:1,customer:.66,finance:.34,security:.12,analytics:.42}},facility:{name:'Facility outage',icon:'⌁',subtitle:'Loss of a business location',factor:1.17,loss:.75,exposure:{cloud:.2,operations:1,customer:.65,finance:.3,security:.32,analytics:.25}}}; | |
| const TASKS=[['governance','Confirm an accountable owner for every critical service','Ownership is needed before declaring recovery responsibilities.'],['backups','Test restoration of essential backups','Record evidence that recovery points are achievable.'],['communications','Document customer and internal communication paths','Include escalation contacts and alternate channels.'],['dependencies','Review upstream and third-party dependencies','Validate single points of failure and substitution options.'],['exercises','Run a documented tabletop recovery exercise','Log assumptions, issues and improvements.'],['approval','Approve recovery thresholds and response playbooks','Have responsible stakeholders sign off on RTO and RPO.']]; | |
| const $=id=>document.getElementById(id);const clone=x=>JSON.parse(JSON.stringify(x));const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); | |
| const safe=(v,min,max)=>typeof v==='number'&&Number.isFinite(v)&&v>=min&&v<=max;const textValid=(v,max)=>typeof v==='string'&&v.trim().length>0&&v.trim().length<=max; | |
| const money=x=>new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',maximumFractionDigits:0}).format(x);const compact=x=>!Number.isFinite(x)?'—':Math.abs(x)>=1e6?'$'+(x/1e6).toFixed(2)+'m':Math.abs(x)>=1000?'$'+(x/1000).toFixed(1)+'k':money(x);const round=(x,d=1)=>Number(x.toFixed(d));const median=arr=>{let a=[...arr].sort((x,y)=>x-y);return a.length?(a[Math.floor((a.length-1)/2)]+a[Math.ceil((a.length-1)/2)])/2:0}; | |
| const validService=s=>s&&textValid(s.id,50)&&textValid(s.name,70)&&textValid(s.owner,60)&&Object.prototype.hasOwnProperty.call(PRESETS.cloud.exposure,s.domain)&&Number.isInteger(s.criticality)&&safe(s.criticality,1,5)&&safe(s.rto,.25,720)&&safe(s.rpo,0,43200)&&safe(s.hourlyLoss,0,1e8)&&Number.isInteger(s.readiness)&&safe(s.readiness,0,100)&&Array.isArray(s.deps)&&s.deps.length<=70&&s.deps.every(d=>textValid(d,50))&&typeof s.archived==='boolean'; | |
| function parseBackup(json){if(!json||typeof json!=='object'||Array.isArray(json)||json.version!==1||!Array.isArray(json.services)||json.services.length>100||!json.services.every(validService))throw Error('Invalid or unsupported workspace structure');const ids=json.services.map(s=>s.id);if(new Set(ids).size!==ids.length||json.services.some(s=>s.deps.some(d=>!ids.includes(d)||d===s.id)))throw Error('Invalid dependency or duplicate service ID');if(!json.scenario||!Object.hasOwn(PRESETS,json.scenario.type)||!Number.isInteger(json.scenario.hours)||!safe(json.scenario.hours,1,72)||!safe(json.scenario.containment,0,100)||!safe(json.scenario.budget,0,100000)||!Number.isInteger(json.scenario.seed)||!safe(json.scenario.seed,0,2147483647))throw Error('Scenario fields out of range');if(!json.checks||typeof json.checks!=='object'||Array.isArray(json.checks)||Object.entries(json.checks).length>30||Object.entries(json.checks).some(([k,v])=>!TASKS.some(t=>t[0]===k)||typeof v!=='boolean'))throw Error('Invalid recovery checklist');return {version:1,theme:json.theme==='dark'?'dark':'light',scenario:{...json.scenario},checks:Object.fromEntries(TASKS.map(t=>[t[0],!!json.checks[t[0]]])),audits:[],services:json.services.map(s=>({...s,deps:[...new Set(s.deps)]}))};} | |
| let state=clone(DEMO),currentTab='overview',editing=null,toastTimer=null,searchTerm=''; | |
| try{let saved=localStorage.getItem(KEY);if(saved)state=parseBackup(JSON.parse(saved));}catch(e){state=clone(DEMO)} | |
| function save(){try{localStorage.setItem(KEY,JSON.stringify(state))}catch(e){notify('Browser storage is unavailable; export a backup.')}} | |
| function log(message){state.audits.unshift({message:String(message).slice(0,125),when:new Date().toISOString()});state.audits=state.audits.slice(0,25);save()} | |
| function notify(message){let t=$('toast');t.textContent=message;t.classList.remove('hidden');clearTimeout(toastTimer);toastTimer=setTimeout(()=>t.classList.add('hidden'),3200)} | |
| const active=()=>state.services.filter(s=>!s.archived); | |
| function exposure(s,services,scenarioType){const factor=PRESETS[scenarioType].exposure;let base=factor[s.domain]||0;for(const dep of s.deps){const d=services.find(v=>v.id===dep&&!v.archived);if(d&&(factor[d.domain]||0)>.6){base=Math.min(1,base+.25);break}}return base} | |
| function model(scenario=state.scenario,services=active()){const cfg=PRESETS[scenario.type];const rows=services.map(s=>{const e=exposure(s,services,scenario.type);const readinessFactor=1+(100-s.readiness)/160;const containmentFactor=1-scenario.containment/200;const budgetFactor=1/(1+scenario.budget/25000);const raw=s.rto*readinessFactor*cfg.factor*containmentFactor*budgetFactor+scenario.hours*e*.18;const down=e===0?0:Math.min(scenario.hours*e,Math.max(0,raw));const loss=down*s.hourlyLoss*e;const dataWindow=Math.min(down*60,s.rpo*cfg.loss*e);const risk=100*(.35*s.criticality/5+.3*(100-s.readiness)/100+.2*Math.min(1,8/(s.rto+1))+.15*Math.min(1,s.deps.length/3));return {...s,exposure:e,downtime:down,loss,dataWindow,risk,breach:down>s.rto+1e-8}});return {rows,totalLoss:rows.reduce((a,r)=>a+r.loss,0),totalDown:rows.reduce((a,r)=>a+r.downtime,0),breaches:rows.filter(r=>r.breach).length,maxData:Math.max(0,...rows.map(r=>r.dataWindow))};} | |
| function rng(seed){let x=seed>>>0;return ()=>((x=(1664525*x+1013904223)>>>0)/4294967296)} | |
| function simulate(scenario=state.scenario,services=active(),trials=400){const random=rng(scenario.seed);let values=[];for(let i=0;i<trials;i++){const hours=Math.max(1,Math.min(72,scenario.hours*(.7+random()*.7)));const intens=.85+random()*.35;const containment=Math.max(0,Math.min(100,scenario.containment+(-10+random()*20)));const out=model({...scenario,hours,containment},services);values.push(out.totalLoss*intens)}values.sort((a,b)=>a-b);const at=p=>values.length?values[Math.floor((values.length-1)*p)]:0;return {p10:at(.1),p50:at(.5),p90:at(.9),min:values[0]||0,max:values.at(-1)||0,trials,values}} | |
| const band=s=>s.readiness>=80?'low':s.readiness>=55?'medium':'high';const riskText=v=>v>=72?'High':v>=48?'Medium':'Low';const riskBand=v=>v>=72?'high':v>=48?'medium':'low'; | |
| function downloadFile(name,content,mime){const url=URL.createObjectURL(new Blob([content],{type:mime}));const a=document.createElement('a');a.href=url;a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),5000)} | |
| function csvCell(x){let t=String(x??'');if(/^[\s]*[=+@\-\t\r]/.test(t))t="'"+t;return '"'+t.replace(/"/g,'""')+'"'} | |
| const csv=rows=>rows.map(r=>r.map(csvCell).join(',')).join('\r\n')+'\r\n'; | |
| function render(){document.body.classList.toggle('dark',state.theme==='dark');$('themeToggle').textContent=state.theme==='dark'?'☼':'☾';const services=active();const m=model();const done=TASKS.filter(t=>state.checks[t[0]]).length;const ready=services.length?Math.round(services.reduce((a,s)=>a+s.readiness,0)/services.length):0;$('heroScore').textContent=ready+'/100';$('kActive').textContent=services.length;$('kRto').textContent=round(median(services.map(s=>s.rto)))+'h';$('kExposure').textContent=compact(m.totalLoss);$('kTasks').textContent=Math.round(done/TASKS.length*100)+'%'; | |
| $('readinessBars').innerHTML=services.length?services.slice().sort((a,b)=>a.readiness-b.readiness).map(s=>`<div class="barrow"><strong title="${esc(s.name)}">${esc(s.name)}</strong><div class="track"><span style="width:${s.readiness}%;--bar:${s.readiness>=80?'#07938d':s.readiness>=55?'#dda348':'#de665c'}"></span></div><b>${s.readiness}/100</b></div>`).join(''):'<div class="empty">Add a service to see its readiness profile.</div>'; | |
| const sorted=services.slice().sort((a,b)=>a.readiness-b.readiness);const biggest=m.rows.slice().sort((a,b)=>b.loss-a.loss)[0];let insights=[];if(!services.length)insights.push(['◇','No active services','Add an operational service to begin modeling risk.']);else{if(sorted[0])insights.push(['◷','Lowest readiness: '+sorted[0].name,`Current estimated readiness ${sorted[0].readiness}/100. Validate recovery playbooks and ownership.`]);if(biggest)insights.push(['↗','Largest modeled loss: '+biggest.name,`${compact(biggest.loss)} of ${compact(m.totalLoss)} in the selected disruption exercise.`]);insights.push(['▤',m.breaches+' services exceed RTO',`Recovery times are modeled using current objectives, readiness and response assumptions.`]);insights.push(['☑',done+' of '+TASKS.length+' actions completed','Check off the recovery plan as your team verifies each step.'])} $('insightList').innerHTML=insights.map(a=>`<div class="insight"><span class="bubble">${a[0]}</span><div><strong>${esc(a[1])}</strong><p>${esc(a[2])}</p></div></div>`).join('');$('activeScenarioTag').textContent=PRESETS[state.scenario.type].name.toUpperCase(); | |
| const impact=m.rows.slice().sort((a,b)=>b.loss-a.loss).slice(0,7);const max=Math.max(1,...impact.map(r=>r.loss));$('impactChart').innerHTML=impact.length?'<div class="bars" style="width:100%">'+impact.map(r=>`<div class="barrow" style="grid-template-columns:minmax(100px,1fr) minmax(40px,2.3fr) 72px"><strong title="${esc(r.name)}">${esc(r.name)}</strong><div class="track"><span style="width:${Math.max(1,r.loss/max*100)}%;--bar:${r.loss/max>.7?'#0b8f91':'#69c4ba'}"></span></div><b>${compact(r.loss)}</b></div>`).join('')+'</div>':'<div class="empty">No active services in the scenario.</div>'; | |
| $('scenarioPicks').innerHTML=Object.entries(PRESETS).map(([id,p])=>`<button class="scenario-pick ${state.scenario.type===id?'active':''}" data-preset="${id}" aria-pressed="${state.scenario.type===id}"><span class="symbol">${p.icon}</span><b>${p.name}</b><small>${p.subtitle}</small></button>`).join('');for(let [id,value] of [['duration',state.scenario.hours],['containment',state.scenario.containment],['budget',state.scenario.budget]])$(id+'Slider').value=value;$('durationOutput').textContent=state.scenario.hours+' hours';$('containmentOutput').textContent=state.scenario.containment+'%';$('budgetOutput').textContent=money(state.scenario.budget);$('simLoss').textContent=money(m.totalLoss);$('simDowntime').textContent=round(m.totalDown)+'h';$('simBreaches').textContent=m.breaches+' / '+services.length;$('simBudget').textContent=money(state.scenario.budget);$('simDataWindow').textContent=Math.round(m.maxData)+' min';$('simTag').textContent=PRESETS[state.scenario.type].name.toUpperCase(); | |
| let stress=simulate();const series=[['P10',stress.p10,'#71d0bc'],['P50',stress.p50,'#0b9094'],['P90',stress.p90,'#df9b61']];const peak=Math.max(1,stress.p90*1.08);$('stressChart').innerHTML=`<svg viewBox="0 0 610 250" role="img" aria-label="Simulated modeled loss quantiles: 10th, 50th, and 90th percentile"><line x1="120" y1="202" x2="570" y2="202" stroke="currentColor" opacity=".16"/><line x1="120" y1="20" x2="120" y2="202" stroke="currentColor" opacity=".16"/>${series.map((v,i)=>`<text x="12" y="${55+i*63}" font-size="13" fill="currentColor" opacity=".8">${v[0]}</text><rect x="120" y="${33+i*63}" height="25" width="${Math.max(1,v[1]/peak*436)}" rx="6" fill="${v[2]}"/><text x="${Math.min(500,130+v[1]/peak*436)}" y="${51+i*63}" font-size="12" font-weight="bold" fill="currentColor">${esc(compact(v[1]))}</text>`).join('')}<text x="120" y="226" font-size="10" fill="currentColor" opacity=".55">$0</text><text x="528" y="226" font-size="10" fill="currentColor" opacity=".55">${esc(compact(peak))}</text></svg>`;$('stressStats').innerHTML=`<span>Trials: <b>${stress.trials}</b></span><span>Seed: <b>${state.scenario.seed}</b></span><span>Range: <b>${compact(stress.min)} – ${compact(stress.max)}</b></span>`; | |
| $('scenarioTable').innerHTML=m.rows.length?`<table class="data-table" style="min-width:580px"><thead><tr><th>Service</th><th>Downtime</th><th>Modeled loss</th><th>RTO</th></tr></thead><tbody>${m.rows.slice().sort((a,b)=>b.loss-a.loss).map(r=>`<tr><td class="name">${esc(r.name)}</td><td>${round(r.downtime)}h</td><td class="mini">${compact(r.loss)}</td><td><span class="risk ${r.breach?'high':'low'}">${r.breach?'Above':'Within'}</span></td></tr>`).join('')}</tbody></table>`:'<div class="empty">No services to evaluate.</div>'; | |
| const planned=m.rows.slice().sort((a,b)=>b.loss-a.loss||b.criticality-a.criticality);$('recoveryPriorities').innerHTML=planned.length?planned.map((r,i)=>`<div class="metricline"><div style="display:flex;gap:12px;align-items:center;min-width:0"><span style="height:31px;width:31px;flex:none;background:var(--pale);color:var(--teal);border-radius:9px;display:grid;place-items:center;font-weight:850">${i+1}</span><div><b style="font-size:11px">${esc(r.name)}</b><div class="small-note">Owner: ${esc(r.owner)} · RTO ${r.rto}h</div></div></div><b>${compact(r.loss)}</b></div>`).join(''):'<div class="empty">Add services to generate a recovery priority list.</div>'; | |
| $('taskBadge').textContent=done+'/'+TASKS.length+' DONE';$('recoveryChecks').innerHTML=TASKS.map(([key,title,desc])=>`<label class="check-item ${state.checks[key]?'checked':''}"><input type="checkbox" data-check="${key}" ${state.checks[key]?'checked':''}><span><strong>${esc(title)}</strong><small>${esc(desc)}</small></span></label>`).join('');$('auditList').innerHTML=state.audits.length?state.audits.map(a=>`<div class="audit-line"><span class="dot"></span><div><b>${esc(a.message)}</b><small>${esc(a.when.slice(0,16).replace('T',' '))} UTC</small></div></div>`).join(''):'<div class="empty">Changes you make to this workspace will appear here.</div>';renderServices();} | |
| function renderServices(){const a=active().filter(s=>(s.name+' '+s.owner+' '+s.domain).toLowerCase().includes(searchTerm));const tr=s=>`<tr><td><div class="name">${esc(s.name)}</div><div class="sub">${esc(s.owner)}</div></td><td>${esc(s.domain)}</td><td><span class="risk ${band(s)}">${s.readiness}/100</span></td><td class="mini">${s.rto}h / ${s.rpo}m</td><td>${s.criticality}/5</td><td class="mini">${compact(s.hourlyLoss)}/h</td><td><div class="controls"><button class="button small" data-edit="${esc(s.id)}">Edit</button><button class="button small warn" data-archive="${esc(s.id)}">Archive</button></div></td></tr>`;$('servicesTable').innerHTML=a.length?`<table class="data-table"><thead><tr><th>Service / owner</th><th>Function</th><th>Readiness</th><th>RTO / RPO</th><th>Critical</th><th>Hourly loss</th><th>Actions</th></tr></thead><tbody>${a.map(tr).join('')}</tbody></table>`:'<div class="empty"><b>No matching active services</b>Add a new service or adjust the search.</div>'; | |
| let archived=state.services.filter(s=>s.archived);$('archivedList').innerHTML=archived.length?archived.map(s=>`<div class="metricline"><div><b style="font-size:11px">${esc(s.name)}</b><div class="small-note">${esc(s.owner)}</div></div><button class="button small" data-restore="${esc(s.id)}">Restore</button></div>`).join(''):'<div class="empty">No archived services.</div>';} | |
| function tab(name){currentTab=name;document.querySelectorAll('.view').forEach(v=>v.classList.toggle('active',v.id===name));document.querySelectorAll('[data-tab]').forEach(b=>{b.classList.toggle('active',b.dataset.tab===name);if(b.closest('nav'))b.setAttribute('aria-current',b.dataset.tab===name?'page':'false')});$('crumb').textContent={overview:'RESILIENCE OVERVIEW',services:'SERVICE REGISTRY',scenario:'SCENARIO LAB',recovery:'RECOVERY PLAN'}[name]||'OVERVIEW';window.scrollTo({top:0,behavior:'instant'})} | |
| function openModal(id=null){editing=id;const item=id?state.services.find(s=>s.id===id):null;if(id&&!item)return;$('modalTitle').textContent=item?'Edit critical service':'Add critical service';$('svcName').value=item?.name||'';$('svcOwner').value=item?.owner||'';$('svcDomain').value=item?.domain||'operations';$('svcCritical').value=item?.criticality??4;$('svcReadiness').value=item?.readiness??65;$('svcRto').value=item?.rto??8;$('svcRpo').value=item?.rpo??60;$('svcLoss').value=item?.hourlyLoss??10000;$('depList').innerHTML=active().filter(s=>s.id!==id).map(s=>`<label><input type="checkbox" value="${esc(s.id)}" ${item?.deps.includes(s.id)?'checked':''}>${esc(s.name)}</label>`).join('')||'<div class="small-note">No other active services available.</div>';$('modalBack').classList.remove('hidden');$('svcName').focus()} | |
| function closeModal(){$('modalBack').classList.add('hidden');editing=null} | |
| function serviceSubmit(e){e.preventDefault();let s={id:editing||'svc-'+Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,8),name:$('svcName').value.trim(),owner:$('svcOwner').value.trim(),domain:$('svcDomain').value,criticality:Number($('svcCritical').value),readiness:Number($('svcReadiness').value),rto:Number($('svcRto').value),rpo:Number($('svcRpo').value),hourlyLoss:Number($('svcLoss').value),deps:[...document.querySelectorAll('#depList input:checked')].map(c=>c.value),archived:false};if(!validService(s)){notify('Please correct invalid or out-of-range fields.');return}if(editing){let original=state.services.find(r=>r.id===editing);state.services=state.services.map(r=>r.id===editing?{...s,archived:original?.archived??false}:r)}else{if(state.services.length>=100){notify('Maximum of 100 services reached.');return}state.services.push(s)}log((editing?'Updated':'Added')+' service: '+s.name);closeModal();render();notify('Service saved locally.')} | |
| function csvServices(){return csv([['Service','Owner','Domain','Criticality','RTO (hours)','RPO (minutes)','Hourly loss USD','Readiness','Dependencies'],...active().map(s=>[s.name,s.owner,s.domain,s.criticality,s.rto,s.rpo,s.hourlyLoss,s.readiness,s.deps.join(';')])])} | |
| function csvScenario(){const m=model();return csv([['Service','Domain','Exposure factor','Modeled downtime h','RTO target h','Estimated impact USD','Potential data window min'],...m.rows.map(r=>[r.name,r.domain,round(r.exposure,3),round(r.downtime,3),r.rto,round(r.loss,2),round(r.dataWindow,2)])])} | |
| function inputFile(){let f=$('fileInput');f.value='';f.click()} | |
| async function restoreFile(e){const f=e.target.files?.[0];if(!f)return;if(f.size>150000){notify('Backup exceeds 150 KB size limit.');return}try{const v=JSON.parse(await f.text());const fresh=parseBackup(v);if(!confirm('Replace this workspace with the imported backup? Export current work first if needed.'))return;state=fresh;log('Restored validated local workspace backup');render();notify('Workspace restored successfully.')}catch(err){notify('Backup rejected: '+err.message)}} | |
| function handleClick(e){const b=e.target.closest('button');if(!b)return;const ds=b.dataset;if(ds.tab){tab(ds.tab);return}if(ds.preset){state.scenario.type=ds.preset;log('Changed scenario to '+PRESETS[ds.preset].name);render();return}if(ds.edit){openModal(ds.edit);return}if(ds.archive){const s=state.services.find(s=>s.id===ds.archive);if(s&&confirm('Archive '+s.name+'? It can be restored later.')){s.archived=true;log('Archived service: '+s.name);render();notify('Service archived.')}return}if(ds.restore){const s=state.services.find(s=>s.id===ds.restore);if(s){s.archived=false;log('Restored service: '+s.name);render();notify('Service restored.')}return}} | |
| document.addEventListener('click',handleClick);document.addEventListener('change',e=>{if(e.target.dataset.check){state.checks[e.target.dataset.check]=e.target.checked;log((e.target.checked?'Completed':'Reopened')+' task: '+e.target.dataset.check);render()}}); | |
| for(const [name,field] of [['duration','hours'],['containment','containment'],['budget','budget']])$(name+'Slider').addEventListener('input',e=>{state.scenario[field]=Number(e.target.value);save();render()}); | |
| $('runStress').addEventListener('click',()=>{state.scenario.seed=(state.scenario.seed+1)%2147483647;log('Reran 400 illustrative stress trials with new seed');render();notify('400 seeded trials recalculated.')});$('themeToggle').addEventListener('click',()=>{state.theme=state.theme==='light'?'dark':'light';save();render()});$('serviceSearch').addEventListener('input',e=>{searchTerm=e.target.value.toLowerCase().trim();renderServices()});$('addService').addEventListener('click',()=>openModal());$('closeModal').addEventListener('click',closeModal);$('cancelModal').addEventListener('click',closeModal);$('modalBack').addEventListener('click',e=>{if(e.target===$('modalBack'))closeModal()});document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});$('serviceForm').addEventListener('submit',serviceSubmit); | |
| $('exportServices').addEventListener('click',()=>{downloadFile('continuityos-services.csv',csvServices(),'text/csv;charset=utf-8');notify('Service CSV prepared.')});$('downloadScenario').addEventListener('click',()=>{downloadFile('continuityos-scenario.csv',csvScenario(),'text/csv;charset=utf-8');notify('Scenario CSV prepared.')});$('exportJson').addEventListener('click',()=>{downloadFile('continuityos-workspace.json',JSON.stringify({...state,audits:[]},null,2),'application/json');notify('Workspace backup prepared.')});$('importJson').addEventListener('click',inputFile);$('fileInput').addEventListener('change',restoreFile);$('resetDemo').addEventListener('click',()=>{if(!confirm('Reset all current workspace data to fictional sample data? Download a backup first.'))return;let theme=state.theme;state=clone(DEMO);state.theme=theme;log('Reset illustrative demo workspace');searchTerm='';$('serviceSearch').value='';render();notify('Sample workspace restored.')});$('printPlan').addEventListener('click',()=>window.print()); | |
| window.__CONTINUITY_TEST__={model,simulate,parseBackup,validService,exposure,getState:()=>clone(state),getScenario:()=>clone(state.scenario)}; | |
| render(); | |
| })(); | |
| </script> | |
| </body></html> | |