HuggingMes / env-builder.html
somratpro's picture
fix(health-server): resolve iframe redirect causing 'refused to connect' (#11)
aa128fa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HuggingMes · ENV Builder</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Syne:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0b0c0f;
--bg2: #111318;
--bg3: #181c23;
--bg4: #1e2330;
--border: #252b38;
--border2: #2e3648;
--amber: #f5a623;
--amber2: #ffbe55;
--amber-dim: rgba(245,166,35,.12);
--amber-glow:rgba(245,166,35,.22);
--green: #3dd68c;
--red: #f05f5f;
--blue: #5b8af5;
--text: #e4e8f0;
--text2: #8d97ad;
--text3: #535f76;
--mono: 'JetBrains Mono', monospace;
--sans: 'Syne', sans-serif;
--r: 8px;
--r2: 12px;
--sidebar-w: 220px;
--panel-w: 340px;
}
html { scroll-behavior: smooth; height: 100%; }
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.topbar {
position: sticky;
top: 0;
z-index: 100;
height: 52px;
background: rgba(11,12,15,.9);
backdrop-filter: blur(14px);
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
padding: 0 20px;
gap: 16px;
flex-shrink: 0;
}
.topbar-logo {
display: flex;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.topbar-logo .logo-emoji { font-size: 24px; line-height: 1; }
.topbar-wordmark {
font-weight: 800;
font-size: 14px;
letter-spacing: -.2px;
color: var(--text);
white-space: nowrap;
}
.topbar-wordmark em {
color: var(--amber);
font-style: normal;
}
.topbar-divider {
width: 1px;
height: 22px;
background: var(--border2);
}
.topbar-title {
font-size: 12px;
font-weight: 600;
color: var(--text2);
letter-spacing: .5px;
text-transform: uppercase;
}
.topbar-spacer { flex: 1; }
.topbar-pill {
font-family: var(--mono);
font-size: 10px;
color: var(--amber);
background: var(--amber-dim);
border: 1px solid var(--amber-glow);
border-radius: 20px;
padding: 3px 10px;
letter-spacing: .5px;
}
.layout {
display: flex;
flex: 1;
min-height: 0;
height: calc(100vh - 52px);
}
.sidebar-wrap {
width: var(--sidebar-w);
flex-shrink: 0;
border-right: 1px solid var(--border);
background: var(--bg2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.sidebar-scroll {
flex: 1;
overflow-y: auto;
padding: 14px 10px;
}
.sidebar-scroll::-webkit-scrollbar { width: 4px; }
.sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
.sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
.sb-label {
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text3);
padding: 0 8px 10px;
}
.nav-btn {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
border: none;
background: transparent;
cursor: pointer;
border-radius: var(--r);
text-align: left;
color: var(--text2);
font-family: var(--sans);
font-size: 12.5px;
font-weight: 500;
transition: background .15s, color .15s;
margin-bottom: 2px;
}
.nav-btn:hover { background: var(--bg3); color: var(--text); }
.nav-btn.active {
background: var(--amber-dim);
color: var(--amber);
border: 1px solid var(--amber-glow);
}
.nav-icon { font-size: 13px; flex-shrink: 0; }
.nav-label { flex: 1; }
.nav-count {
font-family: var(--mono);
font-size: 10px;
font-weight: 600;
color: var(--text3);
background: var(--bg3);
border-radius: 10px;
padding: 1px 6px;
min-width: 20px;
text-align: center;
transition: background .2s, color .2s;
}
.nav-btn.active .nav-count {
background: var(--amber-glow);
color: var(--amber2);
}
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border-bottom: 1px solid var(--border);
background: var(--bg2);
flex-shrink: 0;
flex-wrap: wrap;
}
.search-wrap {
position: relative;
flex: 1;
min-width: 160px;
max-width: 340px;
}
.search-icon {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
color: var(--text3);
pointer-events: none;
font-size: 12px;
}
#search {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border2);
border-radius: var(--r);
padding: 7px 10px 7px 30px;
font-family: var(--mono);
font-size: 12px;
color: var(--text);
outline: none;
transition: border-color .15s;
}
#search:focus { border-color: var(--amber); }
#search::placeholder { color: var(--text3); }
.tb-sep {
width: 1px;
height: 24px;
background: var(--border2);
}
.btn {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 6px 13px;
border-radius: var(--r);
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text2);
font-family: var(--sans);
font-size: 11.5px;
font-weight: 600;
cursor: pointer;
transition: all .15s;
white-space: nowrap;
}
.btn:hover { background: var(--bg4); color: var(--text); border-color: var(--border2); }
.btn-amber {
background: var(--amber);
color: #0b0c0f;
border-color: var(--amber);
}
.btn-amber:hover { background: var(--amber2); border-color: var(--amber2); }
.btn-ghost {
background: transparent;
border-color: transparent;
color: var(--text3);
}
.btn-ghost:hover { background: var(--bg3); color: var(--text2); border-color: var(--border2); }
.content-wrap {
flex: 1;
display: flex;
min-height: 0;
overflow: hidden;
}
.sections-scroll {
flex: 1;
overflow-y: auto;
padding: 16px 20px 80px;
min-width: 0;
}
.sections-scroll::-webkit-scrollbar { width: 5px; }
.sections-scroll::-webkit-scrollbar-track { background: transparent; }
.sections-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
.sec { margin-bottom: 28px; }
.sec.sec-hidden { display: none !important; }
.sec-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.sec-icon { font-size: 14px; }
.sec-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1.2px;
color: var(--text3);
}
.sec-line {
flex: 1;
height: 1px;
background: var(--border);
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 10px;
}
.env-card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--r2);
padding: 12px;
transition: border-color .2s, background .2s;
}
.env-card:hover { border-color: var(--border2); }
.env-card.hidden { display: none; }
.env-card.selected {
border-color: var(--amber-glow);
background: linear-gradient(135deg, var(--bg2) 80%, rgba(245,166,35,.04));
}
.card-top {
display: flex;
align-items: flex-start;
gap: 9px;
margin-bottom: 9px;
}
.card-check {
width: 15px;
height: 15px;
accent-color: var(--amber);
flex-shrink: 0;
margin-top: 2px;
cursor: pointer;
}
.card-info { flex: 1; min-width: 0; }
.card-key {
font-family: var(--mono);
font-size: 11.5px;
font-weight: 600;
color: var(--text);
letter-spacing: .3px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.card-lbl {
font-size: 11px;
color: var(--text3);
margin-top: 2px;
line-height: 1.35;
}
.badge {
flex-shrink: 0;
font-family: var(--mono);
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .6px;
padding: 2px 7px;
border-radius: 20px;
}
/* Tag badge styles */
.badge-critical { background: rgba(240,80,80,.14); color: #f05f5f; border: 1px solid rgba(240,80,80,.3); }
.badge-credential{ background: rgba(220,140,60,.13); color: #e09040; border: 1px solid rgba(220,140,60,.28); }
.badge-feature { background: rgba(70,140,250,.12); color: #5a9eff; border: 1px solid rgba(70,140,250,.25); }
.badge-optional { background: rgba(61,214,140,.10); color: #3dd68c; border: 1px solid rgba(61,214,140,.22); }
.badge-advanced { background: rgba(160,100,230,.12);color: #b07ae0; border: 1px solid rgba(160,100,230,.25); }
.badge-build { background: rgba(240,185,60,.12); color: #e0b030; border: 1px solid rgba(240,185,60,.28); }
/* Card left-border accents */
.env-card:has(.badge-critical) { border-left: 3px solid rgba(240,80,80,.4); }
.env-card:has(.badge-critical):hover { border-left-color: rgba(240,80,80,.7); }
.env-card:has(.badge-critical).selected { border-left-color: #f05f5f; }
.env-card:has(.badge-credential){ border-left: 3px solid rgba(220,140,60,.3); }
/* Section count badge */
.sec-count {
font-family: var(--mono);
font-size: 10px;
color: var(--text3);
background: var(--bg3);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1px 7px;
}
/* Tag legend */
.tag-legend {
margin-bottom: 14px;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--r);
overflow: hidden;
}
.legend-summary {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 12px;
cursor: pointer;
list-style: none;
user-select: none;
outline: none;
}
.legend-summary::-webkit-details-marker { display: none; }
.legend-chips { display: flex; gap: 5px; flex-wrap: wrap; flex: 1; }
.legend-hint { font-size: 10px; color: var(--text3); white-space: nowrap; flex-shrink: 0; }
.tag-legend[open] .legend-hint { opacity: 0; }
.legend-body {
padding: 8px 12px 10px;
border-top: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 6px;
}
.legend-row { display: flex; align-items: center; gap: 10px; font-size: 11px; color: var(--text2); }
.legend-row .badge { flex-shrink: 0; width: 74px; text-align: center; }
.legend-tip { font-size: 9.5px; color: var(--text3); margin-top: 4px; padding-top: 6px; border-top: 1px solid var(--border); }
.toolbar-hint { color: var(--text3); font-size: 12px; margin-right: 6px; white-space: nowrap; }
.card-input { position: relative; }
.card-input input[type="text"],
.card-input input[type="password"],
.card-input input[type="number"],
.card-input textarea,
.card-input select {
width: 100%;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 7px 10px;
font-family: var(--mono);
font-size: 11.5px;
color: var(--text);
outline: none;
transition: border-color .15s;
resize: vertical;
}
.card-input input[type="text"]:focus,
.card-input input[type="password"]:focus,
.card-input input[type="number"]:focus,
.card-input textarea:focus,
.card-input select:focus {
border-color: var(--amber);
}
.card-input textarea { min-height: 64px; }
.card-input select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238d97ad' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
padding-right: 28px;
}
.card-input optgroup { color: var(--text2); font-weight: 600; }
.card-input option { color: var(--text); background: var(--bg3); }
.toggle-shell { display: flex; align-items: center; gap: 8px; }
.tog {
padding: 5px 14px;
border-radius: 20px;
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text3);
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: all .18s;
letter-spacing: .5px;
}
.tog.on {
background: rgba(61,214,140,.15);
border-color: rgba(61,214,140,.4);
color: var(--green);
}
.picker-shell { display: flex; flex-direction: column; gap: 6px; }
.picker-row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.picker-select {
flex: 1;
min-width: 0;
padding: 6px 28px 6px 8px !important;
font-size: 11px !important;
}
.mini-btn {
padding: 5px 9px;
border-radius: var(--r);
border: 1px solid var(--border2);
background: var(--bg3);
color: var(--text2);
font-family: var(--mono);
font-size: 10px;
font-weight: 600;
cursor: pointer;
transition: all .15s;
white-space: nowrap;
}
.mini-btn:hover { background: var(--bg4); color: var(--text); }
.right-panel {
width: var(--panel-w);
flex-shrink: 0;
border-left: 1px solid var(--border);
background: var(--bg2);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-scroll {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.panel-scroll::-webkit-scrollbar { width: 4px; }
.panel-scroll::-webkit-scrollbar-track { background: transparent; }
.panel-scroll::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
.pblock {
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r2);
overflow: hidden;
}
.pblock-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid var(--border);
}
.pblock-title {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text3);
display: flex;
align-items: center;
gap: 6px;
}
.pblock-body { padding: 12px 14px; display: flex; flex-direction: column; gap: 8px; }
.pblock-body textarea,
.pblock-body input[type="text"] {
width: 100%;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 8px 10px;
font-family: var(--mono);
font-size: 10.5px;
color: var(--text2);
outline: none;
resize: vertical;
transition: border-color .15s;
}
.pblock-body textarea:focus,
.pblock-body input[type="text"]:focus {
border-color: var(--amber);
color: var(--text);
}
#importText { min-height: 80px; }
#bundleOut { min-height: 60px; color: var(--amber2); }
#envLineOut { font-size: 10px; }
.row-btns { display: flex; gap: 6px; flex-wrap: wrap; }
#summary {
font-size: 11.5px;
color: var(--text2);
line-height: 1.6;
}
#summary strong {
font-size: 15px;
color: var(--amber);
font-family: var(--mono);
}
.sum-keys {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.sum-key {
font-family: var(--mono);
font-size: 9.5px;
color: var(--text2);
background: var(--bg4);
border: 1px solid var(--border2);
border-radius: 4px;
padding: 2px 6px;
}
#customSec { margin-top: 8px; }
.custom-row {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 8px;
}
.custom-row input {
flex: 1;
background: var(--bg3);
border: 1px solid var(--border);
border-radius: var(--r);
padding: 7px 10px;
font-family: var(--mono);
font-size: 11px;
color: var(--text);
outline: none;
transition: border-color .15s;
min-width: 0;
}
.custom-row input:focus { border-color: var(--amber); }
.custom-row input:first-child { flex: 0 0 40%; }
#toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(20px);
background: var(--bg4);
border: 1px solid var(--border2);
color: var(--amber);
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
padding: 9px 20px;
border-radius: 30px;
z-index: 9999;
opacity: 0;
transition: opacity .2s, transform .2s;
pointer-events: none;
box-shadow: 0 8px 32px rgba(0,0,0,.5);
}
#toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 4px; }
@media (max-width: 900px) {
:root { --panel-w: 280px; --sidebar-w: 180px; }
}
@media (max-width: 700px) {
.right-panel { display: none; }
:root { --sidebar-w: 160px; }
}
@media (max-width: 520px) {
.sidebar-wrap { display: none; }
.topbar-divider, .topbar-title { display: none; }
}
</style>
</head>
<body>
<header class="topbar">
<div class="topbar-logo">
<span class="logo-emoji">🪽</span>
<span class="topbar-wordmark">Hugging<em>Mes</em></span>
</div>
<div class="topbar-divider"></div>
<span class="topbar-title">ENV Builder</span>
<div class="topbar-spacer"></div>
<span class="topbar-pill">v2025</span>
</header>
<div class="layout">
<aside class="sidebar-wrap">
<div class="sidebar-scroll">
<div id="sidebar"></div>
</div>
</aside>
<main class="main">
<div class="toolbar">
<span class="toolbar-hint">Tip: Start with <strong>⚡ Required</strong>, then fill keys and click <strong># Generate Bundle</strong>.</span>
<div class="search-wrap">
<span class="search-icon"></span>
<input id="search" type="text" placeholder="Search variables…" autocomplete="off" spellcheck="false">
</div>
<div class="tb-sep"></div>
<button id="selectRequired" class="btn">⚡ Required</button>
<button id="selectCommon" class="btn">★ Common</button>
<button id="selectVisible" class="btn">☑ Visible</button>
<button id="clearAll" class="btn btn-ghost">✕ Clear</button>
</div>
<div class="content-wrap">
<div class="sections-scroll">
<details class="tag-legend">
<summary class="legend-summary">
<div class="legend-chips">
<span class="badge badge-critical">critical</span>
<span class="badge badge-credential">credential</span>
<span class="badge badge-feature">feature</span>
<span class="badge badge-optional">optional</span>
<span class="badge badge-advanced">advanced</span>
<span class="badge badge-build">build</span>
</div>
<span class="legend-hint">▸ Tag legend</span>
</summary>
<div class="legend-body">
<div class="legend-row"><span class="badge badge-critical">critical</span> Required for the space to function at all</div>
<div class="legend-row"><span class="badge badge-credential">credential</span> API keys, tokens, secrets — keep private</div>
<div class="legend-row"><span class="badge badge-feature">feature</span> Unlocks an optional feature or integration</div>
<div class="legend-row"><span class="badge badge-optional">optional</span> Useful but not required; has a default</div>
<div class="legend-row"><span class="badge badge-advanced">advanced</span> Fine-tuning for specific deployments</div>
<div class="legend-row"><span class="badge badge-build">build</span> Affects Docker build, not runtime</div>
<div class="legend-tip">Use <strong>⚡ Required</strong> to auto-select all critical fields, then fill in credential keys.</div>
</div>
</details>
<div id="sections"></div>
<div id="customSec" class="sec" data-section="Custom Env">
<div class="sec-header">
<span class="sec-icon">🔧</span>
<span class="sec-title">Custom Env</span>
<div class="sec-line"></div>
</div>
<div id="customRows"></div>
<button id="addCustom" class="btn" style="margin-top:6px;">+ Add variable</button>
</div>
</div>
<aside class="right-panel">
<div class="panel-scroll">
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📊 Summary</span>
</div>
<div class="pblock-body">
<div id="summary">No variables selected yet.</div>
</div>
</div>
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📦 Bundle Output</span>
</div>
<div class="pblock-body">
<textarea id="bundleOut" placeholder="Select variables and click # Generate Bundle…" readonly spellcheck="false"></textarea>
<input type="text" id="envLineOut" placeholder="HUGGINGMES_ENV_BUNDLE=…" readonly spellcheck="false">
<div class="row-btns">
<button id="generateBundle" class="btn btn-amber" style="width:100%;"># Generate Bundle</button>
</div>
<div class="row-btns">
<button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
<button id="copyEnvLine" class="btn">⎘ Env Line</button>
<button id="copyJson" class="btn">⎘ JSON</button>
<button id="applyBundle" class="btn btn-ghost">↺ Apply</button>
</div>
</div>
</div>
<div class="pblock">
<div class="pblock-head">
<span class="pblock-title">📥 Import</span>
</div>
<div class="pblock-body">
<textarea id="importText" placeholder="Paste .env, JSON, or HUGGINGMES_ENV_BUNDLE=… here" spellcheck="false"></textarea>
<button id="applyImport" class="btn btn-amber" style="width:100%;">↓ Import & Apply</button>
</div>
</div>
</div>
</aside>
</div>
</main>
</div>
<div id="toast">Copied ✓</div>
<script src="env-builder.js"></script>
</body>
</html>