AND / static /index.html
asons's picture
Update static/index.html
6383e85 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cloud Phone Assistant</title>
<style>
:root {
--primary: #0d6efd; /* Blue - Login Fix */
--primary2: #fd7e14; /* Orange - Account Sync */
--primary3: #20c997; /* Green - Hubble Sync */
--ok: #198754;
--err: #dc3545;
--warn: #ffc107;
--bg: #f5f7fa;
--card: #ffffff;
--muted: #6c757d;
--border: #e9ecef;
}
* { box-sizing: border-box; }
body {
margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Microsoft YaHei", sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; color: #212529; display: flex; align-items: center;
justify-content: center; padding: 20px;
}
.hidden { display: none !important; }
.container {
background: var(--card); border-radius: 16px; padding: 32px;
box-shadow: 0 20px 50px rgba(0,0,0,0.18);
width: 100%; max-width: 820px;
}
h1 { font-size: 22px; font-weight: 600; margin: 0 0 6px 0; text-align: center; }
.subtitle { text-align: center; color: var(--muted); font-size: 13px; margin-bottom: 24px; }
.login-card { max-width: 380px; margin: 0 auto; }
.field { margin-bottom: 14px; }
label { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; font-weight: 500; }
input[type=text], input[type=password] {
width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
font-size: 14px; transition: border-color .15s;
}
input:focus { outline: none; border-color: var(--primary); }
input:disabled { background: #f8f9fa; color: var(--muted); }
.btn {
width: 100%; padding: 11px; border: none; border-radius: 8px; color: white;
font-size: 15px; font-weight: 500; cursor: pointer; transition: all .15s;
}
.btn-primary { background: var(--primary); }
.btn-primary:hover:not(:disabled) { background: #0b5ed7; }
.btn-orange { background: var(--primary2); }
.btn-orange:hover:not(:disabled) { background: #e66a00; }
.btn-teal { background: var(--primary3); }
.btn-teal:hover:not(:disabled) { background: #12b886; }
.btn-ok { background: var(--ok); }
.btn-err { background: var(--err); }
.btn-warn { background: var(--warn); color: #000; }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.btn-text {
background: transparent; color: var(--muted); border: 1px solid var(--border);
}
/* Mode Selection Segmented Control */
.mode-seg {
display: flex; gap: 0; background: #f1f3f5; border-radius: 8px;
padding: 4px; margin-bottom: 14px;
}
.mode-seg .seg-btn {
flex: 1; padding: 9px 10px; text-align: center; font-size: 13px;
border: none; background: transparent; color: var(--muted); cursor: pointer;
border-radius: 6px; font-weight: 500; transition: all .15s;
}
.mode-seg .seg-btn.active {
background: var(--card); color: #212529;
box-shadow: 0 1px 3px rgba(0,0,0,0.1); font-weight: 600;
}
/* Highlight active seg with corresponding colors by mode */
.mode-seg .seg-btn.active[data-mode="restore"] { border-top: 2px solid var(--primary); }
.mode-seg .seg-btn.active[data-mode="sync"] { border-top: 2px solid var(--primary2); }
.mode-seg .seg-btn.active[data-mode="hubble"] { border-top: 2px solid var(--primary3); }
.mode-seg .seg-btn.active[data-mode="hubble_sync"] { border-top: 2px solid var(--primary3); }
.mode-seg .seg-btn.active[data-mode="hubble_launch"] { border-top: 2px solid var(--primary3); }
.mode-seg .seg-btn.active[data-mode="hubble_quick"] { border-top: 2px solid var(--primary3); }
.mode-seg .seg-btn:hover:not(.active):not(:disabled) { color: #212529; }
.mode-seg .seg-btn:disabled { cursor: not-allowed; opacity: .6; }
.mode-hint {
font-size: 12px; color: var(--muted); margin-top: -6px; margin-bottom: 14px;
padding: 8px 10px; background: #f8f9fa; border-radius: 6px;
border-left: 3px solid var(--primary);
}
.mode-hint.sync { border-left-color: var(--primary2); background: #fff4ea; }
.mode-hint.hubble { border-left-color: var(--primary3); background: #e6fcf5; }
/* Workspace */
.workspace { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; align-items: start; }
@media (max-width: 680px) { .workspace { grid-template-columns: 1fr; } }
.form-area textarea {
width: 100%; padding: 10px 12px; border: 1px solid var(--border); border-radius: 8px;
font-size: 13px; font-family: Consolas, monospace; resize: vertical;
}
.ring-area {
display: flex; flex-direction: column; align-items: center; justify-content: center;
padding-top: 12px;
}
.ring-wrap { position: relative; width: 220px; height: 220px; }
.ring-wrap svg { transform: rotate(-90deg); width: 100%; height: 100%; }
.ring-bg { stroke: var(--border); }
.ring-fg {
stroke: var(--primary);
transition: stroke-dashoffset .35s ease-out, stroke .3s;
stroke-linecap: round;
}
.ring-fg.ok { stroke: var(--ok); }
.ring-fg.err { stroke: var(--err); }
.ring-fg.run { stroke: var(--primary); }
.ring-fg.run.sync { stroke: var(--primary2); }
.ring-fg.run.hubble { stroke: var(--primary3); }
.ring-center {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center;
}
.ring-pct { font-size: 44px; font-weight: 700; color: #212529; line-height: 1; }
.ring-pct small { font-size: 20px; color: var(--muted); font-weight: 500; }
.ring-label { font-size: 13px; color: var(--muted); margin-top: 8px; }
.ring-label.ok { color: var(--ok); font-weight: 600; }
.ring-label.err { color: var(--err); font-weight: 600; }
.ring-label.run { color: var(--primary); font-weight: 600; }
.ring-label.run.sync { color: var(--primary2); font-weight: 600; }
.ring-label.run.hubble { color: var(--primary3); font-weight: 600; }
.spinner {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: currentColor; margin-right: 6px;
animation: pulse 1.2s ease-in-out infinite;
vertical-align: middle;
}
@keyframes pulse { 0%,100% { opacity: .3; } 50% { opacity: 1; } }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; font-size: 13px; }
.topbar .muted { color: var(--muted); }
.topbar a { color: var(--muted); text-decoration: none; cursor: pointer; }
.topbar a:hover { color: var(--primary); }
.err-box {
margin-top: 14px; padding: 10px 14px; border-radius: 6px;
background: #fff3f3; border: 1px solid #f5c2c7; color: var(--err);
font-size: 13px;
}
.admin-entry { text-align: center; margin-top: 14px; font-size: 12px; }
.admin-entry a { color: var(--muted); text-decoration: none; cursor: pointer; }
.admin-entry a:hover { color: var(--primary); }
/* Admin Panel */
.container.admin { max-width: 960px; }
.admin-head {
display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;
}
.admin-head h1 { margin: 0; text-align: left; }
.admin-gen {
background: #f8f9fa; padding: 14px 16px; border-radius: 8px;
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
margin-bottom: 16px;
}
.admin-gen label { margin: 0; font-size: 13px; color: var(--muted); }
.admin-gen input[type=number] {
width: 90px; padding: 6px 10px; border: 1px solid var(--border); border-radius: 5px;
font-size: 13px;
}
.admin-gen .btn { width: auto; padding: 7px 16px; font-size: 13px; margin-left: auto; }
.admin-msg {
padding: 9px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 12px;
}
.admin-msg.ok { background: #d1e7dd; color: #0f5132; border: 1px solid #badbcc; }
.admin-msg.err { background: #f8d7da; color: #842029; border: 1px solid #f5c2c7; }
table.admin-table { width: 100%; border-collapse: collapse; font-size: 13px; }
table.admin-table th {
text-align: left; padding: 10px; background: #f8f9fa; color: var(--muted);
font-weight: 500; border-bottom: 2px solid var(--border); font-size: 12px;
}
table.admin-table td {
padding: 10px; border-bottom: 1px solid var(--border); vertical-align: middle;
}
table.admin-table tr:hover td { background: #fafbfc; }
table.admin-table code {
color: #d63384; background: #f1f3f5; padding: 2px 6px; border-radius: 3px;
font-family: Consolas, monospace;
}
.badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.badge-green { background: #d1e7dd; color: #0f5132; }
.badge-gray { background: #e9ecef; color: #6c757d; }
.btn-tiny {
padding: 4px 10px; font-size: 12px; width: auto; border-radius: 5px;
border: 1px solid var(--err); background: transparent; color: var(--err); cursor: pointer;
}
.btn-tiny:hover { background: var(--err); color: #fff; }
.link-edit {
cursor: pointer; color: var(--primary); margin-left: 6px;
font-size: 13px; user-select: none;
}
.muted-time { color: #adb5bd; font-size: 12px; }
</style>
</head>
<body>
<div class="container">
<!-- Login Section -->
<section id="login">
<h1>🔧 Cloud Phone Assistant</h1>
<div class="subtitle">Enter access key to continue</div>
<div class="login-card">
<div class="field">
<label>Access Key</label>
<input type="text" id="key" placeholder="VIP-XXXX-XXXX" autofocus>
</div>
<button class="btn btn-primary" id="btn-login" onclick="login()">Verify & Enter</button>
<div class="admin-entry">
<a onclick="adminLogin()">Admin Gateway</a>
</div>
</div>
</section>
<!-- Workspace Section -->
<section id="work" class="hidden">
<div class="topbar">
<div class="muted" id="uses-info"></div>
<a onclick="logout()">Logout</a>
</div>
<h1>🔧 Cloud Phone Assistant</h1>
<div class="subtitle">Paste credentials, execute with one click</div>
<!-- Operation Mode Segmented Control -->
<div class="mode-seg" id="mode-seg">
<button class="seg-btn active" data-mode="restore" onclick="selectMode('restore')">
① Fix Login Page
</button>
<button class="seg-btn" data-mode="sync" onclick="selectMode('sync')">
② Account Sync
</button>
<button class="seg-btn" data-mode="hubble" onclick="selectMode('hubble')">
③ Hubble Sync
</button>
<button class="seg-btn" data-mode="hubble_sync" onclick="selectMode('hubble_sync')">
④ Sync Only
</button>
<button class="seg-btn" data-mode="hubble_launch" onclick="selectMode('hubble_launch')">
⑤ Launch Page
</button>
<button class="seg-btn" data-mode="hubble_quick" onclick="selectMode('hubble_quick')">
⑥ Quick Launch
</button>
</div>
<div class="mode-hint" id="mode-hint">
Restores login page configuration for Hong Kong mobile package (Google / Email entry etc.). Does not affect accounts or alter the did.
</div>
<div class="workspace">
<!-- Left Panel: Input Fields -->
<div class="form-area">
<div class="field">
<label>① SSH Command</label>
<textarea id="ssh-cmd" rows="3" placeholder="ssh -oHostKeyAlgorithms=... user@host -p 1824 -L 7798:adb-proxy:60063 -Nf"></textarea>
</div>
<div class="field">
<label>② Password (Token)</label>
<input type="password" id="ssh-pwd" placeholder="base64 long string">
</div>
<button class="btn btn-primary" id="btn-start" onclick="startTask()">🚀 Start Repair</button>
<div class="err-box hidden" id="err-box"></div>
</div>
<!-- Right Panel: Progress Ring -->
<div class="ring-area">
<div class="ring-wrap">
<svg viewBox="0 0 120 120">
<circle cx="60" cy="60" r="54" fill="none" stroke-width="8" class="ring-bg"/>
<circle cx="60" cy="60" r="54" fill="none" stroke-width="8"
id="ring-fg" class="ring-fg"
stroke-dasharray="339.29" stroke-dashoffset="339.29"/>
</svg>
<div class="ring-center">
<div class="ring-pct"><span id="pct-num">0</span><small>%</small></div>
</div>
</div>
<div class="ring-label" id="ring-label">Ready</div>
</div>
</div>
</section>
<!-- Admin Console Panel -->
<section id="admin" class="hidden">
<div class="admin-head">
<h1>👑 Key Management Console</h1>
<a class="muted" style="cursor:pointer;font-size:13px;color:var(--muted);text-decoration:none" onclick="logout()">Exit Console</a>
</div>
<div class="admin-gen">
<label>Generate</label>
<input type="number" id="gen-count" value="1" min="1" max="50">
<label>keys, with</label>
<input type="number" id="gen-uses" value="9999" min="1">
<label>uses each</label>
<button class="btn btn-primary" onclick="adminGenerate()">Generate</button>
<button class="btn btn-text" style="width:auto;padding:7px 14px;" onclick="adminLoadKeys()">Refresh</button>
</div>
<div id="admin-msg"></div>
<table class="admin-table">
<thead>
<tr>
<th>Access Key</th>
<th style="width:100px">Status</th>
<th style="width:130px">Uses Remaining</th>
<th style="width:160px">Created At</th>
<th style="width:160px">Last Used</th>
<th style="width:80px">Action</th>
</tr>
</thead>
<tbody id="admin-keys"></tbody>
</table>
</section>
</div>
<script>
// ============= Settings =============
const API_BASE = ""; // Standard endpoint, proxies via same-domain Pages Functions
let token = "";
let currentRole = "";
let running = false;
let currentMode = "restore"; // "restore" | "sync"
const RING_LEN = 339.29; // 2πr r=54
// Metadata Configuration mapping for execution modes
const MODES = {
restore: {
endpoint: "/api/v2/submit_task",
btnLabel: "🚀 Start Repair",
btnLabelDone: "✓ Repair Again",
btnColor: "btn-primary",
hint: "Reset to HK login page interface (Google/Email/Line entry). ⚠ If target package is currently signed in, account data will be cleared, requiring re-authentication.",
ringColor: "", // Blue
},
sync: {
endpoint: "/api/v2/sync",
btnLabel: "🔄 Start Sync",
btnLabelDone: "✓ Sync Again",
btnColor: "btn-orange",
hint: "Local hardware profile account synchronization.",
ringColor: "sync",
},
hubble: {
endpoint: "/api/v2/hubble",
btnLabel: "📦 Sync + Launch",
btnLabelDone: "✓ Try Again",
btnColor: "btn-teal",
hint: "Cross-platform mobile → hubble credentials migration + automatic launch authorization gateway. Expect an initial ~5 minutes provisioning setup time.",
ringColor: "hubble",
},
hubble_sync: {
endpoint: "/api/v2/hubble_sync",
btnLabel: "📋 Sync Data Only",
btnLabelDone: "✓ Re-sync Data",
btnColor: "btn-teal",
hint: "Migrates mobile → hubble profile information data (keva) without loading page layout. Execution time approx. 30 seconds.",
ringColor: "hubble",
},
hubble_launch: {
endpoint: "/api/v2/hubble_launch",
btnLabel: "🚀 Launch Page Only",
btnLabelDone: "✓ Re-launch Page",
btnColor: "btn-teal",
hint: "Warmup sequences + environment check + load gateway layer. Recommended for troubleshooting white screens following data sync. Runtime approx 2 mins.",
ringColor: "hubble",
},
hubble_quick: {
endpoint: "/api/v2/hubble_quick",
btnLabel: "⚡ Quick Launch",
btnLabelDone: "⚡ Trigger Again",
btnColor: "btn-teal",
hint: "Dispatches direct setup commands immediately skipping initialization pipelines. ~15 seconds runtime speed, execution success not guaranteed.",
ringColor: "hubble",
},
};
function selectMode(m) {
if (running) return;
currentMode = m;
// Update selection states on container UI indicators
for (const b of document.querySelectorAll("#mode-seg .seg-btn")) {
b.classList.toggle("active", b.dataset.mode === m);
}
// Update user contextual notifications
const hint = document.getElementById("mode-hint");
hint.textContent = MODES[m].hint;
hint.classList.remove("sync", "hubble");
if (m === "sync") hint.classList.add("sync");
if (m.startsWith("hubble")) hint.classList.add("hubble");
// Update context action trigger configuration
const btn = document.getElementById("btn-start");
btn.className = "btn " + MODES[m].btnColor;
btn.textContent = MODES[m].btnLabel;
// Clean monitoring ring elements
setRing(0, "idle");
}
function lockMode(lock) {
for (const b of document.querySelectorAll("#mode-seg .seg-btn")) {
b.disabled = lock;
}
}
// ============= Processing Visualization Ring =============
function setRing(pct, state) {
pct = Math.max(0, Math.min(100, pct));
document.getElementById("pct-num").textContent = Math.round(pct);
const fg = document.getElementById("ring-fg");
const offset = RING_LEN * (1 - pct / 100);
// UI Optimisation: If rendering resets back to zero state while executing, disable transitions cleanly to avoid asset shadows.
if (pct === 0 && state === "run") {
fg.style.transition = 'none';
fg.setAttribute("stroke-dashoffset", offset);
void fg.getBoundingClientRect(); // Triggers continuous element redrawing cycles
fg.style.transition = '';
} else {
fg.setAttribute("stroke-dashoffset", offset);
}
const label = document.getElementById("ring-label");
// Performance Optimization: Cache processing workflows via target data attributes preventing performance blockades due to extreme layout calculations.
if (label.dataset.state !== state || label.dataset.mode !== currentMode) {
fg.classList.remove("ok", "err", "run", "sync");
label.classList.remove("ok", "err", "run", "sync");
const modeCls = MODES[currentMode].ringColor;
if (state === "run") {
fg.classList.add("run");
label.classList.add("run");
if (modeCls) { fg.classList.add(modeCls); label.classList.add(modeCls); }
const runLabel = {restore: "Processing", sync: "Syncing", hubble: "Syncing", hubble_sync: "Syncing", hubble_launch: "Launching", hubble_quick: "Launching"}[currentMode] || "Executing";
label.innerHTML = `<span class="spinner"></span>${runLabel}`;
} else if (state === "ok") {
fg.classList.add("ok");
label.classList.add("ok");
const okLabel = {restore: "✓ Fixed Successfully", sync: "✓ Synced Successfully", hubble: "✓ Synced Successfully", hubble_sync: "✓ Synced Successfully", hubble_launch: "✓ Launched Successfully", hubble_quick: "⚡ Command Sent"}[currentMode] || "✓ Completed";
label.textContent = okLabel;
} else if (state === "err") {
fg.classList.add("err");
label.classList.add("err");
const errLabel = {restore: "✗ Repair Failed", sync: "✗ Sync Failed", hubble: "✗ Sync Failed", hubble_sync: "✗ Sync Failed", hubble_launch: "✗ Launch Failed", hubble_quick: "✗ Failed"}[currentMode] || "✗ Failed";
label.textContent = errLabel;
} else {
label.textContent = "Ready";
}
label.dataset.state = state;
label.dataset.mode = currentMode;
}
}
// ============= API Configuration Pipeline =============
async function api(path, opts = {}) {
opts.headers = Object.assign({ "Content-Type": "application/json" }, opts.headers || {});
if (token) opts.headers["Authorization"] = token;
const r = await fetch(API_BASE + path, opts);
if (!r.ok) {
let msg = `HTTP ${r.status}`;
try { msg = (await r.json()).detail || msg; } catch {}
throw new Error(msg);
}
return r.json();
}
// ============= Account Access Authentication =============
async function login() {
const key = document.getElementById("key").value.trim();
if (!key) return alert("Please enter your access key");
const btn = document.getElementById("btn-login");
btn.disabled = true;
btn.textContent = "Verifying…";
try {
const r = await api("/api/verify", {
method: "POST",
body: JSON.stringify({ key }),
});
token = r.token;
currentRole = r.role;
if (r.role === "admin") {
document.getElementById("login").classList.add("hidden");
document.getElementById("admin").classList.remove("hidden");
document.querySelector(".container").classList.add("admin");
adminLoadKeys();
} else {
document.getElementById("uses-info").textContent =
`Key: ${key.slice(0, 6)}*** · Remaining: ${r.uses_left ?? "?"} allocation pools`;
document.getElementById("login").classList.add("hidden");
document.getElementById("work").classList.remove("hidden");
selectMode("restore");
setRing(0, "idle");
}
} catch (e) {
alert("Authentication failed: " + e.message);
} finally {
btn.disabled = false;
btn.textContent = "Verify & Enter";
}
}
// ============= Execution Controls Manager =============
async function startTask() {
if (running) return;
const ssh = document.getElementById("ssh-cmd").value.trim();
const pwd = document.getElementById("ssh-pwd").value.trim();
if (!ssh) return alert("Please enter the SSH command parameter");
if (!pwd) return alert("Please enter your authorization token credentials");
const raw = ssh + "\n" + pwd;
lastRingVal = -1;
setRing(0, "run");
hideErr();
lockMode(true);
const btn = document.getElementById("btn-start");
btn.disabled = true;
btn.className = "btn btn-warn";
btn.textContent = {restore: "Fixing…", sync: "Syncing…", hubble: "Syncing…", hubble_sync: "Syncing…", hubble_launch: "Launching…", hubble_quick: "Sending…"}[currentMode] || "Executing…";
running = true;
for (const id of ["ssh-cmd", "ssh-pwd"])
document.getElementById(id).disabled = true;
const endpoint = MODES[currentMode].endpoint;
try {
const resp = await fetch(API_BASE + endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": token,
},
body: JSON.stringify({ raw_input: raw }),
});
if (!resp.ok) {
let msg = "HTTP " + resp.status;
try { msg = (await resp.json()).detail || msg; } catch {}
throw new Error(msg);
}
const reader = resp.body.getReader();
const decoder = new TextDecoder("utf-8");
let buf = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
// Processing architecture regex updates to parse data buffers matching standard \r\n\r\n and \n\n boundaries
const parts = buf.split(/\r?\n\r?\n/);
buf = parts.pop();
for (const chunk of parts) {
// Normalize standard \r platform layouts within line segmentation targets
const dline = chunk.split(/\r?\n/).find(l => l.startsWith("data: "));
if (!dline) continue;
try {
handleEvent(JSON.parse(dline.slice(6)));
} catch (e) {
console.warn("JSON parsing failure encountered parsing dataset:", dline);
}
}
}
// Stream resolution fallbacks
if (running) {
setRing(100, "ok");
finishUI(true);
}
} catch (e) {
showErr(e.message);
setRing(0, "err");
finishUI(false);
}
}
let lastRingVal = -1;
function applyDone(ev) {
if (ev.ok) {
setRing(100, "ok");
} else {
showErr(ev.error || "Unknown response error status");
setRing(ev.value || 0, "err");
}
if (ev.uses_left != null) {
const el = document.getElementById("uses-info");
if (el) el.textContent = el.textContent.replace(/Remaining: \S+ allocation pools/, `Remaining: ${ev.uses_left} allocation pools`);
}
finishUI(ev.ok);
}
function handleEvent(ev) {
if (ev.type === "progress") {
if (ev.value !== lastRingVal) {
setRing(ev.value, "run");
lastRingVal = ev.value;
}
} else if (ev.type === "done") {
applyDone(ev);
}
}
function finishUI(ok) {
running = false;
lockMode(false);
const btn = document.getElementById("btn-start");
btn.disabled = false;
if (ok) {
btn.className = "btn btn-ok";
btn.textContent = MODES[currentMode].btnLabelDone;
} else {
btn.className = "btn btn-err";
btn.textContent = "✗ Retry Action";
}
for (const id of ["ssh-cmd", "ssh-pwd"])
document.getElementById(id).disabled = false;
}
function showErr(msg) {
const e = document.getElementById("err-box");
e.textContent = "❌ " + msg;
e.classList.remove("hidden");
}
function hideErr() { document.getElementById("err-box").classList.add("hidden"); }
function logout() {
if (running && !confirm("An operation sequence is executing. Are you sure you want to terminate sessions?")) return;
token = ""; currentRole = ""; running = false;
document.getElementById("key").value = "";
const pwd = document.getElementById("ssh-pwd");
if (pwd) pwd.value = "";
document.getElementById("work").classList.add("hidden");
document.getElementById("admin").classList.add("hidden");
document.querySelector(".container").classList.remove("admin");
document.getElementById("login").classList.remove("hidden");
setRing(0, "idle");
}
document.getElementById("key").addEventListener("keydown", e => {
if (e.key === "Enter") login();
});
function adminLogin() {
const pwd = prompt("Enter system administrator credential passphrase:");
if (!pwd) return;
document.getElementById("key").value = pwd;
login();
}
// ============= System Administrator Configuration Modules =============
async function adminLoadKeys() {
try {
const rows = await api("/api/admin/list_keys");
renderKeyTable(rows);
} catch (e) {
showAdminMsg("Failed to load keys: " + e.message, "err");
}
}
async function adminGenerate() {
const count = parseInt(document.getElementById("gen-count").value, 10) || 1;
const uses = parseInt(document.getElementById("gen-uses").value, 10) || 9999;
try {
const r = await api("/api/admin/generate_keys", {
method: "POST",
body: JSON.stringify({ count, uses }),
});
showAdminMsg(
`Generated ${r.keys.length} keys (${r.uses_left} usages each): <code>${r.keys.join(", ")}</code>`,
"ok"
);
adminLoadKeys();
} catch (e) {
showAdminMsg("Generation sequence failed: " + e.message, "err");
}
}
async function adminDelete(key) {
if (!confirm(`Confirm absolute removal authorization profiles for key ${key}?`)) return;
try {
await api("/api/admin/delete_key", {
method: "POST",
body: JSON.stringify({ key }),
});
adminLoadKeys();
} catch (e) {
alert("Removal transaction failed: " + e.message);
}
}
async function adminEditUses(key, current) {
const v = prompt(`Modify database transaction allocation pools for key ${key} (Currently ${current}):`, current);
if (v === null) return;
const n = parseInt(v, 10);
if (isNaN(n) || n < 0) return alert("Parameters provided must contain positive values");
try {
await api("/api/admin/set_uses", {
method: "POST",
body: JSON.stringify({ key, uses: n }),
});
adminLoadKeys();
} catch (e) {
alert("Database status updates rejected: " + e.message);
}
}
function fmtTime(t) { return t ? new Date(t * 1000).toLocaleString() : "-"; }
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c =>
({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]));
}
function renderKeyTable(rows) {
const tb = document.getElementById("admin-keys");
tb.innerHTML = "";
if (!rows.length) {
tb.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:24px;color:var(--muted)">No keys found in database</td></tr>';
return;
}
for (const r of rows) {
const uses = r.uses_left ?? 0;
const badge = r.status === "unused"
? '<span class="badge badge-green">Unused</span>'
: '<span class="badge badge-gray">Active</span>';
const safeKey = escapeHtml(r.key);
tb.innerHTML += `<tr>
<td><code>${safeKey}</code></td>
<td>${badge}</td>
<td>
<span style="color:${uses > 0 ? 'var(--ok)' : 'var(--muted)'}">${uses}</span>
<span class="link-edit" title="Edit uses" onclick="adminEditUses('${safeKey}', ${uses})">✎</span>
</td>
<td><span class="muted-time">${fmtTime(r.created_at)}</span></td>
<td><span class="muted-time">${fmtTime(r.used_at)}</span></td>
<td><button class="btn-tiny" onclick="adminDelete('${safeKey}')">Delete</button></td>
</tr>`;
}
}
function showAdminMsg(html, cls) {
document.getElementById("admin-msg").innerHTML =
`<div class="admin-msg ${cls}">${html}</div>`;
if (cls === "ok") {
setTimeout(() => {
const el = document.getElementById("admin-msg");
if (el) el.innerHTML = "";
}, 8000);
}
}
</script>
</body>
</html>