<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Warmplane Control Deck</title>
<style>
:root {
color-scheme: dark;
--bg-app: #0c0d10;
--surface: #13151a;
--surface-card: #181b22;
--surface-hover: #1f232c;
--surface-elevated: #242934;
--border: #262b36;
--border-focus: #e58b20;
--border-subtle: #1c202a;
--amber-300: #fbbf24;
--amber-400: #f59e0b;
--amber-500: #d97706;
--amber-glow: rgba(245, 158, 11, 0.15);
--green-400: #34d399;
--red-400: #f87171;
--cyan-400: #22d3ee;
--text-main: #f3f4f6;
--text-muted: #9ca3af;
--text-dim: #6b7280;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--ff-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--ff-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
/* Transitions */
--ease-out-snappy: cubic-bezier(0.16, 1, 0.3, 1);
--transition-fast: 0.12s var(--ease-out-snappy);
--transition-normal: 0.18s var(--ease-out-snappy);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body {
color-scheme: dark;
background-color: var(--bg-app);
color: var(--text-main);
font-family: var(--ff-sans);
font-size: 13px;
line-height: 1.5;
display: flex;
height: 100vh;
overflow: hidden;
-webkit-font-smoothing: antialiased;
}
/* Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: var(--bg-app);
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
transition: background-color var(--transition-fast);
}
::-webkit-scrollbar-thumb:hover {
background: var(--surface-elevated);
}
/* Typography & Badges */
code, pre {
font-family: var(--ff-mono);
}
.brand-badge {
font-size: 10.5px;
font-weight: 600;
padding: 2px 7px;
border-radius: 20px;
background: rgba(245, 158, 11, 0.12);
color: var(--amber-400);
border: 1px solid rgba(245, 158, 11, 0.25);
font-family: var(--ff-mono);
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: var(--radius-sm);
font-size: 12px;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
transition: background-color var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast), box-shadow var(--transition-fast), transform 0.08s ease;
font-family: var(--ff-sans);
user-select: none;
}
.btn:active {
transform: translateY(1px);
}
.btn-primary {
background: var(--amber-500);
color: #000;
font-weight: 600;
}
.btn-primary:hover {
background: var(--amber-400);
box-shadow: 0 0 12px var(--amber-glow);
}
.btn-primary:active {
background: var(--amber-500);
}
.btn-ghost {
background: var(--surface-card);
color: var(--text-main);
border-color: var(--border);
}
.btn-ghost:hover {
background: var(--surface-hover);
border-color: var(--text-dim);
}
.btn-danger {
background: rgba(248, 113, 113, 0.1);
color: var(--red-400);
border-color: rgba(248, 113, 113, 0.25);
}
.btn-danger:hover {
background: rgba(248, 113, 113, 0.2);
border-color: rgba(248, 113, 113, 0.4);
}
/* Form inputs & Select dropdowns */
.form-group {
margin-bottom: 14px;
}
.form-label {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
}
.form-input, .form-textarea, .input-control, select, select.form-input {
width: 100%;
background: var(--surface);
background-color: var(--surface);
border: 1px solid var(--border);
padding: 8px 12px;
border-radius: var(--radius-sm);
color: var(--text-main);
color-scheme: dark;
font-family: var(--ff-mono);
font-size: 12px;
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), background-color var(--transition-fast);
-webkit-appearance: none;
appearance: none;
}
select, select.form-input, select.input-control {
cursor: pointer;
background-color: var(--surface);
color: var(--text-main);
color-scheme: dark;
padding-right: 28px;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
}
select option {
background-color: #181b22;
color: #f3f4f6;
padding: 6px 10px;
}
.form-input:focus, .form-textarea:focus, .input-control:focus, select:focus, select.form-input:focus {
outline: none;
border-color: var(--amber-400);
box-shadow: 0 0 0 2px var(--amber-glow);
}
/* Bento Grid */
.bento-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.bento-card {
background: var(--surface-card);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 16px 18px;
position: relative;
transition: border-color var(--transition-normal), background-color var(--transition-normal), box-shadow var(--transition-normal);
}
.col-3 { grid-column: span 3; }
.col-4 { grid-column: span 4; }
.col-6 { grid-column: span 6; }
.col-8 { grid-column: span 8; }
.col-12 { grid-column: span 12; }
.stat-label {
font-size: 11px;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.stat-value {
font-size: 24px;
font-weight: 700;
font-family: var(--ff-mono);
color: var(--text-main);
margin: 6px 0 2px;
}
.stat-sub {
font-size: 11px;
color: var(--text-dim);
}
/* Modals with smooth fade & scale */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity var(--transition-normal), visibility var(--transition-normal);
}
.modal-backdrop.active {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.modal-box {
width: 540px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 24px;
box-shadow: 0 20px 40px rgba(0,0,0,0.8);
transform: scale(0.97) translateY(4px);
transition: transform var(--transition-normal);
}
.modal-backdrop.active .modal-box {
transform: scale(1) translateY(0);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.modal-title {
font-size: 16px;
font-weight: 700;
color: var(--text-main);
}
/* Prefers Reduced Motion */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
</style>
<style>
.ambient-glow {
position: fixed;
top: -150px;
right: 15%;
width: 450px;
height: 450px;
background: radial-gradient(circle, rgba(245, 158, 11, 0.07) 0%, rgba(12, 13, 16, 0) 70%);
pointer-events: none;
z-index: 0;
}
aside.sidebar {
width: 240px;
background: var(--surface);
border-right: 1px solid var(--border);
display: flex;
flex-direction: column;
padding: 16px 12px;
z-index: 10;
}
.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 8px 18px;
border-bottom: 1px solid var(--border);
margin-bottom: 16px;
}
.brand-icon {
width: 28px;
height: 28px;
border-radius: var(--radius-sm);
background: linear-gradient(135deg, var(--amber-400), var(--amber-500));
display: flex;
align-items: center;
justify-content: center;
font-weight: 900;
color: #000;
font-size: 15px;
}
.nav-group-label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
color: var(--text-dim);
padding: 8px 10px 4px;
letter-spacing: 0.5px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: var(--radius-sm);
color: var(--text-muted);
cursor: pointer;
font-size: 12.5px;
font-weight: 500;
transition: background-color var(--transition-fast), color var(--transition-fast), border-left-color var(--transition-fast);
margin-bottom: 2px;
border-left: 2.5px solid transparent;
}
.nav-item:hover {
background: var(--surface-hover);
color: var(--text-main);
}
.nav-item.active {
background: var(--surface-card);
color: var(--amber-400);
font-weight: 600;
border-left-color: var(--amber-400);
}
.nav-item svg {
width: 16px;
height: 16px;
transition: transform var(--transition-fast);
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
height: 100vh;
overflow: hidden;
position: relative;
z-index: 1;
}
.top-bar {
height: 52px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
background: rgba(19, 21, 26, 0.7);
backdrop-filter: blur(8px);
}
.content-viewport {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.cap-item {
padding: 9px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
margin-bottom: 2px;
transition: background-color var(--transition-fast), border-left-color var(--transition-fast);
border-left: 2px solid transparent;
}
.cap-item:hover {
background: var(--surface-hover);
}
.cap-item.active {
background: var(--surface-card);
border-left-color: var(--amber-400);
}
.feed-row {
display: grid;
padding: 8px 14px;
border-bottom: 1px solid rgba(255,255,255,0.03);
align-items: center;
font-size: 11.5px;
transition: background-color var(--transition-fast);
}
.feed-row:hover {
background: var(--surface-hover);
}
</style>
</head>
<body>
<div class="ambient-glow"></div>
<!-- SIDEBAR -->
<aside class="sidebar">
<div class="brand">
<div class="brand-icon">W</div>
<div>
<div style="font-weight: 800; font-size: 13px; letter-spacing: 0.5px;">WARMPLANE</div>
<div style="font-size: 10px; color: var(--text-dim);">CONTROL DECK</div>
</div>
<span class="brand-badge" style="margin-left: auto;">v0.25.1</span>
</div>
<div class="nav-group-label">Mission Control</div>
<div class="nav-item active" data-tab="overview" onclick="window.app.switchTab('overview')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect><rect x="14" y="14" width="7" height="7"></rect><rect x="3" y="14" width="7" height="7"></rect></svg>
Overview Cockpit
</div>
<div class="nav-item" data-tab="servers" onclick="window.app.switchTab('servers')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="8" rx="2"></rect><rect x="2" y="14" width="20" height="8" rx="2"></rect><line x1="6" y1="6" x2="6.01" y2="6"></line><line x1="6" y1="18" x2="6.01" y2="18"></line></svg>
Server Hub
</div>
<div class="nav-item" data-tab="playground" onclick="window.app.switchTab('playground')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
MCP Playground
</div>
<div class="nav-item" data-tab="tasks" onclick="window.app.switchTab('tasks')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path><path d="m9 12 2 2 4-4"></path></svg>
<span>Tasks & Approvals</span>
<span id="nav-approvals-badge" class="brand-badge" style="margin-left: auto; display: none; background: rgba(245, 158, 11, 0.2); color: var(--amber-400); border-color: rgba(245, 158, 11, 0.4); font-size: 10px; padding: 1px 6px;"></span>
</div>
<div class="nav-group-label" style="margin-top: 14px;">Governance</div>
<div class="nav-item" data-tab="audit" onclick="window.app.switchTab('audit')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path><polyline points="14 2 14 8 20 8"></polyline><line x1="16" y1="13" x2="8" y2="13"></line><line x1="16" y1="17" x2="8" y2="17"></line><polyline points="10 9 9 9 8 9"></polyline></svg>
WORM Audit Trail
</div>
<div class="nav-item" data-tab="policy" onclick="window.app.switchTab('policy')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
Policy & Redaction
</div>
<div class="nav-item" data-tab="aliases" onclick="window.app.switchTab('aliases')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>
Alias Studio
</div>
<div class="nav-item" data-tab="profiles" onclick="window.app.switchTab('profiles')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"></polygon></svg>
Profiles Studio
</div>
<div style="margin-top: auto; padding: 10px; background: var(--surface-card); border-radius: var(--radius-sm); border: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-size: 11px;">
<span style="display: flex; align-items: center; gap: 6px;">
<span style="width: 7px; height: 7px; border-radius: 50%; background: var(--green-400);"></span>
<span id="daemon-port-label">Daemon</span>
</span>
<span style="color: var(--green-400); font-weight: 700;">WARM</span>
</div>
</aside>
<!-- MAIN AREA -->
<main class="main-wrapper">
<header class="top-bar">
<div style="display: flex; align-items: center; gap: 14px;">
<div style="font-size: 16px; font-weight: 700; color: var(--text-main);" id="top-title">Overview Cockpit</div>
<div style="display: flex; align-items: center; gap: 6px; background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 2px 8px;">
<span style="font-size: 11px; color: var(--text-dim); font-weight: 600;">PROFILE:</span>
<select id="top-profile-selector" class="form-input" style="height: 24px; padding: 0 6px; font-size: 11.5px; border: none; background: transparent; color: var(--amber-400); font-weight: 600; cursor: pointer;" onchange="window.app.setActiveProfile(this.value || null)">
<option value="">All Servers (Unrestricted)</option>
</select>
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost" onclick="window.app.openImportModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
Import Config
</button>
<button class="btn btn-ghost" onclick="window.app.openAddServerModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
Add Custom
</button>
<button class="btn btn-primary" onclick="window.app.openTemplateCatalog()">
✨ Browse Templates
</button>
</div>
</header>
<div class="content-viewport" id="app-main">
<!-- Dynamic workspace content rendered here -->
</div>
</main>
<!-- MODAL: ADD / EDIT SERVER -->
<div class="modal-backdrop" id="modal-add-server" onclick="if(event.target === this) window.app.closeModals()">
<div class="modal-box">
<div class="modal-header">
<div class="modal-title" id="modal-srv-title">Add Upstream MCP Server</div>
<button class="btn btn-ghost" onclick="window.app.closeModals()" style="padding: 2px 6px;">✕</button>
</div>
<div id="modal-srv-template-banner" style="background: var(--surface-card); border: 1px dashed var(--border); border-radius: var(--radius-sm); padding: 10px 14px; margin-bottom: 14px; display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 11.5px; color: var(--text-muted);">Looking for popular pre-configured servers (Postgres, GitHub, Playwright, etc.)?</div>
<button class="btn btn-primary" style="font-size: 11px; padding: 3px 8px;" onclick="window.app.openTemplateCatalog()">Browse 25 Templates →</button>
</div>
<div class="form-group">
<label class="form-label">Server Identifier</label>
<input type="text" class="form-input" id="modal-srv-name" placeholder="e.g. postgres, weather, linear">
</div>
<div class="form-group">
<label class="form-label">Transport Type</label>
<select class="form-input" id="modal-srv-transport" onchange="document.getElementById('modal-group-cmd').style.display = this.value === 'stdio' ? 'block' : 'none'; document.getElementById('modal-group-url').style.display = this.value === 'http' ? 'block' : 'none';">
<option value="stdio">Stdio (Local Process / CLI)</option>
<option value="http">HTTP / SSE (Remote Endpoint)</option>
</select>
</div>
<div class="form-group" id="modal-group-cmd">
<label class="form-label">Executable Command & Args</label>
<input type="text" class="form-input" id="modal-srv-command" placeholder="npx -y @modelcontextprotocol/server-postgres postgresql://...">
</div>
<div class="form-group" id="modal-group-url" style="display: none;">
<label class="form-label">Remote Endpoint URL</label>
<input type="text" class="form-input" id="modal-srv-url" placeholder="https://api.example.com/sse">
</div>
<details style="margin-top: 14px; background: rgba(0,0,0,0.2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px;">
<summary style="font-size: 11.5px; font-weight: 600; color: var(--amber-400); cursor: pointer;">
🛡️ Fault Tolerance & Process Supervision (Optional)
</summary>
<div style="margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div>
<label class="form-label" style="font-size: 10.5px;">Failure Threshold</label>
<input type="number" class="form-input" id="modal-srv-ft" placeholder="3" value="3">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Cooldown (ms)</label>
<input type="number" class="form-input" id="modal-srv-cd" placeholder="30000" value="30000">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Auto-Restart</label>
<select class="form-input" id="modal-srv-autorestart">
<option value="true">Enabled (Default)</option>
<option value="false">Disabled</option>
</select>
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Max Restarts</label>
<input type="number" class="form-input" id="modal-srv-maxrestarts" placeholder="5" value="5">
</div>
</div>
</details>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px;">
<button class="btn btn-ghost" onclick="window.app.closeModals()">Cancel</button>
<button class="btn btn-primary" onclick="window.app.submitAddServer()">Save & Connect</button>
</div>
</div>
</div>
<!-- MODAL: IMPORT CONFIG -->
<div class="modal-backdrop" id="modal-import" onclick="if(event.target === this) window.app.closeModals()">
<div class="modal-box" style="width: 560px;">
<div class="modal-header">
<div class="modal-title">Import Existing MCP Configurations</div>
<button class="btn btn-ghost" onclick="window.app.closeModals()" style="padding: 2px 6px;">✕</button>
</div>
<p style="color: var(--text-dim); font-size: 12px; margin-bottom: 14px;">
Discovered standard MCP configurations from your IDE ecosystem:
</p>
<div style="display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px;" id="modal-eco-list">
<!-- Ecosystem sources populated here -->
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px;">
<button class="btn btn-ghost" onclick="window.app.closeModals()">Cancel</button>
<button class="btn btn-primary" onclick="window.app.submitImport()">Import Discovered Servers</button>
</div>
</div>
</div>
<!-- MODAL: TEMPLATE CATALOG -->
<div class="modal-backdrop" id="modal-templates" onclick="if(event.target === this) window.app.closeModals()">
<div class="modal-box" style="width: 880px; max-width: 95vw; max-height: 88vh; display: flex; flex-direction: column;">
<div class="modal-header" style="margin-bottom: 12px;">
<div>
<div class="modal-title" style="display: flex; align-items: center; gap: 8px;">
<span>✨ MCP Server Template Catalog</span>
<span class="brand-badge">25 Curated Servers</span>
</div>
<div style="font-size: 11.5px; color: var(--text-dim); margin-top: 2px;">
1-Click configurations for the most popular tools, browsers, databases, and services in the MCP ecosystem.
</div>
</div>
<button class="btn btn-ghost" onclick="window.app.closeModals()" style="padding: 2px 6px;">✕</button>
</div>
<!-- Filters & Search -->
<div style="display: flex; gap: 10px; align-items: center; margin-bottom: 14px;">
<input type="text" class="form-input" id="tmpl-search-input" placeholder="Search templates (e.g. postgres, github, playwright, redis)..." oninput="window.app.filterTemplates(this.value)" style="flex: 1; min-width: 0;">
<div style="display: flex; gap: 4px; flex-shrink: 0;" id="tmpl-category-tabs">
<button class="btn btn-ghost tmpl-cat-btn active" data-category="all" onclick="window.app.setTemplateCategory('all')">All</button>
<button class="btn btn-ghost tmpl-cat-btn" data-category="devtools" onclick="window.app.setTemplateCategory('devtools')">DevTools</button>
<button class="btn btn-ghost tmpl-cat-btn" data-category="browser" onclick="window.app.setTemplateCategory('browser')">Browser & Search</button>
<button class="btn btn-ghost tmpl-cat-btn" data-category="database" onclick="window.app.setTemplateCategory('database')">Databases</button>
<button class="btn btn-ghost tmpl-cat-btn" data-category="productivity" onclick="window.app.setTemplateCategory('productivity')">Productivity</button>
<button class="btn btn-ghost tmpl-cat-btn" data-category="cloud" onclick="window.app.setTemplateCategory('cloud')">Cloud & Infra</button>
</div>
</div>
<!-- Template Grid List -->
<div style="flex: 1; overflow-y: auto; overflow-x: hidden; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; padding-right: 4px;" id="tmpl-grid">
<!-- Rendered dynamically -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 14px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 11px; color: var(--text-dim);">
<div>Need a custom MCP command or SSE URL?</div>
<button class="btn btn-ghost" onclick="window.app.openAddServerModal()">+ Custom Manual Server</button>
</div>
</div>
</div>
<!-- MODAL: CONFIGURE TEMPLATE -->
<div class="modal-backdrop" id="modal-configure-template" onclick="if(event.target === this) window.app.closeModals()">
<div class="modal-box" style="width: 580px;">
<div class="modal-header">
<div>
<div class="modal-title" id="cfg-tmpl-title">Configure Server Template</div>
<div style="font-size: 11px; color: var(--text-dim);" id="cfg-tmpl-desc">Fill in required credentials and launch.</div>
</div>
<button class="btn btn-ghost" onclick="window.app.closeModals()" style="padding: 2px 6px;">✕</button>
</div>
<div id="cfg-tmpl-form">
<!-- Dynamic fields rendered here -->
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px;">
<button class="btn btn-ghost" onclick="window.app.openTemplateCatalog()">← Back to Catalog</button>
<button class="btn btn-primary" onclick="window.app.submitTemplateServer()">Save & Connect Server</button>
</div>
</div>
</div>
<!-- MODAL: ADD / EDIT PROFILE -->
<div class="modal-backdrop" id="modal-add-profile" onclick="if(event.target === this) window.app.closeModals()">
<div class="modal-box" style="width: 520px;">
<div class="modal-header">
<div class="modal-title" id="modal-prof-title">Create Server Constellation Profile</div>
<button class="btn btn-ghost" onclick="window.app.closeModals()" style="padding: 2px 6px;">✕</button>
</div>
<input type="hidden" id="modal-prof-mode" value="create">
<div class="form-group">
<label class="form-label">Profile Identifier (e.g. coding, support, data)</label>
<input type="text" class="form-input" id="modal-prof-name" placeholder="e.g. coding">
</div>
<div class="form-group">
<label class="form-label">Description (Optional)</label>
<input type="text" class="form-input" id="modal-prof-desc" placeholder="e.g. Software engineering and database inspection tools">
</div>
<div class="form-group">
<label class="form-label">Included MCP Upstream Servers</label>
<div id="modal-prof-servers-list" style="max-height: 180px; overflow-y: auto; background: var(--surface-elevated); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px; display: flex; flex-direction: column; gap: 4px;">
<!-- Rendered dynamically -->
</div>
</div>
<div style="display: flex; justify-content: flex-end; gap: 8px; margin-top: 20px;">
<button class="btn btn-ghost" onclick="window.app.closeModals()">Cancel</button>
<button class="btn btn-primary" onclick="window.app.saveProfile()">Save Profile</button>
</div>
</div>
</div>
<script>
class F{state={configPath:"mcp_servers.json",config:{mcpServers:{}},serverStatuses:{},circuitBreakers:[],capabilities:[],resources:[],prompts:[],catalogEvents:[],tasks:[],selectedTaskId:null,taskFilterStatus:"all",approvals:[],auditEvents:[],auditTotal:0,auditFilters:{search:"",status:"all",eventType:"all",serverId:"all",limit:25,offset:0},auditSelectedEvent:null,auditStats:null,auditVerification:null,selectedCapabilityId:null,selectedResourceId:null,selectedPromptId:null,playgroundMode:"tools",playgroundArgs:{},isExecuting:!1,playgroundAsyncTask:!1,activeRequestId:null,isBatchModalOpen:!1,batchSteps:[{id:"step_1",capability_id:"",argsJson:"{}",continue_on_error:!1},{id:"step_2",capability_id:"",argsJson:"{}",continue_on_error:!0}],activeTab:"overview",activeProfile:null,eventLogs:[],executionResult:null,resourceReadResult:null,promptGetResult:null,metrics:{totalCatalogRequests:0,totalEtagHits:0,totalToolCalls:0,totalToolDurationUs:0}};listeners=[];getState(){return this.state}setState(e){this.state={...this.state,...e},this.listeners.forEach((t)=>t(this.state))}subscribe(e){return this.listeners.push(e),()=>{this.listeners=this.listeners.filter((t)=>t!==e)}}addEventLog(e,t,a,i){let s=[{time:new Date().toLocaleTimeString(),method:e,target:t,status:a,latency:i},...this.state.eventLogs].slice(0,50);this.setState({eventLogs:s})}}var l=new F;class D{baseUrl;constructor(e=""){this.baseUrl=e}async getConfig(){return(await fetch(`${this.baseUrl}/v1/config`)).json()}async listCapabilities(e){let t={};if(e)t["X-Warmplane-Profile"]=e;return(await fetch(`${this.baseUrl}/v1/capabilities`,{headers:t})).json()}async listResources(e){let t={};if(e)t["X-Warmplane-Profile"]=e;return(await fetch(`${this.baseUrl}/v1/resources`,{headers:t})).json()}async readResource(e,t){let a=performance.now(),i={"Content-Type":"application/json"};if(t)i["X-Warmplane-Profile"]=t;let r=await fetch(`${this.baseUrl}/v1/resources/read`,{method:"POST",headers:i,body:JSON.stringify(e)}),s=performance.now()-a,n=await r.json();return{status:r.status,durationMs:s,data:n}}async listPrompts(e){let t={};if(e)t["X-Warmplane-Profile"]=e;return(await fetch(`${this.baseUrl}/v1/prompts`,{headers:t})).json()}async getPrompt(e,t){let a=performance.now(),i={"Content-Type":"application/json"};if(t)i["X-Warmplane-Profile"]=t;let r=await fetch(`${this.baseUrl}/v1/prompts/get`,{method:"POST",headers:i,body:JSON.stringify(e)}),s=performance.now()-a,n=await r.json();return{status:r.status,durationMs:s,data:n}}async getCatalogEvents(e){let t=e?`?after=${encodeURIComponent(e)}`:"";return(await fetch(`${this.baseUrl}/v1/catalog/events${t}`)).json()}async callCapability(e,t){let a=performance.now(),i={"Content-Type":"application/json"};if(t)i["X-Warmplane-Profile"]=t;let r=await fetch(`${this.baseUrl}/v1/tools/call`,{method:"POST",headers:i,body:JSON.stringify(e)}),s=performance.now()-a,n=await r.json();return{status:r.status,durationMs:s,data:n}}async batchCallCapabilities(e,t){let a=performance.now(),i={"Content-Type":"application/json"};if(t)i["X-Warmplane-Profile"]=t;let r=await fetch(`${this.baseUrl}/v1/tools/batch_call`,{method:"POST",headers:i,body:JSON.stringify({steps:e})}),s=performance.now()-a,n=await r.json();return{status:r.status,durationMs:s,data:n}}async cancelOperation(e){return(await fetch(`${this.baseUrl}/v1/operations/${encodeURIComponent(e)}/cancel`,{method:"POST"})).json()}async completeArgument(e){return(await fetch(`${this.baseUrl}/v1/completion/complete`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(e)})).json()}async upsertServer(e,t){return(await fetch(`${this.baseUrl}/v1/config/servers`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,server:t})})).json()}async deleteServer(e){return(await fetch(`${this.baseUrl}/v1/config/servers/${encodeURIComponent(e)}`,{method:"DELETE"})).json()}async upsertProfile(e,t,a){return(await fetch(`${this.baseUrl}/v1/config/profiles`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:e,servers:t,description:a})})).json()}async deleteProfile(e){return(await fetch(`${this.baseUrl}/v1/config/profiles/${encodeURIComponent(e)}`,{method:"DELETE"})).json()}async getEcosystemSources(){return(await fetch(`${this.baseUrl}/v1/config/ecosystem`)).json()}async importConfig(e,t=!1){return(await fetch(`${this.baseUrl}/v1/config/import`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source_path:e,overwrite:t})})).json()}async savePolicy(e){let t={allow:e.allow||[],deny:e.deny||[],redactKeys:e.redact_keys||e.redactKeys||[],requireApproval:e.require_approval||e.requireApproval||[],approvalTimeoutSecs:e.approvalTimeoutSecs||e.approval_timeout_secs||300,webhook:e.webhook};return(await fetch(`${this.baseUrl}/v1/config/policy`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)})).json()}async listTasks(){return(await fetch(`${this.baseUrl}/v1/tasks`)).json()}async getTask(e){return(await fetch(`${this.baseUrl}/v1/tasks/${encodeURIComponent(e)}`)).json()}async updateTask(e,t){return(await fetch(`${this.baseUrl}/v1/tasks/${encodeURIComponent(e)}/update`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({inputResponses:t})})).json()}async cancelTask(e,t){return(await fetch(`${this.baseUrl}/v1/tasks/${encodeURIComponent(e)}/cancel`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({reason:t})})).json()}async listApprovals(){return(await fetch(`${this.baseUrl}/v1/approvals`)).json()}async approveTicket(e,t,a){return(await fetch(`${this.baseUrl}/v1/approvals/${encodeURIComponent(e)}/approve`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({operator:t,modified_args:a})})).json()}async rejectTicket(e,t,a){return(await fetch(`${this.baseUrl}/v1/approvals/${encodeURIComponent(e)}/reject`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({operator:t,reason:a})})).json()}async updateAlias(e,t,a){return(await fetch(`${this.baseUrl}/v1/config/alias`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({kind:e,alias:t,target:a})})).json()}async reloadConfig(){return(await fetch(`${this.baseUrl}/v1/config/reload`,{method:"POST"})).json()}async listAuditEvents(e){let t=new URLSearchParams;if(e?.actor_id)t.set("actor_id",e.actor_id);if(e?.server_id&&e.server_id!=="all")t.set("server_id",e.server_id);if(e?.capability_id)t.set("capability_id",e.capability_id);if(e?.event_type&&e.event_type!=="all")t.set("event_type",e.event_type);if(e?.status&&e.status!=="all")t.set("status",e.status);if(e?.trace_id)t.set("trace_id",e.trace_id);if(e?.request_id)t.set("request_id",e.request_id);if(e?.search)t.set("search",e.search);if(e?.limit)t.set("limit",String(e.limit));if(e?.offset!==void 0)t.set("offset",String(e.offset));let a=t.toString();return(await fetch(`${this.baseUrl}/v1/audit/events${a?`?${a}`:""}`)).json()}getAuditExportUrl(e,t="csv"){let a=new URLSearchParams;if(a.set("format",t),e?.actor_id)a.set("actor_id",e.actor_id);if(e?.server_id&&e.server_id!=="all")a.set("server_id",e.server_id);if(e?.capability_id)a.set("capability_id",e.capability_id);if(e?.event_type&&e.event_type!=="all")a.set("event_type",e.event_type);if(e?.status&&e.status!=="all")a.set("status",e.status);if(e?.trace_id)a.set("trace_id",e.trace_id);if(e?.request_id)a.set("request_id",e.request_id);if(e?.search)a.set("search",e.search);return`${this.baseUrl}/v1/audit/export?${a.toString()}`}async verifyAuditChain(){return(await fetch(`${this.baseUrl}/v1/audit/verify`)).json()}async getAuditStats(){return(await fetch(`${this.baseUrl}/v1/audit/stats`)).json()}}var f=new D;function U(){let e=l.getState(),t=e.config.mcpServers||{},a=Object.keys(t),i=a.length,r="";if(a.length===0)r=`
<div style="grid-column: 1 / -1; padding: 32px; text-align: center; color: var(--text-dim); background: var(--surface-card); border-radius: var(--radius-md); border: 1px dashed var(--border);">
<div style="font-size: 14px; color: var(--text-main); font-weight: 600; margin-bottom: 6px;">No Upstream MCP Servers Connected</div>
<div style="font-size: 12px; margin-bottom: 16px;">Initialize connections by adding a server or syncing existing IDE configurations.</div>
<div style="display: flex; gap: 8px; justify-content: center;">
<button class="btn btn-primary" onclick="window.app.openTemplateCatalog()">✨ Browse Templates</button>
<button class="btn btn-ghost" onclick="window.app.openAddServerModal()">+ Add Custom</button>
<button class="btn btn-ghost" onclick="window.app.openImportModal()">Sync from IDEs</button>
</div>
</div>
`;else r=a.map((x)=>{let C=t[x],b=C.command?"stdio":"http / sse",L=C.command?`${C.command} ${(C.args||[]).join(" ")}`:C.url,I=e.serverStatuses[x]||{status:"connected",protocol_version:"2026-07-28"},H=I.status==="degraded",q=I.status==="error"||I.status==="disconnected",j=H?"var(--amber-400)":q?"var(--red-400)":"var(--green-400)";return`
<div class="bento-card col-4" style="background: var(--surface); border: 1px solid var(--border);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 6px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: ${j}; display: inline-block;"></span>
${P(x)}
</span>
<span class="brand-badge">${b}</span>
</div>
<div style="font-family: var(--ff-mono); font-size: 11.5px; color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-bottom: 12px;" title="${P(L||"")}">
${P(L||"")}
</div>
<div style="display: flex; justify-content: space-between; font-size: 11px; color: var(--text-muted); border-top: 1px solid var(--border); padding-top: 8px;">
<span>Status: <strong style="color: ${j};">${P(I.status)}</strong></span>
<span>Protocol: ${I.protocol_version}</span>
</div>
</div>
`}).join("");let s=e.eventLogs.length===0?`
<div class="feed-row" style="grid-template-columns: 80px 100px 1fr 100px 80px;">
<span style="color: var(--text-dim);">ready</span>
<span style="color: var(--cyan-400); font-weight: 600;">SSE</span>
<span style="color: var(--text-main);">/v1/resources/updates stream active</span>
<span style="color: var(--green-400);">CONNECTED</span>
<span style="color: var(--amber-300); text-align: right;">0.0ms</span>
</div>
`:e.eventLogs.map((x)=>`
<div class="feed-row" style="grid-template-columns: 80px 100px 1fr 100px 80px;">
<span style="color: var(--text-dim);">${P(x.time)}</span>
<span style="color: var(--cyan-400); font-weight: 600;">${P(x.method)}</span>
<span style="color: var(--text-main); font-family: var(--ff-mono);">${P(x.target)}</span>
<span style="color: var(--green-400);">${P(x.status)}</span>
<span style="color: var(--amber-300); text-align: right;">${P(x.latency)}</span>
</div>
`).join(""),n=e.metrics,o=n.totalCatalogRequests,d=n.totalEtagHits,p=o>0?`${(d/o*100).toFixed(1)}%`:"0.0%",u=o>0?`${d} of ${o} requests served via HTTP 304`:"Waiting for client requests",g=n.totalToolCalls,m=g>0?`${(n.totalToolDurationUs/g/1000).toFixed(1)}ms`:"0.0ms",c=g>0?`${g} tool executions processed`:"Local worker task queues warm",h=Object.keys(e.config.capabilityAliases||{}).length+Object.keys(e.config.resourceAliases||{}).length+Object.keys(e.config.promptAliases||{}).length,k=h>0?`${h*18}B / call`:"0B",T=h>0?`${h} active facade aliases pruning prompt size`:"Configure aliases in Studio to reduce prompt size",E=e.tasks||[],_=E.filter((x)=>x.status==="input_required").length,A=E.filter((x)=>x.status==="working"||x.status==="input_required").length;return`
<div class="bento-grid">
<div class="bento-card col-3">
<div class="stat-label">Token Savings Rate</div>
<div class="stat-value" style="color: var(--amber-300);">${k}</div>
<div class="stat-sub">${T}</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">ETag Cache Hit Rate</div>
<div class="stat-value" style="color: var(--cyan-400);">${p}</div>
<div class="stat-sub">${u}</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">Tasks & HITL State</div>
<div class="stat-value" style="color: ${_>0?"var(--amber-400)":"var(--green-400)"};">${_>0?`${_} Action Req`:`${A} Active`}</div>
<div class="stat-sub">${_>0?"Awaiting Human-in-the-Loop decision":`${E.length} total registered tasks`}</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">Connected Upstreams</div>
<div class="stat-value" style="color: var(--green-400);">${i} Active</div>
<div class="stat-sub">${i>0?"Persistent worker task channels":"No active upstream servers"}</div>
</div>
</div>
<div style="display: flex; justify-content: space-between; align-items: center; margin: 24px 0 12px;">
<div style="font-size: 15px; font-weight: 700; color: var(--text-main);">Connected Upstream Servers</div>
<button class="btn btn-ghost" onclick="window.app.switchTab('servers')">Manage All (${i}) →</button>
</div>
<div class="bento-grid" style="margin-bottom: 24px;">
${r}
</div>
<div style="font-size: 15px; font-weight: 700; color: var(--text-main); margin-bottom: 12px;">
Live Control Plane Event Stream
</div>
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; font-family: var(--ff-mono); font-size: 11.5px;">
<div style="display: grid; grid-template-columns: 80px 100px 1fr 100px 80px; padding: 8px 14px; background: var(--surface-hover); border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 600;">
<span>TIME</span>
<span>METHOD</span>
<span>EVENT / TARGET</span>
<span>STATUS</span>
<span style="text-align: right;">LATENCY</span>
</div>
<div id="overview-event-rows">
${s}
</div>
</div>
`}function P(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function J(){let e=l.getState(),t=e.config.mcpServers||{},a=Object.keys(t),i="";if(a.length===0)i=`
<div style="padding: 40px; text-align: center; color: var(--text-dim); background: var(--surface-card); border-radius: var(--radius-md); border: 1px dashed var(--border);">
<div style="font-size: 15px; color: var(--text-main); font-weight: 600; margin-bottom: 8px;">No Servers Configured in ${z(e.configPath)}</div>
<p style="font-size: 12px; margin-bottom: 20px; max-width: 480px; margin-left: auto; margin-right: auto;">
Warmplane bridges local tools and remote MCP servers into one unified facade. Add your first server or import existing configs from Claude Desktop or Cursor.
</p>
<div style="display: flex; gap: 8px; justify-content: center;">
<button class="btn btn-primary" onclick="window.app.openTemplateCatalog()">✨ Browse Templates</button>
<button class="btn btn-ghost" onclick="window.app.openAddServerModal()">+ Add Custom</button>
<button class="btn btn-ghost" onclick="window.app.openImportModal()">Sync from IDEs</button>
</div>
</div>
`;else i=a.map((r)=>{let s=t[r],n=s.command?"stdio":"http / sse",o=s.command?`${s.command} ${(s.args||[]).join(" ")}`:s.url,d=e.serverStatuses[r]||{status:"connected",protocol_version:"2026-07-28"},p=s.env?Object.keys(s.env).map((E)=>`${E}=***`).join(", "):"None",u=(e.circuitBreakers||[]).find((E)=>E.server_id===r),g='<span class="brand-badge" style="color: var(--green-400); border-color: rgba(52, 211, 153, 0.25);">Circuit: CLOSED</span>';if(u){if(u.state==="open")g=`<span class="brand-badge" style="color: var(--red-400); border-color: rgba(248, 113, 113, 0.4); background: rgba(248, 113, 113, 0.1);">Circuit: OPEN (${u.consecutive_failures} failures)</span>`;else if(u.state==="half_open")g=`<span class="brand-badge" style="color: var(--amber-300); border-color: rgba(251, 191, 36, 0.4); background: rgba(251, 191, 36, 0.1);">Circuit: HALF-OPEN (${u.consecutive_successes} probe)</span>`}let m=s.resilience||e.config.resilience,c=m?`FT: ${m.failureThreshold||3} · Cooldown: ${(m.cooldownMs||30000)/1000}s · AutoRestart: ${m.autoRestart!==!1?"ON":"OFF"}`:"Default Resilience",h=d.status==="degraded",k=d.status==="error"||d.status==="disconnected",T=h?"var(--amber-400)":k?"var(--red-400)":"var(--green-400)";return`
<div class="bento-card" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<span style="width: 8px; height: 8px; border-radius: 50%; background: ${T}; display: inline-block;"></span>
<span style="font-size: 15px; font-weight: 700; color: var(--text-main);">${z(r)}</span>
<span class="brand-badge">${n}</span>
<span class="brand-badge" style="color: ${T}; border-color: rgba(245, 158, 11, 0.3);">Status: ${z(d.status)}</span>
<span class="brand-badge" style="color: var(--cyan-400); border-color: rgba(34, 211, 238, 0.25);">Protocol: ${d.protocol_version}</span>
${g}
</div>
<div style="font-family: var(--ff-mono); font-size: 12px; color: var(--text-muted); margin-top: 4px;">
${s.command?"Command: ":"URL: "}<code>${z(o||"")}</code>
</div>
<div style="display: flex; gap: 14px; font-family: var(--ff-mono); font-size: 11px; color: var(--text-dim); margin-top: 4px;">
<span>\uD83D\uDEE1️ ${z(c)}</span>
${s.env&&Object.keys(s.env).length>0?`<span>Env: ${z(p)}</span>`:""}
</div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.openEditServerModal('${z(r)}')">✏️ Edit</button>
<button class="btn btn-danger" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.deleteServer('${z(r)}')">Remove</button>
</div>
</div>
`}).join("");return`
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px;">
<div>
<div style="font-size: 16px; font-weight: 700; color: var(--text-main);">Configured MCP Upstream Servers</div>
<div style="font-size: 11px; color: var(--text-dim);">Active configuration file: <code>${z(e.configPath)}</code></div>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost" onclick="window.app.reloadFromDisk()">⟳ Reload Config</button>
</div>
</div>
${i}
`}function z(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function K(){let e=l.getState(),t=e.playgroundMode||"tools",a=e.capabilities||[],i=e.resources||[],r=e.prompts||[],s=`
<div style="display: flex; gap: 8px; margin-bottom: 12px; align-items: center; justify-content: space-between;">
<div style="display: inline-flex; padding: 3px; background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: var(--radius-sm);">
<button
class="btn ${t==="tools"?"btn-primary":"btn-ghost"}"
style="padding: 4px 12px; font-size: 11.5px; height: 28px;"
onclick="window.app.setPlaygroundMode('tools')"
>
\uD83D\uDEE0️ Tools (${a.length})
</button>
<button
class="btn ${t==="resources"?"btn-primary":"btn-ghost"}"
style="padding: 4px 12px; font-size: 11.5px; height: 28px;"
onclick="window.app.setPlaygroundMode('resources')"
>
\uD83D\uDCC4 Resources (${i.length})
</button>
<button
class="btn ${t==="prompts"?"btn-primary":"btn-ghost"}"
style="padding: 4px 12px; font-size: 11.5px; height: 28px;"
onclick="window.app.setPlaygroundMode('prompts')"
>
\uD83D\uDCAC Prompts (${r.length})
</button>
</div>
<div style="font-size: 11.5px; color: var(--text-dim);">
${t==="tools"?"Interactive Tool Caller & Context Distillation":t==="resources"?"Live MCP Resource Inspector & Reader":"Prompt Template Studio & Variable Binder"}
</div>
</div>
`;if(t==="resources")return`
${s}
${re(e)}
`;if(t==="prompts")return`
${s}
${ae(e)}
`;return`
${s}
${se(e)}
${e.isBatchModalOpen?ie(e):""}
`}function O(e,t=!1){if(!e||!e.properties)return{};let a=e.properties||{},i=Array.isArray(e.required)?e.required:[],r={};for(let[s,n]of Object.entries(a)){let o=i.includes(s);if(t&&!o)continue;if(n.default!==void 0)r[s]=n.default;else if(Array.isArray(n.enum)&&n.enum.length>0)r[s]=n.enum[0];else if(n.examples&&Array.isArray(n.examples)&&n.examples.length>0)r[s]=n.examples[0];else if(n.example!==void 0)r[s]=n.example;else switch(n.type||"string"){case"string":r[s]=o?`sample_${s}`:"";break;case"number":case"integer":r[s]=0;break;case"boolean":r[s]=!0;break;case"array":r[s]=[];break;case"object":r[s]={};break;default:r[s]=`sample_${s}`}}return r}function se(e){let t=e.capabilities||[],a=e.selectedCapabilityId||(t.length>0?t[0].id:null),i=t.find((m)=>m.id===a),r=!!e.isExecuting,s="";if(t.length===0)s=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No tools or capabilities discovered from connected servers.
</div>
`;else s=t.map((m)=>`
<div class="cap-item ${m.id===a?"active":""}" onclick="window.app.selectCapability('${v(m.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${v(m.id)}</span>
<span style="font-size: 10px; color: var(--green-400);">${v(m.mode||"read")}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">server: ${v(m.server||"local")}</div>
</div>
`).join("");let n=i?.input_schema,o=n?.properties||{},d=Array.isArray(n?.required)?n.required:[],p=Object.entries(o),u="";if(p.length>0)u=`
<div style="display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 8px; align-items: center;">
<span style="font-size: 10px; font-weight: 700; color: var(--text-dim); text-transform: uppercase;">Fields:</span>
${p.map(([m,c])=>{let h=d.includes(m),k=c.type||(c.enum?"enum":"any"),T=h?"rgba(239, 68, 68, 0.15)":"rgba(148, 163, 184, 0.1)",E=h?"var(--red-400)":"var(--text-muted)",_=h?"rgba(239, 68, 68, 0.3)":"var(--border)",A=c.description?` - ${c.description}`:"";return`
<button
type="button"
class="btn"
style="padding: 2px 7px; font-size: 10.5px; font-family: var(--ff-mono); background: ${T}; color: ${E}; border: 1px solid ${_}; border-radius: var(--radius-sm);"
title="Click to insert '${m}' (${k}${A})"
onclick="window.app.insertPlaygroundArgKey('${v(m)}', '${v(k)}', ${v(JSON.stringify(c.default??null))})"
>
+ ${v(m)} <span style="font-size: 9px; opacity: 0.7;">(${k}${h?" *":""})</span>
</button>
`}).join("")}
</div>
`;let g="{}";if(a&&e.playgroundArgs&&e.playgroundArgs[a]!==void 0)g=e.playgroundArgs[a];else{let m=O(n,!1);g=JSON.stringify(m,null,2)}return`
<div style="display: grid; grid-template-columns: 320px 1fr; gap: 16px; height: calc(100vh - 165px);">
<!-- Left Sidebar: Capabilities Catalog -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
<input type="text" class="form-input" placeholder="Search ${t.length} capabilities..." oninput="window.app.filterCapabilities(this.value)">
</div>
<div style="flex: 1; overflow-y: auto; padding: 8px;" id="pg-cap-list">
${s}
</div>
</div>
<!-- Right Panel: Capability Execution & Envelope Visualizer -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 15px; font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);" id="pg-selected-title">
${v(i?i.id:"No Capability Selected")}
</div>
<div style="font-size: 11.5px; color: var(--text-dim);" id="pg-selected-desc">
${v(i?i.summary||i.description:"Connect servers to inspect and execute tools")}
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
${r?`
<div style="display: flex; gap: 8px; align-items: center;">
<span class="badge" style="background: rgba(234, 179, 8, 0.15); color: var(--amber-400); font-family: var(--ff-mono); font-size: 11px; padding: 4px 8px; display: inline-flex; align-items: center; gap: 6px;">
<span style="width: 6px; height: 6px; border-radius: 50%; background: var(--amber-400); display: inline-block;"></span>
EXECUTING...
</span>
<button class="btn btn-danger" style="background: rgba(239, 68, 68, 0.2); color: var(--red-400); border: 1px solid rgba(239, 68, 68, 0.4); padding: 5px 12px; font-size: 11.5px;" onclick="window.app.cancelActiveOperation()">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="4" y="4" width="16" height="16" rx="2"></rect></svg>
Cancel Operation
</button>
</div>
`:`
<button class="btn btn-primary" onclick="window.app.executePlaygroundTool()" ${i?"":"disabled"}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
Execute Capability
</button>
`}
</div>
</div>
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden;">
<!-- Request Builder -->
<div style="padding: 16px; border-right: 1px solid var(--border); overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<label class="form-label" style="margin: 0;">Arguments JSON</label>
<div style="display: flex; gap: 6px;">
<button type="button" class="btn btn-ghost" style="padding: 2px 7px; font-size: 10.5px;" title="Fill sample payload from schema" onclick="window.app.fillPlaygroundSampleArgs(false)">✨ Sample Template</button>
${d.length>0?`
<button type="button" class="btn btn-ghost" style="padding: 2px 7px; font-size: 10.5px;" title="Fill only required schema fields" onclick="window.app.fillPlaygroundSampleArgs(true)">\uD83E\uDDF9 Required Only</button>
`:""}
<button type="button" class="btn btn-ghost" style="padding: 2px 7px; font-size: 10.5px;" title="Format JSON" onclick="window.app.formatPlaygroundArgs()">\uD83D\uDCCB Format</button>
<button type="button" class="btn btn-ghost" style="padding: 2px 8px; font-size: 11px;" onclick="window.app.openBatchModal()">⚡ Pipeline Builder</button>
</div>
</div>
${u}
<textarea class="form-textarea" rows="7" id="pg-args-input" oninput="window.app.updatePlaygroundArgs(this.value)">${v(g)}</textarea>
<div style="margin-top: 12px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); border: 1px solid var(--border);">
<div style="font-size: 11px; font-weight: 700; color: var(--cyan-400); margin-bottom: 6px; display: flex; align-items: center; gap: 6px;">
<span>⚡ Context Distillation Filters</span>
</div>
<div class="form-group" style="margin-bottom: 6px;">
<label class="form-label" style="font-size: 10.5px;">JSONPath Filter (e.g. $.items[*].name)</label>
<input type="text" class="form-input" id="pg-jsonpath-input" placeholder="$.result">
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div>
<label class="form-label" style="font-size: 10.5px;">Max Lines</label>
<input type="number" class="form-input" id="pg-limit-lines-input" placeholder="e.g. 50">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Max Bytes</label>
<input type="number" class="form-input" id="pg-truncate-bytes-input" placeholder="e.g. 20480">
</div>
</div>
</div>
<div style="margin-top: 10px; display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: rgba(0,0,0,0.25); border-radius: var(--radius-sm); border: 1px solid var(--border);">
<div>
<div style="font-size: 11.5px; font-weight: 600; color: var(--amber-300); display: flex; align-items: center; gap: 6px;">
<span>⚡ Async Task Mode (SEP-2663)</span>
</div>
<div style="font-size: 10.5px; color: var(--text-dim);">Execute tool asynchronously returning HTTP 202 Accepted Task</div>
</div>
<label style="position: relative; display: inline-block; width: 36px; height: 20px; margin: 0; cursor: pointer;">
<input type="checkbox" id="pg-async-task-toggle" ${e.playgroundAsyncTask?"checked":""} onchange="window.app.togglePlaygroundAsyncTask(this.checked)" style="opacity: 0; width: 0; height: 0;">
<span style="position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: ${e.playgroundAsyncTask?"var(--amber-400)":"var(--border)"}; transition: .3s; border-radius: 20px;">
<span style="position: absolute; content: ''; height: 14px; width: 14px; left: ${e.playgroundAsyncTask?"19px":"3px"}; bottom: 3px; background-color: white; transition: .3s; border-radius: 50%;"></span>
</span>
</label>
</div>
<div class="form-group" style="margin-top: 10px;">
<label class="form-label">Request Context / Operation ID (Optional)</label>
<input type="text" class="form-input" id="pg-context-input" placeholder="e.g. op-dev-test-1">
</div>
${i&&i.input_schema?`
<div style="margin-top: 14px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label class="form-label" style="margin: 0;">Input JSON Schema</label>
<span style="font-size: 10px; color: var(--text-dim); font-family: var(--ff-mono);">${p.length} field${p.length===1?"":"s"} (${d.length} required)</span>
</div>
<pre style="background: var(--surface); padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--border); font-size: 11px; color: var(--text-muted); max-height: 140px; overflow-y: auto;">${v(JSON.stringify(i.input_schema,null,2))}</pre>
</div>
`:""}
</div>
<!-- Response Inspector -->
<div style="padding: 16px; background: var(--bg-app); display: flex; flex-direction: column; overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-dim);">
${e.executionResult&&(e.executionResult.status===202||e.executionResult.data?.resultType==="task")?"SEP-2663 TASK RESPONSE":"NORMALIZED EXECUTION ENVELOPE"}
</span>
<span id="pg-status-badge" style="font-size: 11px; font-weight: 600; color: ${e.executionResult?e.executionResult.status===200?"var(--green-400)":e.executionResult.status===202?"var(--amber-300)":"var(--red-400)":"var(--text-dim)"}; font-family: var(--ff-mono);">
${e.executionResult?`HTTP ${e.executionResult.status} · ${e.executionResult.durationMs.toFixed(1)}ms`:"READY"}
</span>
</div>
${e.executionResult&&(e.executionResult.status===202||e.executionResult.data?.resultType==="task")?`
<div style="margin-bottom: 12px; padding: 12px 14px; background: rgba(245, 158, 11, 0.1); border: 1px solid rgba(245, 158, 11, 0.35); border-radius: var(--radius-sm); display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="brand-badge" style="background: rgba(245, 158, 11, 0.2); color: var(--amber-300); border-color: rgba(245, 158, 11, 0.5);">
${v(e.executionResult.data?.task?.status||e.executionResult.data?.status||"TASK_CREATED").toUpperCase()}
</span>
<span style="font-family: var(--ff-mono); font-size: 12px; font-weight: 700; color: var(--text-main);">${v(e.executionResult.data?.task?.taskId||e.executionResult.data?.taskId||"")}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 4px;">
Execution suspended for Human-in-the-Loop approval or async resolution.
</div>
</div>
<button class="btn btn-primary" style="padding: 4px 10px; font-size: 11px;" onclick="window.app.switchTab('tasks')">
Go to Tasks & Approvals →
</button>
</div>
`:""}
<pre id="pg-response-json" style="flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; color: var(--amber-300); font-size: 11.5px; overflow-y: auto; margin: 0; white-space: pre-wrap; word-break: break-word;">${e.executionResult?v(JSON.stringify(e.executionResult.data,null,2)):"// Response envelope output will be formatted here"}</pre>
</div>
</div>
</div>
</div>
`}function re(e){let t=e.resources||[],a=e.selectedResourceId||(t.length>0?t[0].id:null),i=t.find((n)=>n.id===a),r=e.resourceReadResult,s="";if(t.length===0)s=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No resources exposed by connected MCP servers.
</div>
`;else s=t.map((n)=>{let o=n.id===a?"active":"",d=n.uri?n.uri.split(":")[0]:"res";return`
<div class="cap-item ${o}" onclick="window.app.selectResource('${v(n.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${v(n.name||n.id)}</span>
<span class="badge" style="font-size: 9.5px; background: rgba(56, 189, 248, 0.15); color: var(--cyan-400);">${v(d)}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${v(n.uri)}</div>
<div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-muted); margin-top: 4px;">
<span>server: ${v(n.server||"local")}</span>
<span>${v(n.mime_type||"text/plain")}</span>
</div>
</div>
`}).join("");return`
<div style="display: grid; grid-template-columns: 340px 1fr; gap: 16px; height: calc(100vh - 165px);">
<!-- Left Sidebar: Resources Catalog -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
<input type="text" class="form-input" placeholder="Search ${t.length} resources..." oninput="window.app.filterResources(this.value)">
</div>
<div style="flex: 1; overflow-y: auto; padding: 8px;" id="pg-res-list">
${s}
</div>
</div>
<!-- Right Panel: Resource Content Reader & Metadata Inspector -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 15px; font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">
${v(i?i.name||i.id:"No Resource Selected")}
</div>
<div style="font-size: 11.5px; color: var(--cyan-400); font-family: var(--ff-mono);">
${v(i?i.uri:"Select a resource from the list to read live content")}
</div>
</div>
<button class="btn btn-primary" onclick="window.app.executeReadResource()" ${i?"":"disabled"}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path></svg>
Read Resource Content
</button>
</div>
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden;">
<!-- Request / Distillation Parameters -->
<div style="padding: 16px; border-right: 1px solid var(--border); overflow-y: auto;">
${i?`
<div style="background: rgba(0,0,0,0.25); padding: 12px; border-radius: var(--radius-sm); border: 1px solid var(--border); margin-bottom: 14px;">
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; margin-bottom: 6px;">Resource Metadata</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; font-size: 11.5px;">
<div><span style="color: var(--text-muted);">Server:</span> <strong style="color: var(--text-main);">${v(i.server)}</strong></div>
<div><span style="color: var(--text-muted);">MIME Type:</span> <strong style="color: var(--text-main);">${v(i.mime_type||"text/plain")}</strong></div>
</div>
${i.description?`
<div style="margin-top: 8px; font-size: 11.5px; color: var(--text-dim); border-top: 1px solid rgba(255,255,255,0.05); padding-top: 6px;">
${v(i.description)}
</div>
`:""}
</div>
`:""}
<div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); border: 1px solid var(--border);">
<div style="font-size: 11px; font-weight: 700; color: var(--cyan-400); margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
<span>⚡ Context Distillation Options</span>
</div>
<div class="form-group" style="margin-bottom: 8px;">
<label class="form-label" style="font-size: 10.5px;">JSONPath Expression</label>
<input type="text" class="form-input" id="pg-res-jsonpath-input" placeholder="$.items[*].data">
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div>
<label class="form-label" style="font-size: 10.5px;">Max Lines</label>
<input type="number" class="form-input" id="pg-res-lines-input" placeholder="e.g. 100">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Max Bytes</label>
<input type="number" class="form-input" id="pg-res-bytes-input" placeholder="e.g. 32768">
</div>
</div>
</div>
</div>
<!-- Content Output Preview -->
<div style="padding: 16px; background: var(--bg-app); display: flex; flex-direction: column; overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-dim);">RESOURCE CONTENT ENVELOPE</span>
<span style="font-size: 11px; font-weight: 600; color: ${r?r.status===200?"var(--green-400)":"var(--red-400)":"var(--text-dim)"}; font-family: var(--ff-mono);">
${r?`HTTP ${r.status} · ${r.durationMs.toFixed(1)}ms`:"READY"}
</span>
</div>
<pre style="flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; color: var(--cyan-400); font-size: 11.5px; overflow-y: auto; margin: 0; white-space: pre-wrap; word-break: break-word;">${r?v(JSON.stringify(r.data,null,2)):'// Click "Read Resource Content" to inspect live payload'}</pre>
</div>
</div>
</div>
</div>
`}function ae(e){let t=e.prompts||[],a=e.selectedPromptId||(t.length>0?t[0].id:null),i=t.find((o)=>o.id===a),r=e.promptGetResult,s="";if(t.length===0)s=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No prompt templates registered by connected MCP servers.
</div>
`;else s=t.map((o)=>{let d=o.id===a?"active":"",p=o.arguments?o.arguments.length:0;return`
<div class="cap-item ${d}" onclick="window.app.selectPrompt('${v(o.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${v(o.name||o.id)}</span>
<span class="badge" style="font-size: 9.5px; background: rgba(168, 85, 247, 0.15); color: var(--purple-400);">${p} args</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">${v(o.description||o.title||"Prompt template")}</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">server: ${v(o.server||"local")}</div>
</div>
`}).join("");let n="";if(i&&i.arguments&&i.arguments.length>0)n=i.arguments.map((o)=>`
<div class="form-group" style="margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label class="form-label" style="margin: 0; font-family: var(--ff-mono);">${v(o.name)}</label>
${o.required?'<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: var(--red-400); font-size: 9px;">REQUIRED</span>':'<span style="font-size: 10px; color: var(--text-dim);">optional</span>'}
</div>
${o.description?`<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 4px;">${v(o.description)}</div>`:""}
<input type="text" class="form-input prompt-arg-input" data-arg-name="${v(o.name)}" placeholder="Enter ${v(o.name)}..." />
</div>
`).join("");else if(i)n=`
<div style="padding: 12px; background: rgba(0,0,0,0.2); border-radius: var(--radius-sm); border: 1px solid var(--border); font-size: 11.5px; color: var(--text-dim);">
This prompt template does not require any input arguments.
</div>
`;return`
<div style="display: grid; grid-template-columns: 340px 1fr; gap: 16px; height: calc(100vh - 165px);">
<!-- Left Sidebar: Prompts Catalog -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 12px; border-bottom: 1px solid var(--border);">
<input type="text" class="form-input" placeholder="Search ${t.length} prompts..." oninput="window.app.filterPrompts(this.value)">
</div>
<div style="flex: 1; overflow-y: auto; padding: 8px;" id="pg-prompt-list">
${s}
</div>
</div>
<!-- Right Panel: Prompt Parameter Binder & Message Envelope Preview -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); display: flex; flex-direction: column; overflow: hidden;">
<div style="padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 15px; font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">
${v(i?i.name||i.id:"No Prompt Selected")}
</div>
<div style="font-size: 11.5px; color: var(--text-dim);">
${v(i?i.description||i.title||"Bind variables and render messages":"Select a prompt from the list to test")}
</div>
</div>
<button class="btn btn-primary" onclick="window.app.executeGetPrompt()" ${i?"":"disabled"}>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
Render Prompt Messages
</button>
</div>
<div style="flex: 1; display: grid; grid-template-columns: 1fr 1fr; overflow: hidden;">
<!-- Arguments Form Builder -->
<div style="padding: 16px; border-right: 1px solid var(--border); overflow-y: auto;">
<div style="font-size: 12px; font-weight: 700; color: var(--text-main); margin-bottom: 12px; text-transform: uppercase;">
Template Arguments
</div>
${n}
</div>
<!-- Rendered Messages Output -->
<div style="padding: 16px; background: var(--bg-app); display: flex; flex-direction: column; overflow: hidden;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 600; color: var(--text-dim);">RENDERED PROMPT MESSAGES</span>
<span style="font-size: 11px; font-weight: 600; color: ${r?r.status===200?"var(--green-400)":"var(--red-400)":"var(--text-dim)"}; font-family: var(--ff-mono);">
${r?`HTTP ${r.status} · ${r.durationMs.toFixed(1)}ms`:"READY"}
</span>
</div>
<pre style="flex: 1; background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; color: #c084fc; font-size: 11.5px; overflow-y: auto; margin: 0; white-space: pre-wrap; word-break: break-word;">${r?v(JSON.stringify(r.data,null,2)):'// Click "Render Prompt Messages" to view resolved system/user messages'}</pre>
</div>
</div>
</div>
</div>
`}function ie(e){let t=e.capabilities||[],a=e.batchSteps||[];return`
<div style="position: fixed; inset: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 24px;" onclick="if(event.target === this) window.app.closeBatchModal()">
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); width: 840px; max-width: 95vw; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
<div style="padding: 16px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
<div>
<div style="font-size: 16px; font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
<span>⚡ Visual Multi-Step Batch Pipeline Builder</span>
</div>
<div style="font-size: 11.5px; color: var(--text-dim); margin-top: 2px;">
Execute chained MCP tools with variable reference interpolation and fault tolerance.
</div>
</div>
<button class="btn btn-ghost" style="font-size: 16px; padding: 4px 8px;" onclick="window.app.closeBatchModal()">✕</button>
</div>
<div style="flex: 1; overflow-y: auto; padding: 20px;">
${a.map((r,s)=>{let n=t.find((c)=>c.id===r.capability_id),o=n?.input_schema,d=o?.properties||{},p=Array.isArray(o?.required)?o.required:[],u=Object.entries(d),g=t.map((c)=>`
<option value="${v(c.id)}" ${c.id===r.capability_id?"selected":""}>
${v(c.id)} (${v(c.server||"local")})
</option>
`).join(""),m="";if(u.length>0)m=`
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; margin-bottom: 6px; align-items: center;">
<span style="font-size: 9.5px; font-weight: 700; color: var(--text-dim); text-transform: uppercase;">Parameters:</span>
${u.map(([c,h])=>{let k=p.includes(c),T=h.type||(h.enum?"enum":"any");return`
<span style="font-size: 9.5px; font-family: var(--ff-mono); padding: 1px 5px; background: ${k?"rgba(239, 68, 68, 0.15)":"rgba(148, 163, 184, 0.1)"}; color: ${k?"var(--red-400)":"var(--text-muted)"}; border: 1px solid ${k?"rgba(239, 68, 68, 0.3)":"var(--border)"}; border-radius: 3px;" title="${v(h.description||"")}">
${v(c)} (${T}${k?" *":""})
</span>
`}).join("")}
</div>
`;return`
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 14px; margin-bottom: 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span class="badge" style="background: rgba(56, 189, 248, 0.15); color: var(--cyan-400); font-family: var(--ff-mono); font-weight: 700;">STEP ${s+1}</span>
<span style="font-size: 11px; font-family: var(--ff-mono); color: var(--text-dim);">id: ${v(r.id)}</span>
</div>
<button class="btn btn-ghost" style="padding: 2px 8px; font-size: 11px; color: var(--red-400);" onclick="window.app.removeBatchStep(${s})">
✕ Remove
</button>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 6px;">
<div class="form-group" style="margin: 0;">
<label class="form-label" style="font-size: 11px;">Target Capability</label>
<select class="form-input" style="font-size: 11.5px;" onchange="window.app.updateBatchStepCapability(${s}, this.value)">
<option value="">-- Select Capability --</option>
${g}
</select>
</div>
<div style="display: flex; align-items: flex-end; padding-bottom: 6px;">
<label style="display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-muted); cursor: pointer;">
<input type="checkbox" ${r.continue_on_error?"checked":""} onchange="window.app.updateBatchStepContinueOnError(${s}, this.checked)" />
<span>Continue pipeline on step failure</span>
</label>
</div>
</div>
${m}
<div class="form-group" style="margin: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<div style="display: flex; align-items: center; gap: 8px;">
<label class="form-label" style="margin: 0; font-size: 11px;">Step Arguments JSON</label>
${n?`
<button type="button" class="btn btn-ghost" style="padding: 1px 6px; font-size: 9.5px;" onclick="window.app.fillBatchStepSampleArgs(${s})">✨ Sample Args</button>
`:""}
</div>
<div style="display: flex; gap: 6px; font-size: 10px; color: var(--cyan-400); font-family: var(--ff-mono);">
<span>Helpers:</span>
<code style="cursor: pointer; background: rgba(0,0,0,0.3); padding: 1px 4px; border-radius: 2px;" onclick="window.app.appendBatchVariable(${s}, '\${steps[0].result.id}')">\${steps[0].result.id}</code>
<code style="cursor: pointer; background: rgba(0,0,0,0.3); padding: 1px 4px; border-radius: 2px;" onclick="window.app.appendBatchVariable(${s}, '\${steps[0].result.data}')">\${steps[0].result.data}</code>
</div>
</div>
<textarea
id="batch-step-args-${s}"
class="form-textarea"
rows="3"
style="font-size: 11px; font-family: var(--ff-mono);"
oninput="window.app.updateBatchStepArgs(${s}, this.value)"
>${v(r.argsJson)}</textarea>
</div>
</div>
`}).join("")}
<div style="display: flex; gap: 10px; margin-top: 14px;">
<button class="btn btn-ghost" style="font-size: 11.5px;" onclick="window.app.addBatchStep()">
+ Add Pipeline Step
</button>
</div>
</div>
<div style="padding: 14px 20px; border-top: 1px solid var(--border); background: var(--bg-app); display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 11.5px; color: var(--text-dim);">
${a.length} sequential execution steps configured
</div>
<div style="display: flex; gap: 10px;">
<button class="btn btn-ghost" onclick="window.app.closeBatchModal()">Cancel</button>
<button class="btn btn-primary" onclick="window.app.executeBatchPipeline()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>
Run Batch Pipeline (${a.length} Steps)
</button>
</div>
</div>
</div>
</div>
`}function v(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function G(e){let t=e.tasks||[],a=e.taskFilterStatus||"all",i=t.filter((c)=>c.status==="input_required"),r=t.filter((c)=>c.status==="working"),s=t.filter((c)=>c.status==="completed"),n=t.filter((c)=>c.status==="cancelled"),o=t.filter((c)=>c.status==="failed"),d=a==="all"?t:t.filter((c)=>c.status===a),p=e.config.policy?.require_approval||e.config.policy?.requireApproval||[],u=i.length===0?`
<div style="padding: 36px 24px; text-align: center; background: var(--surface-card); border-radius: var(--radius-md); border: 1px dashed var(--border);">
<div style="width: 44px; height: 44px; border-radius: 50%; background: rgba(52, 211, 153, 0.12); border: 1px solid rgba(52, 211, 153, 0.3); display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; color: var(--green-400); font-size: 18px; font-weight: 700;">
✓
</div>
<div style="font-size: 14.5px; font-weight: 600; color: var(--text-main); margin-bottom: 5px;">No Tasks Awaiting Input or Approval</div>
<div style="font-size: 11.5px; color: var(--text-dim); max-width: 520px; margin: 0 auto; line-height: 1.6;">
Tool calls requiring Human-in-the-Loop approval or returning asynchronous <code style="color: var(--amber-300); font-family: var(--ff-mono);">input_required</code> tasks will suspend here for operator inspection, parameter editing, and response submission.
</div>
</div>
`:i.map((c)=>{let h=c.inputRequests||{},k=Object.keys(h),T=k.length>0,E=Math.floor(Date.now()/1000),_=c.expiresAtEpochSecs?Math.max(0,c.expiresAtEpochSecs-E):c.ttlSeconds||300;return`
<div class="bento-card" style="border: 1px solid rgba(245, 158, 11, 0.35); background: var(--surface-card); margin-bottom: 14px; padding: 18px;">
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px;">
<div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="brand-badge" style="background: rgba(245, 158, 11, 0.2); color: var(--amber-300); border-color: rgba(245, 158, 11, 0.5);">
INPUT REQUIRED
</span>
<span style="font-family: var(--ff-mono); font-size: 12px; color: var(--text-muted);">${S(c.taskId)}</span>
</div>
<div style="margin-top: 6px; display: flex; align-items: center; gap: 8px;">
<span style="font-size: 14.5px; font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">
${S(c.capabilityId||"Tool Execution")}
</span>
${c.serverId?`<span style="font-size: 11px; color: var(--text-dim);">via <span style="color: var(--cyan-400); font-family: var(--ff-mono);">${S(c.serverId)}</span></span>`:""}
</div>
</div>
<div style="text-align: right; font-family: var(--ff-mono); font-size: 11px; color: var(--text-dim);">
${c.createdAtEpochSecs?`<div>Created: <span style="color: var(--text-muted);">${new Date(c.createdAtEpochSecs*1000).toLocaleTimeString()}</span></div>`:""}
<div style="color: var(--amber-400); margin-top: 2px;">TTL Remaining: ${_}s</div>
</div>
</div>
<!-- Caller Context -->
${c.context?`
<div style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px; font-family: var(--ff-mono); font-size: 11px; display: flex; flex-wrap: wrap; gap: 16px; margin-bottom: 12px; color: var(--text-muted);">
${c.context.actor_id?`<div><span style="color: var(--text-dim);">Actor:</span> <span style="color: var(--cyan-400);">${S(c.context.actor_id)}</span></div>`:""}
${c.context.operation_id?`<div><span style="color: var(--text-dim);">Operation:</span> <span style="color: var(--text-main);">${S(c.context.operation_id)}</span></div>`:""}
${c.context.grant_id?`<div><span style="color: var(--text-dim);">Grant:</span> <span style="color: var(--text-main);">${S(c.context.grant_id)}</span></div>`:""}
</div>
`:""}
<!-- Dynamic Input Requests Form -->
<div style="margin-bottom: 14px;">
<div style="font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px;">
${T?"Required Input Responses (MRTR / HITL)":"Input Responses Payload (JSON)"}
</div>
${T?`
<div style="display: flex; flex-direction: column; gap: 10px;">
${k.map((A)=>{let x=h[A]||{},C=typeof x==="string"?x:x.prompt||x.description||x.title||A,b=x.type||"text",L=x.default!==void 0?JSON.stringify(x.default):x.value!==void 0?JSON.stringify(x.value):x.sanitized_args?JSON.stringify(x.sanitized_args,null,2):"";return`
<div style="background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 10px 12px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;">
<label style="font-size: 11.5px; font-weight: 600; color: var(--amber-300); font-family: var(--ff-mono);">${S(A)}</label>
<span class="brand-badge" style="font-size: 9.5px; padding: 1px 5px;">${S(b)}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-bottom: 6px;">${S(C)}</div>
${b==="confirmation"||b==="boolean"?`
<select id="task-input-${S(c.taskId)}-${S(A)}" class="form-input" style="font-size: 11.5px; font-family: var(--ff-mono); padding: 4px 8px;">
<option value="true" selected>true (Approve / Confirm)</option>
<option value="false">false (Reject / Deny)</option>
</select>
`:x.sanitized_args||b==="object"||b==="json"?`
<textarea id="task-input-${S(c.taskId)}-${S(A)}" class="form-textarea" rows="3" style="color: var(--green-400); font-family: var(--ff-mono); font-size: 11.5px;">${S(L)}</textarea>
`:`
<input id="task-input-${S(c.taskId)}-${S(A)}" type="text" class="form-input" value="${S(L)}" placeholder="Enter ${S(A)} response..." style="font-size: 11.5px; font-family: var(--ff-mono);">
`}
</div>
`}).join("")}
</div>
`:`
<textarea id="task-raw-input-${S(c.taskId)}" class="form-textarea" rows="3" style="color: var(--green-400); font-family: var(--ff-mono); font-size: 11.5px;">{}</textarea>
`}
</div>
<!-- Action Footer -->
<div style="display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--border); padding-top: 12px;">
<div style="display: flex; align-items: center; gap: 8px;">
<input id="task-operator-${S(c.taskId)}" type="text" class="form-input" placeholder="Operator ID" value="security-operator" style="width: 180px; padding: 5px 10px; font-size: 11px;">
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<button class="btn btn-danger" onclick="window.app.promptCancelTask('${S(c.taskId)}')">
✕ Cancel Task
</button>
<button class="btn btn-primary" onclick="window.app.submitTaskInputResponses('${S(c.taskId)}')">
✓ Submit & Resume
</button>
</div>
</div>
</div>
`}).join(""),g=p.length===0?`
<div style="color: var(--text-dim); font-size: 11.5px; line-height: 1.5; padding: 8px 0;">
No explicit <code style="color: var(--amber-400);">require_approval</code> rules active. Gated execution rules convert matching tool calls into tasks in real-time.
</div>
`:p.map((c)=>`
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--surface); padding: 8px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border); margin-bottom: 6px;">
<span style="font-family: var(--ff-mono); font-size: 11.5px; color: var(--amber-300); font-weight: 500;">\uD83D\uDEE1️ ${S(c)}</span>
<span class="brand-badge" style="font-size: 9.5px; padding: 1px 5px;">GATED</span>
</div>
`).join(""),m=d.length===0?`
<tr>
<td colspan="6" style="padding: 24px; text-align: center; color: var(--text-dim); font-size: 12px;">
No tasks found matching filter "${S(a)}".
</td>
</tr>
`:d.map((c)=>{let h=c.status==="completed"?"background: rgba(52, 211, 153, 0.12); color: var(--green-400); border-color: rgba(52, 211, 153, 0.3);":c.status==="working"?"background: rgba(56, 189, 248, 0.15); color: var(--cyan-400); border-color: rgba(56, 189, 248, 0.4);":c.status==="input_required"?"background: rgba(245, 158, 11, 0.2); color: var(--amber-300); border-color: rgba(245, 158, 11, 0.5);":c.status==="cancelled"?"background: rgba(148, 163, 184, 0.15); color: var(--text-muted); border-color: rgba(148, 163, 184, 0.3);":"background: rgba(248, 113, 113, 0.12); color: var(--red-400); border-color: rgba(248, 113, 113, 0.3);",k=c.progress!==void 0?Math.round(c.progress*100):c.status==="completed"?100:c.status==="working"?50:0,T=c.createdAtEpochSecs?new Date(c.createdAtEpochSecs*1000).toLocaleTimeString():"—";return`
<tr style="border-bottom: 1px solid rgba(255,255,255,0.03); transition: background 0.15s;" onmouseover="this.style.background='var(--surface-hover)'" onmouseout="this.style.background='transparent'">
<td style="padding: 10px 14px;">
<span class="brand-badge" style="${h}">
${c.status.toUpperCase()}
</span>
</td>
<td style="padding: 10px 14px; font-family: var(--ff-mono); font-weight: 600; color: var(--text-main); font-size: 11.5px;">
${S(c.capabilityId||"Tool Execution")}
</td>
<td style="padding: 10px 14px; font-family: var(--ff-mono); color: var(--text-dim); font-size: 11px;">
${S(c.taskId)}
</td>
<td style="padding: 10px 14px; width: 140px;">
<div style="display: flex; align-items: center; gap: 8px;">
<div style="flex: 1; height: 6px; background: var(--surface-card); border-radius: 3px; overflow: hidden; border: 1px solid var(--border);">
<div style="height: 100%; width: ${k}%; background: ${c.status==="completed"?"var(--green-400)":"var(--amber-400)"}; transition: width 0.3s;"></div>
</div>
<span style="font-size: 10.5px; font-family: var(--ff-mono); color: var(--text-muted);">${k}%</span>
</div>
</td>
<td style="padding: 10px 14px; color: var(--text-dim); font-size: 11px; text-align: right;">
${T}
</td>
<td style="padding: 10px 14px; text-align: right;">
${c.status==="input_required"||c.status==="working"?`
<button class="btn btn-danger" style="padding: 2px 8px; font-size: 10.5px;" onclick="window.app.promptCancelTask('${S(c.taskId)}')">Cancel</button>
`:`
<button class="btn btn-ghost" style="padding: 2px 8px; font-size: 10.5px;" onclick="window.app.inspectTaskDetails('${S(c.taskId)}')">Inspect</button>
`}
</td>
</tr>
`}).join("");return`
<!-- Sub-header & Actions -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span class="brand-badge" style="font-size: 11px; padding: 3px 10px; color: ${i.length>0?"var(--amber-300)":"var(--green-400)"}; border-color: ${i.length>0?"rgba(245, 158, 11, 0.4)":"rgba(52, 211, 153, 0.4)"}; background: ${i.length>0?"rgba(245, 158, 11, 0.1)":"rgba(52, 211, 153, 0.1)"};">
${i.length} ACTION REQUIRED
</span>
<span style="font-size: 12px; color: var(--text-dim);">
SEP-2663 Tasks Extension (<code style="color: var(--amber-300); font-family: var(--ff-mono);">io.modelcontextprotocol/tasks</code>) and Unified Human-in-the-Loop execution control.
</span>
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-ghost" onclick="window.app.refreshTasks()" style="font-size: 11.5px;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38l5.67-5.67"/></svg>
Refresh Tasks
</button>
</div>
</div>
<!-- Top Bento Metrics (Full 12-column span) -->
<div class="bento-grid">
<div class="bento-card col-3">
<div class="stat-label">Input Required (HITL)</div>
<div class="stat-value" style="color: ${i.length>0?"var(--amber-400)":"var(--text-main)"};">${i.length}</div>
<div class="stat-sub">Awaiting operator decision or response</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">Working / In-Flight</div>
<div class="stat-value" style="color: var(--cyan-400);">${r.length}</div>
<div class="stat-sub">Asynchronous active executions</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">Completed Tasks</div>
<div class="stat-value" style="color: var(--green-400);">${s.length}</div>
<div class="stat-sub">Finished successfully</div>
</div>
<div class="bento-card col-3">
<div class="stat-label">Cancelled / Failed</div>
<div class="stat-value" style="color: ${o.length>0?"var(--red-400)":"var(--text-muted)"};">${n.length+o.length}</div>
<div class="stat-sub">Terminated or errored</div>
</div>
</div>
<!-- Main Content Bento Split (8 cols queue / 4 cols rules) -->
<div class="bento-grid">
<!-- Left Column: Input Required Action Queue -->
<div class="col-8">
<div style="font-size: 11px; font-weight: 700; color: var(--amber-400); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; display: flex; align-items: center; gap: 6px;">
<span>⚡ Awaiting Operator Action (${i.length})</span>
</div>
<div>
${u}
</div>
</div>
<!-- Right Column: Active Governance Rules & Architecture -->
<div class="col-4">
<div style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">
<span>\uD83D\uDEE1️ Gating Policy Rules</span>
<button class="btn btn-ghost" style="padding: 2px 8px; font-size: 10.5px;" onclick="window.app.switchTab('policy')">Edit in Policy →</button>
</div>
<div class="bento-card" style="margin-bottom: 14px;">
${g}
</div>
<div class="bento-card">
<div class="stat-label" style="margin-bottom: 8px;">SEP-2663 Protocol Standard</div>
<div style="font-size: 11.5px; color: var(--text-dim); line-height: 1.5;">
Warmplane exposes compliant <code>task_get</code>, <code>task_update</code>, and <code>task_cancel</code> tools directly on the MCP facade, enabling seamless agent delegation with non-blocking lifecycle management.
</div>
</div>
</div>
</div>
<!-- Bottom History & Task Registry Table (Full 12 columns) -->
<div class="bento-card col-12" style="margin-top: 10px;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<div style="font-size: 11px; font-weight: 700; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px;">
\uD83D\uDCDC Task Registry (${d.length})
</div>
<div style="display: flex; gap: 6px; align-items: center;">
<span style="font-size: 11px; color: var(--text-dim);">Filter Status:</span>
<select class="form-input" style="padding: 3px 8px; font-size: 11px; width: 140px;" onchange="window.app.filterTasksByStatus(this.value)">
<option value="all" ${a==="all"?"selected":""}>All Statuses</option>
<option value="input_required" ${a==="input_required"?"selected":""}>input_required</option>
<option value="working" ${a==="working"?"selected":""}>working</option>
<option value="completed" ${a==="completed"?"selected":""}>completed</option>
<option value="cancelled" ${a==="cancelled"?"selected":""}>cancelled</option>
<option value="failed" ${a==="failed"?"selected":""}>failed</option>
</select>
</div>
</div>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-family: var(--ff-mono); font-size: 11.5px; text-align: left;">
<thead>
<tr style="border-bottom: 1px solid var(--border); color: var(--text-muted); font-size: 10.5px; text-transform: uppercase;">
<th style="padding: 10px 14px;">Status</th>
<th style="padding: 10px 14px;">Capability / Tool</th>
<th style="padding: 10px 14px;">Task ID</th>
<th style="padding: 10px 14px;">Progress</th>
<th style="padding: 10px 14px; text-align: right;">Created</th>
<th style="padding: 10px 14px; text-align: right;">Actions</th>
</tr>
</thead>
<tbody>
${m}
</tbody>
</table>
</div>
</div>
`}function S(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function V(){let e=l.getState(),t=e.auditEvents||[],a=e.auditStats||{total_events:0,by_status:{success:0,failed:0,denied:0,intercepted:0}},i=e.auditVerification,r=e.auditFilters,s=e.auditTotal??t.length,n=e.auditSelectedEvent,o=Object.keys(e.config?.mcpServers||{}),d=r.limit||25,p=r.offset||0,u=Math.floor(p/d)+1,g=Math.max(1,Math.ceil(s/d)),m=s===0?0:p+1,c=Math.min(p+d,s),h=f.getAuditExportUrl({actor_id:r.search?void 0:void 0,server_id:r.serverId!=="all"?r.serverId:void 0,event_type:r.eventType!=="all"?r.eventType:void 0,status:r.status!=="all"?r.status:void 0,search:r.search.trim()?r.search.trim():void 0},"csv"),k=f.getAuditExportUrl({server_id:r.serverId!=="all"?r.serverId:void 0,event_type:r.eventType!=="all"?r.eventType:void 0,status:r.status!=="all"?r.status:void 0,search:r.search.trim()?r.search.trim():void 0},"jsonl"),T=i?i.is_valid?`
<div style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: rgba(34, 197, 94, 0.1); border: 1px solid rgba(34, 197, 94, 0.3); border-radius: var(--radius-sm); font-size: 11.5px; color: var(--green-400);">
<span>\uD83D\uDEE1️</span>
<span style="font-weight: 600;">Chain Verified: 100% Tamper Free (${i.total_records} events)</span>
</div>
`:`
<div style="display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: var(--radius-sm); font-size: 11.5px; color: var(--red-400);">
<span>⚠️</span>
<span style="font-weight: 600;">TAMPER DETECTED at Record #${i.corrupted_at_index}</span>
</div>
`:`
<button class="btn btn-ghost" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.verifyAuditChain()">
\uD83D\uDEE1️ Verify Cryptographic Hash Chain
</button>
`,E=o.map((b)=>`<option value="${w(b)}" ${r.serverId===b?"selected":""}>${w(b)}</option>`).join(""),_=`
<div class="bento-card" style="padding: 14px 16px; margin-bottom: 16px; background: rgba(18, 24, 38, 0.7); border: 1px solid var(--border);">
<div style="display: grid; grid-template-columns: 2fr 1fr 1.2fr 1.2fr auto auto; gap: 10px; align-items: center;">
<!-- Full-text search input -->
<div style="position: relative;">
<input
type="text"
id="audit-search-input"
class="form-input"
style="width: 100%; padding-left: 28px; font-size: 12px; height: 32px;"
placeholder="Search trace, actor, capability, hash, error..."
value="${w(r.search)}"
oninput="window.app.handleAuditSearchInput(this.value)"
/>
<span style="position: absolute; left: 8px; top: 7px; font-size: 12px; color: var(--text-dim);">\uD83D\uDD0D</span>
</div>
<!-- Status Filter -->
<div>
<select
class="form-input"
style="width: 100%; font-size: 12px; height: 32px;"
onchange="window.app.handleAuditStatusFilter(this.value)"
>
<option value="all" ${r.status==="all"?"selected":""}>All Statuses</option>
<option value="success" ${r.status==="success"?"selected":""}>\uD83D\uDFE2 Success</option>
<option value="denied" ${r.status==="denied"?"selected":""}>\uD83D\uDD34 Denied</option>
<option value="intercepted" ${r.status==="intercepted"?"selected":""}>\uD83D\uDFE1 HITL Intercept</option>
<option value="failed" ${r.status==="failed"?"selected":""}>❌ Failed</option>
<option value="cancelled" ${r.status==="cancelled"?"selected":""}>⚪ Cancelled</option>
</select>
</div>
<!-- Event Type Filter -->
<div>
<select
class="form-input"
style="width: 100%; font-size: 12px; height: 32px;"
onchange="window.app.handleAuditEventTypeFilter(this.value)"
>
<option value="all" ${r.eventType==="all"?"selected":""}>All Event Types</option>
<option value="tool_execution" ${r.eventType==="tool_execution"?"selected":""}>Tool Execution</option>
<option value="tool_intercepted_hitl" ${r.eventType==="tool_intercepted_hitl"?"selected":""}>HITL Intercept</option>
<option value="approval_granted" ${r.eventType==="approval_granted"?"selected":""}>Approval Granted</option>
<option value="approval_rejected" ${r.eventType==="approval_rejected"?"selected":""}>Approval Rejected</option>
<option value="approval_expired" ${r.eventType==="approval_expired"?"selected":""}>Approval Expired</option>
<option value="policy_violation" ${r.eventType==="policy_violation"?"selected":""}>Policy Violation</option>
<option value="config_mutation" ${r.eventType==="config_mutation"?"selected":""}>Config Mutation</option>
<option value="sampling_call" ${r.eventType==="sampling_call"?"selected":""}>Sampling Call</option>
<option value="resource_access" ${r.eventType==="resource_access"?"selected":""}>Resource Access</option>
</select>
</div>
<!-- Server Filter -->
<div>
<select
class="form-input"
style="width: 100%; font-size: 12px; height: 32px;"
onchange="window.app.handleAuditServerFilter(this.value)"
>
<option value="all" ${r.serverId==="all"?"selected":""}>All MCP Servers</option>
${E}
</select>
</div>
<!-- Clear Filters Button -->
<div>
<button
class="btn btn-ghost"
style="padding: 6px 12px; font-size: 11.5px; height: 32px;"
onclick="window.app.clearAuditFilters()"
title="Reset all search queries and filters"
>
✕ Reset
</button>
</div>
</div>
</div>
`,A=`
<div style="display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; background: rgba(18, 24, 38, 0.5); border-radius: var(--radius-md); border: 1px solid var(--border); margin-top: 16px;">
<div style="font-size: 12px; color: var(--text-dim); display: flex; align-items: center; gap: 8px;">
<span>Showing <strong style="color: var(--text-main);">${m}–${c}</strong> of <strong style="color: var(--text-main);">${s}</strong> events</span>
<span style="color: var(--border);">|</span>
<span>Page Size:</span>
<select
class="form-input"
style="font-size: 11.5px; padding: 2px 24px 2px 8px; height: 28px; width: auto;"
onchange="window.app.handleAuditPageSize(this.value)"
>
<option value="10" ${d===10?"selected":""}>10 / page</option>
<option value="25" ${d===25?"selected":""}>25 / page</option>
<option value="50" ${d===50?"selected":""}>50 / page</option>
<option value="100" ${d===100?"selected":""}>100 / page</option>
</select>
</div>
<div style="display: flex; align-items: center; gap: 6px;">
<button
class="btn btn-ghost"
style="padding: 4px 8px; font-size: 11px; height: 28px;"
${u<=1?'disabled style="opacity: 0.4; cursor: not-allowed;"':""}
onclick="window.app.auditGoToPage(1)"
title="First Page"
>
⏮ First
</button>
<button
class="btn btn-ghost"
style="padding: 4px 8px; font-size: 11px; height: 28px;"
${u<=1?'disabled style="opacity: 0.4; cursor: not-allowed;"':""}
onclick="window.app.auditPrevPage()"
>
◀ Prev
</button>
<span style="font-size: 12px; font-weight: 600; color: var(--text-main); padding: 0 8px;">
Page ${u} of ${g}
</span>
<button
class="btn btn-ghost"
style="padding: 4px 8px; font-size: 11px; height: 28px;"
${u>=g?'disabled style="opacity: 0.4; cursor: not-allowed;"':""}
onclick="window.app.auditNextPage()"
>
Next ▶
</button>
<button
class="btn btn-ghost"
style="padding: 4px 8px; font-size: 11px; height: 28px;"
${u>=g?'disabled style="opacity: 0.4; cursor: not-allowed;"':""}
onclick="window.app.auditGoToPage(${g})"
title="Last Page"
>
Last ⏭
</button>
</div>
</div>
`,x="";if(t.length===0)x=`
<div style="padding: 48px 24px; text-align: center; color: var(--text-dim); background: var(--surface-card); border-radius: var(--radius-md); border: 1px dashed var(--border);">
<div style="font-size: 28px; margin-bottom: 8px;">\uD83D\uDD0D</div>
<div style="font-size: 14px; font-weight: 600; color: var(--text-main); margin-bottom: 4px;">No Matching Audit Events</div>
<div style="font-size: 12px; max-width: 420px; margin: 0 auto;">No audit records match your currently selected filters. Try broadening your search or resetting filters.</div>
<button class="btn btn-ghost" style="margin-top: 14px; font-size: 11.5px;" onclick="window.app.clearAuditFilters()">Reset Filters</button>
</div>
`;else x=t.map((b)=>{let L=new Date(Math.floor(b.timestamp_ns/1e6)).toLocaleString(),I='<span class="badge" style="background: rgba(34, 197, 94, 0.15); color: var(--green-400); font-weight: 600;">SUCCESS</span>';if(b.status==="denied")I='<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: var(--red-400); font-weight: 600;">DENIED</span>';else if(b.status==="intercepted")I='<span class="badge" style="background: rgba(234, 179, 8, 0.15); color: var(--amber-300); font-weight: 600;">HITL INTERCEPT</span>';else if(b.status==="failed")I='<span class="badge" style="background: rgba(239, 68, 68, 0.15); color: var(--red-400); font-weight: 600;">FAILED</span>';else if(b.status==="cancelled")I='<span class="badge" style="background: rgba(148, 163, 184, 0.15); color: var(--text-muted); font-weight: 600;">CANCELLED</span>';let H=b.sanitized_args?JSON.stringify(b.sanitized_args):"-",q=b.actor_id||b.operator_id||"anonymous",j=b.server_id||"system",ee=b.capability_id||b.event_type,te=b.execution_latency_us?`${(b.execution_latency_us/1000).toFixed(1)}ms`:"-";return`
<div class="bento-card" style="margin-bottom: 12px; padding: 16px; border: 1px solid var(--border); transition: border-color 0.15s ease;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-family: var(--ff-mono); font-size: 11px; font-weight: 700; color: var(--text-dim);">${w(b.id)}</span>
${I}
<span style="font-size: 12px; font-weight: 600; color: var(--text-main);">${w(ee)}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<div style="font-family: var(--ff-mono); font-size: 11px; color: var(--text-muted);">${w(L)}</div>
<button
class="btn btn-ghost"
style="padding: 2px 8px; font-size: 11px; height: 24px;"
onclick="window.app.selectAuditEvent('${w(b.id)}')"
title="Inspect event details & cryptographic payload"
>
Inspect \uD83D\uDD0D
</button>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; font-size: 11.5px; background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: var(--radius-sm); margin-bottom: 8px;">
<div><span style="color: var(--text-muted);">Actor:</span> <strong style="color: var(--text-main);">${w(q)}</strong></div>
<div><span style="color: var(--text-muted);">Server:</span> <strong style="color: var(--cyan-400);">${w(j)}</strong></div>
<div><span style="color: var(--text-muted);">Trace:</span> <code style="color: var(--cyan-400); font-size: 10.5px;">${w(b.trace_id)}</code></div>
<div><span style="color: var(--text-muted);">Latency:</span> <span style="color: var(--amber-300);">${te}</span></div>
</div>
<div style="font-family: var(--ff-mono); font-size: 11px; color: var(--text-dim); margin-bottom: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span style="color: var(--text-muted);">Args:</span> ${w(H)}
</div>
<div style="display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--border); padding-top: 6px; font-size: 10.5px; font-family: var(--ff-mono); color: var(--text-muted);">
<div><span style="color: var(--text-dim);">prev_hash:</span> ${w(b.prev_hash.slice(0,16))}...</div>
<div><span style="color: var(--text-dim);">hash:</span> <span style="color: var(--green-400);">${w(b.hash.slice(0,16))}...</span></div>
</div>
</div>
`}).join("");let C="";if(n){let b=new Date(Math.floor(n.timestamp_ns/1e6)).toISOString();C=`
<div style="position: fixed; inset: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 9999; display: flex; align-items: center; justify-content: center; padding: 24px;" onclick="if (event.target === this) window.app.selectAuditEvent(null)">
<div class="bento-card" style="width: 100%; max-width: 720px; max-height: 85vh; display: flex; flex-direction: column; overflow: hidden; background: #0f172a; border: 1px solid var(--border); box-shadow: 0 20px 40px rgba(0,0,0,0.5);">
<!-- Modal Header -->
<div style="display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid var(--border);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 16px;">\uD83D\uDD12</span>
<h2 style="font-size: 15px; font-weight: 700; color: var(--text-main); margin: 0;">Audit Event Details (${w(n.id)})</h2>
</div>
<button class="btn btn-ghost" style="padding: 4px 8px; font-size: 14px;" onclick="window.app.selectAuditEvent(null)">✕</button>
</div>
<!-- Modal Body -->
<div style="padding: 20px; overflow-y: auto; display: flex; flex-direction: column; gap: 14px; font-size: 12px;">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; background: rgba(0,0,0,0.25); padding: 12px; border-radius: var(--radius-sm);">
<div><span style="color: var(--text-muted);">Timestamp:</span> <strong style="color: var(--text-main); font-family: var(--ff-mono); font-size: 11px;">${w(b)}</strong></div>
<div><span style="color: var(--text-muted);">Status:</span> <strong style="color: var(--text-main);">${w(n.status.toUpperCase())}</strong></div>
<div><span style="color: var(--text-muted);">Event Type:</span> <strong style="color: var(--text-main);">${w(n.event_type)}</strong></div>
<div><span style="color: var(--text-muted);">Server:</span> <strong style="color: var(--cyan-400);">${w(n.server_id||"system")}</strong></div>
<div><span style="color: var(--text-muted);">Capability:</span> <strong style="color: var(--text-main);">${w(n.capability_id||"-")}</strong></div>
<div><span style="color: var(--text-muted);">Actor / Operator:</span> <strong style="color: var(--text-main);">${w(n.actor_id||n.operator_id||"anonymous")}</strong></div>
<div><span style="color: var(--text-muted);">Trace ID:</span> <code style="color: var(--cyan-400);">${w(n.trace_id)}</code></div>
<div><span style="color: var(--text-muted);">Request ID:</span> <code style="color: var(--cyan-400);">${w(n.request_id||"-")}</code></div>
<div><span style="color: var(--text-muted);">Client IP:</span> <span style="color: var(--text-main);">${w(n.client_ip||"-")}</span></div>
<div><span style="color: var(--text-muted);">Latency:</span> <span style="color: var(--amber-300);">${n.execution_latency_us?`${(n.execution_latency_us/1000).toFixed(2)} ms`:"-"}</span></div>
</div>
${n.error_message?`
<div style="background: rgba(239, 68, 68, 0.1); border: 1px solid rgba(239, 68, 68, 0.3); border-radius: var(--radius-sm); padding: 10px 12px; color: var(--red-400);">
<div style="font-weight: 700; margin-bottom: 2px;">Error (${w(n.error_code||"ERROR")}):</div>
<div style="font-family: var(--ff-mono); font-size: 11px;">${w(n.error_message)}</div>
</div>
`:""}
<!-- Sanitized Arguments -->
<div>
<div style="font-weight: 600; color: var(--text-main); margin-bottom: 4px;">Sanitized Arguments</div>
<pre style="background: rgba(0,0,0,0.4); padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--border); font-family: var(--ff-mono); font-size: 11px; max-height: 140px; overflow: auto; margin: 0; color: #cbd5e1;">${w(JSON.stringify(n.sanitized_args||{},null,2))}</pre>
</div>
<!-- Sanitized Response -->
${n.sanitized_response?`
<div>
<div style="font-weight: 600; color: var(--text-main); margin-bottom: 4px;">Sanitized Response</div>
<pre style="background: rgba(0,0,0,0.4); padding: 10px; border-radius: var(--radius-sm); border: 1px solid var(--border); font-family: var(--ff-mono); font-size: 11px; max-height: 140px; overflow: auto; margin: 0; color: #cbd5e1;">${w(JSON.stringify(n.sanitized_response,null,2))}</pre>
</div>
`:""}
<!-- Cryptographic Hashes -->
<div style="background: rgba(0,0,0,0.3); padding: 12px; border-radius: var(--radius-sm); border: 1px solid var(--border);">
<div style="font-weight: 600; color: var(--text-main); margin-bottom: 6px;">Tamper-Evidence Cryptographic Hashes</div>
<div style="margin-bottom: 6px;">
<span style="color: var(--text-muted); font-size: 10.5px;">Previous Chain Hash (prev_hash):</span>
<div style="font-family: var(--ff-mono); font-size: 10.5px; color: var(--text-dim); word-break: break-all;">${w(n.prev_hash)}</div>
</div>
<div>
<span style="color: var(--text-muted); font-size: 10.5px;">Record Hash Signature (hash):</span>
<div style="font-family: var(--ff-mono); font-size: 10.5px; color: var(--green-400); word-break: break-all;">${w(n.hash)}</div>
</div>
</div>
</div>
<!-- Modal Footer -->
<div style="padding: 12px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end;">
<button class="btn btn-primary" style="font-size: 12px;" onclick="window.app.selectAuditEvent(null)">Close</button>
</div>
</div>
</div>
`}return`
<!-- Sub-header & Actions -->
<div style="margin-bottom: 18px; display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 12px; color: var(--text-dim);">
Cryptographically tamper-evident, append-only execution log for SOC2 & ISO 27001 compliance.
</div>
<div style="display: flex; gap: 8px; align-items: center;">
${T}
<a href="${h}" download class="btn btn-ghost" style="font-size: 11.5px; text-decoration: none;" title="Export current filtered view as CSV">\uD83D\uDCE5 Export CSV</a>
<a href="${k}" download class="btn btn-ghost" style="font-size: 11.5px; text-decoration: none;" title="Export current filtered view as JSONL">\uD83D\uDCE5 Export JSONL</a>
<button class="btn btn-primary" style="font-size: 11.5px;" onclick="window.app.refreshAuditEvents()">\uD83D\uDD04 Refresh</button>
</div>
</div>
<!-- Stats summary cards -->
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px;">
<div class="bento-card" style="padding: 14px; text-align: center;">
<div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">Total Events</div>
<div style="font-size: 22px; font-weight: 800; color: var(--text-main); margin-top: 4px;">${a.total_events}</div>
</div>
<div class="bento-card" style="padding: 14px; text-align: center;">
<div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">Successful Calls</div>
<div style="font-size: 22px; font-weight: 800; color: var(--green-400); margin-top: 4px;">${a.by_status.success}</div>
</div>
<div class="bento-card" style="padding: 14px; text-align: center;">
<div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">HITL Intercepts</div>
<div style="font-size: 22px; font-weight: 800; color: var(--amber-300); margin-top: 4px;">${a.by_status.intercepted}</div>
</div>
<div class="bento-card" style="padding: 14px; text-align: center;">
<div style="font-size: 11px; color: var(--text-muted); text-transform: uppercase; font-weight: 600;">Policy Denials</div>
<div style="font-size: 22px; font-weight: 800; color: var(--red-400); margin-top: 4px;">${a.by_status.denied}</div>
</div>
</div>
<!-- Search & Filter Toolbar -->
${_}
<!-- Event Timeline List Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<h2 style="font-size: 14px; font-weight: 600; color: var(--text-main);">Sequential Audit Ledger (SHA-256 Hash Chained)</h2>
<span style="font-size: 11.5px; color: var(--text-dim);">${t.length} events loaded on this page</span>
</div>
<!-- Event Rows -->
<div>
${x}
</div>
<!-- Pagination Footer -->
${s>0?A:""}
<!-- Modal Popup for Event Inspection -->
${C}
`}function w(e){if(!e)return"";return String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")}function W(){let t=l.getState().config.policy||{},a=t.allow||[],i=t.deny||[],r=t.redact_keys||t.redactKeys||[],s=t.require_approval||t.requireApproval||[],n=a.length===0?`
<div style="color: var(--text-dim); font-size: 12px;">No allow list (all non-denied operations permitted)</div>
`:a.map((u,g)=>`
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--surface); padding: 8px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border);">
<span style="font-family: var(--ff-mono); font-size: 12px; color: var(--green-400);">✔ ${B(u)}</span>
<button class="btn btn-ghost" style="padding: 2px 6px; font-size: 11px; color: var(--red-400);" onclick="window.app.removePolicyRule('allow', ${g})">✕</button>
</div>
`).join(""),o=i.length===0?`
<div style="color: var(--text-dim); font-size: 12px;">No deny rules configured</div>
`:i.map((u,g)=>`
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--surface); padding: 8px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border);">
<span style="font-family: var(--ff-mono); font-size: 12px; color: var(--red-400);">✖ ${B(u)}</span>
<button class="btn btn-ghost" style="padding: 2px 6px; font-size: 11px; color: var(--red-400);" onclick="window.app.removePolicyRule('deny', ${g})">✕</button>
</div>
`).join(""),d=s.length===0?`
<div style="color: var(--text-dim); font-size: 12px;">No human-in-the-loop approval rules configured</div>
`:s.map((u,g)=>`
<div style="display: flex; justify-content: space-between; align-items: center; background: var(--surface); padding: 8px 12px; border-radius: var(--radius-sm); border: 1px solid var(--border);">
<span style="font-family: var(--ff-mono); font-size: 12px; color: var(--amber-400);">\uD83D\uDEE1️ ${B(u)}</span>
<button class="btn btn-ghost" style="padding: 2px 6px; font-size: 11px; color: var(--red-400);" onclick="window.app.removePolicyRule('requireApproval', ${g})">✕</button>
</div>
`).join(""),p=r.length===0?`
<div style="color: var(--text-dim); font-size: 12px;">No key redaction patterns configured</div>
`:r.map((u,g)=>`
<span class="brand-badge" style="color: var(--amber-300); padding: 5px 10px; font-size: 11px; display: inline-flex; align-items: center; gap: 6px;">
${B(u)}
<span style="cursor: pointer; color: var(--red-400); font-weight: bold;" onclick="window.app.removePolicyRule('redact', ${g})">✕</span>
</span>
`).join("");return`
<!-- Sub-header -->
<div style="margin-bottom: 16px; font-size: 12px; color: var(--text-dim);">
Wildcard capability access control, human-in-the-loop triggers, and sensitive key masking.
</div>
<div class="bento-grid">
<!-- Allow Rules -->
<div class="bento-card col-4">
<div class="stat-header">
<span class="stat-label" style="color: var(--green-400);">Allow List Patterns</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px; margin: 12px 0;">
${n}
</div>
<div style="display: flex; gap: 8px; margin-top: 14px;">
<input type="text" class="form-input" id="policy-new-allow" placeholder="e.g. github.*, db.read_*" onkeydown="if(event.key==='Enter') window.app.submitPolicyRule('allow')">
<button class="btn btn-ghost" onclick="window.app.submitPolicyRule('allow')">Add Allow</button>
</div>
</div>
<!-- Deny Rules -->
<div class="bento-card col-4">
<div class="stat-header">
<span class="stat-label" style="color: var(--red-400);">Deny List Patterns (Strict Precedence)</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px; margin: 12px 0;">
${o}
</div>
<div style="display: flex; gap: 8px; margin-top: 14px;">
<input type="text" class="form-input" id="policy-new-deny" placeholder="e.g. *.drop_*, filesystem.write_*" onkeydown="if(event.key==='Enter') window.app.submitPolicyRule('deny')">
<button class="btn btn-ghost" onclick="window.app.submitPolicyRule('deny')">Add Deny</button>
</div>
</div>
<!-- Require Approval Rules -->
<div class="bento-card col-4">
<div class="stat-header">
<span class="stat-label" style="color: var(--amber-400);">Human-in-the-Loop Triggers</span>
</div>
<div style="display: flex; flex-direction: column; gap: 8px; margin: 12px 0;">
${d}
</div>
<div style="display: flex; gap: 8px; margin-top: 14px;">
<input type="text" class="form-input" id="policy-new-requireApproval" placeholder="e.g. docker.run*, db.write*" onkeydown="if(event.key==='Enter') window.app.submitPolicyRule('requireApproval')">
<button class="btn btn-ghost" onclick="window.app.submitPolicyRule('requireApproval')">Add Approval</button>
</div>
</div>
<!-- Key Redaction -->
<div class="bento-card col-12">
<div class="stat-header">
<span class="stat-label" style="color: var(--amber-300);">Sensitive Key Redaction Patterns</span>
<span style="font-size: 11px; color: var(--text-dim); margin-left: 8px;">Keys automatically masked as <redacted> in logs and envelopes</span>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin: 12px 0;">
${p}
</div>
<div style="display: flex; gap: 8px; margin-top: 14px; max-width: 420px;">
<input type="text" class="form-input" id="policy-new-redact" placeholder="e.g. token, api_key, password, secret" onkeydown="if(event.key==='Enter') window.app.submitPolicyRule('redact')">
<button class="btn btn-ghost" onclick="window.app.submitPolicyRule('redact')">Add Key</button>
</div>
</div>
<!-- Evaluation Sandbox Tester -->
<div class="bento-card col-12">
<div class="stat-header">
<span class="stat-label">Policy Evaluation Sandbox</span>
<span style="font-size: 11px; color: var(--text-dim); margin-left: 8px;">Live verification of capability access against active rules</span>
</div>
<div style="display: flex; gap: 12px; align-items: center; margin-top: 12px;">
<input type="text" class="form-input" placeholder="Type capability identifier, e.g. github.create_issue or sqlite.drop_table" style="flex: 1;" oninput="window.app.testPolicySandbox(this.value)">
<div id="policy-test-verdict" style="font-weight: 700; font-family: var(--ff-mono); font-size: 12.5px; padding: 7px 16px; border-radius: var(--radius-sm); background: var(--surface); border: 1px solid var(--border); color: var(--green-400);">
ALLOWED
</div>
</div>
</div>
</div>
`}function B(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function Q(){let e=l.getState(),t=e.config,a=Object.entries(t.capabilityAliases||{}),i=Object.entries(t.resourceAliases||{}),r=Object.entries(t.promptAliases||{}),s="";if(a.length===0&&i.length===0&&r.length===0)s=`
<div style="padding: 24px; text-align: center; color: var(--text-dim);">
No facade aliases configured in ${R(e.configPath)}. Add short names to prune token payload sizes.
</div>
`;else{for(let[n,o]of a)s+=`
<div class="feed-row" style="grid-template-columns: 90px 180px 1fr 80px;">
<span style="color: var(--cyan-400);">Tool</span>
<span style="font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">${R(n)}</span>
<span style="color: var(--text-dim); font-family: var(--ff-mono);">${R(o)}</span>
<div style="text-align: right;">
<button class="btn btn-ghost" style="padding: 2px 6px; color: var(--red-400);" onclick="window.app.deleteAlias('tool', '${R(n)}')">✕</button>
</div>
</div>
`;for(let[n,o]of i)s+=`
<div class="feed-row" style="grid-template-columns: 90px 180px 1fr 80px;">
<span style="color: var(--green-400);">Resource</span>
<span style="font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">${R(n)}</span>
<span style="color: var(--text-dim); font-family: var(--ff-mono);">${R(o)}</span>
<div style="text-align: right;">
<button class="btn btn-ghost" style="padding: 2px 6px; color: var(--red-400);" onclick="window.app.deleteAlias('resource', '${R(n)}')">✕</button>
</div>
</div>
`;for(let[n,o]of r)s+=`
<div class="feed-row" style="grid-template-columns: 90px 180px 1fr 80px;">
<span style="color: var(--amber-300);">Prompt</span>
<span style="font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">${R(n)}</span>
<span style="color: var(--text-dim); font-family: var(--ff-mono);">${R(o)}</span>
<div style="text-align: right;">
<button class="btn btn-ghost" style="padding: 2px 6px; color: var(--red-400);" onclick="window.app.deleteAlias('prompt', '${R(n)}')">✕</button>
</div>
</div>
`}return`
<!-- Sub-header -->
<div style="margin-bottom: 16px; font-size: 12px; color: var(--text-dim);">
Shorten capability IDs to prune prompt tokens and create stable public interfaces.
</div>
<!-- Quick Add Form -->
<div class="bento-card" style="margin-bottom: 20px; overflow: visible;">
<div class="stat-header" style="margin-bottom: 12px;">
<span class="stat-label">Create New Alias</span>
</div>
<div style="display: grid; grid-template-columns: 140px 1fr 1fr 100px; gap: 10px; align-items: center; position: relative;">
<select class="form-input" id="alias-kind">
<option value="tool">Tool / Capability</option>
<option value="resource">Resource</option>
<option value="prompt">Prompt</option>
</select>
<input type="text" class="form-input" id="alias-name" placeholder="Public alias (e.g. db.query)" onkeydown="if(event.key==='Enter') window.app.createAlias()">
<div style="position: relative; width: 100%;">
<input type="text" class="form-input" id="alias-target" autocomplete="off" placeholder="Target ID (e.g. docker.list_containers)" style="width: 100%;" oninput="window.app.handleAliasTargetInput(this.value)" onkeydown="if(event.key==='Enter') window.app.createAlias()" onfocus="window.app.handleAliasTargetInput(this.value)" onblur="setTimeout(() => window.app.hideAliasDropdown(), 200)">
<div id="alias-suggestions-dropdown" style="display: none; position: absolute; top: calc(100% + 4px); left: 0; right: 0; max-height: 240px; overflow-y: auto; background: var(--surface-elevated); border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(0,0,0,0.4); z-index: 1000; font-family: var(--ff-mono); font-size: 11.5px;"></div>
</div>
<button class="btn btn-primary" onclick="window.app.createAlias()">+ Save</button>
</div>
</div>
<!-- Aliases Table -->
<div style="background: var(--surface-card); border: 1px solid var(--border); border-radius: var(--radius-md); overflow: hidden; font-family: var(--ff-mono); font-size: 12px;">
<div style="display: grid; grid-template-columns: 90px 180px 1fr 80px; padding: 10px 14px; background: var(--surface-hover); border-bottom: 1px solid var(--border); color: var(--text-muted); font-weight: 600;">
<span>TYPE</span>
<span>PUBLIC ALIAS</span>
<span>CANONICAL TARGET</span>
<span style="text-align: right;">ACTION</span>
</div>
${s}
</div>
`}function R(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}function Y(){let e=l.getState(),t=e.config,a=t.profiles||{},i=Object.entries(a),r=t.mcpServers||{},s=e.activeProfile,n="";if(i.length===0)n=`
<div style="padding: 40px; text-align: center; color: var(--text-dim); background: var(--surface-card); border-radius: var(--radius-md); border: 1px dashed var(--border);">
<div style="font-size: 15px; color: var(--text-main); font-weight: 600; margin-bottom: 8px;">No Profiles Configured</div>
<p style="font-size: 12px; margin-bottom: 20px; max-width: 480px; margin-left: auto; margin-right: auto;">
Profiles allow Warmplane to serve multiple task-relevant server constellations (e.g. <code>coding</code>, <code>support</code>, <code>data</code>) from one running daemon process.
</p>
<button class="btn btn-primary" onclick="window.app.openAddProfileModal()">+ Create First Profile</button>
</div>
`;else n=i.map(([o,d])=>{let p=s===o,u=d.servers.map((m)=>`<span class="brand-badge" style="${r[m]?"color: var(--cyan-400); border-color: rgba(34, 211, 238, 0.25); background: rgba(34, 211, 238, 0.05);":"color: var(--red-400); border-color: rgba(248, 113, 113, 0.3); background: rgba(248, 113, 113, 0.05);"}">${M(m)}</span>`).join(" "),g=(e.capabilities||[]).filter((m)=>d.servers.includes(m.server)).length;return`
<div class="bento-card" style="margin-bottom: 14px; border-left: ${p?"3px solid var(--amber-400)":"1px solid var(--border)"};">
<div style="display: flex; justify-content: space-between; align-items: flex-start;">
<div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<span style="font-size: 16px; font-weight: 700; color: var(--text-main); font-family: var(--ff-mono);">${M(o)}</span>
${p?'<span class="brand-badge" style="color: var(--amber-400); border-color: rgba(245, 158, 11, 0.4); background: rgba(245, 158, 11, 0.1);">ACTIVE IN UI</span>':""}
<span class="brand-badge">${d.servers.length} server${d.servers.length===1?"":"s"}</span>
<span class="brand-badge" style="color: var(--text-dim);">${g} capabilities</span>
</div>
<div style="font-size: 12px; color: var(--text-muted); margin-bottom: 10px;">
${M(d.description||"No description provided")}
</div>
<div style="display: flex; flex-wrap: wrap; gap: 6px; align-items: center;">
<span style="font-size: 11px; color: var(--text-dim); font-weight: 600; text-transform: uppercase;">Servers:</span>
${u||'<span style="font-size: 11px; color: var(--text-dim);">None</span>'}
</div>
</div>
<div style="display: flex; gap: 8px; align-items: center;">
${p?`
<button class="btn btn-ghost" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.setActiveProfile(null)">
Deselect
</button>
`:`
<button class="btn btn-primary" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.setActiveProfile('${M(o)}')">
Activate in UI
</button>
`}
<button class="btn btn-ghost" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.openEditProfileModal('${M(o)}')">
✏️ Edit
</button>
<button class="btn btn-danger" style="padding: 4px 10px; font-size: 11.5px;" onclick="window.app.deleteProfile('${M(o)}')">
Remove
</button>
</div>
</div>
</div>
`}).join("");return`
<!-- Sub-header & Actions -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<div style="font-size: 12px; color: var(--text-dim);">
Define named subsets of servers for task-specific agent interactions, dynamic per-request switching, and scoped ETag caching.
</div>
<div style="display: flex; gap: 8px;">
<button class="btn btn-primary" onclick="window.app.openAddProfileModal()">+ Create Profile</button>
</div>
</div>
<!-- Quick Info Box -->
<div class="bento-card" style="margin-bottom: 18px; background: rgba(245, 158, 11, 0.03); border: 1px solid rgba(245, 158, 11, 0.15);">
<div style="display: flex; gap: 12px; align-items: center;">
<span style="font-size: 20px;">\uD83D\uDCA1</span>
<div style="font-size: 12px; color: var(--text-muted); line-height: 1.5;">
HTTP clients can select profiles dynamically using the <code style="color: var(--amber-400);">X-Warmplane-Profile: <name></code> header or <code style="color: var(--amber-400);">?profile=<name></code> query parameter. MCP stdio clients can pass <code style="color: var(--amber-400);">--profile <name></code>.
</div>
</div>
</div>
${n}
`}function M(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}var N=[{id:"github",name:"GitHub",category:"devtools",description:"Explore repositories, issues, pull requests, branches, and commit histories.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-github"],envFields:[{key:"GITHUB_PERSONAL_ACCESS_TOKEN",label:"GitHub Personal Access Token",placeholder:"ghp_...",required:!0,description:"Classic or fine-grained token with repo scope."}]},{id:"git",name:"Git (Local)",category:"devtools",description:"Read local Git repository status, diffs, log histories, and commit changes.",badge:"Official / uvx",command:"uvx",defaultArgs:["mcp-server-git","--repository","."],argsPlaceholder:"mcp-server-git --repository /path/to/repo",envFields:[]},{id:"filesystem",name:"Filesystem",category:"devtools",description:"Secure, sandboxed access to local files and directories for AI workflows.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-filesystem","."],argsPlaceholder:"-y @modelcontextprotocol/server-filesystem /allowed/dir1 /allowed/dir2",envFields:[]},{id:"memory",name:"Memory Graph",category:"devtools",description:"Persistent knowledge-graph based memory for multi-turn agent learning.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-memory"],envFields:[]},{id:"chrome-devtools",name:"Chrome DevTools",category:"devtools",description:"Inspect live DOM, execute scripts, read console logs, and capture network traces in Chrome.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-puppeteer"],envFields:[]},{id:"sentry",name:"Sentry",category:"devtools",description:"Query production error events, stack traces, and issue frequencies directly from Sentry.",badge:"uvx / Telemetry",command:"uvx",defaultArgs:["mcp-server-sentry"],envFields:[{key:"SENTRY_AUTH_TOKEN",label:"Sentry Auth Token",placeholder:"sntrys_...",required:!0}]},{id:"playwright",name:"Playwright Browser",category:"browser",description:"Headless / headed browser automation for scraping, form filling, and UI interaction.",badge:"Popular #1 / npx",command:"npx",defaultArgs:["-y","@executeautomation/playwright-mcp-server"],envFields:[]},{id:"puppeteer",name:"Puppeteer",category:"browser",description:"Official browser automation server for web page scraping and screenshot capture.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-puppeteer"],envFields:[]},{id:"brave-search",name:"Brave Search",category:"browser",description:"Real-time privacy-preserving web search and local point-of-interest query engine.",badge:"Official / Search",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-brave-search"],envFields:[{key:"BRAVE_API_KEY",label:"Brave Search API Key",placeholder:"BSA...",required:!0}]},{id:"tavily",name:"Tavily Search",category:"browser",description:"AI-optimized web search engine structured specifically for LLM context injection.",badge:"Community / Stdio",command:"npx",defaultArgs:["-y","@tavily/mcp-server"],envFields:[{key:"TAVILY_API_KEY",label:"Tavily API Key",placeholder:"tvly-...",required:!0}]},{id:"fetch",name:"Fetch / Web Markdown",category:"browser",description:"Download web pages, strip clutter, and convert raw HTML to clean markdown text.",badge:"Official / uvx",command:"uvx",defaultArgs:["mcp-server-fetch"],envFields:[]},{id:"postgres",name:"PostgreSQL",category:"database",description:"Read schemas, inspect tables, and execute SQL queries against PostgreSQL databases.",badge:"Official / Database",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-postgres","postgresql://user:pass@localhost:5432/mydb"],argsPlaceholder:"-y @modelcontextprotocol/server-postgres postgresql://user:pass@localhost:5432/dbname",envFields:[]},{id:"sqlite",name:"SQLite",category:"database",description:"Local embedded SQLite query runner and schema inspector.",badge:"Official / uvx",command:"uvx",defaultArgs:["mcp-server-sqlite","--db-path","./app.db"],argsPlaceholder:"mcp-server-sqlite --db-path /path/to/database.sqlite",envFields:[]},{id:"supabase",name:"Supabase",category:"database",description:"Query database tables, manage auth policies, and inspect storage in Supabase.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@supabase/mcp-server"],envFields:[{key:"SUPABASE_ACCESS_TOKEN",label:"Supabase Personal Access Token",placeholder:"sbp_...",required:!0},{key:"SUPABASE_PROJECT_REF",label:"Supabase Project Reference ID",placeholder:"abcdefghijklmnop",required:!1}]},{id:"redis",name:"Redis",category:"database",description:"Inspect cached keys, hash sets, lists, TTLs, and pub/sub channels in Redis.",badge:"uvx / Key-Value",command:"uvx",defaultArgs:["mcp-server-redis","--url","redis://localhost:6379"],argsPlaceholder:"mcp-server-redis --url redis://localhost:6379",envFields:[]},{id:"s3",name:"AWS S3 / Cloud Storage",category:"database",description:"Browse S3 buckets, fetch object metadata, and download files from cloud storage.",badge:"uvx / Cloud Storage",command:"uvx",defaultArgs:["mcp-server-s3","--bucket","my-bucket-name"],argsPlaceholder:"mcp-server-s3 --bucket bucket-name --region us-east-1",envFields:[{key:"AWS_ACCESS_KEY_ID",label:"AWS Access Key ID",placeholder:"AKIA...",required:!0},{key:"AWS_SECRET_ACCESS_KEY",label:"AWS Secret Access Key",placeholder:"...",required:!0},{key:"AWS_REGION",label:"AWS Region",placeholder:"us-east-1",required:!1}]},{id:"linear",name:"Linear",category:"productivity",description:"Search, create, and triage Linear issues, cycles, teams, and project roadmaps.",badge:"Productivity / Stdio",command:"npx",defaultArgs:["-y","mcp-linear"],envFields:[{key:"LINEAR_API_KEY",label:"Linear API Key",placeholder:"lin_api_...",required:!0}]},{id:"slack",name:"Slack",category:"productivity",description:"Read channels, post messages, inspect threads, and search team discussions.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-slack"],envFields:[{key:"SLACK_BOT_TOKEN",label:"Slack Bot User Token",placeholder:"xoxb-...",required:!0},{key:"SLACK_TEAM_ID",label:"Slack Team ID",placeholder:"T01234567",required:!0}]},{id:"notion",name:"Notion",category:"productivity",description:"Search Notion workspace pages, read nested blocks, and query database entries.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-notion"],envFields:[{key:"NOTION_API_KEY",label:"Notion Internal Integration Token",placeholder:"secret_...",required:!0}]},{id:"jira",name:"Jira / Atlassian",category:"productivity",description:"Manage Jira issues, search JQL, read sprint statuses, and inspect boards.",badge:"uvx / Atlassian",command:"uvx",defaultArgs:["mcp-server-jira","--url","https://your-domain.atlassian.net","--email","user@example.com"],argsPlaceholder:"mcp-server-jira --url https://org.atlassian.net --email me@org.com",envFields:[{key:"JIRA_API_TOKEN",label:"Atlassian API Token",placeholder:"ATATT3...",required:!0}]},{id:"google-drive",name:"Google Drive",category:"productivity",description:"Search, list, and read documents, spreadsheets, and drive files.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-gdrive"],envFields:[{key:"GOOGLE_APPLICATION_CREDENTIALS",label:"Google Credentials JSON Path",placeholder:"/path/to/credentials.json",required:!0}]},{id:"docker",name:"Docker",category:"cloud",description:"Inspect running containers, tail container logs, list images, and manage compose services.",badge:"uvx / DevOps",command:"uvx",defaultArgs:["mcp-server-docker"],envFields:[]},{id:"kubernetes",name:"Kubernetes (K8s)",category:"cloud",description:"Query cluster pods, services, deployment status, and inspect Kubernetes logs.",badge:"Official / Stdio",command:"npx",defaultArgs:["-y","@modelcontextprotocol/server-kubernetes"],envFields:[{key:"KUBECONFIG",label:"Kubeconfig File Path (Optional)",placeholder:"~/.kube/config",required:!1}]},{id:"cloudflare",name:"Cloudflare",category:"cloud",description:"Manage Cloudflare Workers, KV namespaces, D1 databases, Vectorize indexes, and DNS.",badge:"Official / Cloudflare",command:"npx",defaultArgs:["-y","@cloudflare/mcp-server-cloudflare"],envFields:[{key:"CLOUDFLARE_API_TOKEN",label:"Cloudflare API Token",placeholder:"...",required:!0},{key:"CLOUDFLARE_ACCOUNT_ID",label:"Cloudflare Account ID",placeholder:"...",required:!0}]},{id:"terraform",name:"Terraform",category:"cloud",description:"Inspect Terraform state files, resource dependency graphs, and plan previews.",badge:"uvx / IaC",command:"uvx",defaultArgs:["mcp-server-terraform"],envFields:[]}];class X{activeTemplateCategory="all";activeTemplateFilter="";selectedTemplate=null;async init(){let e=window.location.port?`:${window.location.port}`:"",t=document.getElementById("daemon-port-label");if(t)t.textContent=`Daemon ${e}`;await this.refreshData(),this.initSSE(),this.render(),l.subscribe(()=>{this.render()})}auditSearchTimeout=null;async refreshData(){try{let e=l.getState(),t=e.auditFilters,a=e.activeProfile||void 0,[i,r,s,n,o,d,p,u,g]=await Promise.all([f.getConfig(),f.listCapabilities(a),f.listResources(a),f.listPrompts(a),f.getCatalogEvents(),f.listApprovals(),f.listTasks(),f.listAuditEvents({server_id:t.serverId!=="all"?t.serverId:void 0,event_type:t.eventType!=="all"?t.eventType:void 0,status:t.status!=="all"?t.status:void 0,search:t.search.trim()?t.search.trim():void 0,limit:t.limit,offset:t.offset}),f.getAuditStats()]);if(i.ok)l.setState({configPath:i.config_path,config:i.config,serverStatuses:i.server_statuses||{},circuitBreakers:i.circuit_breakers||[],metrics:{totalCatalogRequests:i.metrics?.total_catalog_requests||0,totalEtagHits:i.metrics?.total_etag_hits||0,totalToolCalls:i.metrics?.total_tool_calls||0,totalToolDurationUs:i.metrics?.total_tool_duration_us||0}});if(r&&Array.isArray(r.capabilities))l.setState({capabilities:r.capabilities});if(s&&Array.isArray(s.resources))l.setState({resources:s.resources});if(n&&Array.isArray(n.prompts))l.setState({prompts:n.prompts});if(o&&Array.isArray(o.events))l.setState({catalogEvents:o.events});if(d&&Array.isArray(d.approvals))l.setState({approvals:d.approvals});if(p&&Array.isArray(p.tasks))l.setState({tasks:p.tasks});if(u&&Array.isArray(u.events))l.setState({auditEvents:u.events,auditTotal:u.total??u.events.length});if(g&&g.ok)l.setState({auditStats:g})}catch(e){console.error("Failed to fetch daemon state:",e)}}async refreshAuditEvents(){try{let t=l.getState().auditFilters,[a,i]=await Promise.all([f.listAuditEvents({server_id:t.serverId!=="all"?t.serverId:void 0,event_type:t.eventType!=="all"?t.eventType:void 0,status:t.status!=="all"?t.status:void 0,search:t.search.trim()?t.search.trim():void 0,limit:t.limit,offset:t.offset}),f.getAuditStats()]);if(a&&Array.isArray(a.events))l.setState({auditEvents:a.events,auditTotal:a.total??a.events.length});if(i&&i.ok)l.setState({auditStats:i})}catch(e){console.error("Failed to refresh audit events:",e)}}handleAuditSearchInput(e){let a={...l.getState().auditFilters,search:e,offset:0};l.setState({auditFilters:a}),clearTimeout(this.auditSearchTimeout),this.auditSearchTimeout=setTimeout(()=>{this.refreshAuditEvents()},250)}handleAuditStatusFilter(e){let t=l.getState();l.setState({auditFilters:{...t.auditFilters,status:e,offset:0}}),this.refreshAuditEvents()}handleAuditEventTypeFilter(e){let t=l.getState();l.setState({auditFilters:{...t.auditFilters,eventType:e,offset:0}}),this.refreshAuditEvents()}handleAuditServerFilter(e){let t=l.getState();l.setState({auditFilters:{...t.auditFilters,serverId:e,offset:0}}),this.refreshAuditEvents()}handleAuditPageSize(e){let t=parseInt(e,10)||25,a=l.getState();l.setState({auditFilters:{...a.auditFilters,limit:t,offset:0}}),this.refreshAuditEvents()}clearAuditFilters(){let e=l.getState();l.setState({auditFilters:{search:"",status:"all",eventType:"all",serverId:"all",limit:e.auditFilters.limit||25,offset:0}}),this.refreshAuditEvents()}auditPrevPage(){let e=l.getState(),{limit:t,offset:a}=e.auditFilters,i=Math.max(0,a-t);if(i!==a)l.setState({auditFilters:{...e.auditFilters,offset:i}}),this.refreshAuditEvents()}auditNextPage(){let e=l.getState(),{limit:t,offset:a}=e.auditFilters,i=e.auditTotal;if(a+t<i)l.setState({auditFilters:{...e.auditFilters,offset:a+t}}),this.refreshAuditEvents()}auditGoToPage(e){let t=l.getState(),{limit:a}=t.auditFilters,i=Math.max(0,(e-1)*a);l.setState({auditFilters:{...t.auditFilters,offset:i}}),this.refreshAuditEvents()}selectAuditEvent(e){if(!e){l.setState({auditSelectedEvent:null});return}let a=l.getState().auditEvents.find((i)=>i.id===e)||null;l.setState({auditSelectedEvent:a})}async verifyAuditChain(){try{let e=await f.verifyAuditChain();if(e&&e.report)l.setState({auditVerification:e.report})}catch(e){console.error("Failed to verify audit chain:",e)}}async refreshApprovals(){try{let e=await f.listApprovals();if(e&&Array.isArray(e.approvals))l.setState({approvals:e.approvals})}catch(e){console.error("Failed to refresh approvals:",e)}}initSSE(){try{let e=new EventSource("/v1/resources/updates");e.onmessage=(t)=>{l.addEventLog("SSE","/v1/resources/updates","UPDATED","0.1ms"),this.refreshData()}}catch(e){console.warn("SSE connection unavailable")}}switchTab(e){l.setState({activeTab:e}),this.refreshData()}render(){let e=l.getState(),t=document.getElementById("app-main");if(!t)return;let a=(e.tasks||[]).filter((d)=>d.status==="input_required").length,i=(e.approvals||[]).filter((d)=>d.status==="pending").length,r=Math.max(a,i),s=document.getElementById("nav-approvals-badge");if(s)s.textContent=r>0?`${r}`:"",s.style.display=r>0?"inline-block":"none";document.querySelectorAll(".nav-item").forEach((d)=>{let p=d.getAttribute("data-tab");if(p===e.activeTab||e.activeTab==="tasks"&&p==="approvals"||e.activeTab==="approvals"&&p==="tasks")d.classList.add("active");else d.classList.remove("active")});let n=document.getElementById("top-title"),o={overview:"Overview Cockpit",servers:"Server Hub & Connections",playground:"MCP Capability Playground",tasks:"SEP-2663 Tasks & HITL Review",approvals:"SEP-2663 Tasks & HITL Review",audit:"WORM Audit & Compliance Ledger",policy:"Security Governance & Redaction",aliases:"Facade & Alias Studio",profiles:"Server Constellation Profiles"};if(n)n.textContent=o[e.activeTab]||"Control Deck";switch(this.renderTopProfileSelector(),e.activeTab){case"overview":t.innerHTML=U();break;case"servers":t.innerHTML=J();break;case"playground":t.innerHTML=K();break;case"tasks":case"approvals":t.innerHTML=G(e);break;case"audit":t.innerHTML=V();break;case"policy":t.innerHTML=W();break;case"aliases":t.innerHTML=Q();break;case"profiles":t.innerHTML=Y();break}}async refreshTasks(){try{let e=await f.listTasks();if(e&&Array.isArray(e.tasks))l.setState({tasks:e.tasks})}catch(e){console.error("Failed to refresh tasks:",e)}}filterTasksByStatus(e){l.setState({taskFilterStatus:e})}togglePlaygroundAsyncTask(e){l.setState({playgroundAsyncTask:e})}async submitTaskInputResponses(e){let a=l.getState().tasks.find((s)=>s.taskId===e)?.inputRequests||{},i=Object.keys(a),r={};if(i.length>0)for(let s of i){let n=document.getElementById(`task-input-${e}-${s}`);if(n){let o=n.value.trim();try{r[s]=JSON.parse(o)}catch{r[s]=o}}}else{let s=document.getElementById(`task-raw-input-${e}`);if(s&&s.value.trim())try{Object.assign(r,JSON.parse(s.value.trim()))}catch{alert("Invalid JSON in raw input responses");return}}try{let s=await f.updateTask(e,r);if(s.ok)await this.refreshTasks();else alert(`Task update failed: ${s.error?.message||s.error||"Unknown error"}`)}catch(s){alert(`Error updating task: ${s.message}`)}}async promptCancelTask(e){let t=prompt("Reason for cancelling task:");if(t===null)return;try{let a=await f.cancelTask(e,t||void 0);if(a.ok)await this.refreshTasks();else alert(`Task cancellation failed: ${a.error?.message||a.error||"Unknown error"}`)}catch(a){alert(`Error cancelling task: ${a.message}`)}}async inspectTaskDetails(e){try{let t=await f.getTask(e);if(t.ok&&t.task)alert(`Task [${t.task.taskId}]
Status: ${t.task.status}
Progress: ${Math.round((t.task.progress||0)*100)}%
Payload: ${JSON.stringify(t.task.result||t.task.error||t.task.inputRequests||{},null,2)}`)}catch(t){alert(`Failed to fetch task: ${t.message}`)}}async submitApproval(e){let t=document.getElementById(`appr-operator-${e}`),a=document.getElementById(`appr-args-${e}`),i=t?.value.trim()||"security-operator",r=void 0;if(a&&a.value.trim())try{r=JSON.parse(a.value.trim())}catch{alert("Invalid JSON in arguments editor");return}let s=await f.approveTicket(e,i,r);if(s.ok)await this.refreshApprovals(),await this.refreshTasks();else alert(`Approval failed: ${s.error||"Unknown error"}`)}async promptReject(e){let t=prompt("Reason for rejection (will be returned to the calling agent):");if(t===null)return;let i=document.getElementById(`appr-operator-${e}`)?.value.trim()||"security-operator",r=await f.rejectTicket(e,i,t);if(r.ok)await this.refreshApprovals(),await this.refreshTasks();else alert(`Rejection failed: ${r.error||"Unknown error"}`)}setPlaygroundMode(e){l.setState({playgroundMode:e})}selectCapability(e){l.setState({selectedCapabilityId:e});let t=l.getState().capabilities.find((i)=>i.id===e),a=document.getElementById("pg-args-input");if(t){let i=O(t.input_schema,!1),r=JSON.stringify(i,null,2);if(a)a.value=r;let s={...l.getState().playgroundArgs||{}};s[e]=r,l.getState().playgroundArgs=s}}selectResource(e){l.setState({selectedResourceId:e})}selectPrompt(e){l.setState({selectedPromptId:e})}filterResources(e){let t=e.toLowerCase().trim(),i=(l.getState().resources||[]).filter((s)=>s.id.toLowerCase().includes(t)||s.name&&s.name.toLowerCase().includes(t)||s.uri&&s.uri.toLowerCase().includes(t)||s.server&&s.server.toLowerCase().includes(t)),r=document.getElementById("pg-res-list");if(r)if(i.length===0)r.innerHTML=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No resources match "${y(e)}"
</div>
`;else r.innerHTML=i.map((s)=>{let n=s.id===l.getState().selectedResourceId?"active":"",o=s.uri?s.uri.split(":")[0]:"res";return`
<div class="cap-item ${n}" onclick="window.app.selectResource('${y(s.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${y(s.name||s.id)}</span>
<span class="badge" style="font-size: 9.5px; background: rgba(56, 189, 248, 0.15); color: var(--cyan-400);">${y(o)}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${y(s.uri)}</div>
<div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-muted); margin-top: 4px;">
<span>server: ${y(s.server||"local")}</span>
<span>${y(s.mime_type||"text/plain")}</span>
</div>
</div>
`}).join("")}filterPrompts(e){let t=e.toLowerCase().trim(),i=(l.getState().prompts||[]).filter((s)=>s.id.toLowerCase().includes(t)||s.name&&s.name.toLowerCase().includes(t)||s.description&&s.description.toLowerCase().includes(t)||s.server&&s.server.toLowerCase().includes(t)),r=document.getElementById("pg-prompt-list");if(r)if(i.length===0)r.innerHTML=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No prompts match "${y(e)}"
</div>
`;else r.innerHTML=i.map((s)=>{let n=s.id===l.getState().selectedPromptId?"active":"",o=s.arguments?s.arguments.length:0;return`
<div class="cap-item ${n}" onclick="window.app.selectPrompt('${y(s.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${y(s.name||s.id)}</span>
<span class="badge" style="font-size: 9.5px; background: rgba(168, 85, 247, 0.15); color: var(--purple-400);">${o} args</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">${y(s.description||s.title||"Prompt template")}</div>
<div style="font-size: 10px; color: var(--text-muted); margin-top: 4px;">server: ${y(s.server||"local")}</div>
</div>
`}).join("")}updatePlaygroundArgs(e){let t=l.getState(),a=t.selectedCapabilityId||(t.capabilities[0]?t.capabilities[0].id:null);if(!a)return;let i={...t.playgroundArgs||{}};i[a]=e,t.playgroundArgs=i}fillPlaygroundSampleArgs(e=!1){let t=l.getState(),a=t.selectedCapabilityId||(t.capabilities[0]?t.capabilities[0].id:null),i=t.capabilities.find((o)=>o.id===a),r=document.getElementById("pg-args-input");if(!r)return;if(!i||!i.input_schema){if(r.value="{}",a){let o={...t.playgroundArgs||{}};o[a]="{}",t.playgroundArgs=o}return}let s=O(i.input_schema,e),n=JSON.stringify(s,null,2);if(r.value=n,a){let o={...t.playgroundArgs||{}};o[a]=n,t.playgroundArgs=o}}formatPlaygroundArgs(){let e=l.getState(),t=e.selectedCapabilityId||(e.capabilities[0]?e.capabilities[0].id:null),a=document.getElementById("pg-args-input");if(a)try{let i=JSON.parse(a.value||"{}"),r=JSON.stringify(i,null,2);if(a.value=r,t){let s={...e.playgroundArgs||{}};s[t]=r,e.playgroundArgs=s}}catch(i){alert(`Cannot format JSON: ${i.message}`)}}insertPlaygroundArgKey(e,t,a){let i=l.getState(),r=i.selectedCapabilityId||(i.capabilities[0]?i.capabilities[0].id:null),s=document.getElementById("pg-args-input");if(s){let n={};try{n=JSON.parse(s.value||"{}")}catch{n={}}if(n[e]===void 0)if(a!==null&&a!==void 0)n[e]=a;else switch(t){case"string":n[e]=`sample_${e}`;break;case"number":case"integer":n[e]=0;break;case"boolean":n[e]=!0;break;case"array":n[e]=[];break;case"object":n[e]={};break;default:n[e]=`sample_${e}`}let o=JSON.stringify(n,null,2);if(s.value=o,r){let d={...i.playgroundArgs||{}};d[r]=o,i.playgroundArgs=d}}}fillBatchStepSampleArgs(e){let t=l.getState(),a=[...t.batchSteps||[]],i=a[e];if(!i||!i.capability_id)return;let r=t.capabilities.find((d)=>d.id===i.capability_id);if(!r||!r.input_schema)return;let s=r.input_schema.properties||{},n={};for(let[d,p]of Object.entries(s))if(p.default!==void 0)n[d]=p.default;else if(Array.isArray(p.enum)&&p.enum.length>0)n[d]=p.enum[0];else switch(p.type||"string"){case"string":n[d]=`sample_${d}`;break;case"number":case"integer":n[d]=0;break;case"boolean":n[d]=!0;break;case"array":n[d]=[];break;case"object":n[d]={};break;default:n[d]=`sample_${d}`}let o=JSON.stringify(n,null,2);a[e]={...a[e],argsJson:o},l.setState({batchSteps:a})}filterCapabilities(e){let t=e.toLowerCase().trim(),i=l.getState().capabilities.filter((s)=>s.id.toLowerCase().includes(t)||s.summary&&s.summary.toLowerCase().includes(t)||s.server&&s.server.toLowerCase().includes(t)),r=document.getElementById("pg-cap-list");if(r)if(i.length===0)r.innerHTML=`
<div style="padding: 24px 16px; text-align: center; color: var(--text-dim); font-size: 11.5px;">
No capabilities match "${y(e)}"
</div>
`;else r.innerHTML=i.map((s)=>`
<div class="cap-item ${s.id===l.getState().selectedCapabilityId?"active":""}" onclick="window.app.selectCapability('${y(s.id)}')">
<div style="display: flex; justify-content: space-between; align-items: center;">
<span style="font-weight: 600; color: var(--text-main); font-family: var(--ff-mono); font-size: 12px;">${y(s.id)}</span>
<span style="font-size: 10px; color: var(--green-400);">${y(s.mode||"read")}</span>
</div>
<div style="font-size: 11px; color: var(--text-dim); margin-top: 2px;">server: ${y(s.server||"local")}</div>
</div>
`).join("")}async executePlaygroundTool(){let e=l.getState(),t=e.selectedCapabilityId||(e.capabilities[0]?e.capabilities[0].id:null);if(!t)return;let a=document.getElementById("pg-args-input")?.value||"{}",i=document.getElementById("pg-context-input")?.value||void 0,r=document.getElementById("pg-jsonpath-input")?.value.trim()||void 0,s=document.getElementById("pg-limit-lines-input")?.value.trim()||void 0,n=document.getElementById("pg-truncate-bytes-input")?.value.trim()||void 0,o={};try{o=JSON.parse(a)}catch{alert("Invalid arguments JSON object");return}if(r)o._jsonpath=r;if(s&&!isNaN(Number(s)))o._limit_lines=Number(s);if(n&&!isNaN(Number(n)))o._truncate_bytes=Number(n);let d=`op-${Date.now()}`;l.setState({isExecuting:!0,activeRequestId:d});let p=e.activeProfile||void 0,u=e.playgroundAsyncTask||!1;try{let g=await f.callCapability({capability_id:t,args:o,request_id:d,async_task:u?!0:void 0,context:{operation_id:i||d}},p);if(l.setState({isExecuting:!1,activeRequestId:null,executionResult:{status:g.status,durationMs:g.durationMs,data:g.data}}),g.status===202||g.data?.resultType==="task")this.refreshTasks();l.addEventLog("POST",`/v1/tools/call → ${t}`,g.status===200?"200 OK":`HTTP ${g.status}`,`${g.durationMs.toFixed(1)}ms`),f.getConfig().then((m)=>{if(m.ok&&m.circuit_breakers)l.setState({circuitBreakers:m.circuit_breakers})})}catch(g){l.setState({isExecuting:!1,activeRequestId:null,executionResult:{status:500,durationMs:0,data:{error:g.toString()}}})}}openBatchModal(){let e=l.getState(),t=e.batchSteps;if(!t||t.length===0)t=[{id:"step_1",capability_id:e.selectedCapabilityId||(e.capabilities[0]?e.capabilities[0].id:""),argsJson:"{}",continue_on_error:!1},{id:"step_2",capability_id:"",argsJson:"{}",continue_on_error:!0}],l.setState({batchSteps:t});l.setState({isBatchModalOpen:!0})}closeBatchModal(){l.setState({isBatchModalOpen:!1})}addBatchStep(){let t=[...l.getState().batchSteps||[]],a=t.length+1;t.push({id:`step_${a}`,capability_id:"",argsJson:"{}",continue_on_error:!1}),l.setState({batchSteps:t})}removeBatchStep(e){let a=[...l.getState().batchSteps||[]];if(a.length<=1){alert("Pipeline must contain at least one execution step.");return}a.splice(e,1);let i=a.map((r,s)=>({...r,id:`step_${s+1}`}));l.setState({batchSteps:i})}updateBatchStepCapability(e,t){let i=[...l.getState().batchSteps||[]];if(i[e])i[e]={...i[e],capability_id:t},l.setState({batchSteps:i})}updateBatchStepContinueOnError(e,t){let i=[...l.getState().batchSteps||[]];if(i[e])i[e]={...i[e],continue_on_error:t},l.setState({batchSteps:i})}updateBatchStepArgs(e,t){let a=l.getState(),i=[...a.batchSteps||[]];if(i[e])i[e]={...i[e],argsJson:t},a.batchSteps[e].argsJson=t}appendBatchVariable(e,t){let i=[...l.getState().batchSteps||[]],r=document.getElementById(`batch-step-args-${e}`);if(r){let s=r.value,n=r.selectionStart||s.length,o=r.selectionEnd||s.length,d=s.substring(0,n)+t+s.substring(o);if(r.value=d,i[e])i[e]={...i[e],argsJson:d},l.setState({batchSteps:i})}}async executeBatchPipeline(){let e=l.getState(),t=e.batchSteps||[],a=[];for(let r=0;r<t.length;r++){let s=t[r];if(!s.capability_id){alert(`Please select a capability for Step ${r+1}`);return}let n={};try{n=JSON.parse(s.argsJson||"{}")}catch{alert(`Invalid JSON in Step ${r+1} arguments`);return}a.push({id:s.id||`step_${r+1}`,capability_id:s.capability_id,args:n,continue_on_error:s.continue_on_error})}l.setState({isBatchModalOpen:!1});let i=e.activeProfile||void 0;try{let r=await f.batchCallCapabilities(a,i);l.setState({executionResult:{status:r.status,durationMs:r.durationMs,data:r.data}}),l.addEventLog("POST",`/v1/tools/batch_call (${t.length} steps)`,r.status===200?"200 OK":`HTTP ${r.status}`,`${r.durationMs.toFixed(1)}ms`)}catch(r){l.setState({executionResult:{status:500,durationMs:0,data:{error:r.toString()}}})}}async executeReadResource(){let e=l.getState(),t=e.selectedResourceId||(e.resources[0]?e.resources[0].id:null);if(!t)return;let a=document.getElementById("pg-res-jsonpath-input")?.value.trim()||void 0,i=document.getElementById("pg-res-lines-input")?.value.trim()||void 0,r=document.getElementById("pg-res-bytes-input")?.value.trim()||void 0,s={resource_id:t};if(a)s._jsonpath=a;if(i&&!isNaN(Number(i)))s._limit_lines=Number(i);if(r&&!isNaN(Number(r)))s._truncate_bytes=Number(r);let n=e.activeProfile||void 0;try{let o=await f.readResource({resource_id:t,input_responses:s},n);l.setState({resourceReadResult:{status:o.status,durationMs:o.durationMs,data:o.data}}),l.addEventLog("POST",`/v1/resources/read → ${t}`,o.status===200?"200 OK":`HTTP ${o.status}`,`${o.durationMs.toFixed(1)}ms`)}catch(o){l.setState({resourceReadResult:{status:500,durationMs:0,data:{error:o.toString()}}})}}async executeGetPrompt(){let e=l.getState(),t=e.selectedPromptId||(e.prompts[0]?e.prompts[0].id:null);if(!t)return;let a=document.querySelectorAll(".prompt-arg-input"),i={};a.forEach((s)=>{let n=s,o=n.getAttribute("data-arg-name");if(o&&n.value.trim())i[o]=n.value.trim()});let r=e.activeProfile||void 0;try{let s=await f.getPrompt({prompt_id:t,arguments:i},r);l.setState({promptGetResult:{status:s.status,durationMs:s.durationMs,data:s.data}}),l.addEventLog("POST",`/v1/prompts/get → ${t}`,s.status===200?"200 OK":`HTTP ${s.status}`,`${s.durationMs.toFixed(1)}ms`)}catch(s){l.setState({promptGetResult:{status:500,durationMs:0,data:{error:s.toString()}}})}}toggleBatchPlayground(){let e=document.getElementById("pg-args-input");if(!e)return;let t=[{id:"step_1",capability_id:"sqlite.read_query",args:{query:"SELECT * FROM users LIMIT 2"}},{id:"step_2",capability_id:"github.issues.search",args:{query:"label:bug"},continue_on_error:!0}];e.value=JSON.stringify(t,null,2)}async submitPolicyRule(e){let t=e==="allow"?"policy-new-allow":e==="deny"?"policy-new-deny":"policy-new-redact",a=document.getElementById(t);if(!a)return;let i=a.value.trim();if(!i)return;await this.addPolicyRule(e,i),a.value=""}async addPolicyRule(e,t){let a=(t||"").trim();if(!a)return;let r=l.getState().config.policy||{},s=[...r.allow||[]],n=[...r.deny||[]],o=[...r.redact_keys||r.redactKeys||[]];if(e==="allow"&&!s.includes(a))s.push(a);if(e==="deny"&&!n.includes(a))n.push(a);if(e==="redact"&&!o.includes(a))o.push(a);let d=await f.savePolicy({allow:s,deny:n,redact_keys:o,redactKeys:o});if(!d.ok)alert(`Failed to save policy rule: ${d.error||"Unknown error"}`);await this.refreshData()}async removePolicyRule(e,t){let i=l.getState().config.policy||{},r=[...i.allow||[]],s=[...i.deny||[]],n=[...i.redact_keys||i.redactKeys||[]];if(e==="allow")r.splice(t,1);if(e==="deny")s.splice(t,1);if(e==="redact")n.splice(t,1);let o=await f.savePolicy({allow:r,deny:s,redact_keys:n,redactKeys:n});if(!o.ok)alert(`Failed to update policy: ${o.error||"Unknown error"}`);await this.refreshData()}testPolicySandbox(e){let t=document.getElementById("policy-test-verdict");if(!t)return;let a=e.trim();if(!a){t.textContent="ENTER ID",t.style.color="var(--text-dim)";return}let r=l.getState().config.policy||{},s=r.deny||[],n=r.allow||[],o=(d,p)=>{if(d==="*")return!0;if(d.endsWith("*"))return p.startsWith(d.slice(0,-1));return d===p};if(s.some((d)=>o(d,a))){t.textContent="DENIED (Strict Block)",t.style.color="var(--red-400)";return}if(n.length>0&&!n.some((d)=>o(d,a))){t.textContent="DENIED (Not in Allow List)",t.style.color="var(--red-400)";return}t.textContent="ALLOWED",t.style.color="var(--green-400)"}async deleteServer(e){if(!confirm(`Are you sure you want to remove server '${e}' from config?`))return;await f.deleteServer(e),await this.refreshData()}openAddServerModal(){this.closeModals();let e=document.getElementById("modal-srv-title"),t=document.getElementById("modal-srv-template-banner"),a=document.getElementById("modal-srv-name"),i=document.getElementById("modal-srv-transport"),r=document.getElementById("modal-srv-command"),s=document.getElementById("modal-srv-url"),n=document.getElementById("modal-srv-ft"),o=document.getElementById("modal-srv-cd"),d=document.getElementById("modal-srv-autorestart"),p=document.getElementById("modal-srv-maxrestarts");if(e)e.textContent="Add Upstream MCP Server";if(t)t.style.display="flex";if(a)a.value="",a.disabled=!1;if(i)i.value="stdio";if(r)r.value="";if(s)s.value="";let u=document.getElementById("modal-group-cmd"),g=document.getElementById("modal-group-url");if(u)u.style.display="block";if(g)g.style.display="none";if(n)n.value="3";if(o)o.value="30000";if(d)d.value="true";if(p)p.value="5";let m=document.getElementById("modal-add-server");if(m)m.classList.add("active")}openEditServerModal(e){this.closeModals();let t=l.getState(),a=t.config.mcpServers?.[e];if(!a){alert(`Server '${e}' not found in configuration.`);return}let i=document.getElementById("modal-srv-title"),r=document.getElementById("modal-srv-template-banner"),s=document.getElementById("modal-srv-name"),n=document.getElementById("modal-srv-transport"),o=document.getElementById("modal-srv-command"),d=document.getElementById("modal-srv-url"),p=document.getElementById("modal-srv-ft"),u=document.getElementById("modal-srv-cd"),g=document.getElementById("modal-srv-autorestart"),m=document.getElementById("modal-srv-maxrestarts");if(i)i.textContent=`Edit Server '${e}'`;if(r)r.style.display="none";if(s)s.value=e,s.disabled=!0;let c=!!a.command;if(n)n.value=c?"stdio":"http";let h=document.getElementById("modal-group-cmd"),k=document.getElementById("modal-group-url");if(h)h.style.display=c?"block":"none";if(k)k.style.display=c?"none":"block";if(o)o.value=c?`${a.command} ${(a.args||[]).join(" ")}`.trim():"";if(d)d.value=a.url||"";let T=a.resilience||t.config.resilience;if(p)p.value=String(T?.failureThreshold??3);if(u)u.value=String(T?.cooldownMs??30000);if(g)g.value=T?.autoRestart===!1?"false":"true";if(m)m.value=String(T?.maxRestarts??5);let E=document.getElementById("modal-add-server");if(E)E.classList.add("active")}async submitAddServer(){let e=document.getElementById("modal-srv-name"),t=e?.value.trim(),a=document.getElementById("modal-srv-transport")?.value;if(!t){alert("Server name is required");return}if(e&&!e.disabled){if((l.getState().config.mcpServers||{})[t]){if(!confirm(`Server '${t}' already exists in configuration. Do you want to overwrite it?`))return}}let i={};if(a==="stdio"){let u=(document.getElementById("modal-srv-command")?.value.trim()).split(/\s+/).filter(Boolean);if(u.length===0){alert("Command is required");return}i.command=u[0],i.args=u.slice(1)}else{let p=document.getElementById("modal-srv-url")?.value.trim();if(!p){alert("URL is required");return}i.url=p}let r=document.getElementById("modal-srv-ft")?.value.trim(),s=document.getElementById("modal-srv-cd")?.value.trim(),n=document.getElementById("modal-srv-autorestart")?.value,o=document.getElementById("modal-srv-maxrestarts")?.value.trim();if(r||s||n||o)i.resilience={failureThreshold:r?Number(r):3,cooldownMs:s?Number(s):30000,autoRestart:n!=="false",maxRestarts:o?Number(o):5};let d=await f.upsertServer(t,i);if(d.ok)this.closeModals(),await this.refreshData();else alert(`Failed to save server: ${d.error}`)}openTemplateCatalog(){this.closeModals();let e=document.getElementById("modal-templates");if(e)e.classList.add("active");this.renderTemplateGrid()}setTemplateCategory(e){this.activeTemplateCategory=e,document.querySelectorAll(".tmpl-cat-btn").forEach((t)=>{if(t.getAttribute("data-category")===e)t.classList.add("active"),t.style.background="var(--surface-elevated)",t.style.color="var(--amber-400)";else t.classList.remove("active"),t.style.background="var(--surface-card)",t.style.color="var(--text-main)"}),this.renderTemplateGrid()}filterTemplates(e){this.activeTemplateFilter=e.toLowerCase().trim(),this.renderTemplateGrid()}renderTemplateGrid(){let e=document.getElementById("tmpl-grid");if(!e)return;let t=N.filter((r)=>{let s=this.activeTemplateCategory==="all"||r.category===this.activeTemplateCategory,n=!this.activeTemplateFilter||r.name.toLowerCase().includes(this.activeTemplateFilter)||r.id.toLowerCase().includes(this.activeTemplateFilter)||r.description.toLowerCase().includes(this.activeTemplateFilter)||r.command.toLowerCase().includes(this.activeTemplateFilter)||r.envFields.some((o)=>o.key.toLowerCase().includes(this.activeTemplateFilter));return s&&n});if(t.length===0){e.innerHTML=`
<div style="grid-column: span 2; padding: 32px; text-align: center; color: var(--text-dim);">
No matching MCP server templates found.
</div>
`;return}let i=l.getState().config.mcpServers||{};e.innerHTML=t.map((r)=>{let s=!!i[r.id],n=`${r.command} ${r.defaultArgs.join(" ")}`;return`
<div class="bento-card" style="display: flex; flex-direction: column; justify-content: space-between; padding: 14px; background: var(--surface); border: 1px solid var(--border); min-width: 0; transition: transform 0.15s, border-color 0.15s;">
<div>
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 6px;">
<div style="display: flex; align-items: center; gap: 8px; min-width: 0;">
<span style="font-weight: 700; font-size: 13.5px; color: var(--text-main); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${y(r.name)}</span>
<span class="brand-badge" style="font-size: 9.5px; padding: 1px 6px; flex-shrink: 0;">${y(r.badge)}</span>
</div>
${s?'<span style="font-size: 10px; color: var(--green-400); font-weight: 600; flex-shrink: 0;">CONNECTED</span>':""}
</div>
<div style="font-size: 11.5px; color: var(--text-muted); line-height: 1.4; margin-bottom: 8px;">
${y(r.description)}
</div>
<div style="font-family: var(--ff-mono); font-size: 10.5px; color: var(--text-dim); background: var(--surface-card); padding: 5px 8px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<code>${y(n)}</code>
</div>
${r.envFields.length>0?`
<div style="font-size: 10.5px; color: var(--amber-400); margin-top: 6px; display: flex; align-items: center; gap: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<span>⚡ Needs:</span>
<code style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${r.envFields.map((o)=>y(o.key)).join(", ")}</code>
</div>
`:""}
</div>
<div style="display: flex; justify-content: flex-end; margin-top: 12px; gap: 6px;">
<button class="btn btn-primary" style="font-size: 11.5px; padding: 4px 10px;" onclick="window.app.selectTemplate('${y(r.id)}')">
${s?"Configure Another":"✨ 1-Click Setup"}
</button>
</div>
</div>
`}).join("")}selectTemplate(e){let t=N.find((d)=>d.id===e);if(!t)return;this.selectedTemplate=t,this.closeModals();let a=document.getElementById("modal-configure-template");if(a)a.classList.add("active");let i=document.getElementById("cfg-tmpl-title"),r=document.getElementById("cfg-tmpl-desc"),s=document.getElementById("cfg-tmpl-form");if(i)i.textContent=`Configure ${t.name} Server`;if(r)r.textContent=t.description;let n=l.getState().config.mcpServers||{},o=t.id;if(n[o]){let d=2;while(n[`${t.id}-${d}`])d++;o=`${t.id}-${d}`}if(s){let d="";if(t.envFields.length>0)d=`
<div style="margin-top: 14px; margin-bottom: 6px; font-weight: 700; font-size: 11px; text-transform: uppercase; color: var(--amber-400); letter-spacing: 0.5px;">
Environment Variables & API Keys
</div>
${t.envFields.map((p)=>`
<div class="form-group">
<label class="form-label">${y(p.label)} ${p.required?'<span style="color: var(--red-400);">*</span>':"(Optional)"}</label>
<input type="password" class="form-input tmpl-env-input" data-key="${y(p.key)}" placeholder="${y(p.placeholder||"")}">
${p.description?`<div style="font-size: 10.5px; color: var(--text-dim); margin-top: 3px;">${y(p.description)}</div>`:""}
</div>
`).join("")}
`;s.innerHTML=`
<div class="form-group">
<label class="form-label">Server Identifier (Name)</label>
<input type="text" class="form-input" id="cfg-srv-id" value="${y(o)}">
<div style="font-size: 10.5px; color: var(--text-dim); margin-top: 3px;">Must be unique across all configured servers.</div>
</div>
<div class="form-group">
<label class="form-label">Command Line Arguments</label>
<input type="text" class="form-input" id="cfg-srv-args" value="${y(t.defaultArgs.join(" "))}" placeholder="${y(t.argsPlaceholder||"")}">
<div style="font-size: 10.5px; color: var(--text-dim); margin-top: 3px;">Executable: <code>${y(t.command)}</code></div>
</div>
${d}
<details style="margin-top: 14px; background: rgba(0,0,0,0.2); border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 8px 12px;">
<summary style="font-size: 11.5px; font-weight: 600; color: var(--amber-400); cursor: pointer;">
\uD83D\uDEE1️ Fault Tolerance & Process Supervision (Optional)
</summary>
<div style="margin-top: 10px; display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
<div>
<label class="form-label" style="font-size: 10.5px;">Failure Threshold</label>
<input type="number" class="form-input" id="cfg-srv-ft" placeholder="3" value="3">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Cooldown (ms)</label>
<input type="number" class="form-input" id="cfg-srv-cd" placeholder="30000" value="30000">
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Auto-Restart</label>
<select class="form-input" id="cfg-srv-autorestart">
<option value="true">Enabled (Default)</option>
<option value="false">Disabled</option>
</select>
</div>
<div>
<label class="form-label" style="font-size: 10.5px;">Max Restarts</label>
<input type="number" class="form-input" id="cfg-srv-maxrestarts" placeholder="5" value="5">
</div>
</div>
</details>
`}}async submitTemplateServer(){if(!this.selectedTemplate)return;let e=this.selectedTemplate,t=document.getElementById("cfg-srv-id")?.value.trim(),a=document.getElementById("cfg-srv-args")?.value.trim();if(!t){alert("Server identifier is required");return}if((l.getState().config.mcpServers||{})[t]){if(!confirm(`Server '${t}' already exists. Do you want to overwrite its configuration?`))return}let s=a?a.split(/\s+/).filter(Boolean):[],n={},o=document.querySelectorAll(".tmpl-env-input");for(let h of Array.from(o)){let k=h.getAttribute("data-key"),T=h.value.trim(),E=e.envFields.find((_)=>_.key===k);if(E?.required&&!T){alert(`Required field '${E.label}' is missing.`);return}if(k&&T)n[k]=T}let d={command:e.command,args:s};if(Object.keys(n).length>0)d.env=n;let p=document.getElementById("cfg-srv-ft")?.value.trim(),u=document.getElementById("cfg-srv-cd")?.value.trim(),g=document.getElementById("cfg-srv-autorestart")?.value,m=document.getElementById("cfg-srv-maxrestarts")?.value.trim();if(p||u||g||m)d.resilience={failureThreshold:p?Number(p):3,cooldownMs:u?Number(u):30000,autoRestart:g!=="false",maxRestarts:m?Number(m):5};let c=await f.upsertServer(t,d);if(c.ok)this.closeModals(),await this.refreshData();else alert(`Failed to save server: ${c.error}`)}async openImportModal(){this.closeModals();let e=document.getElementById("modal-import");if(e)e.classList.add("active");let t=document.getElementById("modal-eco-list");if(!t)return;t.innerHTML='<div style="color: var(--text-dim); padding: 12px; text-align: center;">Scanning IDE configs...</div>';try{let a=await f.getEcosystemSources();if(a.sources&&a.sources.length>0)t.innerHTML=a.sources.map((i)=>`
<label style="display: flex; align-items: center; gap: 10px; background: var(--surface); padding: 10px 14px; border-radius: var(--radius-sm); border: 1px solid var(--border); cursor: pointer;">
<input type="checkbox" class="eco-checkbox" value="${i.path}" checked>
<div>
<div style="font-weight: 600; color: var(--text-main);">${i.name}</div>
<div style="font-size: 11px; color: var(--text-dim);">${i.server_count} servers (${i.servers.join(", ")})</div>
</div>
</label>
`).join("");else t.innerHTML='<div style="color: var(--text-dim); padding: 12px; text-align: center;">No external MCP configuration files found on this system.</div>'}catch{t.innerHTML='<div style="color: var(--red-400); padding: 12px; text-align: center;">Failed to scan ecosystem sources.</div>'}}async submitImport(){let e=document.querySelectorAll(".eco-checkbox:checked");if(e.length===0){alert("No sources selected");return}for(let t of Array.from(e))await f.importConfig(t.value,!1);this.closeModals(),await this.refreshData()}handleAliasTargetInput(e){let t=document.getElementById("alias-suggestions-dropdown");if(!t)return;let a=(e||"").trim().toLowerCase();if(a.length<2){t.style.display="none";return}let r=l.getState().capabilities.filter((s)=>s.id.toLowerCase().includes(a)||s.summary&&s.summary.toLowerCase().includes(a)||s.description&&s.description.toLowerCase().includes(a)||s.server&&s.server.toLowerCase().includes(a)).slice(0,8);if(r.length===0){t.style.display="none";return}t.innerHTML=r.map((s)=>`
<div style="padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--border-subtle); display: flex; justify-content: space-between; align-items: center; transition: background 0.1s;"
onmouseover="this.style.background='var(--surface-hover)'"
onmouseout="this.style.background='transparent'"
onmousedown="window.app.selectAliasSuggestion('${y(s.id)}')">
<div>
<div style="font-weight: 700; color: var(--text-main);">${y(s.id)}</div>
<div style="font-size: 10.5px; color: var(--text-dim); margin-top: 2px;">${y(s.summary||s.description||"")}</div>
</div>
<span style="font-size: 10px; color: var(--cyan-400);">${y(s.server||"local")}</span>
</div>
`).join(""),t.style.display="block"}selectAliasSuggestion(e){let t=document.getElementById("alias-target");if(t)t.value=e;this.hideAliasDropdown()}hideAliasDropdown(){let e=document.getElementById("alias-suggestions-dropdown");if(e)e.style.display="none"}async createAlias(){let e=document.getElementById("alias-kind")?.value,t=document.getElementById("alias-name")?.value.trim(),a=document.getElementById("alias-target")?.value.trim();if(!t||!a){alert("Please provide both alias name and canonical target");return}await f.updateAlias(e,t,a),await this.refreshData()}async deleteAlias(e,t){await f.updateAlias(e,t,void 0),await this.refreshData()}async reloadFromDisk(){try{let e=await f.reloadConfig();if(e.ok){let t="Hot-reload completed successfully!";if(e.mounted&&e.mounted.length>0)t+=`
Mounted: ${e.mounted.join(", ")}`;if(e.unmounted&&e.unmounted.length>0)t+=`
Unmounted: ${e.unmounted.join(", ")}`;if(e.warnings&&e.warnings.length>0)t+=`
Warnings:
${e.warnings.join(`
`)}`;alert(t)}else alert(`Hot-reload failed: ${e.error||"Unknown error"}`)}catch(e){alert(`Error reaching daemon: ${e.message}`)}await this.refreshData()}renderTopProfileSelector(){let e=document.getElementById("top-profile-selector");if(!e)return;let t=l.getState(),a=t.config.profiles||{},i=Object.keys(a),r=t.activeProfile,s='<option value="">All Servers (Unrestricted)</option>';for(let n of i){let o=r===n?"selected":"";s+=`<option value="${y(n)}" ${o}>Profile: ${y(n)}</option>`}e.innerHTML=s}async setActiveProfile(e){l.setState({activeProfile:e||null}),await this.refreshData()}openAddProfileModal(){let e=document.getElementById("modal-prof-title");if(e)e.textContent="Create Server Constellation Profile";let t=document.getElementById("modal-prof-name"),a=document.getElementById("modal-prof-desc"),i=document.getElementById("modal-prof-mode");if(t)t.value="",t.disabled=!1;if(a)a.value="";if(i)i.value="create";this.renderProfileServerCheckboxes([]);let r=document.getElementById("modal-add-profile");if(r)r.classList.add("active")}openEditProfileModal(e){let a=l.getState().config.profiles?.[e];if(!a)return;let i=document.getElementById("modal-prof-title");if(i)i.textContent=`Edit Profile: ${e}`;let r=document.getElementById("modal-prof-name"),s=document.getElementById("modal-prof-desc"),n=document.getElementById("modal-prof-mode");if(r)r.value=e,r.disabled=!0;if(s)s.value=a.description||"";if(n)n.value="edit";this.renderProfileServerCheckboxes(a.servers||[]);let o=document.getElementById("modal-add-profile");if(o)o.classList.add("active")}renderProfileServerCheckboxes(e){let t=document.getElementById("modal-prof-servers-list");if(!t)return;let a=l.getState(),i=Object.keys(a.config.mcpServers||{});if(i.length===0){t.innerHTML='<div style="font-size: 11.5px; color: var(--text-dim);">No MCP servers configured yet. Add servers first.</div>';return}t.innerHTML=i.map((r)=>{let s=e.includes(r)?"checked":"";return`
<label style="display: flex; align-items: center; gap: 8px; font-size: 12px; cursor: pointer; padding: 4px 6px; border-radius: var(--radius-sm); transition: background 0.15s;" onmouseover="this.style.background='var(--surface-hover)'" onmouseout="this.style.background='transparent'">
<input type="checkbox" class="prof-server-checkbox" value="${y(r)}" ${s} style="accent-color: var(--amber-400);">
<span style="font-family: var(--ff-mono); font-weight: 600; color: var(--text-main);">${y(r)}</span>
</label>
`}).join("")}async saveProfile(){let e=document.getElementById("modal-prof-name"),t=document.getElementById("modal-prof-desc"),a=e?.value.trim(),i=t?.value.trim();if(!a){alert("Please enter a profile name");return}let r=document.querySelectorAll(".prof-server-checkbox:checked"),s=[];if(r.forEach((n)=>{s.push(n.value)}),s.length===0){alert("Please select at least one server to include in this constellation");return}try{let n=await f.upsertProfile(a,s,i||void 0);if(n.ok)this.closeModals(),await this.refreshData();else alert(`Failed to save profile: ${n.error||"Unknown error"}`)}catch(n){alert(`Error saving profile: ${n.message}`)}}async deleteProfile(e){if(!confirm(`Are you sure you want to delete profile '${e}'?`))return;try{let t=await f.deleteProfile(e);if(t.ok){if(l.getState().activeProfile===e)l.setState({activeProfile:null});await this.refreshData()}else alert(`Failed to delete profile: ${t.error||"Unknown error"}`)}catch(t){alert(`Error deleting profile: ${t.message}`)}}closeModals(){document.querySelectorAll(".modal-backdrop").forEach((e)=>e.classList.remove("active"))}}function y(e){return String(e||"").replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}var Z=new X;window.app=Z;window.addEventListener("DOMContentLoaded",()=>Z.init());
</script>
</body>
</html>