Files
vmware-api-simulator/app/web/index.html
T

7612 lines
248 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VMware API Emulator</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2064%2064%22%20fill%3D%22none%22%3E%0A%20%20%3Cdefs%3E%0A%20%20%20%20%3ClinearGradient%20id%3D%22vmGreen%22%20x1%3D%228%22%20y1%3D%226%22%20x2%3D%2256%22%20y2%3D%2258%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%0A%20%20%20%20%20%20%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23C4F07E%22/%3E%0A%20%20%20%20%20%20%3Cstop%20offset%3D%221%22%20stop-color%3D%22%237CB84A%22/%3E%0A%20%20%20%20%3C/linearGradient%3E%0A%20%20%3C/defs%3E%0A%20%20%3Cg%20stroke%3D%22url%28%23vmGreen%29%22%20stroke-width%3D%226.5%22%20stroke-linejoin%3D%22round%22%3E%0A%20%20%20%20%3Crect%20x%3D%226.25%22%20y%3D%226.25%22%20width%3D%2233.5%22%20height%3D%2233.5%22%20rx%3D%228%22/%3E%0A%20%20%20%20%3Crect%20x%3D%2224.25%22%20y%3D%2224.25%22%20width%3D%2233.5%22%20height%3D%2233.5%22%20rx%3D%228%22/%3E%0A%20%20%3C/g%3E%0A%20%20%3Crect%20x%3D%2215%22%20y%3D%2215%22%20width%3D%2213.5%22%20height%3D%2213.5%22%20rx%3D%223%22%20fill%3D%22%23F7E56A%22/%3E%0A%20%20%3Crect%20x%3D%2235.5%22%20y%3D%2235.5%22%20width%3D%2213.5%22%20height%3D%2213.5%22%20rx%3D%223%22%20fill%3D%22%23F7E56A%22/%3E%0A%3C/svg%3E%0A">
<script>
(function () {
try {
if (localStorage.getItem("vmware-sim-theme") !== "light") {
document.documentElement.setAttribute("data-theme", "dark");
}
} catch (_) {
document.documentElement.setAttribute("data-theme", "dark");
}
})();
</script>
<style>
:root {
--bg: #eef1f6;
--surface: #ffffff;
--border: #d5dbe6;
--border-light: #e8ecf2;
--text: #1b2430;
--muted: #5c6778;
--accent: #8EC368;
--accent-hover: #6FA84A;
--link: #6FA84A;
--ok: #8EC368;
--ok-bg: #eef8e6;
--warn: #9a6700;
--warn-bg: #fff8e6;
--err: #b42318;
--err-bg: #fdecea;
--info: #6FA84A;
--info-bg: #eef8e6;
--debug: #5c6778;
--debug-bg: #eef1f6;
--surface-raised: #fafbfc;
--surface-hover: #f3f5f8;
--surface-toolbar: #f6f8fb;
--surface-muted-hover: #eef1f6;
--surface-accent: #eef8e6;
--surface-danger: #fffafa;
--border-strong: #c8d0db;
--brand-ink: #0f1419;
--brand-logo-core: var(--brand-ink);
--brand-logo-accent: #8EC368;
--brand-logo-bg: var(--overlay-head-bg);
--brand-logo-border: var(--border-strong);
--brand-subtle: #97a1b1;
--meta-desc-bg: #f2f8ec;
--meta-desc-border: #8FD14F;
--meta-desc-text: #4E9A2E;
--overlay-head-bg: #eef1f6;
--brand-accent: #8EC368;
--brand-accent-hover: #6FA84A;
--panel-accent: #8EC368;
--panel-accent-hover: #6FA84A;
--mono: "SF Mono", "Cascadia Code", Consolas, monospace;
--sans: "Segoe UI", system-ui, sans-serif;
--brand-font: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
--header-h: 52px;
}
html[data-theme="dark"] {
color-scheme: dark;
--bg: #0f141c;
--surface: #1a2230;
--border: #4a5668;
--border-light: #354052;
--border-strong: #667085;
--text: #f2f5f9;
--muted: #b0bac8;
--accent: #A4DB6E;
--accent-hover: #B5E87A;
--link: #A4DB6E;
--ok: #A4DB6E;
--ok-bg: #1a3d28;
--warn: #f0c866;
--warn-bg: #3d3218;
--err: #fca5a5;
--err-bg: #3d2222;
--info: #86efac;
--info-bg: #1a3d28;
--debug: #b0bac8;
--debug-bg: #283240;
--surface-raised: #242e3d;
--surface-hover: #2f3a4b;
--surface-toolbar: #151c27;
--surface-muted-hover: #2f3a4b;
--surface-accent: #1e3a28;
--surface-danger: #3a2626;
--brand-ink: #f2f5f9;
--brand-logo-core: #f2f5f9;
--brand-logo-accent: #8EC368;
--brand-logo-bg: var(--overlay-head-bg);
--brand-logo-border: var(--border-strong);
--brand-subtle: #9aa8ba;
--meta-desc-bg: #1e2e24;
--meta-desc-border: #3d6a45;
--meta-desc-text: #c8efd0;
--overlay-head-bg: #242e3d;
--brand-accent: #8EC368;
--brand-accent-hover: #A4DB6E;
--panel-accent: #8EC368;
--panel-accent-hover: #A4DB6E;
}
html[data-theme="dark"] .header-tool-btn {
color: var(--brand-ink);
}
html[data-theme="dark"] .header-tool-btn:hover,
html[data-theme="dark"] .header-tool-btn[aria-expanded="true"] {
color: var(--accent);
}
html[data-theme="dark"] .header-tool-btn.err {
color: var(--err);
}
html[data-theme="dark"] .header-toolbar {
box-shadow: none;
}
html[data-theme="dark"] .workspace-header {
box-shadow: 0 2px 14px rgba(0, 0, 0, 0.32);
}
html[data-theme="dark"] .workspace-topbar .header-tool-btn:hover {
background: var(--surface);
}
html[data-theme="dark"] .workspace-topbar .header-tool-btn[aria-expanded="true"] {
box-shadow:
inset 0 -2px 0 var(--accent),
0 1px 3px rgba(0, 0, 0, 0.24);
}
html[data-theme="dark"] .header-tool-btn:hover {
background: var(--surface-hover);
box-shadow: none;
}
html[data-theme="dark"] .header-tool-btn[aria-expanded="true"] {
background: var(--surface);
box-shadow: inset 0 -2px 0 var(--accent);
}
html[data-theme="dark"] .method-tab.active {
box-shadow: inset 0 0 0 1px var(--accent);
}
html[data-theme="dark"] .catalog-source {
background: var(--surface-raised);
}
html[data-theme="dark"] textarea,
html[data-theme="dark"] .request-control,
html[data-theme="dark"] .request-line input {
background: var(--surface);
color: var(--text);
}
html[data-theme="dark"] .loading-bar.active {
background: linear-gradient(90deg, transparent, var(--accent), transparent);
}
html[data-theme="dark"] .method-tab.GET { color: #86efac; }
html[data-theme="dark"] .method-tab.POST { color: #93c5fd; }
html[data-theme="dark"] .method-tab.PUT { color: #f0c866; }
html[data-theme="dark"] .method-tab.DELETE { color: #fca5a5; }
html[data-theme="dark"] .nav-item.active {
background: var(--surface-hover);
}
html[data-theme="dark"] .verb-mini.GET { color: #86efac; }
html[data-theme="dark"] .verb-mini.POST { color: #93c5fd; }
html[data-theme="dark"] .verb-mini.PUT { color: #f0c866; }
html[data-theme="dark"] .verb-mini.DELETE { color: #fca5a5; }
html[data-theme="dark"] .icon-btn:hover {
border-color: var(--border-strong);
}
html[data-theme="dark"] .badge.ok {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ok) 35%, transparent);
}
html[data-theme="dark"] .badge.warn {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--warn) 35%, transparent);
}
html[data-theme="dark"] .badge.err {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--err) 35%, transparent);
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
font-family: var(--sans);
font-size: 14px;
line-height: 1.45;
color: var(--text);
background: var(--bg);
}
button, input, select, textarea { font: inherit; }
button { cursor: pointer; }
.layout {
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.block-label,
.help-section-title,
.panel-head {
margin: 0 0 10px;
padding: 8px 10px 8px 7px;
border: 1px solid var(--border);
border-left: 3px solid var(--brand-accent);
border-radius: 0;
background: var(--overlay-head-bg);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}
.help-section-title:not(:first-child) {
margin-top: 16px;
}
.meta-list {
margin: 0;
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
font-size: 12px;
}
.meta-list dt { color: var(--muted); margin: 0; }
.meta-list dd { margin: 0; font-weight: 500; }
.field { margin-bottom: 10px; }
.field:last-child { margin-bottom: 0; }
.field label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
color: var(--muted);
}
.field input, .field select {
width: 100%;
padding: 7px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
}
.field input:focus, .field select:focus {
outline: 2px solid rgba(122, 193, 66, 0.35);
border-color: var(--link);
}
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.btn {
padding: 7px 12px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
font-size: 13px;
font-weight: 500;
}
.btn:hover { background: var(--surface-hover); }
.btn-primary {
background: var(--brand-accent);
border-color: var(--brand-accent);
color: #fff;
}
.btn-primary:hover { background: var(--brand-accent-hover); border-color: var(--brand-accent-hover); }
.btn-sm { padding: 0 10px; font-size: 12px; line-height: 1; }
.request-row .btn-sm {
box-sizing: border-box;
height: 32px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: 600;
}
.badge[hidden] { display: none !important; }
.badge.ok { background: var(--ok-bg); color: var(--ok); }
.badge.warn { background: var(--warn-bg); color: var(--warn); }
.badge.err { background: var(--err-bg); color: var(--err); }
.badge.GET { background: #e8f5eb; color: #1e7e34; }
.badge.POST { background: #eef4ff; color: #0b57d0; }
.badge.PUT { background: #fff8e6; color: #9a6700; }
.badge.DELETE { background: #fdecea; color: #b42318; }
html[data-theme="dark"] .badge.GET { background: #1a3d28; color: #86efac; }
html[data-theme="dark"] .badge.POST { background: #1a2d4a; color: #93c5fd; }
html[data-theme="dark"] .badge.PUT { background: #3d3218; color: #f0c866; }
html[data-theme="dark"] .badge.DELETE { background: #3d2222; color: #fca5a5; }
.method-tabs {
display: flex;
flex-wrap: nowrap;
gap: 4px;
min-width: 0;
align-items: stretch;
}
.method-tab {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
height: 32px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.03em;
color: var(--text);
}
.method-tab:hover { background: var(--surface-muted-hover); }
.method-tab.active {
background: var(--surface);
box-shadow: inset 0 0 0 1px var(--accent);
}
.method-tab.GET { color: #1e7e34; }
.method-tab.POST { color: #0b57d0; }
.method-tab.PUT { color: #9a6700; }
.method-tab.DELETE { color: #b42318; }
.nav-search { margin-bottom: 10px; }
.nav-tree {
flex: 1 1 0;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding-right: 4px;
margin-bottom: 14px;
-webkit-overflow-scrolling: touch;
}
.nav-group { margin-bottom: 8px; }
.nav-group-title {
padding: 8px 10px 8px 7px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text);
background: var(--overlay-head-bg);
border: 1px solid var(--border);
border-left: 3px solid var(--link);
border-radius: 0;
}
.nav-item-path {
display: block;
font-family: var(--mono);
font-size: 11px;
word-break: break-all;
}
.nav-item-desc {
display: block;
margin-top: 3px;
font-size: 11px;
line-height: 1.35;
color: var(--muted);
word-break: break-word;
}
.method-badges {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.verb-mini {
display: inline-block;
padding: 1px 5px;
border-radius: 2px;
font-size: 9px;
font-weight: 700;
letter-spacing: 0.03em;
background: var(--border-light);
color: var(--muted);
}
.verb-mini.GET { color: #1e7e34; }
.verb-mini.POST { color: #0b57d0; }
.verb-mini.PUT { color: #9a6700; }
.verb-mini.DELETE { color: #b42318; }
.nav-item {
display: block;
width: 100%;
text-align: left;
padding: 7px 8px;
margin-top: 2px;
border: none;
border-radius: 3px;
background: transparent;
color: var(--text);
font-size: 12px;
line-height: 1.35;
}
.nav-item:hover {
background: var(--surface-hover);
}
.nav-item.active {
background: var(--surface-raised);
box-shadow: inset 3px 0 0 var(--accent);
font-weight: 500;
}
.nav-item code {
font-family: var(--mono);
font-size: 11px;
word-break: break-all;
}
.verb {
display: inline-block;
min-width: 42px;
margin-right: 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.03em;
text-align: center;
}
.verb.GET { color: #1e7e34; }
.verb.POST { color: #0b57d0; }
.verb.PUT { color: #9a6700; }
.verb.DELETE { color: #b42318; }
.help-links-desc {
margin: 0 0 14px;
padding: 0 0 12px;
border-bottom: 1px solid var(--border);
font-size: 12px;
line-height: 1.5;
color: var(--muted);
}
.help-nav {
display: flex;
flex-wrap: wrap;
gap: 0;
margin-bottom: 14px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--overlay-head-bg);
overflow: hidden;
}
.help-nav-btn,
.help-nav-link {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 34px;
padding: 0 12px;
border: none;
border-right: 1px solid var(--border-light);
border-radius: 0;
background: transparent;
color: var(--muted);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
text-decoration: none;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease;
}
.help-nav-btn:last-child,
.help-nav-link:last-child {
border-right: none;
}
.help-nav-btn:hover,
.help-nav-link:hover {
background: var(--surface-hover);
color: var(--text);
}
.help-nav-btn.active {
background: var(--surface);
color: var(--text);
box-shadow: inset 0 -2px 0 var(--accent);
}
.header-tool-btn.data-badge-btn {
display: inline-flex;
align-items: center;
gap: 6px;
}
.header-tool-btn.data-badge-btn .data-badge-dot {
width: 6px;
height: 6px;
border-radius: 1px;
background: #c8d0db;
flex-shrink: 0;
}
.header-tool-btn.data-badge-btn.loaded .data-badge-dot {
background: var(--accent);
}
.help-demo-panel {
display: flex;
flex-direction: column;
gap: 12px;
padding: 0;
}
.help-demo-head {
position: relative;
padding-right: 0;
}
.help-demo-badge {
position: absolute;
top: 7px;
right: 10px;
padding: 3px 8px;
border-radius: 0;
border: 1px solid var(--border);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.help-demo-badge.empty {
background: var(--overlay-head-bg);
color: var(--muted);
}
.help-demo-badge.loaded {
background: var(--surface);
color: var(--ok);
border-color: color-mix(in srgb, var(--ok) 35%, var(--border));
}
.help-demo-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding-top: 2px;
}
.demo-size-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-top: 4px;
}
@media (max-width: 900px) {
.demo-size-grid {
grid-template-columns: 1fr;
}
}
.demo-size-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 14px 14px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background:
linear-gradient(165deg, color-mix(in srgb, var(--brand-accent) 8%, var(--surface-raised)) 0%, var(--surface-raised) 55%);
min-height: 168px;
}
.demo-size-card.is-active {
border-color: color-mix(in srgb, var(--brand-accent) 55%, var(--border));
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--brand-accent) 25%, transparent);
}
.demo-size-card-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 8px;
}
.demo-size-card-title {
margin: 0;
font-size: 15px;
font-weight: 700;
letter-spacing: 0.01em;
color: var(--text);
text-transform: capitalize;
}
.demo-size-card-chip {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--brand-accent);
background: color-mix(in srgb, var(--brand-accent) 12%, transparent);
border-radius: 999px;
padding: 3px 8px;
white-space: nowrap;
}
.demo-size-card-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px 10px;
margin: 0;
}
.demo-size-card-metrics div {
display: flex;
flex-direction: column;
gap: 1px;
}
.demo-size-card-metrics dt {
margin: 0;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
}
.demo-size-card-metrics dd {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.demo-size-card-note {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: var(--muted);
flex: 1;
}
.demo-size-card .btn-demo-load {
width: 100%;
justify-content: center;
margin-top: auto;
}
.demo-toolbar {
display: flex;
flex-wrap: nowrap;
gap: 8px;
align-items: center;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.demo-toolbar .btn {
flex: 1 1 0;
justify-content: center;
min-width: 0;
}
.btn-demo-load {
background: var(--brand-accent);
border-color: var(--brand-accent);
color: #fff;
font-weight: 600;
}
.btn-demo-load:hover {
background: var(--brand-accent-hover);
border-color: var(--brand-accent-hover);
}
.btn-demo-unload {
border-color: var(--border);
color: var(--text);
background: var(--surface);
}
.btn-demo-unload:hover {
background: var(--surface-hover);
border-color: #c8d0db;
}
.help-demo-note {
margin: 0;
font-size: 12px;
line-height: 1.55;
color: var(--muted);
}
.help-about-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.help-about-copy {
margin: 0;
padding: 8px 14px 0;
font-size: 12px;
line-height: 1.55;
color: var(--muted);
}
.help-about-name {
margin: 0;
padding: 10px 14px 0;
font-size: 14px;
font-weight: 600;
color: var(--text);
}
.help-about-social {
display: flex;
flex-wrap: wrap;
gap: 10px;
padding: 12px 14px 14px;
}
.help-about-social a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border: 1px solid var(--border);
border-radius: 50%;
background: var(--surface-raised);
color: var(--text);
text-decoration: none;
transition: background 0.14s ease, border-color 0.14s ease, color 0.14s ease, transform 0.14s ease;
}
.help-about-social a:hover {
background: var(--surface-hover);
border-color: var(--border-strong);
color: var(--accent);
transform: translateY(-1px);
}
.help-about-social a svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
}
.help-about-social a.help-about-social-github svg {
fill: currentColor;
stroke: none;
}
.help-about-meta {
margin: 0;
padding: 0 14px 12px;
font-size: 11px;
line-height: 1.5;
color: var(--muted);
}
.help-about-meta code {
font-family: var(--mono);
font-size: 11px;
color: var(--text);
}
.help-panel {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 2px;
}
.help-panel-loading,
.help-panel-error,
.help-panel-placeholder {
padding: 24px 12px;
text-align: center;
font-size: 12px;
color: var(--muted);
border: 1px dashed var(--border);
border-radius: 0;
background: var(--surface-raised);
}
.help-panel-error {
color: var(--err);
border-style: solid;
border-left: 3px solid var(--err);
background: var(--surface-danger);
}
.help-section-title {
padding-bottom: 8px;
}
.help-section-note {
margin: 0 0 10px;
font-size: 11px;
line-height: 1.45;
color: var(--muted);
}
.help-section-note code {
font-size: 10px;
}
.help-report-head {
margin-bottom: 12px;
padding: 0;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface);
overflow: hidden;
}
.help-report-head h3 {
margin: 0;
padding: 8px 10px 8px 7px;
border-bottom: 1px solid var(--border);
border-left: 3px solid var(--accent);
background: var(--overlay-head-bg);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}
.help-report-head p {
margin: 0;
padding: 10px 14px 12px;
font-size: 12px;
color: var(--muted);
line-height: 1.5;
}
.help-stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
margin-bottom: 4px;
}
.help-stat-card {
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface-raised);
}
.help-stat-card .label {
display: block;
margin-bottom: 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.help-stat-card .value {
font-size: 16px;
font-weight: 600;
line-height: 1.2;
color: var(--text);
font-variant-numeric: tabular-nums;
}
.help-stat-card .value.ok { color: var(--ok); }
.help-stat-card .value.warn { color: var(--warn); }
.help-stat-card .value.err { color: var(--err); }
.help-meter-list {
display: flex;
flex-direction: column;
gap: 6px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface-raised);
padding: 8px 10px;
}
.help-meter-track {
height: 6px;
border-radius: 0;
background: var(--border-light);
overflow: hidden;
}
.help-meter {
display: grid;
grid-template-columns: 92px 1fr auto;
gap: 8px;
align-items: center;
font-size: 11px;
}
.help-meter-label {
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0.03em;
font-size: 10px;
font-weight: 600;
}
.help-meter-fill {
height: 100%;
border-radius: inherit;
background: #5c6778;
}
.help-meter-fill.ok { background: var(--ok); }
.help-meter-fill.warn { background: var(--warn); }
.help-meter-value {
min-width: 42px;
text-align: right;
font-weight: 600;
color: var(--text);
font-variant-numeric: tabular-nums;
font-size: 10px;
}
.help-table {
width: 100%;
border-collapse: collapse;
font-size: 11px;
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
}
.help-table th,
.help-table td {
padding: 8px 10px;
border-bottom: 1px solid var(--border-light);
text-align: left;
vertical-align: top;
}
.help-table th {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
background: var(--overlay-head-bg);
}
.help-table tr:last-child td { border-bottom: none; }
.help-table td.num {
text-align: right;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.help-chip-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.help-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.03em;
text-transform: uppercase;
color: var(--muted);
}
.help-chip strong {
color: var(--text);
font-variant-numeric: tabular-nums;
}
.help-chip.ok { border-left: 3px solid var(--ok); }
.help-chip.warn { border-left: 3px solid var(--warn); }
.help-chip.err { border-left: 3px solid var(--err); }
.help-status-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.help-status-card {
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface);
}
.help-status-card.ok {
border-left: 3px solid var(--ok);
}
.help-status-card.err {
border-left: 3px solid var(--err);
}
.help-status-card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 6px;
}
.help-status-card-head strong {
font-size: 13px;
font-weight: 600;
color: var(--text);
}
.help-status-card p {
margin: 0 0 4px;
font-size: 12px;
line-height: 1.45;
color: var(--muted);
}
.help-status-card p:last-child {
margin-bottom: 0;
}
.help-status-card code {
font-family: var(--mono);
font-size: 11px;
color: var(--text);
}
.help-status-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 3px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
background: var(--surface-raised);
color: var(--muted);
white-space: nowrap;
}
.help-status-badge.ok {
border-color: #b7e5c1;
color: var(--ok);
}
.help-status-badge.err {
border-color: #e8c9c9;
color: var(--err);
}
.endpoints-drawer-body .nav-search input {
border-radius: 0;
background: var(--surface-raised);
color: var(--text);
border-color: var(--border);
}
.endpoints-drawer-body .nav-search input:focus {
border-color: var(--border-strong);
outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
background: var(--surface-hover);
}
.endpoints-drawer-body .nav-tree {
flex: 1 1 0;
min-height: 200px;
margin-bottom: 0;
}
.empty {
padding: 16px 8px;
text-align: center;
color: var(--muted);
font-size: 12px;
}
/* Main workspace */
.workspace {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
min-height: 0;
}
.workspace-header {
flex-shrink: 0;
padding: 0;
background: var(--surface);
border-bottom: 1px solid var(--border-strong);
box-shadow: 0 2px 10px rgba(15, 23, 42, 0.07);
}
.workspace-topbar {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 0;
padding: 8px 16px;
background: var(--overlay-head-bg);
border-bottom: 1px solid var(--border);
}
.workspace-topbar .header-tool-btn:hover {
background: var(--surface);
}
.workspace-topbar .header-tool-btn[aria-expanded="true"] {
background: var(--surface);
box-shadow:
inset 0 -2px 0 var(--accent),
0 1px 2px rgba(15, 23, 42, 0.08);
}
a.workspace-brand {
text-decoration: none;
color: inherit;
}
a.workspace-brand:hover,
a.workspace-brand:focus,
a.workspace-brand:visited {
color: inherit;
text-decoration: none;
}
.workspace-brand {
display: flex;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.workspace-brand-logo {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-sizing: border-box;
width: 32px;
height: 32px;
padding: 3px;
border: 1px solid var(--brand-logo-border);
border-radius: 8px;
background: var(--brand-logo-bg);
overflow: hidden;
}
.workspace-brand-logo img,
.workspace-brand-logo svg {
display: block;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
object-fit: contain;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
}
.workspace-brand-text {
display: inline-grid;
grid-template-rows: auto auto;
align-content: center;
justify-items: stretch;
gap: 3px;
min-width: 0;
min-height: 36px;
line-height: 1.1;
}
.workspace-brand-name {
grid-row: 1;
display: grid;
position: relative;
font-size: 16.5px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
line-height: 1.15;
white-space: nowrap;
}
.workspace-brand-name-ghost {
grid-area: 1 / 1;
visibility: hidden;
pointer-events: none;
user-select: none;
}
.workspace-brand-name-text {
position: relative;
font-family: var(--brand-font);
letter-spacing: 0.04em;
text-transform: uppercase;
grid-area: 1 / 1;
}
.workspace-brand-vm {
color: var(--brand-logo-core);
font-weight: 400;
}
.workspace-brand-ware,
a.workspace-brand .workspace-brand-ware,
a.workspace-brand:visited .workspace-brand-ware,
a.workspace-brand:hover .workspace-brand-ware,
a.workspace-brand:focus .workspace-brand-ware {
color: var(--brand-logo-accent);
font-weight: 600;
}
.workspace-brand-tm {
position: absolute;
left: 100%;
top: -0.15em;
margin-left: 1px;
font-size: 0.55em;
font-weight: 700;
line-height: 1;
color: var(--brand-logo-accent);
}
.workspace-brand-sub {
grid-row: 2;
display: block;
box-sizing: border-box;
width: 100%;
max-width: 100%;
min-width: 0;
font-size: 7.75px;
font-weight: 500;
letter-spacing: 0.14em;
line-height: 1.15;
text-transform: uppercase;
text-align: center;
color: var(--brand-subtle);
white-space: nowrap;
}
html[data-theme="dark"] .workspace-brand-sub {
color: var(--border-strong);
}
.workspace-topbar-actions {
display: flex;
align-items: stretch;
flex-shrink: 0;
}
.header-toolbar {
display: inline-flex;
align-items: stretch;
gap: 2px;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.header-tool-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-height: 32px;
padding: 0 11px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--brand-ink);
font: inherit;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
line-height: 1.2;
text-transform: uppercase;
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
transition:
background 0.14s ease,
color 0.14s ease,
box-shadow 0.14s ease;
}
.header-tool-btn::before {
display: none;
}
.header-tool-btn:hover {
background: var(--surface-hover);
color: var(--accent);
box-shadow: none;
}
.header-tool-btn[aria-expanded="true"] {
background: var(--surface);
color: var(--accent);
box-shadow: inset 0 -2px 0 var(--accent);
}
.header-tool-btn.err {
color: var(--err);
}
.header-tool-btn.err:hover,
.header-tool-btn.err[aria-expanded="true"] {
color: var(--err);
box-shadow: inset 0 -2px 0 var(--err);
}
.header-tool-btn.header-tool-help,
.header-tool-btn.header-tool-theme {
width: 32px;
min-width: 32px;
padding: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0;
text-transform: none;
color: var(--brand-subtle);
}
.header-tool-btn.header-tool-help:hover,
.header-tool-btn.header-tool-theme:hover,
.header-tool-btn.header-tool-help[aria-expanded="true"],
.header-tool-btn.header-tool-theme[aria-expanded="true"] {
color: var(--accent);
}
.theme-icon {
display: block;
width: 15px;
height: 15px;
stroke: currentColor;
fill: none;
stroke-width: 1.8;
stroke-linecap: round;
stroke-linejoin: round;
}
.theme-icon[hidden] {
display: none;
}
.header-tool-btn:focus-visible {
outline: 2px solid rgba(122, 193, 66, 0.45);
outline-offset: 1px;
}
.header-toolbar-divider {
align-self: stretch;
width: 1px;
margin: 8px 4px;
background: var(--border-light);
flex-shrink: 0;
}
.auth-badge-btn.header-tool-btn {
max-width: 168px;
overflow: hidden;
text-overflow: ellipsis;
}
.catalog-badge-btn.header-tool-btn {
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
}
.history-badge-btn.header-tool-btn,
.endpoints-badge-btn.header-tool-btn {
gap: 5px;
}
.history-badge-count,
.endpoints-badge-count,
.endpoints-drawer-count {
min-width: 15px;
padding: 0 5px;
border-radius: 3px;
border: 1px solid var(--border-light);
background: var(--surface-hover);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.04em;
line-height: 14px;
text-align: center;
color: var(--brand-subtle);
font-variant-numeric: tabular-nums;
}
.history-badge-count:empty,
.endpoints-badge-count:empty,
.endpoints-drawer-count:empty { display: none; }
.header-tool-btn.ok .history-badge-count,
.header-tool-btn.ok .endpoints-badge-count {
border-color: color-mix(in srgb, var(--accent) 28%, var(--border-light));
background: color-mix(in srgb, var(--accent) 10%, var(--surface-raised));
color: var(--accent);
}
.endpoints-drawer-head-title {
display: inline-flex;
align-items: center;
gap: 8px;
flex: 1 1 auto;
min-width: 0;
}
.endpoints-drawer-head-title h2 {
flex: 0 1 auto;
}
.endpoints-drawer-count {
text-transform: none;
letter-spacing: 0.02em;
border-color: color-mix(in srgb, var(--accent) 28%, var(--border-light));
background: color-mix(in srgb, var(--accent) 10%, var(--surface-raised));
color: var(--accent);
}
.catalog-options {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 12px;
}
.catalog-option {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto auto;
gap: 2px 8px;
width: 100%;
padding: 8px 10px;
border: 1px solid var(--border);
border-left: 3px solid transparent;
border-radius: 0;
background: var(--surface-raised);
text-align: left;
cursor: pointer;
font: inherit;
color: var(--text);
}
.catalog-option:hover { background: var(--surface-hover); }
.catalog-option.active {
border-color: var(--border);
border-left-color: var(--accent);
background: var(--overlay-head-bg);
box-shadow: none;
}
.catalog-source {
margin-bottom: 12px;
padding: 0;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface);
overflow: hidden;
}
.catalog-source[hidden] { display: none; }
.catalog-source-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin: 0;
padding: 8px 10px 8px 7px;
border-bottom: 1px solid var(--border);
border-left: 3px solid var(--info);
background: var(--overlay-head-bg);
}
.catalog-source-title {
margin: 0;
padding: 0;
border: none;
background: transparent;
flex: 1 1 auto;
min-width: 0;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}
.catalog-cache-badge {
flex-shrink: 0;
margin-left: auto;
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
border: 1px solid var(--border);
font-size: 10px;
font-weight: 600;
line-height: 1.35;
letter-spacing: 0.02em;
white-space: nowrap;
color: var(--muted);
background: var(--surface-raised);
}
.catalog-cache-badge.ok {
color: var(--ok);
background: var(--ok-bg);
border-color: transparent;
}
.catalog-cache-badge.warn {
color: var(--warn);
background: var(--warn-bg);
border-color: transparent;
}
.catalog-source-list {
display: grid;
grid-template-columns: 92px 1fr;
gap: 6px 10px;
margin: 0;
padding: 12px 14px;
font-size: 12px;
line-height: 1.4;
}
.catalog-option-title {
font-size: 13px;
font-weight: 600;
}
.catalog-option-version {
grid-row: span 2;
align-self: center;
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
}
.catalog-option-meta {
font-size: 11px;
color: var(--muted);
}
.catalog-source-list dt {
margin: 0;
color: var(--muted);
font-weight: 600;
}
.catalog-source-list dd {
margin: 0;
color: var(--text);
word-break: break-word;
}
.catalog-source-list code {
display: block;
font-family: var(--mono);
font-size: 11px;
line-height: 1.45;
color: var(--link);
}
.catalog-source-list .badge-inline {
display: inline-block;
padding: 2px 7px;
border-radius: 0;
border: 1px solid var(--border);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.catalog-source-list .badge-inline.ok {
background: var(--ok-bg);
color: var(--ok);
}
.catalog-source-list .badge-inline.warn {
background: var(--warn-bg);
color: var(--warn);
}
.catalog-coverage {
margin-top: 4px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.catalog-coverage .help-section-title {
margin-top: 0;
}
.catalog-coverage-stat {
margin-bottom: 10px;
}
.catalog-panel-actions {
display: flex;
flex-direction: column;
gap: 8px;
justify-content: stretch;
flex-shrink: 0;
padding: 12px 18px 16px;
border-top: 1px solid var(--border);
background: var(--surface);
}
.catalog-panel-actions-row {
display: flex;
gap: 8px;
align-items: stretch;
}
.catalog-panel-actions-row .btn {
flex: 1 1 0;
width: auto;
min-width: 0;
}
.catalog-panel-actions .catalog-apply-note {
margin: 0;
font-size: 11px;
line-height: 1.4;
color: var(--muted);
}
.auth-panel {
display: flex;
flex-direction: column;
gap: 12px;
}
.auth-panel[hidden],
.auth-panel-user[hidden] {
display: none !important;
}
.auth-drawer-session[hidden] {
display: none !important;
}
.auth-hint {
margin: 0;
font-size: 12px;
line-height: 1.45;
color: var(--muted);
}
.auth-hint code {
font-family: var(--mono);
font-size: 11px;
color: var(--text);
}
.auth-form {
display: flex;
flex-direction: column;
gap: 10px;
}
.auth-form .field {
margin: 0;
}
.auth-drawer-body .field label {
display: block;
margin-bottom: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--muted);
}
.auth-drawer-body .field input {
width: 100%;
box-sizing: border-box;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
font-family: var(--mono);
font-size: 12px;
}
.auth-drawer-body .field input:focus {
outline: 2px solid color-mix(in srgb, var(--accent) 35%, transparent);
border-color: var(--border-strong);
}
.auth-session-user {
font-family: var(--mono);
font-weight: 600;
color: var(--text);
}
.auth-drawer-head-main {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
flex: 1 1 auto;
}
.auth-drawer-session {
margin: 0;
font-size: 12px;
line-height: 1.35;
color: var(--muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.auth-drawer-body-signed-in {
display: flex;
align-items: center;
justify-content: center;
}
.auth-panel-user {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
}
.auth-panel-user .btn {
min-width: 112px;
}
.auth-form .btn,
.auth-panel .btn {
align-self: flex-start;
min-width: 96px;
}
.params-inline-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
box-sizing: border-box;
height: 32px;
padding: 0 12px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface);
cursor: pointer;
flex-shrink: 0;
white-space: nowrap;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--brand-ink);
transition: background 0.14s ease, color 0.14s ease;
}
.params-inline-btn[hidden] { display: none; }
.params-inline-btn:hover {
background: var(--surface-hover);
border-color: var(--border-strong);
color: var(--accent);
}
.params-inline-btn .params-badge-count {
min-width: 16px;
padding: 0 5px;
border-radius: 0;
border: 1px solid var(--border-light);
background: var(--overlay-head-bg);
font-size: 9px;
font-weight: 700;
line-height: 16px;
text-align: center;
color: var(--brand-subtle);
}
.params-inline-btn .params-badge-count:empty { display: none; }
.params-backdrop {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(15, 23, 42, 0.32);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.params-backdrop.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.params-drawer {
position: fixed;
top: 0;
left: 0;
z-index: 90;
display: flex;
flex-direction: column;
width: 75vw;
max-width: 100vw;
height: 100%;
background: var(--surface);
border-right: 1px solid var(--border);
box-shadow: 12px 0 40px rgba(27, 36, 48, 0.14);
transform: translateX(-100%);
visibility: hidden;
transition: transform 0.24s ease, visibility 0.24s ease;
}
.params-drawer.open {
transform: translateX(0);
visibility: visible;
}
.params-drawer,
.history-drawer,
.auth-drawer,
.catalog-drawer,
.environment-drawer,
.data-drawer,
.url-drawer,
.endpoints-drawer,
.help-drawer,
.ui-modal {
--accent: var(--panel-accent);
--accent-hover: var(--panel-accent-hover);
}
.params-drawer-head,
.history-drawer-head,
.auth-drawer-head,
.catalog-drawer-head,
.environment-drawer-head,
.data-drawer-head,
.url-drawer-head,
.endpoints-drawer-head,
.help-drawer-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 14px 16px 14px 13px;
border-bottom: 1px solid var(--border);
border-left: 3px solid var(--brand-accent);
background: var(--overlay-head-bg);
flex-shrink: 0;
}
.params-drawer-head h2,
.history-drawer-head h2,
.auth-drawer-head h2,
.catalog-drawer-head h2,
.environment-drawer-head h2,
.data-drawer-head h2,
.url-drawer-head h2,
.endpoints-drawer-head h2,
.help-drawer-head h2 {
margin: 0;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
flex: 1 1 auto;
min-width: 0;
}
.drawer-head-actions {
display: inline-flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
margin-left: auto;
}
.params-drawer-head .icon-btn,
.history-drawer-head .icon-btn,
.auth-drawer-head .icon-btn,
.catalog-drawer-head .icon-btn,
.environment-drawer-head .icon-btn,
.data-drawer-head .icon-btn,
.url-drawer-head .icon-btn,
.endpoints-drawer-head .icon-btn,
.help-drawer-head .icon-btn {
border-color: var(--border);
background: var(--surface);
color: var(--brand-subtle);
}
.params-drawer-head .icon-btn:hover,
.history-drawer-head .icon-btn:hover,
.auth-drawer-head .icon-btn:hover,
.catalog-drawer-head .icon-btn:hover,
.environment-drawer-head .icon-btn:hover,
.data-drawer-head .icon-btn:hover,
.url-drawer-head .icon-btn:hover,
.endpoints-drawer-head .icon-btn:hover,
.help-drawer-head .icon-btn:hover {
border-color: var(--border-strong);
background: var(--surface-hover);
color: var(--brand-accent);
}
.param-section {
margin-bottom: 18px;
}
.param-section:last-child {
margin-bottom: 0;
}
.param-section[hidden] { display: none; }
.param-section .panel {
overflow: hidden;
}
.history-backdrop,
.auth-backdrop,
.catalog-backdrop,
.environment-backdrop,
.data-backdrop,
.url-backdrop,
.endpoints-backdrop,
.help-backdrop {
position: fixed;
inset: 0;
z-index: 80;
background: rgba(15, 23, 42, 0.32);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.history-backdrop.open,
.auth-backdrop.open,
.catalog-backdrop.open,
.environment-backdrop.open,
.data-backdrop.open,
.url-backdrop.open,
.endpoints-backdrop.open,
.help-backdrop.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.history-drawer,
.auth-drawer,
.catalog-drawer,
.environment-drawer,
.data-drawer,
.url-drawer,
.endpoints-drawer,
.help-drawer {
position: fixed;
top: 0;
right: 0;
z-index: 90;
display: flex;
flex-direction: column;
width: 50vw;
max-width: 100vw;
height: 100%;
overflow: hidden;
background: var(--surface);
border-left: 1px solid var(--border);
box-shadow: -12px 0 40px rgba(27, 36, 48, 0.14);
transform: translateX(100%);
visibility: hidden;
transition: transform 0.24s ease, visibility 0.24s ease;
}
.history-drawer.open,
.auth-drawer.open,
.catalog-drawer.open,
.environment-drawer.open,
.data-drawer.open,
.url-drawer.open,
.endpoints-drawer.open,
.help-drawer.open {
transform: translateX(0);
visibility: visible;
}
.params-drawer-body,
.history-drawer-body,
.auth-drawer-body,
.environment-drawer-body,
.data-drawer-body,
.url-drawer-body,
.endpoints-drawer-body,
.help-drawer-body {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 16px 18px 20px;
background: var(--surface);
}
.environment-panel {
display: flex;
flex-direction: column;
gap: 0;
}
.history-drawer-body .btn,
.auth-drawer-body .btn,
.catalog-drawer-body .btn,
.environment-drawer-body .btn,
.data-drawer-body .btn,
.url-drawer-body .btn,
.endpoints-drawer-body .btn,
.help-drawer-body .btn,
.params-drawer-body .btn {
box-sizing: border-box;
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 38px;
padding: 0 16px;
font-size: 13px;
line-height: 1.2;
}
.help-drawer-body {
display: flex;
flex-direction: column;
overflow: hidden;
}
.catalog-drawer-body {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
padding: 0;
background: var(--surface);
}
.catalog-drawer-scroll {
flex: 1;
min-height: 0;
overflow-y: auto;
overflow-x: hidden;
padding: 16px 18px 12px;
-webkit-overflow-scrolling: touch;
}
.history-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.history-item {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 6px;
}
.history-list .nav-item.history-replay {
flex: 1 1 auto;
min-width: 0;
width: auto;
display: flex;
align-items: center;
gap: 6px;
}
.history-list .history-replay .badge {
flex: 0 0 auto;
font-size: 11px;
font-weight: 600;
line-height: 1.25;
padding: 2px 8px;
}
.history-list .history-replay code {
flex: 1 1 auto;
min-width: 0;
}
.history-list .nav-item {
width: 100%;
text-align: left;
margin-top: 0;
}
.request-builder {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 16px 12px;
background: var(--surface);
}
.request-row {
display: flex;
gap: 12px;
align-items: flex-end;
}
.request-field label,
.request-field .endpoint-field-label {
display: block;
margin-bottom: 2px;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted);
}
.request-control {
box-sizing: border-box;
height: 32px;
}
.request-field select {
width: 100%;
padding: 0 9px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
}
.request-field select.request-control {
height: 32px;
}
.request-field select:focus {
outline: 2px solid rgba(122, 193, 66, 0.35);
border-color: var(--link);
}
.endpoint-combobox {
position: relative;
width: 100%;
}
.endpoint-combo-trigger {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 0 9px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--text);
font-family: var(--mono);
font-size: 12px;
text-align: left;
cursor: pointer;
}
.endpoint-combo-trigger:hover {
border-color: var(--border-strong);
background: var(--surface-hover);
}
.endpoint-combo-trigger:focus,
.endpoint-combo-trigger[aria-expanded="true"] {
outline: 2px solid rgba(122, 193, 66, 0.35);
border-color: var(--link);
}
.endpoint-combo-value {
flex: 1 1 auto;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.endpoint-combo-value.is-placeholder {
color: var(--muted);
font-family: var(--sans);
}
.endpoint-combo-caret {
flex: 0 0 auto;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid var(--muted);
}
.endpoint-combo-trigger[aria-expanded="true"] .endpoint-combo-caret {
border-top: 0;
border-bottom: 5px solid var(--link);
}
.endpoint-combo-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 60;
max-height: min(420px, 55vh);
overflow-x: hidden;
overflow-y: auto;
padding: 4px 0;
border: 1px solid var(--border-strong);
border-radius: 6px;
background: var(--surface);
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.18);
-webkit-overflow-scrolling: touch;
}
html[data-theme="dark"] .endpoint-combo-menu {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45);
}
.endpoint-combo-menu[hidden] {
display: none !important;
}
.endpoint-combo-empty {
padding: 10px 12px;
font-size: 12px;
color: var(--muted);
}
.endpoint-combo-group + .endpoint-combo-group {
margin-top: 2px;
border-top: 1px solid var(--border-light);
padding-top: 2px;
}
.endpoint-combo-group-title {
position: sticky;
top: 0;
z-index: 1;
padding: 7px 12px 6px 9px;
font-family: var(--sans);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
background: var(--overlay-head-bg);
border: 1px solid var(--border);
border-left: 3px solid var(--link);
}
html[data-theme="dark"] .endpoint-combo-group-title {
color: var(--text);
background: var(--overlay-head-bg);
border-color: var(--border);
border-left-color: var(--link);
}
.endpoint-combo-option {
display: block;
width: 100%;
padding: 7px 12px 7px 15px;
border: 0;
background: transparent;
color: var(--text);
font-family: var(--mono);
font-size: 12px;
text-align: left;
cursor: pointer;
}
.endpoint-combo-option:hover,
.endpoint-combo-option:focus {
outline: none;
background: var(--surface-hover);
color: var(--link);
}
.endpoint-combo-option[aria-selected="true"] {
background: var(--surface-accent);
color: var(--link);
font-weight: 600;
}
.endpoint-combo-option.is-unimplemented {
color: var(--err);
}
.endpoint-combo-option.is-unimplemented:hover,
.endpoint-combo-option.is-unimplemented:focus {
color: var(--err);
background: var(--err-bg);
}
.endpoint-combo-option.is-unimplemented[aria-selected="true"] {
color: var(--err);
background: var(--err-bg);
font-weight: 600;
}
.method-field {
flex: 0 1 auto;
min-width: 0;
max-width: none;
}
.method-params-row {
display: flex;
align-items: center;
gap: 6px;
min-width: 0;
margin-top: -2px;
}
.method-params-row .method-tabs {
flex: 0 1 auto;
min-width: 0;
}
.endpoint-field {
flex: 1 1 0;
min-width: 0;
}
.send-field {
flex: 0 0 auto;
}
.url-toggle-btn.active {
background: var(--surface-accent);
border-color: var(--link);
color: var(--link);
}
.request-preview-panel {
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 12px;
}
.request-url-block {
display: flex;
flex-direction: column;
gap: 5px;
padding: 8px 10px 9px;
border: 1px solid color-mix(in srgb, var(--link) 28%, var(--border));
border-left: 3px solid var(--link);
background: var(--surface);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05);
}
html[data-theme="dark"] .request-url-block {
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.22);
}
.request-url-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}
.request-preview-row {
display: flex;
align-items: center;
gap: 6px;
}
.request-preview-row .request-preview {
flex: 1;
min-width: 0;
}
.request-preview-row .icon-btn {
flex-shrink: 0;
}
.impl-filter {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--muted);
user-select: none;
white-space: nowrap;
}
.impl-filter input { margin: 0; }
.btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.request-preview {
display: block;
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface-raised);
font-family: var(--mono);
font-size: 12px;
color: var(--text);
word-break: break-all;
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--surface) 80%, transparent);
}
html[data-theme="dark"] .request-preview {
background: color-mix(in srgb, var(--surface-raised) 88%, #000 12%);
}
.request-preview.request-curl {
margin: 0;
flex: 1;
min-width: 0;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.45;
}
.request-line select, .request-line input {
padding: 8px 10px;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
}
.request-line input {
font-family: var(--mono);
font-size: 13px;
}
.request-meta-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 10px;
min-height: 28px;
}
.meta-endpoint {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--text);
white-space: nowrap;
}
.meta-endpoint[hidden] {
display: none !important;
}
.meta-desc {
flex: 1 1 120px;
min-width: 0;
display: inline-block;
padding: 4px 10px;
border-radius: 0;
background: var(--meta-desc-bg);
border: 1px solid var(--border);
border-left: 3px solid var(--info);
font-size: 11px;
font-weight: 500;
line-height: 1.35;
color: var(--meta-desc-text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.meta-desc:empty { display: none; }
.toolbar {
display: flex;
gap: 4px;
flex-shrink: 0;
}
.request-meta-bar .impl-filter {
margin-left: auto;
}
.icon-btn {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
color: var(--muted);
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.icon-btn:hover {
background: var(--surface-hover);
color: var(--text);
border-color: #c8d0db;
}
.icon-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
pointer-events: none;
}
.icon-btn.icon-btn-danger:hover:not(:disabled) {
color: var(--err);
border-color: color-mix(in srgb, var(--err) 35%, var(--border));
background: color-mix(in srgb, var(--err) 8%, var(--surface-hover));
}
.icon-btn:focus-visible {
outline: 2px solid rgba(122, 193, 66, 0.45);
outline-offset: 2px;
}
.icon-btn svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
}
.ui-tooltip {
position: fixed;
z-index: 200;
max-width: min(280px, calc(100vw - 16px));
padding: 6px 10px;
background: #1a2332;
color: #fff;
font-size: 12px;
font-weight: 500;
line-height: 1.35;
border-radius: 4px;
box-shadow: 0 4px 14px rgba(15, 23, 42, 0.18);
pointer-events: none;
white-space: normal;
overflow-wrap: anywhere;
}
.ui-tooltip[hidden] { display: none; }
.workspace-body {
flex: 1;
min-height: 0;
overflow: hidden;
padding: 10px 16px 12px;
display: flex;
flex-direction: column;
gap: 10px;
background: var(--bg);
}
.panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0;
overflow: hidden;
}
.panel .panel-head {
margin: 0;
padding: 8px 12px 8px 7px;
border-top: none;
border-right: none;
border-bottom: 1px solid var(--border);
}
.panel-body { padding: 0; }
.params-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.params-table th,
.params-table td {
padding: 6px 12px;
border-bottom: 1px solid var(--border-light);
text-align: left;
vertical-align: top;
}
.params-table th {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text);
background: var(--overlay-head-bg);
}
.params-table tr:last-child td { border-bottom: none; }
.params-table code {
font-family: var(--mono);
font-size: 12px;
}
.params-table input {
width: 100%;
padding: 6px 8px;
border: 1px solid var(--border);
border-radius: 3px;
font-family: var(--mono);
font-size: 12px;
}
.param-type-badges {
display: inline-flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.param-type-badge {
display: inline-block;
padding: 2px 7px;
border: 1px solid var(--border);
border-radius: 0;
background: var(--surface-accent);
color: var(--info);
font-family: var(--mono);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.03em;
line-height: 1.3;
white-space: nowrap;
}
.param-type-badge.optional {
background: var(--debug-bg);
color: var(--muted);
border-color: var(--border-light);
font-family: var(--sans);
font-size: 9px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.editors {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
flex: 1 1 0;
min-height: 0;
}
.editors.editors-single {
grid-template-columns: 1fr;
}
#request-pane-wrap[hidden] {
display: none !important;
}
.editor-pane {
display: flex;
flex-direction: column;
min-height: 0;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 4px;
overflow: hidden;
}
.editor-head {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
min-height: 36px;
box-sizing: border-box;
padding: 8px 12px 8px 9px;
border-bottom: 1px solid var(--border);
border-left: 3px solid var(--brand-accent);
background: var(--overlay-head-bg);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--text);
flex-shrink: 0;
}
.editor-head-title {
line-height: 1.2;
}
.editor-head-actions {
display: inline-flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.request-pane-tabs {
display: inline-flex;
align-items: center;
gap: 2px;
min-width: 0;
}
.request-pane-tab {
appearance: none;
border: 1px solid transparent;
background: transparent;
color: var(--muted);
font: inherit;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
}
.request-pane-tab:hover {
color: var(--text);
background: var(--surface-accent);
}
.request-pane-tab.active {
color: var(--text);
border-color: var(--border);
background: var(--surface);
}
.request-pane-body {
display: flex;
flex-direction: column;
flex: 1 1 0;
min-height: 0;
}
.request-pane-body[hidden] {
display: none !important;
}
.request-pane-body #body-output {
flex: 1 1 0;
}
.query-pane-scroll {
flex: 1 1 0;
min-height: 0;
overflow: auto;
padding: 0;
}
.query-pane-scroll .params-table {
margin: 0;
border: none;
border-radius: 0;
}
.query-pane-scroll .params-table th,
.query-pane-scroll .params-table td {
padding: 8px 10px;
}
.query-pane-empty {
padding: 16px 12px;
color: var(--muted);
font-size: 12px;
}
.query-preview {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
flex-shrink: 0;
padding: 8px 12px;
border-top: 1px solid var(--border);
background: var(--overlay-head-bg);
}
.query-preview-main {
min-width: 0;
flex: 1 1 auto;
}
.query-preview-label {
display: block;
margin-bottom: 4px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.query-preview-text {
display: block;
margin: 0;
max-height: 4.2em;
overflow: auto;
font-family: var(--mono);
font-size: 11px;
line-height: 1.4;
color: var(--text);
word-break: break-all;
white-space: pre-wrap;
}
.query-preview-text.is-empty {
color: var(--muted);
}
.query-preview .btn {
flex-shrink: 0;
align-self: center;
}
.editor-head .icon-btn {
width: 24px;
height: 24px;
}
.editor-head .icon-btn svg {
width: 14px;
height: 14px;
}
.editor-pane textarea,
.editor-pane pre {
flex: 1;
margin: 0;
padding: 10px 12px;
border: none;
resize: none;
font-family: var(--mono);
font-size: 12px;
line-height: 1.45;
background: var(--surface);
color: var(--text);
min-height: 0;
overflow: auto;
}
.editor-pane textarea:disabled {
background: var(--surface-hover);
color: var(--muted);
}
.editor-pane textarea:focus {
outline: none;
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
}
.json-viewer:focus,
.json-viewer:focus-visible {
outline: none;
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
}
.json-editor {
position: relative;
flex: 1 1 0;
min-height: 0;
}
.json-editor textarea,
.json-editor .json-highlight {
position: absolute;
inset: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
margin: 0;
padding: 10px 12px;
border: none;
font-family: var(--mono);
font-size: 12px;
line-height: 1.45;
white-space: pre;
overflow: auto;
tab-size: 2;
}
.json-editor .json-highlight {
pointer-events: none;
z-index: 1;
color: var(--text);
background: var(--surface);
}
.json-editor textarea {
z-index: 2;
resize: none;
color: transparent;
caret-color: var(--text);
background: transparent;
}
.json-editor textarea:disabled {
background: transparent;
}
.json-editor textarea:disabled + .json-highlight {
background: var(--surface-hover);
}
.json-viewer {
flex: 1 1 0;
min-height: 0;
margin: 0;
padding: 10px 12px;
overflow: auto;
font-family: var(--mono);
font-size: 12px;
line-height: 1.45;
background: var(--surface);
color: var(--text);
tab-size: 2;
}
.json-viewer.json-viewer-plain {
white-space: pre-wrap;
word-break: break-word;
}
.json-viewer-empty {
color: var(--muted);
}
.json-row {
display: flex;
align-items: baseline;
gap: 4px;
min-height: 1.45em;
}
.json-fold {
flex-shrink: 0;
width: 14px;
height: 14px;
padding: 0;
border: none;
border-radius: 0;
background: transparent;
color: var(--muted);
font-size: 10px;
line-height: 14px;
cursor: pointer;
}
.json-fold:hover {
color: var(--accent);
}
.json-fold-spacer {
flex-shrink: 0;
width: 14px;
}
.json-group.is-collapsed > .json-group-body,
.json-group.is-collapsed > .json-row-foot {
display: none;
}
.json-group.is-collapsed > .json-row-head .json-open-punct {
display: none;
}
.json-group.is-collapsed > .json-row-head .json-collapsed-punct {
display: inline;
}
.json-group:not(.is-collapsed) .json-inline-comma {
display: none;
}
.json-open-punct,
.json-collapsed-punct {
color: var(--json-punct);
}
.json-collapsed-punct {
display: none;
}
.json-key { color: var(--json-key); }
.json-string { color: var(--json-string); }
.json-number { color: var(--json-number); }
.json-boolean { color: var(--json-boolean); }
.json-null { color: var(--json-null); font-style: italic; }
.json-punct { color: var(--json-punct); }
:root {
--json-key: #7a4e00;
--json-string: #1e7e34;
--json-number: #0b57d0;
--json-boolean: #9a6700;
--json-null: #5c6778;
--json-punct: #5c6778;
}
html[data-theme="dark"] {
--json-key: #93c5fd;
--json-string: #86efac;
--json-number: #fbbf24;
--json-boolean: #f0c866;
--json-null: #96a0b0;
--json-punct: #96a0b0;
}
.status-code {
flex-shrink: 0;
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
line-height: 1.2;
padding: 2px 8px;
border-radius: 3px;
}
.status-code[hidden] { display: none !important; }
.status-code.ok { background: var(--ok-bg); color: var(--ok); }
.status-code.err { background: var(--err-bg); color: var(--err); }
.loading-bar {
height: 2px;
background: transparent;
flex-shrink: 0;
}
.loading-bar.active {
background: linear-gradient(90deg, transparent, var(--accent), transparent);
background-size: 40% 100%;
animation: load 1s linear infinite;
}
@keyframes load {
from { background-position: -40% 0; }
to { background-position: 140% 0; }
}
.ui-modal-backdrop {
position: fixed;
inset: 0;
z-index: 110;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(15, 23, 42, 0.42);
opacity: 0;
visibility: hidden;
pointer-events: none;
transition: opacity 0.2s ease, visibility 0.2s ease;
}
.ui-modal-backdrop.open {
opacity: 1;
visibility: visible;
pointer-events: auto;
}
.ui-modal {
width: min(440px, 100%);
border: 1px solid var(--border);
border-radius: 4px;
background: var(--surface);
box-shadow: 0 24px 64px rgba(15, 23, 42, 0.22);
transform: translateY(10px) scale(0.98);
opacity: 0;
transition: transform 0.22s ease, opacity 0.22s ease;
overflow: hidden;
}
.ui-modal-backdrop.open .ui-modal {
transform: translateY(0) scale(1);
opacity: 1;
}
.ui-modal-head {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px 14px 13px;
border-bottom: 1px solid var(--border);
border-left: 3px solid var(--brand-accent);
background: var(--overlay-head-bg);
}
.ui-modal-title {
margin: 0 0 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text);
}
.ui-modal-message {
margin: 0;
font-size: 13px;
line-height: 1.5;
color: var(--muted);
}
.ui-modal-actions {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
gap: 8px;
padding: 16px 18px 18px;
border-top: 1px solid var(--border-light);
background: var(--surface);
}
.ui-modal-icon {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 0;
border: 1px solid var(--border);
flex-shrink: 0;
}
.ui-modal-icon svg {
width: 20px;
height: 20px;
}
.ui-modal-icon.info {
background: var(--info-bg);
color: var(--info);
}
.ui-modal-icon.warn {
background: var(--warn-bg);
color: var(--warn);
}
.ui-modal-icon.danger {
background: var(--err-bg);
color: var(--err);
}
.ui-modal-copy {
flex: 1;
min-width: 0;
}
.ui-modal-actions .btn {
display: inline-flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
min-height: 40px;
padding: 0 18px;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
}
.btn-danger {
background: var(--err);
border-color: var(--err);
color: #fff;
}
.btn-danger:hover {
background: #991b1b;
border-color: #991b1b;
}
body.modal-open {
overflow: hidden;
}
.toast-stack {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 100;
display: flex;
flex-direction: column-reverse;
align-items: flex-end;
gap: 8px;
width: min(380px, calc(100vw - 48px));
pointer-events: none;
}
.toast-stack .toast {
position: relative;
right: auto;
bottom: auto;
width: 100%;
max-width: 100%;
opacity: 0;
pointer-events: auto;
transform: translateY(6px);
transition:
opacity 0.18s ease,
transform 0.22s ease;
}
.toast-stack .toast.show {
opacity: 1;
transform: translateY(0);
}
.toast-stack .toast.toast-leaving {
opacity: 0;
transform: translateY(4px);
pointer-events: none;
}
.toast-inner {
position: relative;
display: flex;
align-items: flex-start;
padding: 11px 34px 11px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-left-width: 3px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.08);
}
.toast-body {
display: flex;
flex-direction: column;
gap: 3px;
min-width: 0;
}
.toast-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
line-height: 1.2;
color: var(--muted);
}
.toast-msg {
font-size: 12px;
font-weight: 500;
line-height: 1.45;
color: var(--text);
}
.toast-close {
position: absolute;
top: 8px;
right: 8px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
padding: 0;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
color: var(--muted);
font-size: 16px;
line-height: 1;
cursor: pointer;
transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.toast-close:hover {
background: var(--surface-hover);
border-color: var(--border-light);
color: var(--text);
}
.toast-ok .toast-inner { border-left-color: var(--ok); }
.toast-ok .toast-label { color: var(--ok); }
.toast-warn .toast-inner { border-left-color: var(--warn); }
.toast-warn .toast-label { color: var(--warn); }
.toast-error .toast-inner { border-left-color: var(--err); }
.toast-error .toast-label { color: var(--err); }
.toast-info .toast-inner { border-left-color: var(--info); }
.toast-info .toast-label { color: var(--info); }
.toast-debug .toast-inner { border-left-color: var(--debug); }
.toast-debug .toast-label { color: var(--debug); }
@media (max-width: 960px) {
.request-row { flex-direction: column; }
.method-field { max-width: none; }
.workspace-topbar-actions {
width: 100%;
justify-content: flex-end;
}
.header-toolbar {
flex-wrap: wrap;
max-width: 100%;
}
.header-tool-btn {
padding: 0 8px;
font-size: 10px;
}
.editors { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<div class="layout" id="layout">
<div class="workspace">
<div class="loading-bar" id="loading-bar"></div>
<header class="workspace-header">
<div class="workspace-topbar">
<a href="/" class="workspace-brand" aria-label="VMware API Simulator — home">
<span class="workspace-brand-logo" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<defs>
<linearGradient id="vmGreen" x1="8" y1="6" x2="56" y2="58" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#C4F07E"/>
<stop offset="1" stop-color="#7CB84A"/>
</linearGradient>
</defs>
<g stroke="url(#vmGreen)" stroke-width="6.5" stroke-linejoin="round">
<rect x="6.25" y="6.25" width="33.5" height="33.5" rx="8"/>
<rect x="24.25" y="24.25" width="33.5" height="33.5" rx="8"/>
</g>
<rect x="15" y="15" width="13.5" height="13.5" rx="3" fill="#F7E56A"/>
<rect x="35.5" y="35.5" width="13.5" height="13.5" rx="3" fill="#F7E56A"/>
</svg>
</span>
<span class="workspace-brand-text">
<span class="workspace-brand-name">
<span class="workspace-brand-name-ghost" aria-hidden="true">VMWARE</span>
<span class="workspace-brand-name-text"><span class="workspace-brand-vm">VM</span><span class="workspace-brand-ware">WARE</span><span class="workspace-brand-tm" aria-hidden="true">®</span></span>
</span>
<span class="workspace-brand-sub">API Simulator</span>
</span>
</a>
<div class="workspace-topbar-actions">
<div class="header-toolbar" role="toolbar" aria-label="Workspace controls">
<button type="button" class="header-tool-btn ok catalog-badge-btn" id="catalog-badge" aria-expanded="false" aria-controls="catalog-drawer">API catalog</button>
<button type="button" class="header-tool-btn ok endpoints-badge-btn" id="endpoints-badge" aria-expanded="false" aria-controls="endpoints-drawer">
<span>Endpoints</span>
<span class="endpoints-badge-count" id="endpoints-badge-count" title="Total methods in selected catalog version"></span>
</button>
<button type="button" class="header-tool-btn ok env-badge-btn" id="environment-badge" aria-expanded="false" aria-controls="environment-drawer">Environment</button>
<button type="button" class="header-tool-btn warn history-badge-btn" id="history-badge" aria-expanded="false" aria-controls="history-drawer">
<span>History</span>
<span class="history-badge-count" id="history-badge-count"></span>
</button>
<button type="button" class="header-tool-btn ok data-badge-btn" id="data-badge" aria-expanded="false" aria-controls="data-drawer">
<span>Data</span>
<span class="data-badge-dot" aria-hidden="true"></span>
</button>
<button type="button" class="header-tool-btn err auth-badge-btn" id="auth-status-pill" aria-expanded="false" aria-controls="auth-drawer">Guest</button>
<span class="header-toolbar-divider" aria-hidden="true"></span>
<button
type="button"
class="header-tool-btn header-tool-theme"
id="theme-toggle"
aria-label="Switch to light theme"
data-tooltip="Light theme"
>
<svg class="theme-icon theme-icon-dark" viewBox="0 0 24 24" aria-hidden="true" hidden>
<path d="M12 3.5a7.5 7.5 0 1 0 7.5 7.5 5.5 5.5 0 0 1-7.5-7.5z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
</svg>
<svg class="theme-icon theme-icon-light" viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="1.8"/>
<path d="M12 2.5v2.2M12 19.3v2.2M4.5 4.5l1.6 1.6M17.9 17.9l1.6 1.6M2.5 12h2.2M19.3 12h2.2M4.5 19.5l1.6-1.6M17.9 6.1l1.6-1.6" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
<button type="button" class="header-tool-btn header-tool-help help-badge-btn" id="help-badge" aria-expanded="false" aria-controls="help-drawer" data-tooltip="Documentation &amp; diagnostics" aria-label="Help">?</button>
</div>
</div>
</div>
<div class="request-builder">
<div class="request-row">
<div class="request-field method-field" id="method-field" hidden>
<label>Method</label>
<div class="method-params-row">
<div class="method-tabs" id="method-tabs" role="group" aria-label="HTTP method"></div>
<button type="button" class="badge warn params-inline-btn" id="params-badge" hidden aria-expanded="false" aria-controls="params-drawer">
Params
<span class="params-badge-count" id="params-badge-count"></span>
</button>
</div>
</div>
<div class="request-field endpoint-field">
<span class="endpoint-field-label" id="endpoint-select-label-text">Endpoint</span>
<div class="endpoint-combobox" id="endpoint-combobox">
<button
type="button"
class="request-control endpoint-combo-trigger"
id="endpoint-select-trigger"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="endpoint-select-menu"
aria-labelledby="endpoint-select-label-text"
>
<span class="endpoint-combo-value is-placeholder" id="endpoint-select-value">Select an endpoint</span>
<span class="endpoint-combo-caret" aria-hidden="true"></span>
</button>
<div class="endpoint-combo-menu" id="endpoint-select-menu" role="listbox" hidden></div>
</div>
</div>
<div class="request-field send-field">
<label aria-hidden="true">&nbsp;</label>
<button class="btn btn-primary btn-sm" id="btn-send" type="button">Send</button>
</div>
</div>
<div class="request-meta-bar">
<span class="badge err" id="impl-pill" hidden>Not implemented</span>
<span class="meta-endpoint" id="catalog-meta" hidden></span>
<span class="meta-desc" id="method-desc"></span>
<label class="impl-filter" data-tooltip="Show only API methods implemented in this emulator. Unimplemented contract endpoints are hidden from the list and return HTTP 501 if called.">
<input type="checkbox" id="implemented-only" checked>
Implemented only
</label>
<div class="toolbar">
<button class="icon-btn url-toggle-btn" id="btn-url-toggle" type="button" aria-expanded="false" aria-controls="url-drawer" data-tooltip="Request URLs &amp; cURL" aria-label="Request URLs and cURL">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M10 13a3 3 0 0 0 4.24 0l2.83-2.83a3 3 0 0 0-4.24-4.24l-1.41 1.41" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
<path d="M14 11a3 3 0 0 0-4.24 0L6.93 13.83a3 3 0 0 0 4.24 4.24l1.41-1.41" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
</div>
</div>
</header>
<div class="workspace-body">
<div class="editors editors-single" id="editors">
<div class="editor-pane" id="request-pane-wrap" hidden>
<div class="editor-head">
<div class="request-pane-tabs" id="request-pane-tabs" hidden>
<button type="button" class="request-pane-tab active" data-request-tab="body" id="request-tab-body">Request body</button>
<button type="button" class="request-pane-tab" data-request-tab="query" id="request-tab-query">Query</button>
</div>
<span class="editor-head-title" id="request-pane-title">Request body</span>
<div class="editor-head-actions">
<button class="icon-btn" id="btn-copy-body" type="button" data-tooltip="Copy request body" aria-label="Copy request body">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
<div class="request-pane-body" id="body-pane-content">
<textarea id="body" hidden spellcheck="false" aria-hidden="true"></textarea>
<div class="json-viewer json-viewer-empty" id="body-output" tabindex="0" role="region" aria-label="Request body"></div>
</div>
<div class="request-pane-body" id="query-pane-content" hidden>
<div class="query-pane-scroll">
<table class="params-table">
<thead><tr><th>Name</th><th>Type</th><th>Value</th></tr></thead>
<tbody id="query-pane-fields"></tbody>
</table>
<div class="query-pane-empty" id="query-pane-empty" hidden>No query parameters for this method.</div>
</div>
<div class="query-preview">
<div class="query-preview-main">
<span class="query-preview-label">Query string</span>
<code class="query-preview-text is-empty" id="query-preview-text"></code>
</div>
<button type="button" class="btn btn-sm" id="btn-fill-query-examples" data-tooltip="Fill empty optional fields from catalog examples">Fill examples</button>
</div>
</div>
</div>
<div class="editor-pane">
<div class="editor-head">
<span class="editor-head-title">Response</span>
<div class="editor-head-actions">
<span class="status-code" id="status-badge" hidden></span>
<button class="icon-btn" id="btn-copy-response" type="button" data-tooltip="Copy response body" aria-label="Copy response body">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
<div class="json-viewer json-viewer-empty" id="output" tabindex="0" role="region" aria-label="Response body">Waiting for a request…</div>
</div>
</div>
</div>
</div>
</div>
<div class="params-backdrop" id="params-backdrop" aria-hidden="true"></div>
<aside class="params-drawer" id="params-drawer" aria-labelledby="params-drawer-title" aria-hidden="true">
<div class="params-drawer-head">
<h2 id="params-drawer-title">Request parameters</h2>
<button class="icon-btn" id="params-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="params-drawer-body">
<section class="param-section panel" id="path-fields-wrap" hidden>
<div class="panel-head">Path parameters</div>
<div class="panel-body">
<table class="params-table">
<thead><tr><th>Name</th><th>Type</th><th>Value</th></tr></thead>
<tbody id="path-fields"></tbody>
</table>
</div>
</section>
<section class="param-section panel" id="body-fields-wrap" hidden>
<div class="panel-head">Request body fields</div>
<div class="panel-body">
<table class="params-table">
<thead><tr><th>Name</th><th>Type</th><th>Value</th></tr></thead>
<tbody id="body-fields"></tbody>
</table>
</div>
</section>
</div>
</aside>
<div class="endpoints-backdrop" id="endpoints-backdrop" aria-hidden="true"></div>
<aside class="endpoints-drawer" id="endpoints-drawer" aria-labelledby="endpoints-drawer-title" aria-hidden="true">
<div class="endpoints-drawer-head">
<div class="endpoints-drawer-head-title">
<h2 id="endpoints-drawer-title">Endpoints</h2>
<span class="endpoints-drawer-count" id="endpoints-drawer-count" title="Total methods in selected catalog version"></span>
</div>
<button class="icon-btn" id="endpoints-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="endpoints-drawer-body">
<div class="nav-search field">
<input id="path-filter" placeholder="Filter by path or category…" autocomplete="off">
</div>
<nav class="nav-tree" id="endpoint-tree" aria-label="API endpoints">
<div class="empty">Loading catalog…</div>
</nav>
</div>
</aside>
<div class="environment-backdrop" id="environment-backdrop" aria-hidden="true"></div>
<aside class="environment-drawer" id="environment-drawer" aria-labelledby="environment-drawer-title" aria-hidden="true">
<div class="environment-drawer-head">
<h2 id="environment-drawer-title">Environment</h2>
<button class="icon-btn" id="environment-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="environment-drawer-body">
<div class="environment-panel">
<div class="help-stat-grid">
<div class="help-stat-card">
<span class="label">Runtime</span>
<span class="value" id="stat-runtime"></span>
</div>
<div class="help-stat-card">
<span class="label">Catalog</span>
<span class="value" id="stat-catalog"></span>
</div>
</div>
<h4 class="help-section-title">Inventory</h4>
<div class="help-stat-grid">
<div class="help-stat-card">
<span class="label">Datacenter</span>
<span class="value" id="stat-cluster-name"></span>
</div>
<div class="help-stat-card">
<span class="label">Hosts</span>
<span class="value" id="stat-nodes"></span>
</div>
<div class="help-stat-card">
<span class="label">VMs</span>
<span class="value" id="stat-qemu"></span>
</div>
<div class="help-stat-card">
<span class="label">Clusters</span>
<span class="value" id="stat-lxc"></span>
</div>
<div class="help-stat-card">
<span class="label">Datastores</span>
<span class="value" id="stat-storage"></span>
</div>
<div class="help-stat-card">
<span class="label">Networks</span>
<span class="value" id="stat-quorum"></span>
</div>
</div>
</div>
</div>
</aside>
<div class="data-backdrop" id="data-backdrop" aria-hidden="true"></div>
<aside class="data-drawer" id="data-drawer" aria-labelledby="data-drawer-title" aria-hidden="true">
<div class="data-drawer-head">
<h2 id="data-drawer-title">Data</h2>
<button class="icon-btn" id="data-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="data-drawer-body">
<div class="data-panel" id="data-panel">
<div class="help-panel-loading">Loading demo state…</div>
</div>
</div>
</aside>
<div class="url-backdrop" id="url-backdrop" aria-hidden="true"></div>
<aside class="url-drawer" id="url-drawer" aria-labelledby="url-drawer-title" aria-hidden="true">
<div class="url-drawer-head">
<h2 id="url-drawer-title">Request URL</h2>
<button class="icon-btn" id="url-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="url-drawer-body">
<div class="request-preview-panel" id="request-preview-panel">
<div class="request-url-block">
<span class="request-url-label">cURL — session</span>
<div class="request-preview-row">
<pre class="request-preview request-curl" id="request-preview-curl-password"></pre>
<button class="icon-btn" type="button" data-copy-curl="password" data-tooltip="Copy session cURL" aria-label="Copy session cURL">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
<div class="request-url-block">
<span class="request-url-label">cURL — token</span>
<div class="request-preview-row">
<pre class="request-preview request-curl" id="request-preview-curl-token"></pre>
<button class="icon-btn" type="button" data-copy-curl="token" data-tooltip="Copy token cURL" aria-label="Copy token cURL">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
<div class="request-url-block">
<span class="request-url-label">Cluster URL</span>
<div class="request-preview-row">
<code class="request-preview" id="request-preview-cluster"></code>
<button class="icon-btn" type="button" data-copy-url="cluster" data-tooltip="Copy cluster URL" aria-label="Copy cluster URL">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
<div class="request-url-block">
<span class="request-url-label">Emulator URL</span>
<div class="request-preview-row">
<code class="request-preview" id="request-preview-emulator"></code>
<button class="icon-btn" type="button" data-copy-url="emulator" data-tooltip="Copy emulator URL" aria-label="Copy emulator URL">
<svg viewBox="0 0 24 24" aria-hidden="true">
<rect x="8" y="2" width="8" height="4" rx="1"/>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</aside>
<div class="history-backdrop" id="history-backdrop" aria-hidden="true"></div>
<aside class="history-drawer" id="history-drawer" aria-labelledby="history-drawer-title" aria-hidden="true">
<div class="history-drawer-head">
<h2 id="history-drawer-title">History</h2>
<div class="drawer-head-actions">
<button class="icon-btn icon-btn-danger" id="history-clear" type="button" disabled data-tooltip="Clear history" aria-label="Clear history">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M4 7h16"/>
<path d="M9 7V5.5A1.5 1.5 0 0 1 10.5 4h3A1.5 1.5 0 0 1 15 5.5V7"/>
<path d="M7 7l.8 12.2A2 2 0 0 0 9.8 21h4.4a2 2 0 0 0 2-1.8L17 7"/>
<path d="M10 11v5"/>
<path d="M14 11v5"/>
</svg>
</button>
<button class="icon-btn" id="history-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
</div>
<div class="history-drawer-body">
<div class="history-list" id="history"></div>
</div>
</aside>
<div class="catalog-backdrop" id="catalog-backdrop" aria-hidden="true"></div>
<aside class="catalog-drawer" id="catalog-drawer" aria-labelledby="catalog-drawer-title" aria-hidden="true">
<div class="catalog-drawer-head">
<h2 id="catalog-drawer-title">API catalog</h2>
<button class="icon-btn" id="catalog-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="catalog-drawer-body">
<div class="catalog-drawer-scroll">
<div class="catalog-options" id="catalog-options"></div>
<section class="catalog-source" id="catalog-source-info" hidden aria-live="polite"></section>
<section class="catalog-coverage" id="catalog-coverage" aria-live="polite">
<h4 class="help-section-title">Implementation coverage</h4>
<div class="help-stat-card catalog-coverage-stat">
<span class="label">Implemented</span>
<span class="value ok" id="stat-impl"></span>
</div>
<div class="help-meter-list">
<div class="help-meter">
<span class="help-meter-label">Handlers</span>
<div class="help-meter-track">
<div class="help-meter-fill" id="catalog-impl-fill" style="width:0%"></div>
</div>
<span class="help-meter-value" id="catalog-impl-value"></span>
</div>
</div>
</section>
</div>
<div class="catalog-panel-actions">
<div class="catalog-panel-actions-row">
<button class="btn btn-primary btn-sm" id="btn-contract-apply" type="button">Apply as runtime</button>
<button class="btn btn-sm" id="btn-catalog-refresh" type="button">Update to latest</button>
</div>
<p class="catalog-apply-note" id="catalog-apply-note">
Apply swaps the live API contract in memory. Process restart returns to CONTRACT_SNAPSHOT.
</p>
</div>
</div>
</aside>
<div class="auth-backdrop" id="auth-backdrop" aria-hidden="true"></div>
<aside class="auth-drawer" id="auth-drawer" aria-labelledby="auth-drawer-title" aria-hidden="true">
<div class="auth-drawer-head">
<div class="auth-drawer-head-main">
<h2 id="auth-drawer-title">Authentication</h2>
<p class="auth-drawer-session" id="auth-drawer-session" hidden>
Signed in as <span class="auth-session-user" id="auth-panel-username"></span>
</p>
</div>
<button class="icon-btn" id="auth-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="auth-drawer-body" id="auth-drawer-body">
<div class="auth-panel" id="auth-panel-guest">
<p class="auth-hint">vSphere lab: <code>administrator@vsphere.local</code> / <code>VMware1!</code>. Session header: <code>vmware-api-session-id</code>.</p>
<div class="auth-form">
<div class="field">
<label for="username">Username</label>
<input id="username" value="administrator@vsphere.local" autocomplete="username" spellcheck="false">
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" type="password" value="VMware1!" autocomplete="current-password">
</div>
<div class="field" hidden>
<label for="api-token-id">API token ID</label>
<input id="api-token-id" placeholder="unused" autocomplete="off" spellcheck="false">
</div>
<div class="field" hidden>
<label for="api-token-secret">API token secret</label>
<input id="api-token-secret" type="password" placeholder="unused" autocomplete="off">
</div>
<button class="btn btn-primary btn-sm" id="btn-login" type="button">Sign in</button>
</div>
</div>
<div class="auth-panel-user" id="auth-panel-user" hidden>
<button class="btn btn-sm" id="btn-logout" type="button">Sign out</button>
</div>
</div>
</aside>
<div class="help-backdrop" id="help-backdrop" aria-hidden="true"></div>
<aside class="help-drawer" id="help-drawer" aria-labelledby="help-drawer-title" aria-hidden="true">
<div class="help-drawer-head">
<h2 id="help-drawer-title">Help</h2>
<button class="icon-btn" id="help-drawer-close" type="button" data-tooltip="Close panel" aria-label="Close panel">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6L6 18"/>
</svg>
</button>
</div>
<div class="help-drawer-body">
<p class="help-links-desc">API documentation and emulator diagnostics.</p>
<nav class="help-nav" aria-label="Help sections">
<button type="button" class="help-nav-btn active" data-help-view="about">About</button>
<button type="button" class="help-nav-btn" data-help-view="compatibility">Compatibility</button>
<button type="button" class="help-nav-btn" data-help-view="readiness">Readiness</button>
<a class="help-nav-link" href="/docs" target="_blank" rel="noreferrer">OpenAPI ↗</a>
</nav>
<div class="help-panel" id="help-panel"></div>
</div>
</aside>
<div class="ui-modal-backdrop" id="ui-modal-backdrop" aria-hidden="true">
<div
class="ui-modal"
id="ui-modal"
role="alertdialog"
aria-modal="true"
aria-labelledby="ui-modal-title"
aria-describedby="ui-modal-message"
aria-hidden="true"
tabindex="-1"
>
<div class="ui-modal-head">
<span class="ui-modal-icon warn" id="ui-modal-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none">
<path d="M12 4.5 20.5 19H3.5L12 4.5z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
<path d="M12 10v4.5M12 17h.01" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</span>
<div class="ui-modal-copy">
<h2 class="ui-modal-title" id="ui-modal-title">Confirm action</h2>
<p class="ui-modal-message" id="ui-modal-message"></p>
</div>
</div>
<div class="ui-modal-actions">
<button class="btn btn-sm" id="ui-modal-cancel" type="button">Cancel</button>
<button class="btn btn-sm btn-primary" id="ui-modal-confirm" type="button">Confirm</button>
</div>
</div>
</div>
<div class="ui-tooltip" id="ui-tooltip" role="tooltip" hidden></div>
<div class="toast-stack" id="toast-stack" aria-live="polite" aria-relevant="additions"></div>
<script>
const LS_MAJOR = "vmware-sim-series";
const LS_AUTH = "vmware-sim-auth";
const LS_CATALOG = "vmware-sim-catalog-cache";
const LS_SELECTION = "vmware-sim-selection";
const LS_PARAMS = "vmware-sim-params";
const LS_THEME = "vmware-sim-theme";
const HISTORY_LIMIT = 100;
const PARAMS_STORE_LIMIT = 200;
function readStoredHistory() {
try {
const raw = JSON.parse(localStorage.getItem("vmware-sim-history") || "[]");
return Array.isArray(raw) ? raw.slice(0, HISTORY_LIMIT) : [];
} catch {
return [];
}
}
const state = {
ticket: null,
csrf: null,
username: null,
major: 9,
majors: [],
runtimeVersion: null,
catalog: null,
method: null,
pathValues: {},
bodyValues: {},
selectedPath: null,
selectedVerb: null,
requestPaneTab: "body",
implementedOnly: true,
history: readStoredHistory(),
demoState: null,
lastResponseRaw: "",
};
let methodDetailsRequestId = 0;
const els = {
layout: document.getElementById("layout"),
workspaceBrand: document.querySelector(".workspace-brand"),
catalogBadge: document.getElementById("catalog-badge"),
catalogBackdrop: document.getElementById("catalog-backdrop"),
catalogDrawer: document.getElementById("catalog-drawer"),
catalogDrawerClose: document.getElementById("catalog-drawer-close"),
catalogOptions: document.getElementById("catalog-options"),
catalogSourceInfo: document.getElementById("catalog-source-info"),
btnContractApply: document.getElementById("btn-contract-apply"),
btnCatalogRefresh: document.getElementById("btn-catalog-refresh"),
catalogApplyNote: document.getElementById("catalog-apply-note"),
pathFilter: document.getElementById("path-filter"),
endpointTree: document.getElementById("endpoint-tree"),
methodTabs: document.getElementById("method-tabs"),
methodField: document.getElementById("method-field"),
endpointCombobox: document.getElementById("endpoint-combobox"),
endpointSelectTrigger: document.getElementById("endpoint-select-trigger"),
endpointSelectValue: document.getElementById("endpoint-select-value"),
endpointSelectMenu: document.getElementById("endpoint-select-menu"),
requestPreviewCurlPassword: document.getElementById("request-preview-curl-password"),
requestPreviewCurlToken: document.getElementById("request-preview-curl-token"),
requestPreviewCluster: document.getElementById("request-preview-cluster"),
requestPreviewEmulator: document.getElementById("request-preview-emulator"),
requestPreviewPanel: document.getElementById("request-preview-panel"),
btnUrlToggle: document.getElementById("btn-url-toggle"),
urlBackdrop: document.getElementById("url-backdrop"),
urlDrawer: document.getElementById("url-drawer"),
urlDrawerClose: document.getElementById("url-drawer-close"),
implementedOnly: document.getElementById("implemented-only"),
btnSend: document.getElementById("btn-send"),
environmentBadge: document.getElementById("environment-badge"),
environmentBackdrop: document.getElementById("environment-backdrop"),
environmentDrawer: document.getElementById("environment-drawer"),
environmentDrawerClose: document.getElementById("environment-drawer-close"),
dataBadge: document.getElementById("data-badge"),
dataBackdrop: document.getElementById("data-backdrop"),
dataDrawer: document.getElementById("data-drawer"),
dataDrawerClose: document.getElementById("data-drawer-close"),
dataPanel: document.getElementById("data-panel"),
catalogImplFill: document.getElementById("catalog-impl-fill"),
catalogImplValue: document.getElementById("catalog-impl-value"),
endpointsBadge: document.getElementById("endpoints-badge"),
endpointsBadgeCount: document.getElementById("endpoints-badge-count"),
endpointsBackdrop: document.getElementById("endpoints-backdrop"),
endpointsDrawer: document.getElementById("endpoints-drawer"),
endpointsDrawerCount: document.getElementById("endpoints-drawer-count"),
endpointsDrawerClose: document.getElementById("endpoints-drawer-close"),
authStatusPill: document.getElementById("auth-status-pill"),
authBackdrop: document.getElementById("auth-backdrop"),
authDrawer: document.getElementById("auth-drawer"),
authDrawerClose: document.getElementById("auth-drawer-close"),
authDrawerBody: document.getElementById("auth-drawer-body"),
authDrawerSession: document.getElementById("auth-drawer-session"),
helpBadge: document.getElementById("help-badge"),
themeToggle: document.getElementById("theme-toggle"),
helpBackdrop: document.getElementById("help-backdrop"),
helpDrawer: document.getElementById("help-drawer"),
helpDrawerClose: document.getElementById("help-drawer-close"),
helpPanel: document.getElementById("help-panel"),
helpNav: document.querySelector(".help-nav"),
authPanelGuest: document.getElementById("auth-panel-guest"),
authPanelUser: document.getElementById("auth-panel-user"),
authPanelUsername: document.getElementById("auth-panel-username"),
historyBadge: document.getElementById("history-badge"),
historyBadgeCount: document.getElementById("history-badge-count"),
historyBackdrop: document.getElementById("history-backdrop"),
historyDrawer: document.getElementById("history-drawer"),
historyDrawerClose: document.getElementById("history-drawer-close"),
historyClear: document.getElementById("history-clear"),
username: document.getElementById("username"),
password: document.getElementById("password"),
apiTokenId: document.getElementById("api-token-id"),
apiTokenSecret: document.getElementById("api-token-secret"),
methodDesc: document.getElementById("method-desc"),
implPill: document.getElementById("impl-pill"),
catalogMeta: document.getElementById("catalog-meta"),
paramsBadge: document.getElementById("params-badge"),
paramsBadgeCount: document.getElementById("params-badge-count"),
paramsBackdrop: document.getElementById("params-backdrop"),
paramsDrawer: document.getElementById("params-drawer"),
paramsDrawerClose: document.getElementById("params-drawer-close"),
pathFieldsWrap: document.getElementById("path-fields-wrap"),
pathFields: document.getElementById("path-fields"),
bodyFieldsWrap: document.getElementById("body-fields-wrap"),
bodyFields: document.getElementById("body-fields"),
body: document.getElementById("body"),
bodyOutput: document.getElementById("body-output"),
bodyPaneContent: document.getElementById("body-pane-content"),
queryPaneContent: document.getElementById("query-pane-content"),
queryPaneFields: document.getElementById("query-pane-fields"),
queryPaneEmpty: document.getElementById("query-pane-empty"),
queryPreviewText: document.getElementById("query-preview-text"),
btnFillQueryExamples: document.getElementById("btn-fill-query-examples"),
requestPaneWrap: document.getElementById("request-pane-wrap"),
requestPaneTabs: document.getElementById("request-pane-tabs"),
requestPaneTitle: document.getElementById("request-pane-title"),
requestTabBody: document.getElementById("request-tab-body"),
requestTabQuery: document.getElementById("request-tab-query"),
btnCopyBody: document.getElementById("btn-copy-body"),
editors: document.getElementById("editors"),
output: document.getElementById("output"),
statusBadge: document.getElementById("status-badge"),
history: document.getElementById("history"),
loadingBar: document.getElementById("loading-bar"),
toastStack: document.getElementById("toast-stack"),
uiTooltip: document.getElementById("ui-tooltip"),
uiModalBackdrop: document.getElementById("ui-modal-backdrop"),
uiModal: document.getElementById("ui-modal"),
uiModalIcon: document.getElementById("ui-modal-icon"),
uiModalTitle: document.getElementById("ui-modal-title"),
uiModalMessage: document.getElementById("ui-modal-message"),
uiModalCancel: document.getElementById("ui-modal-cancel"),
uiModalConfirm: document.getElementById("ui-modal-confirm"),
statRuntime: document.getElementById("stat-runtime"),
statCatalog: document.getElementById("stat-catalog"),
statClusterName: document.getElementById("stat-cluster-name"),
statNodes: document.getElementById("stat-nodes"),
statQemu: document.getElementById("stat-qemu"),
statLxc: document.getElementById("stat-lxc"),
statStorage: document.getElementById("stat-storage"),
statQuorum: document.getElementById("stat-quorum"),
statImpl: document.getElementById("stat-impl"),
};
function setText(el, value) {
if (el) el.textContent = value ?? "";
}
state.implementedOnly = els.implementedOnly?.checked ?? true;
let tooltipTarget = null;
let modalResolver = null;
function readStoredTheme() {
try {
return localStorage.getItem(LS_THEME) === "light" ? "light" : "dark";
} catch {
return "dark";
}
}
function applyTheme(theme) {
const dark = theme === "dark";
document.documentElement.setAttribute("data-theme", dark ? "dark" : "light");
if (!els.themeToggle) return;
const moonIcon = els.themeToggle.querySelector(".theme-icon-dark");
const sunIcon = els.themeToggle.querySelector(".theme-icon-light");
els.themeToggle.setAttribute(
"aria-label",
dark ? "Switch to light theme" : "Switch to dark theme",
);
els.themeToggle.dataset.tooltip = dark ? "Light theme" : "Dark theme";
if (moonIcon) moonIcon.hidden = dark;
if (sunIcon) sunIcon.hidden = !dark;
}
function toggleTheme() {
const next = readStoredTheme() === "dark" ? "light" : "dark";
try {
localStorage.setItem(LS_THEME, next);
} catch (_) {}
applyTheme(next);
}
applyTheme(readStoredTheme());
const MODAL_ICONS = {
info: '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M12 11v5M12 8h.01" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
warn: '<svg viewBox="0 0 24 24" fill="none"><path d="M12 4.5 20.5 19H3.5L12 4.5z" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/><path d="M12 10v4.5M12 17h.01" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
danger: '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="1.8"/><path d="M9 9l6 6M15 9l-6 6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>',
};
function modalIsOpen() {
return Boolean(modalResolver);
}
function closeModal(result = false) {
if (!modalResolver) return;
const resolve = modalResolver;
modalResolver = null;
els.uiModalBackdrop.classList.remove("open");
els.uiModal.setAttribute("aria-hidden", "true");
els.uiModalBackdrop.setAttribute("aria-hidden", "true");
document.body.classList.remove("modal-open");
resolve(result);
}
function openModal(options) {
const {
title,
message,
confirmLabel = "Confirm",
cancelLabel = "Cancel",
tone = "warn",
alert = false,
} = options;
els.uiModalTitle.textContent = title;
els.uiModalMessage.textContent = message;
els.uiModalConfirm.textContent = confirmLabel;
els.uiModalCancel.textContent = cancelLabel;
els.uiModalCancel.hidden = alert;
els.uiModalIcon.className = `ui-modal-icon ${tone === "danger" ? "danger" : tone === "info" ? "info" : "warn"}`;
els.uiModalIcon.innerHTML = MODAL_ICONS[tone === "danger" ? "danger" : tone === "info" ? "info" : "warn"];
els.uiModalConfirm.className = "btn btn-sm " + (
tone === "danger" ? "btn-danger" : "btn-primary"
);
els.uiModalBackdrop.classList.add("open");
els.uiModal.setAttribute("aria-hidden", "false");
els.uiModalBackdrop.setAttribute("aria-hidden", "false");
document.body.classList.add("modal-open");
window.requestAnimationFrame(() => {
(alert || tone !== "danger" ? els.uiModalConfirm : els.uiModalCancel).focus();
});
}
function showConfirm(options) {
if (modalResolver) closeModal(false);
return new Promise((resolve) => {
modalResolver = resolve;
openModal({ alert: false, ...options });
});
}
function showAlert(options) {
if (modalResolver) closeModal(false);
return new Promise((resolve) => {
modalResolver = () => resolve(true);
openModal({
tone: "info",
confirmLabel: "OK",
alert: true,
...options,
});
});
}
function bindModal() {
if (els.uiModalBackdrop.dataset.bound) return;
els.uiModalBackdrop.dataset.bound = "1";
els.uiModalCancel.addEventListener("click", () => closeModal(false));
els.uiModalConfirm.addEventListener("click", () => closeModal(true));
els.uiModalBackdrop.addEventListener("click", (event) => {
if (event.target === els.uiModalBackdrop) closeModal(false);
});
els.uiModal.addEventListener("click", (event) => event.stopPropagation());
}
function hideTooltip() {
tooltipTarget = null;
els.uiTooltip.hidden = true;
}
function positionTooltip(target) {
const rect = target.getBoundingClientRect();
const gap = 8;
const padding = 8;
els.uiTooltip.hidden = false;
const ttRect = els.uiTooltip.getBoundingClientRect();
let top = rect.top - ttRect.height - gap;
if (top < padding) {
top = rect.bottom + gap;
}
if (top + ttRect.height > window.innerHeight - padding) {
top = Math.max(padding, window.innerHeight - ttRect.height - padding);
}
let left = rect.left + rect.width / 2 - ttRect.width / 2;
left = Math.max(padding, Math.min(left, window.innerWidth - ttRect.width - padding));
els.uiTooltip.style.top = `${top}px`;
els.uiTooltip.style.left = `${left}px`;
}
function showTooltip(target) {
const text = target.getAttribute("data-tooltip");
if (!text) return;
tooltipTarget = target;
els.uiTooltip.textContent = text;
positionTooltip(target);
}
function bindTooltips() {
document.addEventListener("pointerover", (event) => {
if (!(event.target instanceof Element)) return;
const target = event.target.closest("[data-tooltip]");
if (!target || target === tooltipTarget) return;
showTooltip(target);
});
document.addEventListener("pointerout", (event) => {
if (!(event.target instanceof Element)) return;
const target = event.target.closest("[data-tooltip]");
if (!target || target !== tooltipTarget) return;
const related = event.relatedTarget instanceof Element
? event.relatedTarget.closest("[data-tooltip]")
: null;
if (related === target) return;
hideTooltip();
});
document.addEventListener("focusin", (event) => {
if (!(event.target instanceof Element)) return;
const target = event.target.closest("[data-tooltip]");
if (target) showTooltip(target);
});
document.addEventListener("focusout", (event) => {
if (!(event.target instanceof Element)) return;
if (event.target.closest("[data-tooltip]") === tooltipTarget) hideTooltip();
});
window.addEventListener("scroll", () => {
if (tooltipTarget) positionTooltip(tooltipTarget);
}, true);
window.addEventListener("resize", () => {
if (tooltipTarget) positionTooltip(tooltipTarget);
});
}
const TOAST_LIMIT = 5;
function dismissToast(node) {
if (!node || node.classList.contains("toast-leaving")) return;
clearTimeout(node._timer);
clearTimeout(node._hideTimer);
node.classList.remove("show");
node.classList.add("toast-leaving");
node._hideTimer = setTimeout(() => node.remove(), 320);
}
function toast(message, level = "info") {
const labels = {
ok: "Success",
warn: "Warning",
warning: "Warning",
error: "Error",
debug: "System",
info: "Information",
};
const normalized = level === "warning" ? "warn" : level;
const tag = labels[normalized] ? normalized : "info";
while (els.toastStack.children.length >= TOAST_LIMIT) {
dismissToast(els.toastStack.firstElementChild);
}
const node = document.createElement("div");
node.className = `toast toast-${tag}`;
node.setAttribute("role", "status");
const inner = document.createElement("div");
inner.className = "toast-inner";
const bodyEl = document.createElement("div");
bodyEl.className = "toast-body";
const labelEl = document.createElement("span");
labelEl.className = "toast-label";
labelEl.textContent = labels[tag] || "Information";
const msgEl = document.createElement("span");
msgEl.className = "toast-msg";
msgEl.textContent = message;
const closeBtn = document.createElement("button");
closeBtn.type = "button";
closeBtn.className = "toast-close";
closeBtn.setAttribute("aria-label", "Dismiss notification");
closeBtn.textContent = "×";
closeBtn.addEventListener("click", () => dismissToast(node));
bodyEl.append(labelEl, msgEl);
inner.append(bodyEl, closeBtn);
node.append(inner);
els.toastStack.append(node);
requestAnimationFrame(() => node.classList.add("show"));
node._timer = setTimeout(() => dismissToast(node), 3200);
}
function readStoredMajor() {
const value = Number(localStorage.getItem(LS_MAJOR));
return Number.isInteger(value) ? value : null;
}
function persistMajor(major) {
localStorage.setItem(LS_MAJOR, String(major));
}
function readStoredSelection(major = state.major) {
try {
const raw = localStorage.getItem(LS_SELECTION);
if (!raw) return null;
const data = JSON.parse(raw);
if (data?.path == null || data?.verb == null) return null;
if (data.major != null && Number(data.major) !== Number(major)) return null;
return { path: String(data.path), verb: String(data.verb) };
} catch {
return null;
}
}
function persistSelection() {
if (!state.selectedPath || !state.selectedVerb) {
localStorage.removeItem(LS_SELECTION);
return;
}
localStorage.setItem(
LS_SELECTION,
JSON.stringify({
major: state.major,
path: state.selectedPath,
verb: state.selectedVerb,
}),
);
persistParams();
}
function paramsStoreKey(path = state.selectedPath, verb = state.selectedVerb, major = state.major) {
if (!path || !verb) return null;
return `${Number(major)}|${path}|${String(verb).toUpperCase()}`;
}
function readParamsStore() {
try {
const raw = JSON.parse(localStorage.getItem(LS_PARAMS) || "{}");
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
} catch {
return {};
}
}
function writeParamsStore(store) {
try {
const keys = Object.keys(store).filter((k) => k !== "__fields");
if (keys.length > PARAMS_STORE_LIMIT) {
keys
.map((k) => [k, store[k]?.savedAt || 0])
.sort((a, b) => a[1] - b[1])
.slice(0, keys.length - PARAMS_STORE_LIMIT)
.forEach(([k]) => { delete store[k]; });
}
localStorage.setItem(LS_PARAMS, JSON.stringify(store));
} catch (error) {
console.error(error);
}
}
function readGlobalParamFields() {
const fields = readParamsStore().__fields;
return fields && typeof fields === "object" && !Array.isArray(fields) ? fields : {};
}
function readParamsEntry(store, path, verb, major) {
const key = paramsStoreKey(path, verb, major);
if (!key) return null;
if (store[key] && typeof store[key] === "object") return store[key];
// Tolerate older keys that used mixed-case verbs or string majors.
const needle = `|${path}|${String(verb).toUpperCase()}`;
const majorNeedle = `${Number(major)}|${path}|`;
for (const [k, entry] of Object.entries(store)) {
if (k === "__fields" || !entry || typeof entry !== "object") continue;
if (k === key) return entry;
if (k.endsWith(needle) || (k.startsWith(majorNeedle) && k.toUpperCase().endsWith(needle))) {
return entry;
}
}
return null;
}
function readStoredParams(path = state.selectedPath, verb = state.selectedVerb, major = state.major) {
const entry = readParamsEntry(readParamsStore(), path, verb, major);
if (!entry) return null;
const pathValues = entry.pathValues && typeof entry.pathValues === "object" && !Array.isArray(entry.pathValues)
? entry.pathValues
: {};
const bodyValues = entry.bodyValues && typeof entry.bodyValues === "object" && !Array.isArray(entry.bodyValues)
? entry.bodyValues
: {};
const body = typeof entry.body === "string" ? entry.body : null;
return { pathValues, bodyValues, body };
}
function storedParamValue(store, name) {
if (!store || typeof store !== "object" || !(name in store)) return null;
const value = store[name];
if (value == null) return "";
return String(value);
}
function resolveParamValue(fieldName, endpointStore, example) {
const endpointValue = storedParamValue(endpointStore, fieldName);
if (endpointValue != null) return endpointValue;
const globalValue = storedParamValue(readGlobalParamFields(), fieldName);
if (globalValue != null) return globalValue;
return String(example ?? "");
}
function captureParamInputs() {
if (!state.method) return;
els.pathFields?.querySelectorAll("input[data-field]").forEach((input) => {
state.pathValues[input.dataset.field] = input.value;
});
els.bodyFields?.querySelectorAll("input[data-field]").forEach((input) => {
state.bodyValues[input.dataset.field] = input.value;
});
els.queryPaneFields?.querySelectorAll("input[data-field]").forEach((input) => {
state.bodyValues[input.dataset.field] = input.value;
});
}
function paramsMethodMatchesSelection() {
return Boolean(
state.method
&& state.selectedPath
&& state.selectedVerb
&& state.method.path === state.selectedPath
&& String(state.method.verb).toUpperCase() === String(state.selectedVerb).toUpperCase(),
);
}
function persistParams() {
const key = paramsStoreKey();
if (!key) return;
// Avoid clobbering stored PARAM values with empty state before method details load
// (e.g. on page refresh, syncSelection persists the selection first).
if (!paramsMethodMatchesSelection()) return;
captureParamInputs();
const pathValues = { ...(state.pathValues || {}) };
const bodyValues = { ...(state.bodyValues || {}) };
const showBody = methodUsesRequestBody(state.method)
&& els.bodyPaneContent
&& !els.bodyPaneContent.hidden;
let body = els.body && showBody ? String(els.body.value ?? "") : "";
if (isEmptyRequestBody(body)) body = "";
const store = readParamsStore();
store[key] = {
pathValues,
bodyValues,
body,
savedAt: Date.now(),
};
store.__fields = {
...(store.__fields && typeof store.__fields === "object" ? store.__fields : {}),
...pathValues,
...bodyValues,
};
writeParamsStore(store);
}
function restoreStoredSelection(major = state.major) {
const stored = readStoredSelection(major);
if (!stored) return false;
state.selectedPath = stored.path;
state.selectedVerb = stored.verb;
return true;
}
function clearResponseWorkspace() {
state.lastResponseRaw = "";
if (els.output) {
els.output.className = "json-viewer json-viewer-empty";
els.output.textContent = "Waiting for a request…";
}
updateStatusBadge(null);
}
function clearMethodWorkspace() {
state.method = null;
state.pathValues = {};
state.bodyValues = {};
state.selectedPath = null;
state.selectedVerb = null;
if (els.catalogMeta) {
els.catalogMeta.textContent = "";
els.catalogMeta.hidden = true;
}
if (els.methodDesc) {
els.methodDesc.textContent = "";
els.methodDesc.title = "";
}
updateImplPill(null);
els.pathFieldsWrap.hidden = true;
els.bodyFieldsWrap.hidden = true;
els.pathFields.innerHTML = "";
els.bodyFields.innerHTML = "";
if (els.queryPaneFields) els.queryPaneFields.innerHTML = "";
setRequestPaneVisible(false);
setEndpointSelectValue("");
clearResponseWorkspace();
closeUrlDrawer();
updateResolvedPath();
updateSendState();
updateParamsBadge();
renderMethodTabs();
persistSelection();
}
function isUrlDrawerOpen() {
return Boolean(els.urlDrawer?.classList.contains("open"));
}
function toggleUrlDrawer(force) {
const open = typeof force === "boolean" ? force : !isUrlDrawerOpen();
if (open) updateResolvedPath();
els.urlDrawer.classList.toggle("open", open);
els.urlBackdrop.classList.toggle("open", open);
els.btnUrlToggle.setAttribute("aria-expanded", open ? "true" : "false");
els.btnUrlToggle.classList.toggle("active", open);
els.urlDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.urlBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
}
function closeUrlDrawer() {
toggleUrlDrawer(false);
}
function resetHomeWorkspace() {
closeOtherPanels();
syncSelection({ path: null, verb: null }).catch((error) => showError(String(error)));
}
function validateCurrentSelection() {
const paths = catalogPaths();
if (!state.selectedPath) return;
if (!paths.includes(state.selectedPath)) {
clearMethodWorkspace();
return;
}
const entry = pathEntry(state.selectedPath);
let methods = entry?.methods || [];
if (state.implementedOnly) {
methods = methods.filter((m) => isMethodImplemented(m));
}
if (!methods.length) {
clearMethodWorkspace();
return;
}
if (!methods.some((m) => m.verb === state.selectedVerb)) {
state.selectedVerb = methods[0].verb;
}
}
function readCatalogStore() {
try {
const raw = localStorage.getItem(LS_CATALOG);
return raw ? JSON.parse(raw) : {};
} catch {
return {};
}
}
function writeCatalogStore(store) {
localStorage.setItem(LS_CATALOG, JSON.stringify(store));
}
function getCachedCatalog(major) {
return readCatalogStore()[String(major)] || null;
}
function writeCatalogEntry(major, data) {
const store = readCatalogStore();
store[String(major)] = {
savedAt: new Date().toISOString(),
source_version: data.source_version,
data,
};
writeCatalogStore(store);
}
function formatCacheAge(savedAt) {
const mins = Math.floor((Date.now() - new Date(savedAt).getTime()) / 60000);
if (mins < 1) return "cached just now";
if (mins < 60) return `cached ${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 48) return `cached ${hours}h ago`;
const days = Math.floor(hours / 24);
return `cached ${days}d ago`;
}
function latestMajorVersion(major) {
return state.majors.find((m) => m.major === major)?.latest_version;
}
function renderCatalogBadge() {
if (!els.catalogBadge) return;
const cached = getCachedCatalog(state.major);
const version = state.catalog?.source_version || cached?.source_version;
setText(els.catalogBadge, version ? `${seriesLabel(state.major)} · ${version}` : seriesLabel(state.major));
els.catalogBadge.className = "header-tool-btn catalog-badge-btn " + (cached ? "ok" : "warn");
}
function majorInfo(major) {
return state.majors.find((m) => m.major === major);
}
function seriesLabel(major) {
const info = majorInfo(major);
return info?.series || `vSphere ${major}`;
}
function catalogCacheBadge(major = state.major) {
const cached = getCachedCatalog(major);
const latest = latestMajorVersion(major);
if (!cached) {
return `<span class="catalog-cache-badge">Not cached yet</span>`;
}
if (latest && cached.source_version !== latest) {
return `<span class="catalog-cache-badge warn">${escapeHtml(cached.source_version)} · update available</span>`;
}
return `<span class="catalog-cache-badge ok">${escapeHtml(formatCacheAge(cached.savedAt))} · ${escapeHtml(cached.source_version)}</span>`;
}
function renderCatalogSourceInfo(major = state.major) {
if (!els.catalogSourceInfo) return;
const info = majorInfo(major);
if (!info) {
els.catalogSourceInfo.hidden = true;
els.catalogSourceInfo.replaceChildren();
return;
}
const cached = getCachedCatalog(major);
const activeVersion = major === state.major ? state.catalog?.source_version : null;
const localVersion = cached?.source_version || activeVersion;
const bundleLabel = info.bundled
? `<span class="badge-inline ok">Bundled</span>`
: `<span class="badge-inline warn">Remote only</span>`;
els.catalogSourceInfo.hidden = false;
els.catalogSourceInfo.innerHTML = `
<div class="catalog-source-head">
<h3 class="catalog-source-title">Catalog source · ${seriesLabel(info.major)}</h3>
${catalogCacheBadge(major)}
</div>
<dl class="catalog-source-list">
<dt>Latest</dt><dd>${escapeHtml(info.latest_version)}</dd>
<dt>Local copy</dt><dd>${localVersion ? escapeHtml(localVersion) : "Not cached yet"}</dd>
<dt>Delivery</dt><dd>${bundleLabel}</dd>
<dt>Source URL</dt><dd><code>${escapeHtml(info.artifact_url)}</code></dd>
<dt>Runtime</dt><dd>${escapeHtml(state.runtimeVersion || "—")}${runtimeMajorMatches(major) ? ' <span class="badge-inline ok">active</span>' : ""}</dd>
</dl>
`;
updateApplyButton();
}
function runtimeMajorFromVersion(version) {
if (!version) return null;
const match = String(version).match(/^(\d+)/);
return match ? Number(match[1]) : null;
}
function runtimeMajorMatches(major = state.major) {
return runtimeMajorFromVersion(state.runtimeVersion) === major;
}
function updateApplyButton() {
if (!els.btnContractApply) return;
const active = runtimeMajorMatches(state.major);
els.btnContractApply.disabled = active;
els.btnContractApply.textContent = active
? `Runtime is ${seriesLabel(state.major)}`
: `Apply ${seriesLabel(state.major)} as runtime`;
}
function renderCatalogPanel() {
if (!els.catalogOptions) return;
const store = readCatalogStore();
els.catalogOptions.innerHTML = state.majors.map((m) => {
const cached = store[String(m.major)];
const active = m.major === state.major ? "active" : "";
const version = cached?.source_version || m.latest_version;
const meta = cached ? formatCacheAge(cached.savedAt) : "remote download";
return `
<button type="button" class="catalog-option ${active}" data-major="${m.major}">
<span class="catalog-option-title">${seriesLabel(m.major)}</span>
<span class="catalog-option-version">${version}</span>
<span class="catalog-option-meta">${meta}</span>
</button>`;
}).join("");
renderCatalogSourceInfo(state.major);
}
function toggleCatalogDrawer(force) {
const open = typeof force === "boolean" ? force : !els.catalogDrawer.classList.contains("open");
els.catalogDrawer.classList.toggle("open", open);
els.catalogBackdrop.classList.toggle("open", open);
els.catalogBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.catalogDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.catalogBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) {
renderCatalogPanel();
refreshCatalogCoverage().catch((error) => console.error(error));
}
}
function closeCatalogPanel() {
toggleCatalogDrawer(false);
}
async function applyCatalog(catalog, { preserveSelection = true } = {}) {
state.catalog = catalog;
setText(els.statCatalog, catalog.source_version);
renderCatalogBadge();
renderCatalogPanel();
if (!state.selectedPath && preserveSelection) {
restoreStoredSelection(state.major);
}
validateCurrentSelection();
renderEndpointSelect();
renderEndpointTree();
updateEndpointsCountBadge();
refreshCatalogCoverage().catch((error) => console.error(error));
await syncSelection();
}
function countCatalogMethods(catalog = state.catalog) {
if (!catalog?.categories) return 0;
let total = 0;
for (const category of catalog.categories) {
for (const entry of category.paths || []) {
total += (entry.methods || []).length;
}
}
return total;
}
function updateEndpointsCountBadge() {
const total = countCatalogMethods();
const label = total > 0 ? String(total) : "";
if (els.endpointsBadgeCount) {
els.endpointsBadgeCount.textContent = label;
els.endpointsBadgeCount.title = total
? `${total} methods in selected ${seriesLabel(state.major)} catalog`
: "Total methods in selected catalog version";
}
if (els.endpointsDrawerCount) {
els.endpointsDrawerCount.textContent = label;
els.endpointsDrawerCount.title = total
? `${total} methods in selected ${seriesLabel(state.major)} catalog`
: "Total methods in selected catalog version";
}
}
function readStoredAuth() {
try {
const raw = localStorage.getItem(LS_AUTH);
return raw ? JSON.parse(raw) : null;
} catch {
return null;
}
}
function syncAuthCookie(ticket) {
if (!ticket) return;
document.cookie = `vmware-api-session-id=${encodeURIComponent(ticket)}; path=/; SameSite=Strict`;
}
function persistAuth() {
if (state.ticket && state.username) {
localStorage.setItem(LS_AUTH, JSON.stringify({
ticket: state.ticket,
csrf: state.csrf,
username: state.username,
}));
syncAuthCookie(state.ticket);
} else {
localStorage.removeItem(LS_AUTH);
}
}
async function restoreAuth() {
const stored = readStoredAuth();
if (!stored?.ticket || !stored?.username) {
setAuth(false);
return;
}
state.ticket = stored.ticket;
state.csrf = stored.csrf || "";
state.username = stored.username;
els.username.value = stored.username;
syncAuthCookie(stored.ticket);
try {
const res = await fetch("/api/session", {
credentials: "include",
headers: {
Accept: "application/json",
"vmware-api-session-id": stored.ticket,
},
});
if (!res.ok) throw new Error("session expired");
const user = res.headers.get("x-vmware-session-user");
if (user) state.username = user;
setAuth(true);
} catch {
state.ticket = null;
state.csrf = null;
state.username = null;
localStorage.removeItem(LS_AUTH);
setAuth(false);
}
}
function setLoading(active) {
els.loadingBar.classList.toggle("active", active);
}
function authMenuLabel(username) {
if (!username) return "Guest";
const local = String(username).split("@")[0].trim();
if (local === "administrator") return "admin";
return local || String(username);
}
function setAuth(ok) {
setText(els.authStatusPill, ok ? authMenuLabel(state.username) : "Guest");
if (els.authStatusPill) {
els.authStatusPill.className = "header-tool-btn auth-badge-btn " + (ok ? "ok" : "err");
els.authStatusPill.title = ok && state.username ? state.username : "";
}
renderAuthPanel();
if (isUrlDrawerOpen()) updateResolvedPath();
}
function renderAuthPanel() {
const signedIn = Boolean(state.ticket);
els.authPanelGuest.hidden = signedIn;
els.authPanelUser.hidden = !signedIn;
if (els.authDrawerSession) els.authDrawerSession.hidden = !signedIn;
if (els.authDrawerBody) els.authDrawerBody.classList.toggle("auth-drawer-body-signed-in", signedIn);
if (signedIn) {
els.authPanelUsername.textContent = state.username || "—";
}
}
function toggleAuthDrawer(force) {
const open = typeof force === "boolean" ? force : !els.authDrawer.classList.contains("open");
els.authDrawer.classList.toggle("open", open);
els.authBackdrop.classList.toggle("open", open);
els.authStatusPill.setAttribute("aria-expanded", open ? "true" : "false");
els.authDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.authBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) renderAuthPanel();
}
function closeAuthPanel() {
toggleAuthDrawer(false);
}
function updateHistoryBadge() {
els.historyBadgeCount.textContent = state.history.length ? String(state.history.length) : "";
els.historyBadge.className = "header-tool-btn history-badge-btn " + (state.history.length ? "ok" : "warn");
if (els.historyClear) els.historyClear.disabled = !state.history.length;
}
async function clearHistory() {
if (!state.history.length) return;
const confirmed = await showConfirm({
title: "Clear request history?",
message: "This removes all saved requests from History, including replay links.",
confirmLabel: "Clear history",
cancelLabel: "Cancel",
tone: "danger",
});
if (!confirmed) return;
state.history = [];
localStorage.setItem("vmware-sim-history", "[]");
renderHistory();
toast("History cleared", "ok");
}
function toggleHistoryDrawer(force) {
const open = typeof force === "boolean" ? force : !els.historyDrawer.classList.contains("open");
els.historyDrawer.classList.toggle("open", open);
els.historyBackdrop.classList.toggle("open", open);
els.historyBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.historyDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.historyBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) renderHistory();
}
function closeHistoryDrawer() {
toggleHistoryDrawer(false);
}
function closeParamsDrawer() {
toggleParamsDrawer(false);
}
function methodUsesRequestBody(method) {
if (!method) return false;
const verb = String(method.verb || "").toUpperCase();
if (!["POST", "PUT", "PATCH"].includes(verb)) return false;
const example = method.body_example;
if (example && typeof example === "object" && !Array.isArray(example) && Object.keys(example).length) {
return true;
}
// Body fields that are not OpenAPI query params.
return bodyOnlyFields(method).length > 0;
}
function queryFields(method = state.method) {
return (method?.query_fields || []).filter((f) => f && f.name);
}
function methodHasQueryParams(method = state.method) {
return queryFields(method).length > 0;
}
function bodyOnlyFields(method = state.method) {
if (!method) return [];
const queryNames = new Set(queryFields(method).map((f) => f.name));
return [...(method.body_fields || []), ...(method.indexed_fields || [])]
.filter((f) => f && f.name && !queryNames.has(f.name));
}
function methodHasParams(method = state.method) {
if (!method) return false;
if ((method.path_fields || []).length > 0) return true;
if (bodyOnlyFields(method).length > 0) return true;
return methodUsesRequestBody(method);
}
function isEmptyRequestBody(text) {
const raw = String(text ?? "").trim();
if (!raw || /^\{\s*\}$/.test(raw)) return true;
try {
const parsed = JSON.parse(raw);
return Boolean(
parsed
&& typeof parsed === "object"
&& !Array.isArray(parsed)
&& Object.keys(parsed).length === 0,
);
} catch {
return false;
}
}
function normalizeRequestBodyText() {
if (!els.body) return "";
if (isEmptyRequestBody(els.body.value)) {
if (els.body.value) {
els.body.value = "";
updateBodyHighlight();
}
return "";
}
return String(els.body.value);
}
function methodCanHaveRequestBody(method = state.method) {
return methodUsesRequestBody(method);
}
function setRequestPaneVisible(show, { clear = true } = {}) {
if (!els.requestPaneWrap) return;
els.requestPaneWrap.hidden = !show;
els.editors.classList.toggle("editors-single", !show);
if (els.body) els.body.disabled = !show;
if (!show && clear) {
if (els.body?.value) {
els.body.value = "";
updateBodyHighlight();
}
if (els.queryPaneFields) els.queryPaneFields.innerHTML = "";
updateQueryPreview();
}
}
// Back-compat alias used by older call sites during the request-pane refactor.
function setBodyPaneVisible(show, opts) {
setRequestPaneVisible(show, opts);
}
function syncRequestPaneChrome({ showBody, showQuery } = {}) {
const bodyVisible = Boolean(showBody);
const queryVisible = Boolean(showQuery);
const both = bodyVisible && queryVisible;
if (els.requestPaneTabs) els.requestPaneTabs.hidden = !both;
if (els.requestPaneTitle) {
els.requestPaneTitle.hidden = both;
if (!both) {
els.requestPaneTitle.textContent = queryVisible && !bodyVisible
? "Query parameters"
: "Request body";
}
}
let tab = state.requestPaneTab || "body";
if (both) {
if (tab !== "body" && tab !== "query") tab = "body";
} else if (queryVisible) {
tab = "query";
} else {
tab = "body";
}
state.requestPaneTab = tab;
if (els.requestTabBody) els.requestTabBody.classList.toggle("active", tab === "body");
if (els.requestTabQuery) els.requestTabQuery.classList.toggle("active", tab === "query");
if (els.bodyPaneContent) els.bodyPaneContent.hidden = !(bodyVisible && (!both || tab === "body"));
if (els.queryPaneContent) els.queryPaneContent.hidden = !(queryVisible && (!both || tab === "query"));
if (els.btnCopyBody) {
if (queryVisible && (!both || tab === "query")) {
els.btnCopyBody.dataset.tooltip = "Copy query string";
els.btnCopyBody.setAttribute("aria-label", "Copy query string");
} else {
els.btnCopyBody.dataset.tooltip = "Copy request body";
els.btnCopyBody.setAttribute("aria-label", "Copy request body");
}
}
}
function updateRequestPaneVisibility(method = state.method) {
normalizeRequestBodyText();
const showBody = Boolean(method && methodCanHaveRequestBody(method) && !isEmptyRequestBody(els.body?.value));
const showQuery = methodHasQueryParams(method);
if (!showBody && !showQuery) {
setRequestPaneVisible(false);
return;
}
setRequestPaneVisible(true, { clear: false });
syncRequestPaneChrome({ showBody, showQuery });
if (showBody) updateBodyHighlight();
if (showQuery) updateQueryPreview();
}
function updateBodyPaneVisibility(method = state.method) {
updateRequestPaneVisibility(method);
}
function refreshRequestBody(method = state.method, storedBody = null) {
if (!method) {
setRequestPaneVisible(false);
return;
}
const canBuildBody = methodCanHaveRequestBody(method);
const hasStoredBody = typeof storedBody === "string" && storedBody.trim() && !isEmptyRequestBody(storedBody);
if (!canBuildBody && !hasStoredBody) {
if (els.body) {
els.body.value = "";
updateBodyHighlight();
}
} else if (hasStoredBody) {
els.body.value = isEmptyRequestBody(storedBody) ? "" : storedBody;
updateBodyHighlight();
} else if (
method.body_example
&& typeof method.body_example === "object"
&& !Array.isArray(method.body_example)
&& Object.keys(method.body_example).length > 0
) {
// Prefer full nested body_example over PARAM field stubs.
els.body.value = JSON.stringify(method.body_example, null, 2);
updateBodyHighlight();
} else if (canBuildBody) {
syncBodyFromFields({ syncVisibility: false });
} else {
els.body.value = "";
updateBodyHighlight();
}
renderQueryPane(method);
updateRequestPaneVisibility(method);
}
function formatQueryPreviewString(queryValues) {
if (!queryValues || !Object.keys(queryValues).length) return "";
const params = new URLSearchParams();
for (const [key, raw] of Object.entries(queryValues)) {
let parsed = raw;
if (typeof raw === "string" && ((raw.startsWith("[") && raw.endsWith("]")) || (raw.startsWith("{") && raw.endsWith("}")))) {
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
}
if (Array.isArray(parsed)) {
for (const item of parsed) params.append(key, String(item));
} else if (parsed != null && String(parsed) !== "") {
params.append(key, String(parsed));
}
}
const qs = params.toString();
return qs ? `?${qs}` : "";
}
function updateQueryPreview() {
if (!els.queryPreviewText) return;
const qs = formatQueryPreviewString(collectQueryValues());
if (!qs) {
els.queryPreviewText.textContent = "—";
els.queryPreviewText.classList.add("is-empty");
} else {
els.queryPreviewText.textContent = qs;
els.queryPreviewText.classList.remove("is-empty");
}
}
function renderQueryPane(method = state.method) {
const fields = queryFields(method);
if (!els.queryPaneFields) return;
if (!fields.length) {
els.queryPaneFields.innerHTML = "";
if (els.queryPaneEmpty) els.queryPaneEmpty.hidden = false;
updateQueryPreview();
return;
}
if (els.queryPaneEmpty) els.queryPaneEmpty.hidden = true;
renderParamRows(els.queryPaneFields, fields, "query");
updateQueryPreview();
}
function fillQueryExamples() {
const fields = queryFields(state.method);
if (!fields.length) return;
let filled = 0;
for (const field of fields) {
const name = field.name;
const current = String(state.bodyValues?.[name] ?? "").trim();
if (current) continue;
const example = field.example;
if (example == null || String(example).trim() === "") continue;
state.bodyValues[name] = String(example).trim();
filled += 1;
}
renderQueryPane(state.method);
// Keep Params drawer body inputs in sync if any shared names remain.
const bodyFields = bodyOnlyFields(state.method);
if (bodyFields.length) renderParamRows(els.bodyFields, bodyFields, "body");
updateResolvedPath();
persistParams();
toast(filled ? `Filled ${filled} example${filled === 1 ? "" : "s"}` : "No empty fields to fill", filled ? "ok" : "info");
}
function highlightJsonText(text) {
const source = String(text ?? "");
return escapeHtml(source).replace(
/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*")(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g,
(match, _quoted, _esc, colon) => {
if (match.startsWith('"')) {
if (colon) {
return `<span class="json-key">${match.slice(0, -1)}</span><span class="json-punct">:</span>`;
}
return `<span class="json-string">${match}</span>`;
}
if (match === "true" || match === "false") {
return `<span class="json-boolean">${match}</span>`;
}
if (match === "null") {
return `<span class="json-null">${match}</span>`;
}
return `<span class="json-number">${match}</span>`;
},
);
}
function updateBodyHighlight() {
if (!els.bodyOutput) return;
const raw = String(els.body?.value ?? "");
if (!raw.trim() || isEmptyRequestBody(raw)) {
els.bodyOutput.className = "json-viewer json-viewer-empty";
els.bodyOutput.textContent = "No request body";
return;
}
try {
const parsed = JSON.parse(raw);
// Same read-only tree as Response: color tokens + collapsible objects/arrays.
if (
parsed
&& typeof parsed === "object"
&& !Array.isArray(parsed)
&& Object.keys(parsed).length === 0
) {
els.bodyOutput.className = "json-viewer json-viewer-empty";
els.bodyOutput.textContent = "No request body";
return;
}
els.bodyOutput.className = "json-viewer";
renderJsonTree(parsed, els.bodyOutput);
return;
} catch {
// Invalid JSON still gets token colors (no fold tree).
els.bodyOutput.className = "json-viewer json-viewer-plain";
els.bodyOutput.innerHTML = highlightJsonText(raw);
}
}
function formatJsonPrimitive(value) {
if (value === null) return '<span class="json-null">null</span>';
if (typeof value === "boolean") {
return `<span class="json-boolean">${value}</span>`;
}
if (typeof value === "number") {
return `<span class="json-number">${value}</span>`;
}
return `<span class="json-string">${escapeHtml(JSON.stringify(value))}</span>`;
}
function createJsonFoldButton() {
const fold = document.createElement("button");
fold.type = "button";
fold.className = "json-fold";
fold.setAttribute("aria-label", "Toggle section");
fold.textContent = "▾";
return fold;
}
function buildJsonPrimitiveRow(key, value, depth, comma = false) {
const row = document.createElement("div");
row.className = "json-row";
row.style.paddingLeft = `${depth * 14}px`;
row.innerHTML = `<span class="json-fold-spacer" aria-hidden="true"></span>`;
const content = document.createElement("span");
if (key != null) {
content.innerHTML = `<span class="json-key">${escapeHtml(JSON.stringify(key))}</span><span class="json-punct">: </span>`;
}
content.innerHTML += formatJsonPrimitive(value);
if (comma) content.innerHTML += '<span class="json-punct">,</span>';
row.appendChild(content);
return row;
}
function buildJsonGroup(value, key, depth, comma = false) {
const isArray = Array.isArray(value);
const entries = isArray ? [...value.entries()] : Object.entries(value);
const open = isArray ? "[" : "{";
const close = isArray ? "]" : "}";
const collapsedHint = isArray ? "[…]" : "{…}";
const group = document.createElement("div");
group.className = "json-group";
const head = document.createElement("div");
head.className = "json-row json-row-head";
head.style.paddingLeft = `${depth * 14}px`;
const fold = createJsonFoldButton();
const label = document.createElement("span");
let labelHtml = "";
if (key != null) {
labelHtml += `<span class="json-key">${escapeHtml(JSON.stringify(key))}</span><span class="json-punct">: </span>`;
}
labelHtml += `<span class="json-open-punct json-punct">${open}</span>`;
labelHtml += `<span class="json-collapsed-punct json-punct">${collapsedHint}</span>`;
if (comma) labelHtml += '<span class="json-inline-comma json-punct">,</span>';
label.innerHTML = labelHtml;
head.append(fold, label);
const body = document.createElement("div");
body.className = "json-group-body";
entries.forEach(([entryKey, entryValue], index) => {
const childKey = isArray ? null : entryKey;
const isLast = index === entries.length - 1;
body.appendChild(buildJsonNode(entryValue, childKey, depth + 1, !isLast));
});
const foot = document.createElement("div");
foot.className = "json-row json-row-foot";
foot.style.paddingLeft = `${depth * 14}px`;
foot.innerHTML = `<span class="json-fold-spacer" aria-hidden="true"></span><span class="json-punct">${close}${comma ? "," : ""}</span>`;
fold.setAttribute("aria-expanded", "true");
fold.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
const collapsed = group.classList.toggle("is-collapsed");
fold.textContent = collapsed ? "▸" : "▾";
fold.setAttribute("aria-expanded", collapsed ? "false" : "true");
});
group.append(head, body, foot);
return group;
}
function buildJsonNode(value, key, depth, comma = false) {
if (value !== null && typeof value === "object") {
return buildJsonGroup(value, key, depth, comma);
}
return buildJsonPrimitiveRow(key, value, depth, comma);
}
function renderJsonTree(value, container) {
container.innerHTML = "";
container.classList.remove("json-viewer-empty", "json-viewer-plain");
if (value !== null && typeof value === "object") {
container.appendChild(buildJsonNode(value, null, 0));
return;
}
container.innerHTML = formatJsonPrimitive(value);
}
function renderPlainOutput(text) {
state.lastResponseRaw = String(text ?? "");
els.output.className = "json-viewer json-viewer-plain";
els.output.textContent = state.lastResponseRaw;
}
function renderHighlightedOutput(text) {
state.lastResponseRaw = String(text ?? "");
els.output.className = "json-viewer json-viewer-plain";
els.output.innerHTML = highlightJsonText(state.lastResponseRaw);
}
function renderJsonOutput(value) {
state.lastResponseRaw = JSON.stringify(value, null, 2);
els.output.className = "json-viewer";
renderJsonTree(value, els.output);
}
function updateParamsBadge() {
const pathCount = (state.method?.path_fields || []).length;
const bodyCount = bodyOnlyFields(state.method).length;
const total = pathCount + bodyCount;
// Params drawer: path + request-body fields. Query filters live in the left pane.
const show = methodHasParams(state.method);
if (els.paramsBadge) {
els.paramsBadge.hidden = !show;
if (els.paramsBadgeCount) els.paramsBadgeCount.textContent = total ? String(total) : "";
}
if (!show) closeParamsDrawer();
}
function toggleParamsDrawer(force) {
const open = typeof force === "boolean" ? force : !els.paramsDrawer.classList.contains("open");
if (!open && els.paramsDrawer.classList.contains("open")) persistParams();
els.paramsDrawer.classList.toggle("open", open);
els.paramsBackdrop.classList.toggle("open", open);
const expanded = open ? "true" : "false";
els.paramsBadge?.setAttribute("aria-expanded", expanded);
els.paramsDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.paramsBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) closeOtherPanels("params");
}
function setStatValue(el, value, tone = "") {
setText(el, value);
if (el) el.className = "value" + (tone ? ` ${tone}` : "");
}
function resetClusterStats(message = "—", tone = "") {
const guestTone = message === "Sign in" ? "warn" : tone;
setStatValue(els.statClusterName, message, guestTone);
setStatValue(els.statNodes, message, guestTone);
setStatValue(els.statQemu, message, guestTone);
setStatValue(els.statLxc, message, guestTone);
setStatValue(els.statStorage, message, guestTone);
setStatValue(els.statQuorum, message, guestTone);
}
async function refreshClusterStats() {
let demo = null;
try {
const demoRes = await fetch("/ui/api/demo/state");
if (demoRes.ok) demo = await demoRes.json();
} catch (error) {
console.warn(error);
}
const vsphere = demo?.vsphere || {};
if (!state.ticket) {
const byType = vsphere.by_type || {};
const networks = (byType.Network || 0) + (byType.DistributedVirtualPortgroup || 0);
setStatValue(els.statClusterName, vsphere.datacenters != null ? `DC ×${vsphere.datacenters}` : "Sign in", vsphere.datacenters ? "ok" : "warn");
setStatValue(els.statNodes, vsphere.hosts ?? "Sign in", vsphere.hosts ? "ok" : "warn");
setStatValue(els.statQemu, vsphere.vms ?? "Sign in", vsphere.vms ? "ok" : "warn");
setStatValue(els.statLxc, vsphere.clusters ?? "—", vsphere.clusters ? "ok" : "");
setStatValue(els.statStorage, vsphere.datastores ?? "—", vsphere.datastores ? "ok" : "");
setStatValue(els.statQuorum, networks || "—", networks ? "ok" : "");
return;
}
try {
const [hostsRes, vmsRes, dsRes, netRes, dcRes, clusterRes] = await Promise.all([
api("GET", "/api/vcenter/host"),
api("GET", "/api/vcenter/vm?limit=1"),
api("GET", "/api/vcenter/datastore"),
api("GET", "/api/vcenter/network"),
api("GET", "/api/vcenter/datacenter"),
api("GET", "/api/vcenter/cluster"),
]);
// VM list may be truncated by limit; prefer demo state totals when available.
const hosts = Array.isArray(hostsRes.body) ? hostsRes.body.length : (vsphere.hosts || 0);
const vms = vsphere.vms ?? (Array.isArray(vmsRes.body) ? vmsRes.body.length : 0);
const datastores = Array.isArray(dsRes.body) ? dsRes.body.length : (vsphere.datastores || 0);
const networks = Array.isArray(netRes.body) ? netRes.body.length : 0;
const dcs = Array.isArray(dcRes.body) ? dcRes.body : [];
const clusters = Array.isArray(clusterRes.body) ? clusterRes.body : [];
setStatValue(els.statClusterName, dcs[0]?.name || `DC ×${dcs.length}`, dcs.length ? "ok" : "");
setStatValue(els.statNodes, String(hosts), hosts ? "ok" : "");
setStatValue(els.statQemu, String(vms), vms ? "ok" : "");
setStatValue(els.statLxc, String(clusters.length || vsphere.clusters || 0), (clusters.length || vsphere.clusters) ? "ok" : "");
setStatValue(els.statStorage, String(datastores), datastores ? "ok" : "");
setStatValue(els.statQuorum, String(networks), networks ? "ok" : "");
} catch (error) {
console.error(error);
if (vsphere.vms != null) {
setStatValue(els.statClusterName, `DC ×${vsphere.datacenters || 0}`, "ok");
setStatValue(els.statNodes, String(vsphere.hosts ?? "—"), vsphere.hosts ? "ok" : "");
setStatValue(els.statQemu, String(vsphere.vms ?? "—"), vsphere.vms ? "ok" : "");
setStatValue(els.statLxc, String(vsphere.clusters ?? "—"), vsphere.clusters ? "ok" : "");
setStatValue(els.statStorage, String(vsphere.datastores ?? "—"), vsphere.datastores ? "ok" : "");
setStatValue(els.statQuorum, demo?.label || "—", "");
} else {
resetClusterStats("—");
}
}
}
function updateCatalogCoverage(data) {
const implEl = els.statImpl;
if (!implEl) return;
if (!data?.levels?.implemented) {
implEl.textContent = "—";
implEl.className = "value";
if (els.catalogImplFill) {
els.catalogImplFill.style.width = "0%";
els.catalogImplFill.className = "help-meter-fill";
}
if (els.catalogImplValue) els.catalogImplValue.textContent = "—";
return;
}
const implemented = data.levels.implemented.count;
const declared = data.total_declared ?? data.levels.declared?.count ?? 0;
const score = declared ? implemented / declared : 0;
implEl.textContent = `${implemented} / ${declared}`;
implEl.className = "value " + (score >= 0.75 ? "ok" : score >= 0.35 ? "warn" : "err");
const pct = formatPercent(score);
if (els.catalogImplFill) {
els.catalogImplFill.style.width = pct;
els.catalogImplFill.className = "help-meter-fill " + helpMeterClass(score);
}
if (els.catalogImplValue) {
els.catalogImplValue.textContent = `${implemented} · ${pct}`;
}
}
async function refreshCatalogCoverage() {
const c = await fetch(`/ui/api/compatibility?major=${state.major}`).then((r) => r.json()).catch(() => null);
updateCatalogCoverage(c);
}
function toggleEnvironmentDrawer(force) {
const open = typeof force === "boolean" ? force : !els.environmentDrawer.classList.contains("open");
els.environmentDrawer.classList.toggle("open", open);
els.environmentBackdrop.classList.toggle("open", open);
els.environmentBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.environmentDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.environmentBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) {
refreshOverview().catch((error) => console.error(error));
}
}
function closeEnvironmentDrawer() {
toggleEnvironmentDrawer(false);
}
function toggleDataDrawer(force) {
const open = typeof force === "boolean" ? force : !els.dataDrawer.classList.contains("open");
els.dataDrawer.classList.toggle("open", open);
els.dataBackdrop.classList.toggle("open", open);
els.dataBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.dataDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.dataBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) {
renderDataPanel().catch((error) => console.error(error));
}
}
function closeDataDrawer() {
toggleDataDrawer(false);
}
function buildDemoDataHtml(data) {
const vsphere = data?.vsphere || data || {};
const profile = String(vsphere.profile_hint || data?.profile || "minimal").toLowerCase();
const loaded = Boolean(data?.loaded ?? ["small", "large", "big"].includes(profile));
const badgeLabel = loaded ? String(profile) : "Minimal";
const sizes = [
{
id: "small",
label: "Small",
chip: "lab",
hosts: 3,
vms: 50,
datastores: 2,
networks: 2,
note: "1 cluster · extras ×1 · light smoke & cookbooks",
},
{
id: "large",
label: "Large",
chip: "default",
hosts: 10,
vms: 1000,
datastores: 4,
networks: 4,
note: "1 cluster · extras ×2 · default API matrix",
},
{
id: "big",
label: "Big",
chip: "heavy",
hosts: 20,
vms: 2000,
datastores: 8,
networks: 8,
note: "1 cluster · extras ×4 · enterprise-scale dump",
},
];
const stats = [
["Profile", profile || "—", loaded ? "ok" : ""],
["Hosts", vsphere.hosts ?? "—", ""],
["VMs", vsphere.vms ?? "—", ""],
["Datastores", vsphere.datastores ?? "—", ""],
["Clusters", vsphere.clusters ?? "—", ""],
["Networks", vsphere.networks ?? "—", ""],
["Objects", vsphere.objects ?? "—", ""],
];
return `
<div class="help-demo-panel">
<div class="help-report-head help-demo-head">
<h3>Cluster data</h3>
<p>Pick a cluster size — hosts, VMs, datastores, networks, folders, and content extras scale together.</p>
<span class="help-demo-badge ${loaded ? "loaded" : "empty"}">${escapeHtml(badgeLabel)}</span>
</div>
<p class="help-demo-note">
Minimal = 3 hosts · 5 cookbook VMs · 1 datastore. Loading replaces current inventory.
</p>
<div class="help-stat-grid">
${stats.map(([label, value, tone]) => `
<div class="help-stat-card">
<span class="label">${escapeHtml(label)}</span>
<span class="value ${tone}">${escapeHtml(String(value))}</span>
</div>
`).join("")}
</div>
<div class="demo-size-grid" role="group" aria-label="Cluster sizes">
${sizes.map((s) => {
const active = profile === s.id;
return `
<article class="demo-size-card ${active ? "is-active" : ""}" data-size-card="${s.id}">
<div class="demo-size-card-head">
<h4 class="demo-size-card-title">${escapeHtml(s.label)}</h4>
<span class="demo-size-card-chip">${escapeHtml(s.chip)}</span>
</div>
<dl class="demo-size-card-metrics">
<div><dt>Hosts</dt><dd>${s.hosts}</dd></div>
<div><dt>VMs</dt><dd>${s.vms}</dd></div>
<div><dt>Datastores</dt><dd>${s.datastores}</dd></div>
<div><dt>Networks</dt><dd>${s.networks}</dd></div>
</dl>
<p class="demo-size-card-note">${escapeHtml(s.note)}</p>
<button class="btn btn-sm btn-demo-load" data-demo-size="${s.id}" type="button">
Load ${escapeHtml(s.label.toLowerCase())}
</button>
</article>`;
}).join("")}
</div>
<div class="demo-toolbar">
<button class="btn btn-sm btn-demo-unload" id="btn-demo-unload" type="button">Reset to minimal</button>
<button class="btn btn-sm" id="btn-demo-refresh" type="button">Refresh stats</button>
</div>
</div>
`;
}
function updateDataBadge(data) {
if (!els.dataBadge) return;
els.dataBadge.classList.toggle("loaded", Boolean(data?.loaded));
}
async function renderDataPanel() {
els.dataPanel.innerHTML = `<div class="help-panel-loading">Loading demo state…</div>`;
try {
await refreshDemoState();
els.dataPanel.innerHTML = buildDemoDataHtml(state.demoState);
} catch {
els.dataPanel.innerHTML = `<div class="help-panel-error">Failed to load demo state.</div>`;
}
}
function renderDemoState(data) {
state.demoState = data;
updateDataBadge(data);
if (els.dataDrawer?.classList.contains("open") && els.dataPanel) {
els.dataPanel.innerHTML = buildDemoDataHtml(data);
}
}
async function refreshDemoState() {
const res = await fetch("/ui/api/demo/state");
if (!res.ok) {
throw new Error(`Demo state request failed: ${res.status}`);
}
renderDemoState(await res.json());
}
async function loadDemoData(size = "large") {
const loadBtns = document.querySelectorAll("[data-demo-size]");
loadBtns.forEach((btn) => { btn.disabled = true; });
setLoading(true);
try {
const res = await fetch(`/ui/api/demo/load?size=${encodeURIComponent(size)}`, { method: "POST" });
const body = await res.json().catch(() => ({}));
if (!res.ok) {
const detail = Array.isArray(body.detail)
? body.detail.map((d) => d.msg || d).join("; ")
: body.detail;
throw new Error(detail || `Load failed: ${res.status}`);
}
await refreshDemoState();
if (state.ticket) syncAuthCookie(state.ticket);
refreshOverview();
const seed = body.vsphere || body.seed || {};
toast(
`Cluster ${body.profile || size} loaded (${seed.hosts ?? "?"} hosts · ${seed.vms ?? "?"} VMs)`,
"ok",
);
if (!state.ticket) {
toast("Sign in (administrator@vsphere.local / VMware1!) to call API endpoints", "warn");
}
} finally {
loadBtns.forEach((btn) => { btn.disabled = false; });
setLoading(false);
}
}
async function unloadDemoData() {
const confirmed = await showConfirm({
title: "Reset to minimal?",
message: "This wipes current inventory (including API-created objects) and loads the minimal cluster: 3 hosts · 5 cookbook VMs · 1 datastore.",
confirmLabel: "Reset to minimal",
cancelLabel: "Keep current data",
tone: "danger",
});
if (!confirmed) return;
const unloadBtn = document.getElementById("btn-demo-unload");
if (unloadBtn) unloadBtn.disabled = true;
setLoading(true);
try {
const res = await fetch("/ui/api/demo/unload", { method: "POST" });
const body = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(body.detail || `Reset failed: ${res.status}`);
}
await refreshDemoState();
refreshOverview();
toast("Reset to minimal cluster", "info");
} finally {
if (unloadBtn) unloadBtn.disabled = false;
setLoading(false);
}
}
function bindDataPanelActions() {
if (!els.dataPanel || els.dataPanel.dataset.demoBound) return;
els.dataPanel.dataset.demoBound = "1";
els.dataPanel.addEventListener("click", (event) => {
const loadBtn = event.target.closest("[data-demo-size]");
if (loadBtn) {
const size = loadBtn.getAttribute("data-demo-size") || "large";
loadDemoData(size).catch((error) => {
showError(String(error));
toast("Failed to load demo data", "error");
});
return;
}
const target = event.target.closest("#btn-demo-unload, #btn-demo-refresh");
if (!target) return;
if (target.id === "btn-demo-unload") {
unloadDemoData().catch((error) => {
showError(String(error));
toast("Failed to reset to minimal", "error");
});
} else if (target.id === "btn-demo-refresh") {
renderDataPanel().catch((error) => showError(String(error)));
}
});
}
function formatHelpLabel(value) {
const labels = {
declared: "Registry total",
schema_only: "Gated (HTTP 501)",
gated: "Gated (HTTP 501)",
implemented: "Available at major",
observed: "Surface probed",
verified: "Matrix verified",
route_method: "All registered routes",
get: "GET",
post: "POST",
put: "PUT",
patch: "PATCH",
delete: "DELETE",
auth_session: "Session / CIS auth",
inventory: "vCenter inventory",
legacy_rest: "Legacy /rest wrappers",
fully_compatible: "Available",
available: "Available",
unsupported: "Gated (501)",
gated_501: "Gated (501)",
};
if (labels[value]) return labels[value];
return String(value || "").replaceAll("_", " ");
}
function formatPercent(score) {
return `${Math.round(Number(score || 0) * 100)}%`;
}
function helpMeterClass(score) {
if (score >= 0.75) return "ok";
if (score >= 0.35) return "warn";
return "";
}
function compatibilitySubtitle(data) {
const runtime = data.runtime_version
? ` Runtime: ${data.runtime_version}.`
: "";
if (data.plane === "vsphere-rest" || data.evidence_scope === "vsphere-registry") {
const impl = data.levels?.implemented?.count ?? data.summary?.implemented ?? "—";
const total = data.total_declared ?? data.summary?.methods ?? "—";
const gated = data.levels?.gated?.count
?? data.levels?.schema_only?.count
?? data.summary?.unsupported_in_version
?? 0;
const cov = data.levels?.implemented?.score != null
? formatPercent(data.levels.implemented.score)
: (data.summary?.coverage != null ? formatPercent(data.summary.coverage) : "—");
return `${impl} of ${total} registry methods available at this major (${cov}); ${gated} gated to HTTP 501.${runtime}`;
}
if (data.evidence_scope === "full") {
return `Full-surface verified ledger for the selected VMware API catalog (levels + all 13 dimensions).${runtime}`;
}
return `Catalog coverage for the selected contract.${runtime} Dimensions come from the per-version evidence ledger after Apply as runtime.`;
}
function buildVsphereCompatibilityHtml(data) {
const levels = data.levels || {};
const summary = data.summary || {};
const dims = data.dimensions || {};
const implemented = levels.implemented?.count || summary.implemented || 0;
const declared = data.total_declared || levels.declared?.count || summary.methods || 0;
const gated = levels.gated?.count ?? levels.schema_only?.count ?? summary.unsupported_in_version ?? 0;
const verified = levels.verified?.count || 0;
const coverage = declared ? implemented / declared : 0;
const catalogVersion = data.catalog_version || data.source_version || "—";
const byVerb = summary.by_verb || {};
const universeByVerb = summary.universe_by_verb || {};
const verbOrder = ["GET", "POST", "PATCH", "PUT", "DELETE"];
const verbs = verbOrder.filter((v) => (universeByVerb[v] || byVerb[v] || 0) > 0)
.concat(Object.keys({ ...universeByVerb, ...byVerb }).filter((v) => !verbOrder.includes(v)));
const levelOrder = [
["declared", "Registry total"],
["implemented", "Available at major"],
["gated", "Gated (HTTP 501)"],
["verified", "Matrix verified"],
];
const levelRows = levelOrder.map(([key, label]) => {
const item = levels[key] || (key === "gated" ? levels.schema_only : null);
if (!item) return "";
const score = Number(item.score || 0);
// For gated, a high share is bad → invert meter tone; bar still shows gated share.
const toneScore = key === "gated" ? Math.max(0, 1 - score) : score;
return `<div class="help-meter">
<span class="help-meter-label">${escapeHtml(label)}</span>
<div class="help-meter-track"><div class="help-meter-fill ${helpMeterClass(toneScore)}" style="width:${formatPercent(score)}"></div></div>
<span class="help-meter-value">${item.count} · ${formatPercent(score)}</span>
</div>`;
}).join("");
const verbRows = verbs.map((verb) => {
const avail = byVerb[verb] || 0;
const total = universeByVerb[verb] || avail;
const score = total ? avail / total : 0;
return `<tr>
<td>${escapeHtml(verb)}</td>
<td class="num">${avail}</td>
<td class="num">${total}</td>
<td class="num">${formatPercent(score)}</td>
</tr>`;
}).join("");
const dimOrder = ["route_method", "get", "post", "patch", "delete", "auth_session", "inventory", "legacy_rest"];
const dimensionRows = dimOrder
.filter((name) => dims[name])
.map((name) => {
const item = dims[name];
return `<tr>
<td>${escapeHtml(formatHelpLabel(name))}</td>
<td class="num">${item.count}</td>
<td class="num">${formatPercent(item.score)}</td>
</tr>`;
}).join("");
const chips = [
["Available", implemented, "ok"],
["Gated (501)", gated, gated ? "warn" : "ok"],
["Coverage", formatPercent(coverage), coverage >= 0.75 ? "ok" : "warn"],
].map(([label, count, tone]) => `<span class="help-chip ${tone}">${escapeHtml(String(label))} <strong>${count}</strong></span>`).join("");
return `
<div class="help-report-head">
<h3>${escapeHtml(data.series || seriesLabel(state.major))} · ${escapeHtml(catalogVersion)}</h3>
<p>${escapeHtml(compatibilitySubtitle(data))}</p>
</div>
<div class="help-stat-grid">
<div class="help-stat-card">
<span class="label">Registry methods</span>
<span class="value">${declared}</span>
</div>
<div class="help-stat-card">
<span class="label">Available @ major ${state.major}</span>
<span class="value ok">${implemented}</span>
</div>
<div class="help-stat-card">
<span class="label">Gated (501)</span>
<span class="value ${gated ? "warn" : "ok"}">${gated}</span>
</div>
<div class="help-stat-card">
<span class="label">Coverage</span>
<span class="value ${helpMeterClass(coverage)}">${formatPercent(coverage)}</span>
</div>
</div>
<h4 class="help-section-title">At a glance</h4>
<div class="help-chip-row">${chips}</div>
<h4 class="help-section-title">Coverage levels</h4>
<div class="help-meter-list">${levelRows}</div>
<h4 class="help-section-title">By HTTP method</h4>
<p class="help-section-note">Available at the selected major vs full simulator registry (universe).</p>
<table class="help-table">
<thead><tr><th>Verb</th><th>Available</th><th>Universe</th><th>Share</th></tr></thead>
<tbody>${verbRows || `<tr><td colspan="4">No verb data.</td></tr>`}</tbody>
</table>
<h4 class="help-section-title">Domain dimensions</h4>
<p class="help-section-note">Live counts from <code>coverage.py</code> + <code>PATH_FLOOR</code>; evidence: <code>evidence/vsphere-${escapeHtml(catalogVersion)}.json</code>. Matrix probe: <code>make vsphere-matrix</code>.</p>
<table class="help-table">
<thead><tr><th>Dimension</th><th>Available</th><th>Score</th></tr></thead>
<tbody>${dimensionRows || `<tr><td colspan="3">No dimension data.</td></tr>`}</tbody>
</table>
<p class="help-section-note">Verified ${verified} / ${declared} · Runtime ${escapeHtml(data.runtime_version || "—")} · Plane ${escapeHtml(data.plane || "vsphere-rest")}</p>
`;
}
function buildCompatibilityHtml(data) {
if (data.plane === "vsphere-rest" || data.evidence_scope === "vsphere-registry") {
return buildVsphereCompatibilityHtml(data);
}
const levels = data.levels || {};
const implemented = levels.implemented?.count || 0;
const verified = levels.verified?.count || 0;
const declared = data.total_declared || levels.declared?.count || 0;
const classifications = data.classifications || {};
const catalogVersion = data.catalog_version || data.source_version || "—";
const levelOrder = ["declared", "schema_only", "implemented", "observed", "verified"];
const levelRows = levelOrder
.filter((name) => levels[name])
.map((name) => {
const item = levels[name];
const score = item.score || 0;
return `<div class="help-meter">
<span class="help-meter-label">${escapeHtml(formatHelpLabel(name))}</span>
<div class="help-meter-track"><div class="help-meter-fill ${helpMeterClass(score)}" style="width:${formatPercent(score)}"></div></div>
<span class="help-meter-value">${item.count} · ${formatPercent(score)}</span>
</div>`;
}).join("");
const dimensionRows = Object.entries(data.dimensions || {}).map(([name, item]) => `
<tr>
<td>${escapeHtml(formatHelpLabel(name))}</td>
<td class="num">${item.count}</td>
<td class="num">${formatPercent(item.score)}</td>
</tr>
`).join("");
const chips = [
["Fully compatible", classifications.fully_compatible?.length || 0, "ok"],
["Partially compatible", classifications.partially_compatible?.length || 0, "warn"],
["Incompatible", classifications.incompatible?.length || 0, "err"],
["Regressions", classifications.regressions?.length || 0, "err"],
["Unsupported", classifications.unsupported?.length || 0, ""],
].map(([label, count, tone]) => `<span class="help-chip ${tone}">${escapeHtml(label)} <strong>${count}</strong></span>`).join("");
return `
<div class="help-report-head">
<h3>${escapeHtml(seriesLabel(state.major))} · ${escapeHtml(catalogVersion)} compatibility</h3>
<p>${escapeHtml(compatibilitySubtitle(data))}</p>
</div>
<div class="help-stat-grid">
<div class="help-stat-card">
<span class="label">Declared methods</span>
<span class="value">${declared}</span>
</div>
<div class="help-stat-card">
<span class="label">Implemented</span>
<span class="value ok">${implemented}</span>
</div>
<div class="help-stat-card">
<span class="label">Verified</span>
<span class="value ${verified ? "ok" : "warn"}">${verified}</span>
</div>
<div class="help-stat-card">
<span class="label">Coverage</span>
<span class="value">${declared ? formatPercent(implemented / declared) : "—"}</span>
</div>
</div>
<h4 class="help-section-title">Compatibility levels</h4>
<div class="help-meter-list">${levelRows}</div>
<h4 class="help-section-title">Classifications</h4>
<div class="help-chip-row">${chips}</div>
<h4 class="help-section-title">Dimensions</h4>
<p class="help-section-note">Coverage is driven by <code>app/vsphere/rest/coverage.py</code> and <code>evidence/vsphere-*.json</code>.</p>
<table class="help-table">
<thead><tr><th>Dimension</th><th>Verified</th><th>Score</th></tr></thead>
<tbody>${dimensionRows || `<tr><td colspan="3">No dimension data.</td></tr>`}</tbody>
</table>
`;
}
function buildReadinessHtml(checks) {
const cards = checks.map((check) => {
const ok = check.ok;
return `
<article class="help-status-card ${ok ? "ok" : "err"}">
<div class="help-status-card-head">
<strong>${escapeHtml(check.title)}</strong>
<span class="help-status-badge ${ok ? "ok" : "err"}">${ok ? "OK" : "Unavailable"}</span>
</div>
<p>${escapeHtml(check.description)}</p>
<p><code>${escapeHtml(check.endpoint)}</code> · HTTP ${check.status}</p>
</article>
`;
}).join("");
return `
<div class="help-report-head">
<h3>Emulator readiness</h3>
<p>Live and readiness probes for the running simulator instance.</p>
</div>
<div class="help-status-list">${cards}</div>
`;
}
function buildAboutHtml() {
const version = state.runtimeVersion || "—";
const catalogMajor = state.major || "—";
return `
<div class="help-about-panel">
<div class="help-report-head">
<h3>VMware API Simulator</h3>
<p>
Stateful asynchronous VMware / vSphere API simulator for testing API clients,
automation, and infrastructure tooling without a real hypervisor cluster.
</p>
<p class="help-about-meta">
Runtime <code>${escapeHtml(version)}</code> · catalog <code>${escapeHtml(seriesLabel(catalogMajor))}</code>
</p>
</div>
<div class="help-report-head">
<h3>Author</h3>
<p class="help-about-name">Sergey Antropov</p>
<p class="help-about-copy">
Infrastructure, platform and operations practitioner. Two decades from development and
sysops to DevOps leadership and big-data infrastructure — hybrid clouds, Kubernetes,
virtualization, distributed storage, automation, observability, and CI/CD/GitOps.
Focuses on reproducible, observable and reversible systems, and on teams that ship
reliably without heroics on call.
</p>
<div class="help-about-social">
<a href="https://antropoff.ru/en/" target="_blank" rel="noopener noreferrer" aria-label="antropoff.ru" data-tooltip="antropoff.ru">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="12" cy="12" r="9"/>
<path d="M3 12h18"/>
<path d="M12 3c2.6 2.9 2.6 14.1 0 18"/>
<path d="M12 3c-2.6 2.9-2.6 14.1 0 18"/>
</svg>
</a>
<a href="https://devops.org.ru" target="_blank" rel="noopener noreferrer" aria-label="devops.org.ru" data-tooltip="devops.org.ru">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 18h14"/>
<path d="M7 14h10"/>
<path d="M9 10h6"/>
<path d="M11 6h2"/>
</svg>
</a>
<a class="help-about-social-github" href="https://github.com/sergeyantropoff" target="_blank" rel="noopener noreferrer" aria-label="GitHub" data-tooltip="github.com/sergeyantropoff">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 2a10 10 0 0 0-3.2 19.5c.5.1.7-.2.7-.5v-1.8c-3 .7-3.6-1.3-3.6-1.3-.5-1.1-1.1-1.4-1.1-1.4-.9-.6.1-.6.1-.6 1 .1 1.5 1 1.5 1 .9 1.5 2.4 1.1 3 .8.1-.7.4-1.1.7-1.4-2.4-.3-5-1.2-5-5.5a4.3 4.3 0 0 1 1.2-3 4.3 4.3 0 0 1 3.2-1.3c1.2 0 2.2.4 3 1a7.5 7.5 0 0 1 4.6-1.6c.5 0 1 .1 1.5.2a4.3 4.3 0 0 1 1.2 3c0 3.3-2.6 4.1-5 4.3-1.1.9-1 1.4-1 3.6v2.7c0 .3.2.6.7.5A10 10 0 0 0 12 2z"/>
</svg>
</a>
</div>
</div>
</div>
`;
}
function renderHelpAbout() {
els.helpPanel.innerHTML = buildAboutHtml();
}
async function renderHelpCompatibility() {
els.helpPanel.innerHTML = `<div class="help-panel-loading">Loading compatibility report…</div>`;
try {
const res = await fetch(`/ui/api/compatibility?major=${state.major}`);
if (!res.ok) throw new Error(String(res.status));
els.helpPanel.innerHTML = buildCompatibilityHtml(await res.json());
} catch {
els.helpPanel.innerHTML = `<div class="help-panel-error">Failed to load compatibility report.</div>`;
}
}
function refreshHelpIfCompatibilityActive() {
const active = els.helpNav?.querySelector('[data-help-view="compatibility"].active');
if (active && els.helpDrawer.classList.contains("open")) {
renderHelpCompatibility().catch((error) => console.error(error));
}
}
async function renderHelpReadiness() {
els.helpPanel.innerHTML = `<div class="help-panel-loading">Checking readiness…</div>`;
try {
const specs = [
{
title: "Readiness probe",
endpoint: "/health/ready",
description: "Database dependency is available and the emulator can serve stateful requests.",
},
{
title: "Liveness probe",
endpoint: "/health/live",
description: "Application process is running and responding to health checks.",
},
];
const checks = await Promise.all(specs.map(async (spec) => {
const res = await fetch(spec.endpoint);
const body = await res.json().catch(() => ({}));
return {
...spec,
status: res.status,
ok: res.ok && body.status === "ok",
};
}));
els.helpPanel.innerHTML = buildReadinessHtml(checks);
} catch {
els.helpPanel.innerHTML = `<div class="help-panel-error">Failed to load readiness status.</div>`;
}
}
function setHelpView(view) {
els.helpNav?.querySelectorAll("[data-help-view]").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.helpView === view);
});
if (view === "readiness") renderHelpReadiness();
else if (view === "about") renderHelpAbout();
else renderHelpCompatibility();
}
function bindHelpNav() {
if (!els.helpNav || els.helpNav.dataset.bound) return;
els.helpNav.dataset.bound = "1";
els.helpNav.addEventListener("click", (event) => {
const btn = event.target.closest("[data-help-view]");
if (!btn) return;
event.preventDefault();
setHelpView(btn.dataset.helpView);
});
}
function toggleHelpDrawer(force) {
const open = typeof force === "boolean" ? force : !els.helpDrawer.classList.contains("open");
els.helpDrawer.classList.toggle("open", open);
els.helpBackdrop.classList.toggle("open", open);
els.helpBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.helpDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.helpBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) {
const active = els.helpNav?.querySelector("[data-help-view].active");
setHelpView(active?.dataset.helpView || "about");
}
}
function closeHelpDrawer() {
toggleHelpDrawer(false);
}
function toggleEndpointsDrawer(force) {
const open = typeof force === "boolean" ? force : !els.endpointsDrawer.classList.contains("open");
els.endpointsDrawer.classList.toggle("open", open);
els.endpointsBackdrop.classList.toggle("open", open);
els.endpointsBadge.setAttribute("aria-expanded", open ? "true" : "false");
els.endpointsDrawer.setAttribute("aria-hidden", open ? "false" : "true");
els.endpointsBackdrop.setAttribute("aria-hidden", open ? "false" : "true");
if (open) renderEndpointTree();
}
function closeEndpointsDrawer() {
toggleEndpointsDrawer(false);
}
function closeOtherPanels(except = null) {
if (except !== "auth") closeAuthPanel();
if (except !== "history") closeHistoryDrawer();
if (except !== "catalog") closeCatalogPanel();
if (except !== "params") closeParamsDrawer();
if (except !== "environment") closeEnvironmentDrawer();
if (except !== "data") closeDataDrawer();
if (except !== "url") closeUrlDrawer();
if (except !== "endpoints") closeEndpointsDrawer();
if (except !== "help") closeHelpDrawer();
if (except !== "endpoint-combo") closeEndpointCombo();
}
async function login() {
const username = (els.username.value || "").trim();
const password = els.password.value || "";
const basic = btoa(`${username}:${password}`);
const res = await fetch("/api/session", {
method: "POST",
headers: {
Accept: "application/json",
Authorization: `Basic ${basic}`,
},
credentials: "include",
});
let sessionId = null;
try {
const body = await res.json();
sessionId = typeof body === "string" ? body : body?.value || body?.session || null;
} catch {
sessionId = null;
}
sessionId = sessionId || res.headers.get("vmware-api-session-id");
if (res.ok && sessionId) {
state.ticket = sessionId;
state.csrf = "";
state.username = username;
setAuth(true);
persistAuth();
closeAuthPanel();
refreshOverview();
toast("Signed in", "ok");
} else {
state.ticket = null;
state.csrf = null;
state.username = null;
setAuth(false);
persistAuth();
toast("Sign in failed", "error");
}
}
async function logout() {
try {
if (state.ticket) {
await fetch("/api/session", {
method: "DELETE",
headers: { "vmware-api-session-id": state.ticket },
credentials: "include",
});
}
} catch (error) {
console.warn(error);
}
state.ticket = null;
state.csrf = null;
state.username = null;
document.cookie = "vmware-api-session-id=; Max-Age=0; path=/; SameSite=Strict";
setAuth(false);
persistAuth();
closeAuthPanel();
resetClusterStats("—");
toast("Signed out", "info");
}
function updateStatusBadge(status) {
if (!els.statusBadge) return;
const code = Number(status);
if (Number.isFinite(code)) {
const ok = code >= 200 && code < 300;
els.statusBadge.hidden = false;
els.statusBadge.textContent = String(code);
els.statusBadge.className = "status-code " + (ok ? "ok" : "err");
return;
}
if (status === "ERR") {
els.statusBadge.hidden = false;
els.statusBadge.textContent = "ERR";
els.statusBadge.className = "status-code err";
return;
}
els.statusBadge.hidden = true;
els.statusBadge.textContent = "";
els.statusBadge.className = "status-code";
}
function showResponse(result) {
if (result.status === 401) {
renderPlainOutput(
"HTTP 401 — authentication required\n\n" +
"Most API endpoints require a session. Open Authentication → sign in as administrator@vsphere.local / VMware1!, then send again.",
);
} else if (result.status === 501 && result.body?.errors) {
renderPlainOutput(
`HTTP 501 — ${result.body.errors}\n\n` +
"This method exists in the VMware API contract but has no emulator handler yet. " +
"Use the filter above to browse only implemented methods.",
);
} else if (result.body !== null && typeof result.body === "object") {
renderJsonOutput(result.body);
} else if (typeof result.body === "string") {
try {
const parsed = JSON.parse(result.body);
if (parsed !== null && typeof parsed === "object") {
renderJsonOutput(parsed);
} else {
renderHighlightedOutput(result.body);
}
} catch {
renderPlainOutput(result.body);
}
} else {
renderPlainOutput(String(result.body ?? ""));
}
updateStatusBadge(result.status);
}
function showError(message) {
renderPlainOutput(message);
updateStatusBadge("ERR");
}
function resolveFetchPath(path) {
let url = String(path || "").trim();
if (!url) return "/";
if (url.startsWith("/api2/json")) url = url.slice("/api2/json".length) || "/";
if (!url.startsWith("/")) url = `/${url}`;
if (url === "/version") return "/api/appliance/system/version";
if (url.startsWith("/api/") || url.startsWith("/rest/") || url.startsWith("/sdk") || url.startsWith("/ui/")) {
return url;
}
// Relative leftover Proxmox-style paths → map common ones, else leave as /api prefix guess
const map = {
"/cluster/status": "/api/vcenter/host",
"/cluster/resources": "/api/vcenter/vm",
"/nodes": "/api/vcenter/host",
};
return map[url] || url;
}
async function api(method, path, body) {
const headers = { Accept: "application/json" };
if (state.ticket) headers["vmware-api-session-id"] = state.ticket;
const opts = { method, headers, credentials: "include" };
if (body && method !== "GET" && method !== "DELETE") {
headers["Content-Type"] = "application/json";
opts.body = JSON.stringify(body);
}
const started = performance.now();
const url = resolveFetchPath(path);
const res = await fetch(url, opts);
const text = await res.text();
let parsed;
try { parsed = JSON.parse(text); } catch { parsed = text; }
return { status: res.status, body: parsed, durationMs: Math.round(performance.now() - started) };
}
function escapeAttr(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll('"', "&quot;")
.replaceAll("<", "&lt;");
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
function endpointDescription(path, methods) {
const visible = (methods || []).filter((m) => !state.implementedOnly || isMethodImplemented(m));
if (!visible.length) return "";
if (path === state.selectedPath && state.selectedVerb) {
const current = visible.find((m) => m.verb === state.selectedVerb);
if (current?.description) return current.description;
}
const preferred = visible.find((m) => m.verb === "GET" && m.description);
if (preferred?.description) return preferred.description;
const any = visible.find((m) => m.description);
return any?.description || "";
}
function pathEntry(path) {
return state.catalog?.categories
.flatMap((c) => c.paths.map((e) => ({ ...e, tag: c.tag })))
.find((e) => e.path === path);
}
function filteredCategories() {
if (!state.catalog) return [];
const q = (els.pathFilter?.value || "").trim().toLowerCase();
if (!q) return state.catalog.categories;
return state.catalog.categories
.map((c) => ({
...c,
paths: c.paths.filter((e) =>
[e.path, c.tag, ...(e.methods || []).flatMap((m) => [m.verb, m.name, m.description])]
.join(" ").toLowerCase().includes(q)
),
}))
.filter((c) => c.paths.length > 0);
}
function renderEndpointTree() {
if (!els.endpointTree) return;
const categories = filteredCategories();
const visible = state.implementedOnly
? categories.map((c) => ({
...c,
paths: c.paths.filter((p) => {
const entry = pathEntry(p.path);
return (entry?.methods || []).some((m) => isMethodImplemented(m));
}),
})).filter((c) => c.paths.length > 0)
: categories;
if (!visible.length) {
els.endpointTree.innerHTML = `<div class="empty">No matching endpoints.</div>`;
return;
}
els.endpointTree.innerHTML = visible.map((c) => {
const items = c.paths.map((e) => {
const active = e.path === state.selectedPath ? "active" : "";
const entry = pathEntry(e.path);
const methods = entry?.methods || e.methods || [];
const description = endpointDescription(e.path, methods);
const badges = methods.map((m) => {
if (state.implementedOnly && !isMethodImplemented(m)) return "";
const dim = m.implemented === false ? " style='opacity:0.45'" : "";
return `<span class="verb-mini ${m.verb}"${dim}>${m.verb}</span>`;
}).join("");
const descHtml = description
? `<span class="nav-item-desc" title="${escapeAttr(description)}">${escapeHtml(description)}</span>`
: "";
return `<button type="button" class="nav-item ${active}" data-path="${escapeAttr(e.path)}">
<span class="nav-item-path">${escapeHtml(e.path)}</span>
${descHtml}
<span class="method-badges">${badges}</span>
</button>`;
}).join("");
return `<div class="nav-group"><div class="nav-group-title">${c.tag}</div>${items}</div>`;
}).join("");
}
function bindEndpointTree() {
if (!els.endpointTree || els.endpointTree.dataset.bound) return;
els.endpointTree.dataset.bound = "1";
els.endpointTree.addEventListener("click", (event) => {
const btn = event.target.closest(".nav-item[data-path]");
if (!btn) return;
event.preventDefault();
closeEndpointsDrawer();
syncSelection({ path: btn.dataset.path }).catch((error) => showError(String(error)));
});
}
function bindCatalogOptions() {
if (!els.catalogOptions || els.catalogOptions.dataset.bound) return;
els.catalogOptions.dataset.bound = "1";
els.catalogOptions.addEventListener("click", async (event) => {
const btn = event.target.closest(".catalog-option[data-major]");
if (!btn) return;
event.preventDefault();
event.stopPropagation();
const major = Number(btn.dataset.major);
renderCatalogSourceInfo(major);
if (major === state.major) return;
try {
await loadCatalog({ major, preserveSelection: false, silent: true });
toggleCatalogDrawer(true);
} catch (error) {
showError(String(error));
}
});
els.catalogOptions.addEventListener("mouseover", (event) => {
const btn = event.target.closest(".catalog-option[data-major]");
if (!btn) return;
renderCatalogSourceInfo(Number(btn.dataset.major));
});
}
function renderMethodTabs() {
const entry = pathEntry(state.selectedPath);
let methods = entry?.methods || [];
if (state.implementedOnly) {
methods = methods.filter((m) => isMethodImplemented(m));
}
if (!state.selectedPath || !methods.length) {
els.methodField.hidden = true;
els.methodTabs.innerHTML = "";
return;
}
els.methodField.hidden = false;
const current = state.selectedVerb || methods[0]?.verb;
els.methodTabs.innerHTML = methods.map((m) => {
const active = m.verb === current ? "active" : "";
return `<button type="button" class="method-tab ${m.verb} ${active}" data-verb="${m.verb}">${m.verb}</button>`;
}).join("");
}
function bindMethodTabs() {
if (!els.methodTabs || els.methodTabs.dataset.bound) return;
els.methodTabs.dataset.bound = "1";
els.methodTabs.addEventListener("click", (event) => {
const btn = event.target.closest(".method-tab[data-verb]");
if (!btn) return;
event.preventDefault();
syncSelection({ verb: btn.dataset.verb }).catch((error) => showError(String(error)));
});
}
function catalogPaths() {
if (!state.catalog) return [];
return state.catalog.categories.flatMap((c) => c.paths.map((p) => p.path));
}
function catalogCategories() {
if (!state.catalog) return [];
if (!state.implementedOnly) return state.catalog.categories;
return state.catalog.categories
.map((c) => ({
...c,
paths: c.paths
.map((p) => ({
...p,
methods: (p.methods || []).filter((m) => isMethodImplemented(m)),
}))
.filter((p) => p.methods.length > 0),
}))
.filter((c) => c.paths.length > 0);
}
function updateSendState() {
els.btnSend.disabled = !state.selectedPath || !state.selectedVerb;
}
let endpointSelectValue = "";
function getEndpointSelectValue() {
return endpointSelectValue || "";
}
function isEndpointComboOpen() {
return Boolean(els.endpointSelectMenu && !els.endpointSelectMenu.hidden);
}
function closeEndpointCombo() {
if (!els.endpointSelectMenu || !els.endpointSelectTrigger) return;
els.endpointSelectMenu.hidden = true;
els.endpointSelectTrigger.setAttribute("aria-expanded", "false");
}
function openEndpointCombo() {
if (!els.endpointSelectMenu || !els.endpointSelectTrigger) return;
closeOtherPanels();
els.endpointSelectMenu.hidden = false;
els.endpointSelectTrigger.setAttribute("aria-expanded", "true");
const selected = els.endpointSelectMenu.querySelector('.endpoint-combo-option[aria-selected="true"]');
(selected || els.endpointSelectMenu.querySelector(".endpoint-combo-option"))?.focus?.();
}
function toggleEndpointCombo(force) {
const open = typeof force === "boolean" ? force : !isEndpointComboOpen();
if (open) openEndpointCombo();
else closeEndpointCombo();
}
function updateEndpointComboDisplay() {
if (!els.endpointSelectValue) return;
const value = getEndpointSelectValue();
if (!value) {
els.endpointSelectValue.textContent = state.catalog
? (catalogCategories().length ? "Select an endpoint" : "No matching endpoints")
: "Loading catalog…";
els.endpointSelectValue.classList.add("is-placeholder");
} else {
els.endpointSelectValue.textContent = value;
els.endpointSelectValue.classList.remove("is-placeholder");
}
if (!els.endpointSelectMenu) return;
els.endpointSelectMenu.querySelectorAll(".endpoint-combo-option").forEach((btn) => {
btn.setAttribute("aria-selected", btn.dataset.path === value ? "true" : "false");
});
}
function setEndpointSelectValue(path) {
endpointSelectValue = path || "";
updateEndpointComboDisplay();
}
function renderEndpointSelect() {
if (!els.endpointSelectMenu) return;
if (!state.catalog) {
els.endpointSelectMenu.innerHTML = `<div class="endpoint-combo-empty">Loading catalog…</div>`;
setEndpointSelectValue("");
return;
}
const categories = catalogCategories();
if (!categories.length) {
els.endpointSelectMenu.innerHTML = `<div class="endpoint-combo-empty">No matching endpoints</div>`;
setEndpointSelectValue("");
return;
}
els.endpointSelectMenu.innerHTML = categories.map((c) => {
const options = c.paths.map((p) => {
const impl = (p.methods || []).some((m) => isMethodImplemented(m));
const cls = impl
? "endpoint-combo-option"
: "endpoint-combo-option is-unimplemented";
return `<button type="button" class="${cls}" role="option" data-path="${escapeAttr(p.path)}" aria-selected="false">${escapeHtml(p.path)}</button>`;
}).join("");
return `<div class="endpoint-combo-group"><div class="endpoint-combo-group-title">${escapeHtml(String(c.tag || ""))}</div>${options}</div>`;
}).join("");
if (state.selectedPath && catalogPaths().includes(state.selectedPath)) {
setEndpointSelectValue(state.selectedPath);
} else {
setEndpointSelectValue("");
}
}
function bindEndpointCombobox() {
if (!els.endpointCombobox || els.endpointCombobox.dataset.bound) return;
els.endpointCombobox.dataset.bound = "1";
els.endpointSelectTrigger?.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
toggleEndpointCombo();
});
els.endpointSelectMenu?.addEventListener("click", (event) => {
const option = event.target.closest(".endpoint-combo-option[data-path]");
if (!option) return;
event.preventDefault();
const path = option.dataset.path || "";
setEndpointSelectValue(path);
closeEndpointCombo();
syncSelection({ path: path || null }).catch((e) => showError(String(e)));
});
document.addEventListener("click", (event) => {
if (!isEndpointComboOpen()) return;
if (els.endpointCombobox.contains(event.target)) return;
closeEndpointCombo();
});
}
async function syncSelection({ path, verb } = {}) {
const switching =
(path !== undefined && (path || null) !== state.selectedPath)
|| (verb !== undefined && (verb || null) !== state.selectedVerb);
if (switching) persistParams();
if (path !== undefined) state.selectedPath = path || null;
if (verb !== undefined) state.selectedVerb = verb || null;
// Drop previous method's Request body immediately while details load.
if (switching) {
if (els.body) {
els.body.value = "";
updateBodyHighlight();
}
setBodyPaneVisible(false);
}
if (!state.selectedPath) {
clearMethodWorkspace();
renderEndpointSelect();
renderEndpointTree();
renderMethodTabs();
return;
}
const entry = pathEntry(state.selectedPath);
let methods = entry?.methods || [];
if (state.implementedOnly) {
methods = methods.filter((m) => isMethodImplemented(m));
}
const verbs = methods.map((m) => m.verb);
if (!verbs.length) {
clearMethodWorkspace();
renderEndpointSelect();
renderEndpointTree();
renderMethodTabs();
return;
}
if (!verbs.includes(state.selectedVerb)) {
state.selectedVerb = verbs[0] || entry?.methods?.[0]?.verb || "GET";
}
renderEndpointSelect();
if (catalogPaths().includes(state.selectedPath)) {
setEndpointSelectValue(state.selectedPath);
}
renderMethodTabs();
renderEndpointTree();
persistSelection();
await loadMethodDetails();
}
function formatParamTypeBadges(field) {
const baseType = field.type || "any";
const badges = [`<span class="param-type-badge">${escapeHtml(baseType)}</span>`];
if (field.optional) {
badges.push('<span class="param-type-badge optional">optional</span>');
}
return `<span class="param-type-badges">${badges.join("")}</span>`;
}
function renderParamRows(container, fields, kind) {
if (!fields.length) {
container.innerHTML = "";
return;
}
container.innerHTML = fields.map((f) => {
const stored = kind === "path" ? state.pathValues[f.name] : state.bodyValues[f.name];
const val = stored ?? (kind === "query" && f.optional ? "" : (f.example ?? ""));
const desc = f.description ? `<div style="font-size:11px;color:var(--muted);margin-top:2px">${f.description}</div>` : "";
const placeholder = kind === "query" && f.example != null && String(f.example).trim()
? ` placeholder="${String(f.example).replaceAll('"', "&quot;")}"`
: "";
return `<tr>
<td><code>${f.name}</code>${desc}</td>
<td>${formatParamTypeBadges(f)}</td>
<td><input data-field="${f.name}" data-kind="${kind}" value="${String(val).replaceAll('"', "&quot;")}"${placeholder}></td>
</tr>`;
}).join("");
container.querySelectorAll("input[data-field]").forEach((input) => {
const commit = () => {
if (input.dataset.kind === "path") {
state.pathValues[input.dataset.field] = input.value;
updateResolvedPath();
} else if (input.dataset.kind === "query") {
state.bodyValues[input.dataset.field] = input.value;
updateResolvedPath();
updateQueryPreview();
} else {
state.bodyValues[input.dataset.field] = input.value;
syncBodyFromFields();
}
persistParams();
};
input.addEventListener("input", commit);
input.addEventListener("change", commit);
});
}
function clusterHost() {
return "vcenter.lab.local";
}
function resolveApiPath(method = state.method) {
if (!method) return null;
let path = method.path;
for (const f of method.path_fields || []) {
const v = state.pathValues[f.name] ?? f.example ?? f.name;
path = path.replace(`{${f.name}}`, v);
}
path = path.startsWith("/") ? path : `/${path}`;
return appendQueryString(path, collectQueryValues());
}
function buildClusterUrl(apiPath) {
if (!apiPath) return "—";
const path = normalizeApiPath(apiPath) || apiPath;
return `https://${clusterHost()}:443${path}`;
}
function buildEmulatorUrl(apiPath) {
if (!apiPath) return "—";
const path = normalizeApiPath(apiPath) || apiPath;
return `${location.origin}${path}`;
}
function normalizeApiPath(path) {
if (!path) return null;
let value = String(path).trim();
if (!value || value === "—") return null;
if (value.startsWith("/api2/json")) value = value.slice("/api2/json".length) || "/";
const fromUrl = value.match(/\/api2\/json(\/.*)?$/i);
if (fromUrl) value = fromUrl[1] || "/";
if (!value.startsWith("/")) value = `/${value}`;
if (value === "/version") return "/api/appliance/system/version";
return value;
}
function apiPathFromPreview(text) {
const value = String(text || "").trim();
if (!value || value === "—") return null;
const apiMatch = value.match(/\/api2\/json(\/.*)?$/i);
if (apiMatch) return apiMatch[1] || "/";
if (value.startsWith("/")) return value;
return `/${value}`;
}
function resolveCatalogPath(source) {
const apiPath = apiPathFromPreview(source);
if (!apiPath || !state.catalog) return null;
const apiSegments = apiPath.split("/").filter(Boolean);
let match = null;
for (const catalogPath of catalogPaths()) {
const templateSegments = catalogPath.split("/").filter(Boolean);
if (templateSegments.length !== apiSegments.length) continue;
const pathValues = {};
let ok = true;
for (let i = 0; i < templateSegments.length; i++) {
const token = templateSegments[i];
const segment = apiSegments[i];
if (token.startsWith("{") && token.endsWith("}")) {
pathValues[token.slice(1, -1)] = segment;
} else if (token !== segment) {
ok = false;
break;
}
}
if (ok) match = { path: catalogPath, pathValues };
}
return match;
}
function applyHistoryFormState(pathValues = {}, body = null) {
if (!state.method) return;
for (const field of state.method.path_fields || []) {
if (pathValues[field.name] != null && pathValues[field.name] !== "") {
state.pathValues[field.name] = String(pathValues[field.name]);
}
}
const pathFields = state.method.path_fields || [];
els.pathFieldsWrap.hidden = !pathFields.length;
renderParamRows(els.pathFields, pathFields, "path");
const bodyFields = bodyOnlyFields(state.method);
els.bodyFieldsWrap.hidden = !bodyFields.length;
if (bodyFields.length) {
if (body != null) {
try {
const parsed = JSON.parse(body || "{}");
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
for (const field of bodyFields) {
const value = getByPath(parsed, field.name);
if (value !== undefined && value !== null) {
state.bodyValues[field.name] = String(value);
}
}
}
} catch {
/* keep existing bodyValues */
}
}
renderParamRows(els.bodyFields, bodyFields, "body");
} else {
els.bodyFields.innerHTML = "";
}
refreshRequestBody(state.method, body);
updateResolvedPath();
updateParamsBadge();
updateSendState();
persistParams();
}
async function restoreFromHistory(entry) {
const mapping = entry.catalogPath
? { path: entry.catalogPath, pathValues: { ...(entry.pathValues || {}) } }
: resolveCatalogPath(entry.path);
if (!mapping?.path) {
toast("Cannot restore this history entry in the catalog", "warn");
return;
}
await syncSelection({ path: mapping.path, verb: entry.method });
applyHistoryFormState(
{ ...mapping.pathValues, ...(entry.pathValues || {}) },
entry.body ?? null,
);
closeHistoryDrawer();
await sendRequest();
}
function collectQueryValues() {
const values = {};
const fields = queryFields(state.method);
if (!fields.length) return values;
const queryNames = new Set(fields.map((f) => f.name));
const inputs = [
...(els.queryPaneFields?.querySelectorAll("input[data-field]") || []),
...(els.bodyFields?.querySelectorAll("input[data-field]") || []),
];
for (const input of inputs) {
const name = input.dataset.field;
if (!queryNames.has(name)) continue;
const raw = input.value.trim();
if (raw) values[name] = raw;
}
for (const field of fields) {
const name = field.name;
if (values[name] != null) continue;
const stored = state.bodyValues?.[name];
if (stored != null && String(stored).trim()) {
values[name] = String(stored).trim();
continue;
}
// Only auto-fill required query params (e.g. ?action=start).
if (field.optional === false && field.example != null && String(field.example).trim()) {
values[name] = String(field.example).trim();
}
}
return values;
}
function appendQueryString(path, queryValues) {
if (!path || !queryValues || !Object.keys(queryValues).length) return path;
const params = new URLSearchParams();
for (const [key, raw] of Object.entries(queryValues)) {
let parsed = raw;
if (typeof raw === "string" && ((raw.startsWith("[") && raw.endsWith("]")) || (raw.startsWith("{") && raw.endsWith("}")))) {
try { parsed = JSON.parse(raw); } catch { parsed = raw; }
}
if (Array.isArray(parsed)) {
for (const item of parsed) params.append(key, String(item));
} else if (parsed != null && String(parsed) !== "") {
params.append(key, String(parsed));
}
}
const qs = params.toString();
if (!qs) return path;
return path.includes("?") ? `${path}&${qs}` : `${path}?${qs}`;
}
function semanticApiPath() {
if (!state.method?.path) return null;
let path = state.method.path;
for (const f of state.method.path_fields || []) {
const value = state.pathValues?.[f.name] ?? f.example ?? f.name;
path = path.replace(`{${f.name}}`, encodeURIComponent(String(value)));
}
path = path.startsWith("/") ? path : `/${path}`;
return appendQueryString(path, collectQueryValues());
}
function updateResolvedPath() {
const apiPath = resolveApiPath();
if (!state.method || !apiPath) {
setText(els.requestPreviewCurlPassword, "—");
setText(els.requestPreviewCurlToken, "—");
setText(els.requestPreviewCluster, "—");
setText(els.requestPreviewEmulator, "—");
updateQueryPreview();
return;
}
setText(els.requestPreviewCurlPassword, buildCurlPassword());
setText(els.requestPreviewCurlToken, buildCurlToken());
setText(els.requestPreviewCluster, buildClusterUrl(apiPath));
setText(els.requestPreviewEmulator, buildEmulatorUrl(apiPath));
const version = state.catalog?.source_version || latestMajorVersion(state.major);
els.requestPreviewCluster.title = version
? `${seriesLabel(state.major)} (${version}) endpoint URL (as on a real vCenter)`
: "Simulated VMware / vCenter endpoint URL";
els.requestPreviewEmulator.title = "Request URL on this emulator (current host)";
updateQueryPreview();
}
function buildRequestUrl(target = "emulator") {
const apiPath = normalizeApiPath(resolveApiPath());
if (!apiPath) return "";
return target === "cluster" ? buildClusterUrl(apiPath) : buildEmulatorUrl(apiPath);
}
function deepCloneJson(value) {
if (value == null) return value;
return JSON.parse(JSON.stringify(value));
}
function getByPath(root, path) {
if (!path) return root;
const parts = String(path).split(".");
let cur = root;
for (const part of parts) {
if (cur == null || typeof cur !== "object") return undefined;
cur = cur[part];
}
return cur;
}
function setByPath(root, path, value) {
const parts = String(path).split(".");
let cur = root;
for (let i = 0; i < parts.length - 1; i += 1) {
const part = parts[i];
const next = parts[i + 1];
const wantArray = /^\d+$/.test(next);
if (cur[part] == null || typeof cur[part] !== "object") {
cur[part] = wantArray ? [] : {};
}
cur = cur[part];
}
cur[parts[parts.length - 1]] = value;
}
function deleteByPath(root, path) {
const parts = String(path).split(".");
let cur = root;
for (let i = 0; i < parts.length - 1; i += 1) {
if (cur == null || typeof cur !== "object") return;
cur = cur[parts[i]];
}
if (cur && typeof cur === "object") delete cur[parts[parts.length - 1]];
}
function syncBodyFromFields({ syncVisibility = true } = {}) {
if (!state.method) return;
if (!methodUsesRequestBody(state.method)) {
if (els.body) {
els.body.value = "";
updateBodyHighlight();
}
if (syncVisibility) updateBodyPaneVisibility(state.method);
return;
}
const queryNames = new Set((state.method.query_fields || []).map((f) => f.name));
const inputs = [...(els.bodyFields?.querySelectorAll("input[data-field]") || [])];
const example = state.method.body_example;
const root = deepCloneJson(
example && typeof example === "object" && !Array.isArray(example) ? example : {},
) || {};
let touched = false;
inputs.forEach((input) => {
const name = input.dataset.field;
if (!name || queryNames.has(name)) return;
const raw = input.value.trim();
const existing = getByPath(root, name);
if (!raw && existing === undefined) return;
touched = true;
if (!raw) {
deleteByPath(root, name);
return;
}
let value = raw;
if (raw === "true" || raw === "false") value = raw === "true";
else if (/^-?\d+$/.test(raw)) value = Number(raw);
else if (/^-?\d+\.\d+$/.test(raw)) value = Number(raw);
else if ((raw.startsWith("{") && raw.endsWith("}")) || (raw.startsWith("[") && raw.endsWith("]"))) {
try { value = JSON.parse(raw); }
catch { value = raw; }
}
setByPath(root, name, value);
});
// Empty PARAM inputs → keep full nested body_example; edits merge by dotted path.
if (!touched && example && typeof example === "object" && !Array.isArray(example)) {
els.body.value = Object.keys(example).length ? JSON.stringify(example, null, 2) : "";
updateBodyHighlight();
if (syncVisibility) updateBodyPaneVisibility(state.method);
return;
}
els.body.value = Object.keys(root).length ? JSON.stringify(root, null, 2) : "";
updateBodyHighlight();
if (syncVisibility) {
updateBodyPaneVisibility(state.method);
}
}
function isMethodImplemented(method) {
return method?.implemented === true;
}
function updateImplPill(method = state.method) {
if (!els.implPill) return;
const hasSelection = Boolean(state.selectedPath && state.selectedVerb && method);
const matchesSelection = hasSelection
&& method.path === state.selectedPath
&& method.verb === state.selectedVerb;
const show = matchesSelection
&& method.implemented === false
&& !state.implementedOnly;
els.implPill.hidden = !show;
if (show) {
els.implPill.textContent = "Not implemented";
els.implPill.className = "badge err";
}
}
function applyMethodDetails(d) {
state.method = d;
state.selectedPath = d.path;
state.selectedVerb = d.verb;
setText(els.statCatalog, d.source_version);
setText(els.catalogMeta, `${d.verb} ${d.name || d.path}`);
if (els.catalogMeta) els.catalogMeta.hidden = false;
const desc = d.description || "";
if (els.methodDesc) {
els.methodDesc.textContent = desc;
els.methodDesc.title = desc;
}
updateImplPill(d);
const storedParams = readStoredParams(d.path, d.verb, state.major);
state.pathValues = {};
for (const f of d.path_fields || []) {
state.pathValues[f.name] = resolveParamValue(f.name, storedParams?.pathValues, f.example);
}
const pathFields = d.path_fields || [];
els.pathFieldsWrap.hidden = !pathFields.length;
renderParamRows(els.pathFields, pathFields, "path");
const qFields = queryFields(d);
const bodyFields = bodyOnlyFields(d);
state.bodyValues = {};
for (const f of qFields) {
const defaultExample = f.optional ? "" : f.example;
state.bodyValues[f.name] = resolveParamValue(f.name, storedParams?.bodyValues, defaultExample);
}
for (const f of bodyFields) {
state.bodyValues[f.name] = resolveParamValue(f.name, storedParams?.bodyValues, f.example);
}
els.bodyFieldsWrap.hidden = !bodyFields.length;
renderParamRows(els.bodyFields, bodyFields, "body");
if (methodHasQueryParams(d) && !methodUsesRequestBody(d)) {
state.requestPaneTab = "query";
} else {
state.requestPaneTab = "body";
}
refreshRequestBody(d, storedParams?.body);
updateResolvedPath();
if (getEndpointSelectValue() !== d.path) {
setEndpointSelectValue(d.path);
}
renderMethodTabs();
renderEndpointTree();
updateSendState();
updateParamsBadge();
persistSelection();
persistParams();
}
function renderHistory() {
updateHistoryBadge();
if (!state.history.length) {
els.history.innerHTML = `<div class="empty">No requests yet.</div>`;
return;
}
els.history.innerHTML = state.history.map((e, i) => `
<div class="history-item">
<button type="button" class="nav-item history-replay" data-index="${i}">
<span class="badge ${e.method}">${e.method}</span>
<code>${escapeHtml(e.path)}</code>
<span class="badge ${e.status < 300 ? "ok" : "err"}">${e.status}</span>
</button>
</div>`).join("");
els.history.querySelectorAll(".history-replay").forEach((btn) => {
btn.addEventListener("click", async () => {
const entry = state.history[Number(btn.dataset.index)];
try {
await restoreFromHistory(entry);
} catch (error) {
showError(String(error));
}
});
});
}
function pushHistory(method, path, status) {
const entry = { method, path, status };
if (state.selectedPath) entry.catalogPath = state.selectedPath;
if (state.pathValues && Object.keys(state.pathValues).length) {
entry.pathValues = { ...state.pathValues };
}
if (state.method && methodUsesRequestBody(state.method)) {
entry.body = els.body.value;
}
state.history = [
entry,
...state.history.filter((e) => !(e.method === method && e.path === path)),
].slice(0, HISTORY_LIMIT);
localStorage.setItem("vmware-sim-history", JSON.stringify(state.history));
renderHistory();
}
async function loadVersions() {
const p = await fetch("/ui/api/versions").then((r) => r.json());
state.runtimeVersion = p.runtime_version;
state.majors = p.majors || [];
setText(els.statRuntime, p.runtime_version || "—");
const storedMajor = readStoredMajor();
if (storedMajor != null && state.majors.some((m) => m.major === storedMajor)) {
state.major = storedMajor;
} else {
state.major = state.majors.find((m) => m.major === 9)?.major || state.majors[0]?.major || 9;
}
renderCatalogBadge();
renderCatalogPanel();
restoreStoredSelection(state.major);
}
async function loadCatalog({ force = false, major = state.major, preserveSelection = major === state.major, silent = false } = {}) {
setLoading(true);
try {
state.major = major;
persistMajor(major);
if (!preserveSelection) {
state.method = null;
if (!restoreStoredSelection(major)) {
state.selectedPath = null;
state.selectedVerb = null;
}
}
const cached = !force ? getCachedCatalog(major)?.data : null;
let catalog = null;
try {
const res = await fetch(`/ui/api/catalog?major=${major}`);
if (res.ok) {
catalog = await res.json();
writeCatalogEntry(major, catalog);
}
} catch (error) {
console.error(error);
}
if (!catalog) {
catalog = cached;
}
if (!catalog) {
throw new Error(`catalog load failed: no cached copy for ${seriesLabel(major)}`);
}
if (!silent && catalog !== cached) {
toast(force ? "Catalog updated" : "Catalog refreshed", force ? "ok" : "debug");
} else if (!silent && !cached) {
toast("Catalog saved locally", "debug");
}
await applyCatalog(catalog, { preserveSelection });
refreshHelpIfCompatibilityActive();
} finally {
setLoading(false);
}
}
async function refreshCatalog() {
els.btnCatalogRefresh.disabled = true;
try {
await loadCatalog({ force: true, preserveSelection: true, silent: true });
toggleCatalogDrawer(true);
toast("Catalog updated to latest", "ok");
} finally {
els.btnCatalogRefresh.disabled = false;
}
}
async function applyRuntimeContract() {
if (!els.btnContractApply || els.btnContractApply.disabled) return;
const major = state.major;
els.btnContractApply.disabled = true;
setLoading(true);
try {
const res = await fetch(`/ui/api/contract/apply?major=${major}`, { method: "POST" });
const payload = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(payload.detail || `Apply failed: ${res.status}`);
}
state.runtimeVersion = payload.runtime_version || state.runtimeVersion;
setText(els.statRuntime, state.runtimeVersion || "—");
await loadVersions();
await loadCatalog({ major, preserveSelection: true, silent: true });
await refreshOverview();
toast(`Runtime switched to ${payload.runtime_version || seriesLabel(major)}`, "ok");
} finally {
setLoading(false);
updateApplyButton();
}
}
async function loadMethodDetails() {
const path = state.selectedPath;
const verb = state.selectedVerb;
if (!path || !verb) return;
const requestId = ++methodDetailsRequestId;
const res = await fetch(`/ui/api/method?${new URLSearchParams({ major: String(state.major), path, verb })}`);
if (requestId !== methodDetailsRequestId) return;
if (!res.ok) { showError(`Method lookup failed: ${res.status}`); return; }
const payload = await res.json();
if (requestId !== methodDetailsRequestId) return;
if (payload.path !== state.selectedPath || payload.verb !== state.selectedVerb) return;
applyMethodDetails(payload);
}
async function refreshOverview() {
try {
if (state.ticket) {
const v = await api("GET", "/api/appliance/system/version");
setText(els.statRuntime, v.body?.version ?? state.runtimeVersion ?? "—");
} else {
setText(els.statRuntime, state.runtimeVersion ?? "—");
}
await refreshClusterStats();
await refreshCatalogCoverage();
} catch (e) { console.error(e); }
}
async function sendRequest() {
if (
state.selectedPath &&
state.selectedVerb &&
(state.selectedPath !== state.method?.path || state.selectedVerb !== state.method?.verb)
) {
await loadMethodDetails();
}
const method = state.selectedVerb || state.method?.verb;
const path = semanticApiPath();
if (!path) {
showError("No endpoint selected.");
return;
}
if (!state.ticket && method === "GET" && !String(path).includes("/appliance/system/version") && path !== "/api/session") {
toast("Sign in first — most endpoints require authentication", "warn");
}
let body;
if (methodUsesRequestBody(state.method)) {
syncBodyFromFields();
if (!isEmptyRequestBody(els.body.value)) {
try { body = JSON.parse(els.body.value); }
catch { showError("Invalid JSON in request body."); return; }
}
}
setLoading(true);
try {
const result = await api(method, path.startsWith("/") ? path : `/${path}`, body);
showResponse(result);
updateResolvedPath();
pushHistory(
method,
buildClusterUrl(normalizeApiPath(path.startsWith("/") ? path : `/${path}`)),
result.status,
);
const toastLevel = result.status >= 500 ? "error" : result.status >= 400 ? "warn" : "ok";
toast(`HTTP ${result.status} · ${result.durationMs} ms`, toastLevel);
} finally {
setLoading(false);
}
}
function escapeCurlSingleQuoted(value) {
return String(value ?? "").replaceAll("'", "'\\''");
}
function curlAuthUsername() {
return (state.username || els.username.value || "administrator@vsphere.local").trim();
}
function curlAuthPassword() {
return els.password.value || "VMware1!";
}
function curlAuthTokenId() {
return (els.apiTokenId?.value || "").trim() || "TOKEN_ID";
}
function curlAuthTokenSecret() {
return (els.apiTokenSecret?.value || "").trim() || "TOKEN_SECRET";
}
function appendCurlJsonBody(parts, method) {
const upper = String(method).toUpperCase();
const bodyText = normalizeRequestBodyText();
const usesJsonBody = methodUsesRequestBody(state.method)
&& !["GET", "DELETE"].includes(upper)
&& !(state.selectedPath === "/access/ticket" && upper === "POST")
&& !isEmptyRequestBody(bodyText);
if (usesJsonBody) {
parts.push(`-H 'Content-Type: application/json'`, `-d '${bodyText.replaceAll("'", "'\\''")}'`);
}
}
function buildCurlPassword() {
const method = state.selectedVerb || state.method?.verb;
if (!method) return "—";
const url = buildRequestUrl("emulator");
if (!url) return "—";
const username = curlAuthUsername();
const password = curlAuthPassword();
const sessionUrl = `${location.origin}/api/session`;
const sessionCurl = [
`SID=$(curl -sk -u '${escapeCurlSingleQuoted(username)}:${escapeCurlSingleQuoted(password)}' -X POST '${sessionUrl}' | tr -d '"')`,
].join(" \\\n ");
if (methodUsesRequestBody(state.method)) syncBodyFromFields();
const mainParts = [
`curl -sk -X ${method} '${url}'`,
`-H 'Accept: application/json'`,
`-H "vmware-api-session-id: $SID"`,
];
appendCurlJsonBody(mainParts, method);
return `${sessionCurl}\n\n${mainParts.join(" \\\n ")}`;
}
function buildCurlToken() {
// vSphere Automation uses Basic → session; token curl mirrors session flow.
return buildCurlPassword();
}
document.getElementById("btn-login").addEventListener("click", () => login().catch((e) => showError(String(e))));
document.getElementById("btn-logout").addEventListener("click", () => logout().catch((e) => showError(String(e))));
window.addEventListener("pagehide", () => persistParams());
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") persistParams();
});
els.workspaceBrand?.addEventListener("click", (event) => {
resetHomeWorkspace();
const path = window.location.pathname;
if (path === "/" || path.endsWith("/index.html")) {
event.preventDefault();
}
});
els.authStatusPill.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("auth");
toggleAuthDrawer(!els.authDrawer.classList.contains("open"));
});
els.password.addEventListener("keydown", (e) => {
if (e.key === "Enter") login().catch((err) => showError(String(err)));
});
for (const input of [els.username, els.password, els.apiTokenId, els.apiTokenSecret]) {
input?.addEventListener("input", () => {
if (isUrlDrawerOpen()) updateResolvedPath();
});
}
els.historyBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("history");
toggleHistoryDrawer(!els.historyDrawer.classList.contains("open"));
});
els.catalogBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("catalog");
toggleCatalogDrawer(!els.catalogDrawer.classList.contains("open"));
});
const openParamsPanel = (e) => {
e.stopPropagation();
closeOtherPanels("params");
toggleParamsDrawer(!els.paramsDrawer.classList.contains("open"));
};
els.paramsBadge?.addEventListener("click", openParamsPanel);
els.endpointsBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("endpoints");
toggleEndpointsDrawer(!els.endpointsDrawer.classList.contains("open"));
});
els.environmentBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("environment");
toggleEnvironmentDrawer(!els.environmentDrawer.classList.contains("open"));
});
els.dataBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("data");
toggleDataDrawer(!els.dataDrawer.classList.contains("open"));
});
els.themeToggle.addEventListener("click", () => toggleTheme());
els.helpBadge.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("help");
toggleHelpDrawer(!els.helpDrawer.classList.contains("open"));
});
els.btnCatalogRefresh.addEventListener("click", () => {
refreshCatalog().catch((error) => showError(String(error)));
});
if (els.btnContractApply) {
els.btnContractApply.addEventListener("click", () => {
applyRuntimeContract().catch((error) => showError(String(error)));
});
}
els.catalogDrawerClose.addEventListener("click", () => closeCatalogPanel());
els.catalogBackdrop.addEventListener("click", () => closeCatalogPanel());
els.paramsDrawerClose.addEventListener("click", () => closeParamsDrawer());
els.paramsBackdrop.addEventListener("click", () => closeParamsDrawer());
els.endpointsDrawerClose.addEventListener("click", () => closeEndpointsDrawer());
els.endpointsBackdrop.addEventListener("click", () => closeEndpointsDrawer());
els.environmentDrawerClose.addEventListener("click", () => closeEnvironmentDrawer());
els.environmentBackdrop.addEventListener("click", () => closeEnvironmentDrawer());
els.dataDrawerClose.addEventListener("click", () => closeDataDrawer());
els.dataBackdrop.addEventListener("click", () => closeDataDrawer());
els.historyDrawerClose.addEventListener("click", () => closeHistoryDrawer());
els.historyBackdrop.addEventListener("click", () => closeHistoryDrawer());
els.historyClear.addEventListener("click", () => {
clearHistory().catch((error) => showError(String(error)));
});
els.authDrawerClose.addEventListener("click", () => closeAuthPanel());
els.authBackdrop.addEventListener("click", () => closeAuthPanel());
els.helpDrawerClose.addEventListener("click", () => closeHelpDrawer());
els.helpBackdrop.addEventListener("click", () => closeHelpDrawer());
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
if (modalIsOpen()) {
e.preventDefault();
closeModal(false);
return;
}
closeOtherPanels();
}
});
document.getElementById("btn-send").addEventListener("click", () => sendRequest());
els.btnUrlToggle.addEventListener("click", (e) => {
e.stopPropagation();
closeOtherPanels("url");
toggleUrlDrawer(!isUrlDrawerOpen());
});
els.urlDrawerClose.addEventListener("click", () => closeUrlDrawer());
els.urlBackdrop.addEventListener("click", () => closeUrlDrawer());
els.requestPreviewPanel.addEventListener("click", async (e) => {
const curlBtn = e.target.closest("[data-copy-curl]");
if (curlBtn) {
const variant = curlBtn.dataset.copyCurl || "token";
const curl = variant === "password" ? buildCurlPassword() : buildCurlToken();
if (!curl || curl === "—") {
toast("No cURL command to copy", "warn");
return;
}
await navigator.clipboard.writeText(curl);
toast(variant === "password" ? "Password cURL copied" : "Token cURL copied", "info");
return;
}
const urlBtn = e.target.closest("[data-copy-url]");
if (!urlBtn) return;
const url = buildRequestUrl(urlBtn.dataset.copyUrl === "cluster" ? "cluster" : "emulator");
if (!url) {
toast("No URL to copy", "warn");
return;
}
await navigator.clipboard.writeText(url);
toast(urlBtn.dataset.copyUrl === "cluster" ? "Cluster URL copied" : "Emulator URL copied", "info");
});
els.body.addEventListener("input", () => {
updateBodyHighlight();
updateBodyPaneVisibility();
if (isUrlDrawerOpen()) updateResolvedPath();
persistParams();
});
els.bodyOutput?.addEventListener("mousedown", (event) => {
if (event.target.closest(".json-fold")) return;
els.bodyOutput.focus({ preventScroll: true });
});
els.output.addEventListener("mousedown", (event) => {
if (event.target.closest(".json-fold")) return;
els.output.focus({ preventScroll: true });
});
document.getElementById("btn-copy-body").addEventListener("click", async () => {
const onQuery = els.queryPaneContent && !els.queryPaneContent.hidden;
if (onQuery) {
const qs = formatQueryPreviewString(collectQueryValues()) || "";
await navigator.clipboard.writeText(qs);
toast(qs ? "Query string copied" : "No query string to copy", qs ? "info" : "warn");
return;
}
await navigator.clipboard.writeText(els.body.value || "");
toast("Request body copied", "info");
});
els.btnFillQueryExamples?.addEventListener("click", () => fillQueryExamples());
els.requestTabBody?.addEventListener("click", () => {
state.requestPaneTab = "body";
updateRequestPaneVisibility(state.method);
});
els.requestTabQuery?.addEventListener("click", () => {
state.requestPaneTab = "query";
updateRequestPaneVisibility(state.method);
});
async function copyResponseToClipboard() {
await navigator.clipboard.writeText(state.lastResponseRaw || els.output.textContent || "");
toast("Response copied", "info");
}
document.getElementById("btn-copy-response").addEventListener("click", copyResponseToClipboard);
els.implementedOnly.addEventListener("change", async () => {
state.implementedOnly = els.implementedOnly.checked;
validateCurrentSelection();
renderEndpointSelect();
renderEndpointTree();
await syncSelection();
updateImplPill(state.method);
});
els.pathFilter?.addEventListener("input", renderEndpointTree);
bindCatalogOptions();
bindHelpNav();
bindDataPanelActions();
bindModal();
bindTooltips();
bindEndpointTree();
bindEndpointCombobox();
bindMethodTabs();
updateBodyHighlight();
renderHistory();
renderAuthPanel();
async function bootConsole() {
try {
await loadVersions();
renderHelpAbout();
await restoreAuth();
await loadCatalog({ silent: true });
await refreshOverview();
try {
await refreshDemoState();
} catch (error) {
console.warn("Demo state unavailable:", error);
}
} catch (error) {
console.error(error);
showError(String(error));
}
}
bootConsole();
window.addEventListener("beforeunload", () => {
persistParams();
});
document.addEventListener("keydown", (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") { e.preventDefault(); sendRequest(); }
});
</script>
</body>
</html>