821 lines
28 KiB
HTML
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 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>
|