| """FastAPI entrypoint for the OpenEnv validator and Hugging Face Space runtime."""
|
| from __future__ import annotations
|
|
|
| from fastapi.responses import HTMLResponse
|
|
|
| from sentinel.api.server import app
|
|
|
| SPACE_URL = "https://harry1911-sentinel.hf.space"
|
| SPACE_REPO_URL = "https://huggingface.co/spaces/harry1911/sentinel"
|
| GITHUB_URL = "https://github.com/sayantikalaskar/sentinel"
|
|
|
|
|
| @app.get("/", include_in_schema=False)
|
| async def root() -> HTMLResponse:
|
| """Render the public Space landing and live evaluation dashboard."""
|
| return HTMLResponse(_dashboard_html())
|
|
|
|
|
| @app.get("/dashboard", include_in_schema=False)
|
| async def dashboard() -> HTMLResponse:
|
| """Render the live evaluation dashboard at the advertised URL."""
|
| return HTMLResponse(_dashboard_html())
|
|
|
|
|
| def _dashboard_html() -> str:
|
| return f"""<!doctype html>
|
| <html lang="en">
|
| <head>
|
| <meta charset="utf-8">
|
| <meta name="viewport" content="width=device-width, initial-scale=1">
|
| <title>SENTINEL - Live Incident Response Environment</title>
|
| <style>
|
| :root {{
|
| color-scheme: dark;
|
| --bg: #07111f;
|
| --panel: #101b2d;
|
| --panel-2: #14243a;
|
| --panel-3: #0b1627;
|
| --line: rgba(174, 190, 214, 0.22);
|
| --text: #eef4ff;
|
| --muted: #a9b8ce;
|
| --cyan: #22d3ee;
|
| --green: #34d399;
|
| --amber: #fbbf24;
|
| --rose: #fb7185;
|
| --blue: #60a5fa;
|
| }}
|
| * {{ box-sizing: border-box; }}
|
| body {{
|
| margin: 0;
|
| min-height: 100vh;
|
| font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| background:
|
| linear-gradient(rgba(96, 165, 250, 0.055) 1px, transparent 1px),
|
| linear-gradient(90deg, rgba(34, 211, 238, 0.05) 1px, transparent 1px),
|
| radial-gradient(circle at 18% 0%, rgba(34, 211, 238, 0.18), transparent 31rem),
|
| radial-gradient(circle at 86% 8%, rgba(52, 211, 153, 0.13), transparent 30rem),
|
| radial-gradient(circle at 55% 100%, rgba(251, 113, 133, 0.08), transparent 34rem),
|
| linear-gradient(145deg, #07111f 0%, #0f172a 50%, #111827 100%);
|
| background-size: 42px 42px, 42px 42px, auto, auto, auto, auto;
|
| color: var(--text);
|
| overflow-x: hidden;
|
| }}
|
| body::before {{
|
| content: "";
|
| position: fixed;
|
| inset: 0;
|
| pointer-events: none;
|
| background:
|
| linear-gradient(105deg, transparent 0%, rgba(34, 211, 238, 0.09) 48%, transparent 56%),
|
| repeating-linear-gradient(180deg, rgba(255,255,255,0.028) 0 1px, transparent 1px 6px);
|
| mix-blend-mode: screen;
|
| opacity: .48;
|
| animation: sweep 11s linear infinite;
|
| }}
|
| @keyframes sweep {{
|
| 0% {{ transform: translateX(-35%); }}
|
| 100% {{ transform: translateX(35%); }}
|
| }}
|
| a {{ color: inherit; text-decoration: none; }}
|
| button, input, select {{ font: inherit; }}
|
| .wrap {{ max-width: 1240px; margin: 0 auto; padding: 24px 18px 42px; }}
|
| .nav {{
|
| position: sticky;
|
| top: 0;
|
| z-index: 3;
|
| display: flex;
|
| align-items: center;
|
| justify-content: space-between;
|
| gap: 16px;
|
| margin-bottom: 28px;
|
| padding: 10px;
|
| border: 1px solid rgba(174, 190, 214, 0.16);
|
| border-radius: 8px;
|
| background: rgba(7, 17, 31, 0.72);
|
| backdrop-filter: blur(18px);
|
| }}
|
| .brand {{ display: flex; align-items: center; gap: 12px; font-weight: 900; }}
|
| .mark {{ width: 36px; height: 36px; display: grid; place-items: center; border-radius: 8px; background: linear-gradient(135deg, var(--cyan), var(--green)); color: #04111f; font-weight: 900; }}
|
| .links {{ display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; }}
|
| .pill, .btn {{
|
| border: 1px solid var(--line);
|
| background: rgba(15, 23, 42, 0.78);
|
| color: var(--muted);
|
| padding: 9px 12px;
|
| border-radius: 8px;
|
| font-size: 14px;
|
| cursor: pointer;
|
| transition: transform .16s ease, border-color .16s ease, color .16s ease, background .16s ease, box-shadow .16s ease;
|
| }}
|
| .pill:hover, .btn:hover {{
|
| transform: translateY(-1px);
|
| color: var(--text);
|
| border-color: rgba(34, 211, 238, 0.55);
|
| box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.13), 0 14px 38px rgba(0, 0, 0, 0.24);
|
| }}
|
| .pill.primary, .btn.primary {{ background: var(--green); color: #05111d; border-color: transparent; font-weight: 850; }}
|
| .btn.cyan {{ background: var(--cyan); color: #05111d; border-color: transparent; font-weight: 850; }}
|
| .btn.rose {{ background: rgba(251, 113, 133, 0.16); color: #fecdd3; border-color: rgba(251, 113, 133, 0.36); }}
|
| .hero {{ display: grid; grid-template-columns: minmax(0, 1fr) minmax(380px, 0.92fr); gap: 18px; align-items: stretch; }}
|
| .hero-copy, .panel {{
|
| position: relative;
|
| overflow: hidden;
|
| border: 1px solid var(--line);
|
| background:
|
| linear-gradient(145deg, rgba(16, 27, 45, 0.9), rgba(9, 18, 32, 0.78)),
|
| linear-gradient(90deg, rgba(34, 211, 238, 0.08), rgba(52, 211, 153, 0.04));
|
| border-radius: 8px;
|
| padding: 22px;
|
| box-shadow: 0 28px 90px rgba(0, 0, 0, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.045);
|
| }}
|
| .hero-copy::before, .panel::before {{
|
| content: "";
|
| position: absolute;
|
| inset: 0;
|
| pointer-events: none;
|
| border-radius: inherit;
|
| padding: 1px;
|
| background: linear-gradient(135deg, rgba(34, 211, 238, .68), rgba(52, 211, 153, .16), rgba(251, 191, 36, .2), rgba(251, 113, 133, .32));
|
| mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
|
| mask-composite: exclude;
|
| }}
|
| .hero-copy > *, .panel > * {{ position: relative; z-index: 1; }}
|
| .hero-copy::after {{
|
| content: "LIVE INCIDENT RESPONSE ENVIRONMENT";
|
| position: absolute;
|
| right: -34px;
|
| bottom: 14px;
|
| color: rgba(174, 190, 214, 0.055);
|
| font-size: 44px;
|
| font-weight: 950;
|
| letter-spacing: .08em;
|
| white-space: nowrap;
|
| }}
|
| .hero-copy {{ background: rgba(11, 22, 39, 0.72); }}
|
| .eyebrow {{ display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }}
|
| .tag {{ border: 1px solid var(--line); color: var(--muted); background: rgba(20, 36, 58, 0.82); border-radius: 8px; padding: 7px 9px; font-size: 12px; }}
|
| h1 {{
|
| font-size: clamp(42px, 6.4vw, 88px);
|
| line-height: 0.92;
|
| margin: 0 0 18px;
|
| letter-spacing: 0;
|
| max-width: 920px;
|
| background: linear-gradient(95deg, #f8fbff 0%, #c7f9ff 42%, #b7f7d5 72%, #ffe5a3 100%);
|
| -webkit-background-clip: text;
|
| background-clip: text;
|
| color: transparent;
|
| text-shadow: 0 0 46px rgba(34, 211, 238, 0.12);
|
| }}
|
| .lead {{ font-size: 18px; line-height: 1.55; color: var(--muted); max-width: 780px; margin: 0 0 20px; }}
|
| .cta {{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 16px; }}
|
| .stats {{ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-top: 18px; }}
|
| .stat {{ border-left: 3px solid var(--green); background: rgba(20, 36, 58, 0.7); border-radius: 8px; padding: 12px; box-shadow: inset 0 0 32px rgba(34, 211, 238, 0.035); }}
|
| .stat:nth-child(2) {{ border-left-color: var(--cyan); }}
|
| .stat:nth-child(3) {{ border-left-color: var(--amber); }}
|
| .stat:nth-child(4) {{ border-left-color: var(--rose); }}
|
| .stat strong {{ display: block; font-size: 24px; margin-bottom: 2px; }}
|
| .stat span {{ color: var(--muted); font-size: 12px; line-height: 1.35; }}
|
| .panel h2, .section h2 {{ margin: 0 0 14px; font-size: 21px; }}
|
| .controls {{ display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 12px; }}
|
| label {{ color: var(--muted); font-size: 12px; display: grid; gap: 6px; }}
|
| input, select, textarea {{
|
| width: 100%;
|
| color: var(--text);
|
| background: rgba(7, 17, 31, 0.92);
|
| border: 1px solid var(--line);
|
| border-radius: 8px;
|
| padding: 10px 11px;
|
| outline: none;
|
| }}
|
| textarea {{ min-height: 136px; resize: vertical; font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; line-height: 1.45; }}
|
| .button-grid {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }}
|
| .button-grid .btn {{ min-height: 46px; text-align: left; }}
|
| .section {{ margin-top: 18px; }}
|
| .kpis {{ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }}
|
| .kpi {{
|
| background: linear-gradient(145deg, rgba(16, 27, 45, 0.86), rgba(8, 17, 31, 0.72));
|
| border: 1px solid var(--line);
|
| border-radius: 8px;
|
| padding: 14px;
|
| box-shadow: inset 0 0 38px rgba(96, 165, 250, 0.035);
|
| }}
|
| .kpi span {{ display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .06em; }}
|
| .kpi strong {{ display: block; margin-top: 5px; font-size: 21px; overflow-wrap: anywhere; }}
|
| .workspace {{ display: grid; grid-template-columns: minmax(0, 1.1fr) minmax(340px, 0.9fr); gap: 18px; margin-top: 18px; }}
|
| .tabs {{ display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }}
|
| .tab.active {{ color: #05111d; background: var(--cyan); border-color: transparent; font-weight: 850; }}
|
| pre {{
|
| margin: 0;
|
| min-height: 360px;
|
| max-height: 560px;
|
| overflow: auto;
|
| background: rgba(5, 10, 20, 0.86);
|
| border: 1px solid var(--line);
|
| border-radius: 8px;
|
| padding: 16px;
|
| color: #d7e3f4;
|
| font: 12px/1.55 ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
|
| white-space: pre-wrap;
|
| overflow-wrap: anywhere;
|
| }}
|
| .log {{ min-height: 196px; max-height: 230px; }}
|
| .cards {{ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }}
|
| .card {{
|
| background: linear-gradient(145deg, rgba(16, 27, 45, 0.86), rgba(7, 17, 31, 0.74));
|
| border: 1px solid var(--line);
|
| border-radius: 8px;
|
| padding: 15px;
|
| min-height: 132px;
|
| transition: transform .16s ease, border-color .16s ease, box-shadow .16s ease;
|
| }}
|
| .card:hover {{ transform: translateY(-2px); border-color: rgba(52, 211, 153, 0.48); box-shadow: 0 18px 48px rgba(0,0,0,.24); }}
|
| .card h3 {{ margin: 0 0 8px; font-size: 15px; }}
|
| .card p {{ margin: 0 0 12px; color: var(--muted); font-size: 13px; line-height: 1.45; }}
|
| .endpoint {{ color: var(--cyan); font-family: ui-monospace, SFMono-Regular, Consolas, monospace; font-size: 12px; }}
|
| .toast {{ margin-top: 12px; min-height: 22px; color: var(--muted); font-size: 13px; }}
|
| .topology {{
|
| margin-top: 18px;
|
| display: grid;
|
| grid-template-columns: repeat(10, minmax(0, 1fr));
|
| gap: 7px;
|
| max-width: 760px;
|
| }}
|
| .node {{
|
| height: 30px;
|
| border-radius: 8px;
|
| border: 1px solid rgba(174, 190, 214, 0.17);
|
| background: linear-gradient(145deg, rgba(34, 211, 238, 0.19), rgba(52, 211, 153, 0.08));
|
| box-shadow: 0 0 22px rgba(34, 211, 238, 0.08), inset 0 0 18px rgba(255,255,255,.025);
|
| }}
|
| .node.hot {{ background: linear-gradient(145deg, rgba(251, 113, 133, 0.42), rgba(251, 191, 36, 0.13)); border-color: rgba(251, 113, 133, 0.45); }}
|
| .node.warn {{ background: linear-gradient(145deg, rgba(251, 191, 36, 0.35), rgba(34, 211, 238, 0.08)); border-color: rgba(251, 191, 36, 0.4); }}
|
| .panel-title {{
|
| display: flex;
|
| align-items: center;
|
| justify-content: space-between;
|
| gap: 12px;
|
| margin-bottom: 14px;
|
| }}
|
| .panel-title h2 {{ margin: 0; }}
|
| .live-chip {{
|
| display: inline-flex;
|
| align-items: center;
|
| gap: 7px;
|
| color: #b7f7d5;
|
| border: 1px solid rgba(52, 211, 153, .38);
|
| background: rgba(52, 211, 153, .1);
|
| padding: 6px 8px;
|
| border-radius: 8px;
|
| font-size: 12px;
|
| font-weight: 800;
|
| }}
|
| .live-chip::before {{
|
| content: "";
|
| width: 7px;
|
| height: 7px;
|
| border-radius: 50%;
|
| background: var(--green);
|
| box-shadow: 0 0 14px var(--green);
|
| }}
|
| @media (max-width: 980px) {{
|
| .hero, .workspace, .cards, .kpis, .stats {{ grid-template-columns: 1fr; }}
|
| .nav {{ align-items: flex-start; flex-direction: column; }}
|
| .links {{ justify-content: flex-start; }}
|
| }}
|
| </style>
|
| </head>
|
| <body>
|
| <main class="wrap">
|
| <nav class="nav">
|
| <a class="brand" href="/"><span class="mark">S</span><span>SENTINEL</span></a>
|
| <div class="links">
|
| <a class="pill primary" href="/docs" target="_blank" rel="noopener noreferrer">API Docs</a>
|
| <a class="pill" href="/health" target="_blank" rel="noopener noreferrer">Health</a>
|
| <a class="pill" href="{SPACE_REPO_URL}" target="_blank" rel="noopener noreferrer">HF Space</a>
|
| <a class="pill" href="{GITHUB_URL}" target="_blank" rel="noopener noreferrer">GitHub</a>
|
| <a class="pill" href="{GITHUB_URL}/blob/main/README.md" target="_blank" rel="noopener noreferrer">README</a>
|
| <a class="pill" href="{GITHUB_URL}/blob/main/Blog.MD" target="_blank" rel="noopener noreferrer">Blog</a>
|
| </div>
|
| </nav>
|
|
|
| <section class="hero">
|
| <div class="hero-copy">
|
| <div class="eyebrow">
|
| <span class="tag">Live OpenEnv API</span>
|
| <span class="tag">FastAPI Space runtime</span>
|
| <span class="tag">Judge-ready endpoint surface</span>
|
| </div>
|
| <h1>Multi-agent incident response, live in this Space.</h1>
|
| <p class="lead">Reset a 30-service cloud outage, run investigation and remediation actions, inspect observations, and verify the same HTTP endpoints judges will call from the public Space URL.</p>
|
| <div class="cta">
|
| <button class="btn cyan" onclick="resetEpisode()">Reset Episode</button>
|
| <button class="btn primary" onclick="runSmokeTest()">Run Smoke Test</button>
|
| <a class="btn" href="/docs" target="_blank" rel="noopener noreferrer">Open API Schema</a>
|
| <a class="btn" href="{GITHUB_URL}/tree/main/results" target="_blank" rel="noopener noreferrer">Results</a>
|
| </div>
|
| <div class="stats">
|
| <div class="stat"><strong>30</strong><span>services in NexaStack</span></div>
|
| <div class="stat"><strong>7</strong><span>observation channels</span></div>
|
| <div class="stat"><strong>5</strong><span>agent roles</span></div>
|
| <div class="stat"><strong>4</strong><span>reward axes</span></div>
|
| </div>
|
| <div class="topology" aria-label="NexaStack service topology preview">
|
| <span class="node"></span><span class="node"></span><span class="node warn"></span><span class="node"></span><span class="node"></span>
|
| <span class="node hot"></span><span class="node warn"></span><span class="node"></span><span class="node"></span><span class="node"></span>
|
| <span class="node"></span><span class="node warn"></span><span class="node hot"></span><span class="node warn"></span><span class="node"></span>
|
| <span class="node"></span><span class="node"></span><span class="node"></span><span class="node warn"></span><span class="node"></span>
|
| </div>
|
| </div>
|
|
|
| <aside class="panel">
|
| <div class="panel-title"><h2>Live Controls</h2><span class="live-chip">RUNNING</span></div>
|
| <div class="controls">
|
| <label>Seed <input id="seed" type="number" value="42"></label>
|
| <label>Incident
|
| <select id="incident">
|
| <option>E1</option><option>E2</option><option>E3</option>
|
| <option>M1</option><option>M2</option><option>M3</option><option>M4</option>
|
| <option>H1</option><option>H2</option><option>H3</option>
|
| </select>
|
| </label>
|
| </div>
|
| <div class="button-grid">
|
| <button class="btn cyan" onclick="resetEpisode()">Reset</button>
|
| <button class="btn primary" onclick="runSmokeTest()">Smoke Test</button>
|
| <button class="btn" onclick="callPreset('queryLogs')">Holmes Logs</button>
|
| <button class="btn" onclick="callPreset('queryMetrics')">Argus Metrics</button>
|
| <button class="btn" onclick="callPreset('restart')">Forge Restart</button>
|
| <button class="btn" onclick="callPreset('close')">Hermes Close</button>
|
| <button class="btn" onclick="loadRender()">Render</button>
|
| <button class="btn rose" onclick="closeEpisode()">Close API</button>
|
| </div>
|
| <div id="toast" class="toast">Ready. Click Reset Episode to initialize the environment.</div>
|
| </aside>
|
| </section>
|
|
|
| <section class="section kpis">
|
| <div class="kpi"><span>Status</span><strong id="kpi-status">Idle</strong></div>
|
| <div class="kpi"><span>Episode</span><strong id="kpi-episode">-</strong></div>
|
| <div class="kpi"><span>Incident</span><strong id="kpi-incident">-</strong></div>
|
| <div class="kpi"><span>Last Reward</span><strong id="kpi-reward">-</strong></div>
|
| </section>
|
|
|
| <section class="workspace">
|
| <div class="panel">
|
| <div class="tabs">
|
| <button class="pill tab active" onclick="showTab('observation', this)">Observation</button>
|
| <button class="pill tab" onclick="showTab('step', this)">Step Result</button>
|
| <button class="pill tab" onclick="showTab('render', this)">Render</button>
|
| <button class="pill tab" onclick="showTab('custom', this)">Custom Action</button>
|
| </div>
|
| <pre id="output">{{"message":"No episode yet. Press Reset Episode."}}</pre>
|
| <div id="custom-box" style="display:none;margin-top:10px;">
|
| <textarea id="custom-action">{{
|
| "agent": "holmes",
|
| "category": "investigative",
|
| "name": "FormHypothesis",
|
| "params": {{
|
| "service": "cart-service",
|
| "failure_type": "memory_leak",
|
| "confidence": 0.7
|
| }}
|
| }}</textarea>
|
| <div class="cta"><button class="btn primary" onclick="runCustomAction()">Run Custom Action</button></div>
|
| </div>
|
| </div>
|
| <div class="panel">
|
| <h2>Action Log</h2>
|
| <pre id="log" class="log">No actions yet.</pre>
|
| <div class="section">
|
| <h2>Judge Quickstart</h2>
|
| <pre class="log">POST {SPACE_URL}/reset
|
| {{"seed": 42}}
|
|
|
| POST {SPACE_URL}/step
|
| {{"action": {{"agent": "holmes", "category": "investigative", "name": "QueryLogs", "params": {{"service": "cart-service", "time_range": [0, 60]}}}}}}</pre>
|
| </div>
|
| </div>
|
| </section>
|
|
|
| <section class="section">
|
| <h2>Submission Links</h2>
|
| <div class="cards">
|
| <a class="card" href="{GITHUB_URL}" target="_blank" rel="noopener noreferrer"><h3>Source Code</h3><p>Environment, agents, server, tests, and training pipeline.</p><span class="endpoint">GitHub repository</span></a>
|
| <a class="card" href="{GITHUB_URL}/blob/main/openenv.yaml" target="_blank" rel="noopener noreferrer"><h3>OpenEnv Manifest</h3><p>Runtime declaration used by validators and deployment.</p><span class="endpoint">openenv.yaml</span></a>
|
| <a class="card" href="{GITHUB_URL}/blob/main/sentinel_colab_training.ipynb" target="_blank" rel="noopener noreferrer"><h3>Training Notebook</h3><p>Colab workflow for reproducing training and plots.</p><span class="endpoint">sentinel_colab_training.ipynb</span></a>
|
| <a class="card" href="{GITHUB_URL}/tree/main/results" target="_blank" rel="noopener noreferrer"><h3>Results</h3><p>Evaluation logs, TensorBoard events, and training curves.</p><span class="endpoint">results/</span></a>
|
| </div>
|
| </section>
|
| </main>
|
|
|
| <script>
|
| const presets = {{
|
| queryLogs: {{agent:"holmes", category:"investigative", name:"QueryLogs", params:{{service:"cart-service", time_range:[0,60]}}}},
|
| queryMetrics: {{agent:"argus", category:"investigative", name:"QueryMetrics", params:{{service:"order-service", metric_name:"cpu", time_range:[0,60]}}}},
|
| restart: {{agent:"forge", category:"remediation", name:"RestartService", params:{{service:"cart-service"}}}},
|
| close: {{agent:"hermes", category:"meta", name:"CloseIncident", params:{{resolution_summary:"Closed from live Space dashboard"}}}}
|
| }};
|
| let lastObservation = null;
|
| let lastStep = null;
|
| let currentTab = "observation";
|
| const logs = [];
|
|
|
| function $(id) {{ return document.getElementById(id); }}
|
| function pretty(value) {{ return JSON.stringify(value, null, 2); }}
|
| function toast(message) {{ $("toast").textContent = message; }}
|
| function appendLog(message) {{
|
| logs.unshift(new Date().toLocaleTimeString() + " " + message);
|
| $("log").textContent = logs.slice(0, 24).join("\\n");
|
| }}
|
| function setOutput(value) {{ $("output").textContent = typeof value === "string" ? value : pretty(value); }}
|
| function updateKpis(extra={{}}) {{
|
| $("kpi-status").textContent = extra.status || (lastStep?.terminated ? "Done" : lastObservation ? "Active" : "Idle");
|
| $("kpi-episode").textContent = extra.episode || lastObservation?.info?.episode_id || "-";
|
| $("kpi-incident").textContent = extra.incident || lastObservation?.info?.incident_id || "-";
|
| $("kpi-reward").textContent = typeof lastStep?.reward === "number" ? lastStep.reward.toFixed(3) : "-";
|
| }}
|
| async function api(path, options={{}}) {{
|
| const response = await fetch(path, {{
|
| method: options.method || "GET",
|
| headers: {{"Content-Type": "application/json"}},
|
| body: options.body ? JSON.stringify(options.body) : undefined
|
| }});
|
| const text = await response.text();
|
| let data;
|
| try {{ data = text ? JSON.parse(text) : {{}}; }} catch {{ data = text; }}
|
| if (!response.ok) throw new Error(typeof data === "string" ? data : pretty(data));
|
| return data;
|
| }}
|
| async function resetEpisode() {{
|
| try {{
|
| toast("Resetting episode...");
|
| const seed = Number($("seed").value || 42);
|
| const incident = $("incident").value;
|
| const data = await api("/reset", {{method:"POST", body:{{seed, options:{{incident_id: incident}}}}}});
|
| lastObservation = data;
|
| lastStep = null;
|
| showData("observation", data);
|
| updateKpis({{status:"Active", incident:data.info?.incident_id || incident}});
|
| appendLog("reset(seed=" + seed + ", incident=" + incident + ") -> ok");
|
| toast("Episode initialized.");
|
| }} catch (err) {{ toast("Reset failed: " + err.message); appendLog("reset -> ERROR"); setOutput(err.message); }}
|
| }}
|
| async function callPreset(name) {{
|
| try {{
|
| if (!lastObservation) await resetEpisode();
|
| const action = presets[name];
|
| toast("Running " + action.agent + "::" + action.name + "...");
|
| const data = await api("/step", {{method:"POST", body:{{action}}}});
|
| lastStep = data;
|
| lastObservation = {{observation:data.observation, info:data.info}};
|
| showData("step", data);
|
| updateKpis();
|
| appendLog(action.agent + "::" + action.name + " -> reward=" + Number(data.reward).toFixed(3) + ", done=" + data.terminated);
|
| toast("Action completed.");
|
| }} catch (err) {{ toast("Action failed: " + err.message); appendLog(name + " -> ERROR"); setOutput(err.message); }}
|
| }}
|
| async function runSmokeTest() {{
|
| await resetEpisode();
|
| await callPreset("queryLogs");
|
| await callPreset("queryMetrics");
|
| await loadRender();
|
| toast("Smoke test passed: reset, two steps, and render completed.");
|
| appendLog("smoke_test -> passed");
|
| }}
|
| async function runCustomAction() {{
|
| try {{
|
| if (!lastObservation) await resetEpisode();
|
| const action = JSON.parse($("custom-action").value);
|
| const data = await api("/step", {{method:"POST", body:{{action}}}});
|
| lastStep = data;
|
| lastObservation = {{observation:data.observation, info:data.info}};
|
| showData("step", data);
|
| updateKpis();
|
| appendLog(action.agent + "::" + action.name + " -> reward=" + Number(data.reward).toFixed(3));
|
| toast("Custom action completed.");
|
| }} catch (err) {{ toast("Custom action failed: " + err.message); setOutput(err.message); appendLog("custom_action -> ERROR"); }}
|
| }}
|
| async function loadRender() {{
|
| try {{
|
| const data = await api("/render");
|
| showData("render", data.render || data);
|
| appendLog("render() -> ok");
|
| }} catch (err) {{ toast("Render requires reset first."); setOutput(err.message); }}
|
| }}
|
| async function closeEpisode() {{
|
| try {{
|
| const data = await api("/close", {{method:"POST"}});
|
| lastObservation = null; lastStep = null;
|
| updateKpis({{status:"Closed", episode:"-", incident:"-"}});
|
| showData("step", data);
|
| appendLog("close() -> ok");
|
| toast("Environment closed.");
|
| }} catch (err) {{ toast("Close failed: " + err.message); setOutput(err.message); }}
|
| }}
|
| function showData(tab, data) {{
|
| currentTab = tab;
|
| $("custom-box").style.display = tab === "custom" ? "block" : "none";
|
| if (tab !== "custom") setOutput(data);
|
| document.querySelectorAll(".tab").forEach(btn => btn.classList.remove("active"));
|
| const labels = {{observation:0, step:1, render:2, custom:3}};
|
| document.querySelectorAll(".tab")[labels[tab]]?.classList.add("active");
|
| }}
|
| function showTab(tab, button) {{
|
| document.querySelectorAll(".tab").forEach(btn => btn.classList.remove("active"));
|
| button.classList.add("active");
|
| $("custom-box").style.display = tab === "custom" ? "block" : "none";
|
| if (tab === "observation") setOutput(lastObservation || {{"message":"No observation yet. Press Reset Episode."}});
|
| if (tab === "step") setOutput(lastStep || {{"message":"No step result yet."}});
|
| if (tab === "render") loadRender();
|
| if (tab === "custom") setOutput({{"message":"Edit the JSON below and run a custom action."}});
|
| }}
|
| api("/health").then(data => {{
|
| updateKpis({{status:data.initialized ? "Active" : "Idle", episode:data.episode_id || "-"}});
|
| appendLog("health() -> " + data.status);
|
| }}).catch(() => appendLog("health() -> unavailable"));
|
| </script>
|
| </body>
|
| </html>"""
|
|
|