| <!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; |
| --primary2: #fd7e14; |
| --primary3: #20c997; |
| --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-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; |
| } |
| |
| .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 { 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); } |
| |
| |
| .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"> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
| |
| const API_BASE = ""; |
| |
| let token = ""; |
| let currentRole = ""; |
| let running = false; |
| let currentMode = "restore"; |
| const RING_LEN = 339.29; |
| |
| |
| 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: "", |
| }, |
| 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; |
| |
| for (const b of document.querySelectorAll("#mode-seg .seg-btn")) { |
| b.classList.toggle("active", b.dataset.mode === m); |
| } |
| |
| 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"); |
| |
| const btn = document.getElementById("btn-start"); |
| btn.className = "btn " + MODES[m].btnColor; |
| btn.textContent = MODES[m].btnLabel; |
| |
| setRing(0, "idle"); |
| } |
| |
| function lockMode(lock) { |
| for (const b of document.querySelectorAll("#mode-seg .seg-btn")) { |
| b.disabled = lock; |
| } |
| } |
| |
| |
| 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); |
| |
| |
| if (pct === 0 && state === "run") { |
| fg.style.transition = 'none'; |
| fg.setAttribute("stroke-dashoffset", offset); |
| void fg.getBoundingClientRect(); |
| fg.style.transition = ''; |
| } else { |
| fg.setAttribute("stroke-dashoffset", offset); |
| } |
| |
| const label = document.getElementById("ring-label"); |
| |
| |
| 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; |
| } |
| } |
| |
| 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(); |
| } |
| |
| |
| 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"; |
| } |
| } |
| |
| |
| 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 }); |
| |
| |
| const parts = buf.split(/\r?\n\r?\n/); |
| buf = parts.pop(); |
| |
| for (const chunk of parts) { |
| |
| 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); |
| } |
| } |
| } |
| |
| 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(); |
| } |
| |
| |
| 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 => |
| ({"&":"&","<":"<",">":">","\"":""","'":"'"}[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> |