Files
static/management.html
2026-05-27 18:30:35 -03:00

821 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Server Manager · my-biz.app</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=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #080c14;
--bg2: #0d1220;
--surface: #111827;
--surface2: #161f30;
--border: #1e2d45;
--border-hi: #243450;
--glow: #3b82f6;
--glow2: #06b6d4;
--text: #e2e8f0;
--text-muted: #64748b;
--text-subtle: #334155;
--accent: #3b82f6;
--accent-hover: #2563eb;
--accent-dim: rgba(59,130,246,.12);
--cyan: #06b6d4;
--cyan-dim: rgba(6,182,212,.1);
--success: #10b981;
--success-dim: rgba(16,185,129,.12);
--warn: #f59e0b;
--warn-dim: rgba(245,158,11,.12);
--error: #ef4444;
--error-dim: rgba(239,68,68,.12);
--radius-sm: 5px;
--radius: 8px;
--radius-lg: 12px;
--mono: 'JetBrains Mono', 'Fira Code', monospace;
}
html { font-size: 15px; }
body {
font-family: 'Inter', system-ui, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5;
}
/* ── Scrollbar ── */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border-hi); border-radius: 99px; }
/* ── Header ── */
header {
background: rgba(8,12,20,.85);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 1.5rem;
height: 52px;
display: flex;
align-items: center;
justify-content: space-between;
position: sticky;
top: 0;
z-index: 100;
}
.logo {
font-size: .9rem;
font-weight: 700;
letter-spacing: -.02em;
display: flex;
align-items: center;
gap: .3rem;
color: var(--text);
}
.logo .dot { color: var(--accent); }
.logo .sep { color: var(--text-subtle); font-weight: 300; margin: 0 .35rem; }
.logo .sub { font-weight: 400; color: var(--text-muted); font-size: .82rem; }
.uuid-chip {
display: inline-flex;
align-items: center;
gap: .4rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: .28rem .65rem;
font-size: .72rem;
font-family: var(--mono);
color: var(--text-muted);
cursor: pointer;
transition: border-color .15s, color .15s;
}
.uuid-chip:hover { border-color: var(--accent); color: var(--accent); }
.uuid-chip svg { width: 11px; height: 11px; flex-shrink: 0; }
/* ── Main ── */
main {
max-width: 860px;
margin: 0 auto;
padding: 1.5rem 1.25rem 3rem;
}
/* ── Overlay / Modal ── */
.overlay {
position: fixed;
inset: 0;
background: rgba(5,8,16,.75);
backdrop-filter: blur(8px);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
padding: 1rem;
}
.overlay.hidden { display: none; }
.modal {
background: var(--surface);
border: 1px solid var(--border-hi);
border-radius: var(--radius-lg);
box-shadow: 0 0 0 1px rgba(59,130,246,.08), 0 24px 48px rgba(0,0,0,.6);
width: 100%;
max-width: 400px;
overflow: hidden;
}
.modal-header {
padding: 1.5rem 1.5rem .75rem;
text-align: center;
border-bottom: 1px solid var(--border);
}
.modal-logo {
font-size: 1rem;
font-weight: 700;
letter-spacing: -.02em;
color: var(--text);
margin-bottom: 1rem;
}
.modal-logo .dot { color: var(--accent); }
.modal-header h2 { font-size: 1rem; font-weight: 600; color: var(--text); }
.modal-header p { font-size: .8rem; color: var(--text-muted); margin-top: .3rem; }
.modal-body { padding: 1.25rem 1.5rem 1.5rem; }
/* ── Form elements ── */
.field { margin-bottom: .9rem; }
.field:last-of-type { margin-bottom: 0; }
label {
display: block;
font-size: .72rem;
font-weight: 600;
letter-spacing: .06em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: .3rem;
}
input[type="text"],
input[type="email"],
input[type="date"],
input[type="number"],
input[type="password"],
textarea,
select {
width: 100%;
padding: .5rem .75rem;
background: var(--bg2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
font-family: inherit;
font-size: .85rem;
color: var(--text);
outline: none;
transition: border-color .15s, box-shadow .15s;
-webkit-appearance: none;
}
input:focus, textarea:focus, select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(59,130,246,.15);
}
textarea { resize: vertical; min-height: 64px; }
select {
cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%2364748b' stroke-width='1.5' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right .7rem center;
padding-right: 2rem;
}
/* ── Buttons ── */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: .4rem;
padding: .5rem 1rem;
border-radius: var(--radius-sm);
font-family: inherit;
font-size: .82rem;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
transition: all .15s;
outline: none;
white-space: nowrap;
}
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn-primary {
background: var(--accent);
color: #fff;
box-shadow: 0 0 16px rgba(59,130,246,.3);
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-hover);
box-shadow: 0 0 24px rgba(59,130,246,.45);
}
.btn-ghost {
background: transparent;
color: var(--text-muted);
border-color: var(--border);
}
.btn-ghost:hover:not(:disabled) { background: var(--surface2); color: var(--text); }
.btn-full { width: 100%; margin-top: .75rem; padding: .6rem; font-size: .875rem; border-radius: var(--radius); }
/* ── Alert ── */
.alert {
display: flex;
align-items: flex-start;
gap: .6rem;
padding: .65rem .85rem;
border-radius: var(--radius-sm);
font-size: .8rem;
margin-bottom: .9rem;
border: 1px solid;
}
.alert svg { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; }
.alert-error { background: var(--error-dim); color: var(--error); border-color: rgba(239,68,68,.25); }
.alert-success { background: var(--success-dim); color: var(--success); border-color: rgba(16,185,129,.25); }
/* ── Spinner ── */
.spinner {
width: 13px; height: 13px;
border: 2px solid rgba(255,255,255,.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin .55s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Toast ── */
#toast {
position: fixed;
bottom: 1.25rem;
right: 1.25rem;
z-index: 500;
min-width: 240px;
max-width: 340px;
}
@keyframes fadeUp {
from { opacity:0; transform:translateY(6px); }
to { opacity:1; transform:translateY(0); }
}
/* ══════════════════════════════════════════
READ-ONLY INFO PANEL
══════════════════════════════════════════ */
.info-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
padding: 1rem 1.25rem;
margin-bottom: 1rem;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: .1rem 0;
}
.info-row {
display: contents;
}
.info-block {
padding: .5rem .75rem;
border-right: 1px solid var(--border);
}
.info-block:last-child,
.info-block:nth-child(4n) { border-right: none; }
.info-block .i-label {
font-size: .65rem;
font-weight: 600;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--text-subtle);
margin-bottom: .2rem;
}
.info-block .i-val {
font-size: .82rem;
font-weight: 500;
color: var(--text);
font-family: var(--mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.info-block .i-val.long {
font-size: .72rem;
}
.info-block .i-val.dim { color: var(--text-muted); font-family: inherit; }
/* divider between rows inside the panel */
.info-divider {
grid-column: 1 / -1;
height: 1px;
background: var(--border);
margin: .25rem 0;
}
/* status badge */
.badge {
display: inline-flex;
align-items: center;
gap: .28rem;
padding: .18rem .5rem;
border-radius: 99px;
font-size: .68rem;
font-weight: 600;
letter-spacing: .04em;
font-family: inherit;
}
.badge-dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
.badge.active { background: var(--success-dim); color: var(--success); }
.badge.test { background: var(--warn-dim); color: var(--warn); }
.badge.inactive,
.badge.suspended{ background: var(--error-dim); color: var(--error); }
/* ══════════════════════════════════════════
EDIT CARD
══════════════════════════════════════════ */
.edit-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
overflow: hidden;
}
.edit-card-header {
padding: .75rem 1.25rem;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface2);
}
.edit-card-title {
font-size: .78rem;
font-weight: 600;
color: var(--text-muted);
letter-spacing: .06em;
text-transform: uppercase;
display: flex;
align-items: center;
gap: .5rem;
}
.edit-card-title svg { width: 14px; height: 14px; stroke: var(--accent); }
.edit-body {
padding: 1rem 1.25rem;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: .6rem 1rem;
}
.edit-body .span2 { grid-column: span 2; }
.edit-body .span3 { grid-column: span 3; }
.compact-field label {
font-size: .65rem;
margin-bottom: .22rem;
}
.compact-field input,
.compact-field textarea,
.compact-field select {
padding: .38rem .6rem;
font-size: .82rem;
}
.compact-field textarea { min-height: 56px; }
.edit-footer {
padding: .75rem 1.25rem;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
background: var(--surface2);
gap: .75rem;
}
/* ── Misc ── */
.hidden { display: none !important; }
.mono { font-family: var(--mono); }
/* glow accent line on top of cards */
.glow-top {
position: relative;
}
.glow-top::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, var(--accent), var(--cyan), transparent);
opacity: .6;
}
</style>
</head>
<body>
<!-- ══ UUID MODAL ══ -->
<div class="overlay" id="uuidOverlay">
<div class="modal glow-top">
<div class="modal-header">
<div class="modal-logo">my-biz<span class="dot">.</span>app</div>
<h2>Server Manager</h2>
<p>Enter your UUID to load server data.</p>
</div>
<div class="modal-body">
<div id="uuidError" class="alert alert-error hidden">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
<span id="uuidErrorMsg">Could not fetch server data.</span>
</div>
<div class="field">
<label for="uuidInput">UUID</label>
<input type="text" id="uuidInput" placeholder="006-003-xxxx-xxxx-xxxx-xxxxxxxxxxxx" autocomplete="off" spellcheck="false" style="font-family:var(--mono);font-size:.8rem" />
</div>
<button class="btn btn-primary btn-full" id="uuidSubmitBtn" onclick="submitUUID()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
Load Server
</button>
</div>
</div>
</div>
<!-- ══ AUTH MODAL ══ -->
<div class="overlay hidden" id="authOverlay">
<div class="modal glow-top">
<div class="modal-header">
<div class="modal-logo">my-biz<span class="dot">.</span>app</div>
<h2>Authentication Required</h2>
<p>Enter your credentials to continue.</p>
</div>
<div class="modal-body">
<div class="field">
<label for="authUser">Username</label>
<input type="text" id="authUser" placeholder="username" autocomplete="username" />
</div>
<div class="field">
<label for="authPass">Password</label>
<input type="password" id="authPass" placeholder="••••••••" autocomplete="current-password" />
</div>
<button class="btn btn-primary btn-full" id="authSubmitBtn">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
Connect
</button>
</div>
</div>
</div>
<!-- ══ HEADER ══ -->
<header>
<div class="logo">
my-biz<span class="dot">.</span>app
<span class="sep">/</span>
<span class="sub">Server Manager</span>
</div>
<div id="uuidChip" class="uuid-chip hidden" onclick="resetUUID()" title="Click to change">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
<span id="uuidChipLabel"></span>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>
</div>
</header>
<!-- ══ MAIN ══ -->
<main>
<!-- READ-ONLY INFO PANEL -->
<div id="infoPanel" class="info-panel glow-top hidden">
<div class="info-block">
<div class="i-label">ID</div>
<div class="i-val" id="i_ID"></div>
</div>
<div class="info-block">
<div class="i-label">Status</div>
<div class="i-val" id="i_status"></div>
</div>
<div class="info-block">
<div class="i-label">Created</div>
<div class="i-val dim" id="i_created"></div>
</div>
<div class="info-block">
<div class="i-label">Expires</div>
<div class="i-val dim" id="i_expires"></div>
</div>
<div class="info-divider"></div>
<div class="info-block" style="grid-column:span 4">
<div class="i-label">UUID</div>
<div class="i-val long" id="i_UUID"></div>
</div>
<div class="info-divider"></div>
<div class="info-block">
<div class="i-label">HDD</div>
<div class="i-val" id="i_hdd"></div>
</div>
<div class="info-block">
<div class="i-label">Workers</div>
<div class="i-val" id="i_workers"></div>
</div>
<div class="info-block">
<div class="i-label">Backup Slots</div>
<div class="i-val" id="i_backup_slots"></div>
</div>
<div class="info-block">
<div class="i-label">Domain Slots</div>
<div class="i-val" id="i_domains_slots"></div>
</div>
<div class="info-divider"></div>
<div class="info-block">
<div class="i-label">Git</div>
<div class="i-val" id="i_git"></div>
</div>
<div class="info-block" style="grid-column:span 2">
<div class="i-label">Domains</div>
<div class="i-val dim" id="i_domains"></div>
</div>
<div class="info-block">
<div class="i-label">Image</div>
<div class="i-val dim" id="i_image"></div>
</div>
</div>
<!-- EDIT CARD -->
<div id="editCard" class="edit-card glow-top hidden">
<div class="edit-card-header">
<div class="edit-card-title">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>
Edit Configuration
</div>
<div id="formAlert" class="hidden" style="margin:0;font-size:.75rem"></div>
</div>
<div class="edit-body">
<!-- Row 1: email, tags -->
<div class="compact-field">
<label for="f_email">Email</label>
<input type="email" id="f_email" placeholder="user@example.com" />
</div>
<div class="compact-field">
<label for="f_tags">Tags</label>
<input type="text" id="f_tags" placeholder="odoo_19, production" />
</div>
<!-- Row 2: env list -->
<div class="compact-field span3">
<label for="f_env_list">Environment Variables</label>
<textarea id="f_env_list" placeholder="KEY=value&#10;OTHER=value"></textarea>
</div>
<!-- Row 3: UTM all in one line -->
<div class="compact-field">
<label for="f_utm_source">UTM Source</label>
<input type="text" id="f_utm_source" placeholder="google" />
</div>
<div class="compact-field">
<label for="f_utm_medium">UTM Medium</label>
<input type="text" id="f_utm_medium" placeholder="cpc" />
</div>
<div class="compact-field">
<label for="f_utm_campaign">UTM Campaign</label>
<input type="text" id="f_utm_campaign" placeholder="launch_2026" />
</div>
<!-- Row 4: comment -->
<div class="compact-field span3">
<label for="f_comment">Comment</label>
<textarea id="f_comment" placeholder="Internal notes…" style="min-height:48px"></textarea>
</div>
<!-- Row 4: history -->
<div class="compact-field span3">
<label for="f_history">History</label>
<textarea id="f_history" placeholder="Change history…" style="min-height:48px"></textarea>
</div>
</div>
<div class="edit-footer">
<button class="btn btn-ghost" onclick="resetUUID()" style="font-size:.75rem">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 12H5"/><path d="m12 5-7 7 7 7"/></svg>
Change UUID
</button>
<button class="btn btn-primary" id="saveBtn" onclick="saveServer()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg>
Save Changes
</button>
</div>
</div>
</main>
<div id="toast"></div>
<script>
const WEBHOOK = 'https://002-001-5dd6e535-4d1c-46bc-9bd9-42ad4bc5f082.odoo4projects.com/webhook/6ffa7889-993b-4b12-b7de-29d743f956dd';
let currentUUID = '';
let credentials = null;
let serverData = null;
const $ = id => document.getElementById(id);
/* ── Auth ── */
function authHeaders() {
if (!credentials) return {};
return { 'Authorization': 'Basic ' + btoa(credentials.user + ':' + credentials.pass) };
}
function showAuthOverlay(onSuccess) {
$('authOverlay').classList.remove('hidden');
$('authUser').focus();
$('authSubmitBtn').onclick = () => {
const user = $('authUser').value.trim();
const pass = $('authPass').value;
if (!user) return;
credentials = { user, pass };
$('authOverlay').classList.add('hidden');
onSuccess();
};
}
async function apiFetch(method, body) {
const opts = {
method,
cache: 'no-store',
credentials: 'omit',
headers: { 'Content-Type': 'application/json', ...authHeaders() },
};
if (body !== undefined) opts.body = JSON.stringify(body);
const url = method === 'GET'
? WEBHOOK + '?uuid=' + encodeURIComponent(currentUUID) + '&_t=' + Date.now()
: WEBHOOK;
const res = await fetch(url, opts);
if (res.status === 401 || res.status === 403) throw new Error('AUTH');
if (!res.ok) throw new Error('HTTP ' + res.status);
const text = await res.text();
try { return text ? JSON.parse(text) : {}; } catch { return {}; }
}
async function withAuth(fn) {
try { return await fn(); }
catch (e) {
if (e.message === 'AUTH' || e.message.includes('NetworkError') || e.message.includes('Failed to fetch')) {
return new Promise((resolve, reject) => {
showAuthOverlay(async () => {
try { resolve(await fn()); } catch (e2) { reject(e2); }
});
});
}
throw e;
}
}
/* ── Toast ── */
function showToast(msg, type = 'success') {
$('toast').innerHTML = `<div class="alert alert-${type}" style="box-shadow:0 8px 32px rgba(0,0,0,.5);animation:fadeUp .2s ease">${msg}</div>`;
setTimeout(() => { $('toast').innerHTML = ''; }, 4000);
}
/* ── UUID submit ── */
async function submitUUID() {
const uuid = $('uuidInput').value.trim();
$('uuidError').classList.add('hidden');
if (!uuid) { flashField('uuidInput'); return; }
currentUUID = uuid;
const btn = $('uuidSubmitBtn');
btn.disabled = true;
btn.innerHTML = `<div class="spinner"></div> Loading…`;
try {
const raw = await withAuth(() => apiFetch('GET'));
const record = Array.isArray(raw) ? raw[0]?.json : raw?.json ?? raw;
if (!record) throw new Error('Unexpected response format.');
serverData = record;
populateAll(record);
$('uuidOverlay').classList.add('hidden');
$('uuidChip').classList.remove('hidden');
$('uuidChipLabel').textContent = uuid.length > 30 ? uuid.slice(0, 30) + '…' : uuid;
$('infoPanel').classList.remove('hidden');
$('editCard').classList.remove('hidden');
} catch (err) {
currentUUID = '';
$('uuidErrorMsg').textContent = err.message || 'Could not load server data.';
$('uuidError').classList.remove('hidden');
btn.disabled = false;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg> Load Server`;
}
}
function flashField(id) {
const el = $(id);
el.style.borderColor = 'var(--error)';
el.focus();
setTimeout(() => { el.style.borderColor = ''; }, 1800);
}
/* ── Populate ── */
function populateAll(d) {
// Info panel (read-only)
const status = (d.status || '').toLowerCase();
const statusLabels = { active: 'Active', test: 'Test', inactive: 'Inactive', suspended: 'Suspended' };
$('i_ID').textContent = d.ID ?? '—';
$('i_status').innerHTML = `<span class="badge ${status}"><span class="badge-dot"></span>${statusLabels[status] ?? status}</span>`;
$('i_created').textContent = d.created || '—';
$('i_expires').textContent = d.expires || '—';
$('i_UUID').textContent = d.UUID || '—';
$('i_hdd').textContent = d.hdd != null ? d.hdd + ' GB' : '—';
$('i_workers').textContent = d.workers ?? '—';
$('i_backup_slots').textContent= d.backup_slots ?? '—';
$('i_domains_slots').textContent= d.domains_slots ?? '—';
$('i_git').textContent = d.git ? 'Enabled' : 'Disabled';
$('i_domains').textContent = d.domains || '—';
$('i_image').textContent = d.image || '—';
// Edit form
const set = (id, val) => { const el = $(id); if (el) el.value = val ?? ''; };
set('f_email', d.email);
set('f_tags', d.tags);
set('f_env_list', d.env_list);
set('f_comment', d.comment);
set('f_utm_source', d.utm_source);
set('f_utm_medium', d.utm_medium);
set('f_utm_campaign', d.utm_campaign);
set('f_history', d.history);
}
/* ── Save ── */
async function saveServer() {
const btn = $('saveBtn');
btn.disabled = true;
btn.innerHTML = `<div class="spinner"></div> Saving…`;
$('formAlert').classList.add('hidden');
const payload = {
...serverData,
email: $('f_email').value,
tags: $('f_tags').value || null,
env_list: $('f_env_list').value || null,
comment: $('f_comment').value || null,
utm_source: $('f_utm_source').value || null,
utm_medium: $('f_utm_medium').value || null,
utm_campaign: $('f_utm_campaign').value || null,
history: $('f_history').value || null,
};
try {
await withAuth(() => apiFetch('POST', { json: payload }));
// Update status badge in info panel live
serverData = { ...serverData, ...payload };
const status = payload.status.toLowerCase();
const statusLabels = { active: 'Active', test: 'Test', inactive: 'Inactive', suspended: 'Suspended' };
$('i_status').innerHTML = `<span class="badge ${status}"><span class="badge-dot"></span>${statusLabels[status] ?? status}</span>`;
btn.disabled = false;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg> Save Changes`;
showToast(`<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="flex-shrink:0"><polyline points="20 6 9 17 4 12"/></svg> <span><strong>Saved</strong> — server record updated.</span>`);
} catch (err) {
btn.disabled = false;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17,21 17,13 7,13 7,21"/><polyline points="7,3 7,8 15,8"/></svg> Save Changes`;
const a = $('formAlert');
a.className = 'alert alert-error';
a.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg><span>${err.message}</span>`;
a.classList.remove('hidden');
}
}
/* ── Reset ── */
function resetUUID() {
currentUUID = '';
credentials = null;
serverData = null;
$('uuidInput').value = '';
$('authUser').value = '';
$('authPass').value = '';
$('uuidError').classList.add('hidden');
$('infoPanel').classList.add('hidden');
$('editCard').classList.add('hidden');
$('uuidChip').classList.add('hidden');
$('formAlert').classList.add('hidden');
$('uuidOverlay').classList.remove('hidden');
const btn = $('uuidSubmitBtn');
btn.disabled = false;
btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg> Load Server`;
}
/* ── Keyboard ── */
$('uuidInput').addEventListener('keydown', e => { if (e.key === 'Enter') submitUUID(); });
$('authUser').addEventListener('keydown', e => { if (e.key === 'Enter') $('authPass').focus(); });
$('authPass').addEventListener('keydown', e => { if (e.key === 'Enter') $('authSubmitBtn').click(); });
</script>
</body>
</html>