229a705987
Ship Harbor/Hub push with latest+version tags, deploy to vmware.devops.org.ru, and show the package version badge in Help → About.
7657 lines
249 KiB
HTML
7657 lines
249 KiB
HTML
<!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 {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: 0.45rem 0.65rem;
|
||
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-about-version {
|
||
flex-shrink: 0;
|
||
padding: 0.22rem 0.55rem;
|
||
border-radius: 999px;
|
||
border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
|
||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||
color: var(--accent);
|
||
font-family: var(--mono);
|
||
font-size: 10px;
|
||
font-weight: 600;
|
||
letter-spacing: 0.03em;
|
||
line-height: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
|
||
.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 & 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"> </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 & 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,
|
||
appVersion: 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 || "—";
|
||
const appVersion = state.appVersion ? `v${state.appVersion}` : "v—";
|
||
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">
|
||
<span class="help-about-version" title="Simulator package version">${escapeHtml(appVersion)}</span>
|
||
<span>
|
||
Runtime <code>${escapeHtml(version)}</code> · catalog <code>${escapeHtml(String(catalogMajor))}</code>
|
||
</span>
|
||
</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 clearSession(options = {}) {
|
||
const { toastMessage = null, toastKind = "info", closePanel = true } = options;
|
||
state.ticket = null;
|
||
state.csrf = null;
|
||
state.username = null;
|
||
document.cookie = "vmware-api-session-id=; Max-Age=0; path=/; SameSite=Strict";
|
||
try { localStorage.removeItem(LS_AUTH); } catch { /* ignore */ }
|
||
try { sessionStorage.removeItem(LS_AUTH); } catch { /* ignore */ }
|
||
setAuth(false);
|
||
persistAuth();
|
||
if (closePanel) closeAuthPanel();
|
||
resetClusterStats("—");
|
||
if (toastMessage) toast(toastMessage, toastKind);
|
||
}
|
||
|
||
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);
|
||
}
|
||
clearSession({ toastMessage: "Signed out", toastKind: "info" });
|
||
}
|
||
|
||
function expireSession() {
|
||
clearSession({
|
||
toastMessage: "Session expired — sign in again",
|
||
toastKind: "warn",
|
||
closePanel: false,
|
||
});
|
||
}
|
||
|
||
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; }
|
||
if (res.status === 401 && (state.ticket || state.username)) {
|
||
expireSession();
|
||
}
|
||
return { status: res.status, body: parsed, durationMs: Math.round(performance.now() - started) };
|
||
}
|
||
|
||
function escapeAttr(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll('"', """)
|
||
.replaceAll("<", "<");
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replaceAll("&", "&")
|
||
.replaceAll("<", "<")
|
||
.replaceAll(">", ">");
|
||
}
|
||
|
||
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('"', """)}"`
|
||
: "";
|
||
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('"', """)}"${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.appVersion = p.app_version || null;
|
||
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>
|