| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Titan Nexus β Command Center</title> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Tajawal:wght@300;400;500;700&display=swap" rel="stylesheet"> |
| <style> |
| :root { |
| --bg:#050810; --surface:#0b0f1a; --surface2:#0f1525; |
| --border:rgba(99,179,237,.12); --border2:rgba(99,179,237,.25); |
| --accent:#63b3ed; --accent2:#4fd1c5; --gold:#f6c90e; |
| --danger:#fc5c65; --success:#26de81; --warn:#fd9644; |
| --text:#e2ecff; --muted:#5a7090; |
| --mono:'Space Mono',monospace; --sans:'Tajawal',sans-serif; |
| } |
| *,*::before,*::after{box-sizing:border-box;margin:0;padding:0} |
| body{background:var(--bg);color:var(--text);font-family:var(--sans);min-height:100vh;overflow-x:hidden} |
| body::before{content:'';position:fixed;inset:0;background-image:linear-gradient(rgba(99,179,237,.03) 1px,transparent 1px),linear-gradient(90deg,rgba(99,179,237,.03) 1px,transparent 1px);background-size:40px 40px;pointer-events:none;z-index:0} |
|
|
| /* LOCK */ |
| #lockScreen{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;z-index:100;background:var(--bg);flex-direction:column} |
| .lock-card{background:var(--surface);border:1px solid var(--border2);border-radius:20px;padding:44px 40px 36px;width:100%;max-width:400px;box-shadow:0 0 80px rgba(99,179,237,.08),0 40px 80px rgba(0,0,0,.6);position:relative;overflow:hidden} |
| .lock-card::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--accent),var(--accent2),transparent)} |
| .lock-logo{font-family:var(--mono);font-size:11px;letter-spacing:4px;color:var(--muted);text-align:center;margin-bottom:32px;text-transform:uppercase} |
| .lock-icon{font-size:48px;text-align:center;display:block;margin-bottom:8px;filter:drop-shadow(0 0 20px rgba(246,201,14,.4))} |
| .lock-title{font-family:var(--mono);font-size:18px;color:var(--accent);text-align:center;margin-bottom:4px;letter-spacing:1px} |
| .lock-sub{font-size:13px;color:var(--muted);text-align:center;margin-bottom:28px} |
| .lock-input{width:100%;background:var(--bg);border:1px solid var(--border2);border-radius:10px;padding:14px 16px;color:var(--text);font-family:var(--mono);font-size:14px;letter-spacing:2px;margin-bottom:14px;transition:.2s;text-align:center;outline:none} |
| .lock-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(99,179,237,.12)} |
| .lock-btn{width:100%;background:linear-gradient(135deg,var(--accent),var(--accent2));color:#000;font-family:var(--mono);font-weight:700;font-size:13px;letter-spacing:2px;border:none;border-radius:10px;padding:14px;cursor:pointer;transition:.2s;text-transform:uppercase} |
| .lock-btn:hover{opacity:.9;transform:translateY(-1px)} |
| .lock-err{color:var(--danger);font-size:12px;font-family:var(--mono);text-align:center;margin-top:12px;min-height:18px} |
|
|
| /* MAIN */ |
| #mainApp{display:none;position:relative;z-index:1} |
|
|
| /* TOPBAR */ |
| .topbar{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border);background:var(--surface);position:sticky;top:0;z-index:50;gap:12px} |
| .topbar-brand{display:flex;align-items:center;gap:10px} |
| .topbar-brand .crown{font-size:22px;filter:drop-shadow(0 0 8px rgba(246,201,14,.5))} |
| .topbar-brand .name{font-family:var(--mono);font-size:13px;letter-spacing:2px;color:var(--accent);text-transform:uppercase} |
| .topbar-brand .version{font-family:var(--mono);font-size:10px;color:var(--muted);background:var(--bg);border:1px solid var(--border);padding:2px 8px;border-radius:20px} |
| .topbar-right{display:flex;align-items:center;gap:12px} |
| .live-dot{width:8px;height:8px;border-radius:50%;background:var(--success);box-shadow:0 0 8px var(--success);animation:pulse 2s infinite} |
| @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} |
| .topbar-time{font-family:var(--mono);font-size:12px;color:var(--muted)} |
| .logout-btn{font-family:var(--mono);font-size:11px;color:var(--muted);background:none;border:1px solid var(--border);border-radius:6px;padding:6px 12px;cursor:pointer;transition:.2s;letter-spacing:1px} |
| .logout-btn:hover{border-color:var(--danger);color:var(--danger)} |
|
|
| /* TABS */ |
| .tabs{display:flex;padding:0 24px;background:var(--surface);border-bottom:1px solid var(--border);overflow-x:auto} |
| .tab{font-family:var(--mono);font-size:11px;letter-spacing:1.5px;color:var(--muted);padding:14px 20px;cursor:pointer;border-bottom:2px solid transparent;transition:.2s;white-space:nowrap;text-transform:uppercase;background:none;border-left:none;border-right:none;border-top:none} |
| .tab:hover{color:var(--text)} |
| .tab.active{color:var(--accent);border-bottom-color:var(--accent)} |
|
|
| /* TAB CONTENT */ |
| .tab-content{display:none;padding:24px;animation:fadeIn .2s ease} |
| .tab-content.active{display:block} |
| @keyframes fadeIn{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}} |
|
|
| /* STAT CARDS */ |
| .stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:12px;margin-bottom:24px} |
| .stat-card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:18px 16px;transition:.2s;position:relative;overflow:hidden} |
| .stat-card::after{content:'';position:absolute;top:0;left:0;right:0;height:2px;background:var(--accent-c,var(--accent));opacity:.6} |
| .stat-card:hover{border-color:var(--border2);transform:translateY(-2px)} |
| .stat-label{font-family:var(--mono);font-size:9px;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:10px} |
| .stat-value{font-family:var(--mono);font-size:22px;color:var(--text);font-weight:700} |
| .stat-sub{font-size:11px;color:var(--muted);margin-top:4px} |
|
|
| /* SECTION TITLE */ |
| .section-title{font-family:var(--mono);font-size:10px;letter-spacing:3px;color:var(--muted);text-transform:uppercase;margin-bottom:14px;display:flex;align-items:center;gap:10px} |
| .section-title::after{content:'';flex:1;height:1px;background:var(--border)} |
|
|
| /* TERMINAL */ |
| .terminal-wrap{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden} |
| .terminal-bar{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:var(--surface2);border-bottom:1px solid var(--border)} |
| .terminal-dots{display:flex;gap:6px} |
| .terminal-dots span{width:10px;height:10px;border-radius:50%} |
| .d1{background:#fc5c65}.d2{background:#fd9644}.d3{background:#26de81} |
| .terminal-title{font-family:var(--mono);font-size:11px;color:var(--muted);letter-spacing:1px} |
| .terminal-status{font-family:var(--mono);font-size:10px;padding:3px 10px;border-radius:20px;letter-spacing:1px} |
| .terminal-status.ready{background:rgba(38,222,129,.1);color:var(--success);border:1px solid rgba(38,222,129,.2)} |
| .terminal-status.running{background:rgba(99,179,237,.1);color:var(--accent);border:1px solid rgba(99,179,237,.2)} |
| .terminal-status.ok{background:rgba(38,222,129,.1);color:var(--success);border:1px solid rgba(38,222,129,.2)} |
| .terminal-status.error{background:rgba(252,92,101,.1);color:var(--danger);border:1px solid rgba(252,92,101,.2)} |
| .terminal-output{font-family:var(--mono);font-size:13px;color:#7ec8e3;padding:20px;min-height:220px;max-height:420px;overflow-y:auto;white-space:pre-wrap;word-break:break-word;line-height:1.7} |
| .terminal-output::-webkit-scrollbar{width:4px} |
| .terminal-output::-webkit-scrollbar-thumb{background:var(--border2);border-radius:4px} |
| .line-success{color:var(--success)}.line-error{color:var(--danger)}.line-warn{color:var(--warn)}.line-info{color:var(--accent)} |
| .cmd-row{display:flex;gap:10px;align-items:stretch;padding:12px 16px;border-top:1px solid var(--border);background:var(--surface2)} |
| .cmd-prompt{font-family:var(--mono);font-size:14px;color:var(--accent);display:flex;align-items:center;padding:0 4px;user-select:none} |
| .cmd-input{flex:1;background:transparent;border:none;color:var(--text);font-family:var(--mono);font-size:14px;outline:none;direction:ltr} |
| .cmd-input::placeholder{color:var(--muted)} |
| .cmd-run{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#000;font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:1.5px;border:none;border-radius:8px;padding:10px 18px;cursor:pointer;transition:.2s;text-transform:uppercase;white-space:nowrap} |
| .cmd-run:hover{opacity:.85} |
| .cmd-run:disabled{opacity:.4;cursor:not-allowed} |
|
|
| /* QUICK BUTTONS */ |
| .quick-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:8px;margin-bottom:20px} |
| .quick-btn{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px 10px;cursor:pointer;transition:.2s;text-align:center;color:var(--text)} |
| .quick-btn:hover{border-color:var(--accent);background:rgba(99,179,237,.05);transform:translateY(-2px)} |
| .quick-btn .qicon{font-size:20px;margin-bottom:6px} |
| .quick-btn .qlabel{font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:1px;text-transform:uppercase} |
|
|
| /* FORMS */ |
| .broadcast-box{background:var(--surface);border:1px solid var(--border);border-radius:16px;overflow:hidden;margin-bottom:16px} |
| .broadcast-header{padding:14px 18px;border-bottom:1px solid var(--border);background:var(--surface2);font-family:var(--mono);font-size:11px;letter-spacing:2px;color:var(--muted);text-transform:uppercase} |
| .broadcast-body{padding:18px} |
| textarea.bcast-input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:14px;color:var(--text);font-family:var(--sans);font-size:15px;resize:vertical;min-height:100px;outline:none;transition:.2s;margin-bottom:12px} |
| textarea.bcast-input:focus{border-color:var(--accent)} |
| .form-row{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:12px} |
| .form-field{flex:1;min-width:200px} |
| .form-field label{display:block;font-family:var(--mono);font-size:10px;letter-spacing:2px;color:var(--muted);text-transform:uppercase;margin-bottom:8px} |
| .form-field input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:10px;padding:12px 14px;color:var(--text);font-family:var(--mono);font-size:13px;outline:none;transition:.2s;direction:ltr} |
| .form-field input:focus{border-color:var(--accent)} |
|
|
| /* ACTION BUTTONS */ |
| .action-btn{border:none;border-radius:10px;padding:12px 24px;font-family:var(--mono);font-size:12px;font-weight:700;letter-spacing:1.5px;cursor:pointer;transition:.2s;text-transform:uppercase} |
| .action-btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));color:#000} |
| .action-btn.primary:hover{opacity:.85;transform:translateY(-1px)} |
| .action-btn.danger{background:rgba(252,92,101,.12);color:var(--danger);border:1px solid rgba(252,92,101,.3)} |
| .action-btn.danger:hover{background:rgba(252,92,101,.2);transform:translateY(-1px)} |
| .action-btn.market{background:rgba(246,201,14,.1);color:var(--gold);border:1px solid rgba(246,201,14,.25)} |
| .action-btn.market:hover{background:rgba(246,201,14,.18);transform:translateY(-1px)} |
|
|
| /* TOAST */ |
| #toast{position:fixed;bottom:28px;left:50%;transform:translateX(-50%) translateY(80px);background:var(--surface);border:1px solid var(--border2);border-radius:12px;padding:14px 24px;font-family:var(--mono);font-size:13px;color:var(--text);z-index:999;transition:transform .3s cubic-bezier(.34,1.56,.64,1);box-shadow:0 20px 60px rgba(0,0,0,.6);white-space:nowrap} |
| #toast.show{transform:translateX(-50%) translateY(0)} |
| #toast.t-success{border-color:rgba(38,222,129,.4);color:var(--success)} |
| #toast.t-error{border-color:rgba(252,92,101,.4);color:var(--danger)} |
|
|
| /* HISTORY */ |
| .history-list{display:flex;flex-direction:column;gap:8px} |
| .history-item{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:12px 16px;display:flex;align-items:center;gap:12px;cursor:pointer;transition:.15s} |
| .history-item:hover{border-color:var(--border2)} |
| .h-time{font-family:var(--mono);font-size:11px;color:var(--muted);min-width:80px} |
| .h-cmd{font-family:var(--mono);font-size:13px;color:var(--accent);flex:1;direction:ltr} |
|
|
| /* MARKET */ |
| .market-trigger-card{background:var(--surface);border:1px solid rgba(246,201,14,.2);border-radius:16px;padding:24px;margin-bottom:16px;text-align:center} |
| .m-icon{font-size:40px;margin-bottom:12px} |
| .m-title{font-family:var(--mono);font-size:14px;color:var(--gold);margin-bottom:6px;letter-spacing:1px} |
| .m-sub{font-size:13px;color:var(--muted);margin-bottom:20px} |
| .schedule-list{display:flex;flex-direction:column;gap:8px} |
| .schedule-item{background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:14px 18px;display:flex;align-items:center;justify-content:space-between} |
| .s-time{font-family:var(--mono);font-size:14px;color:var(--accent2)} |
| .s-label{font-size:13px;color:var(--text)} |
| .s-dot{width:8px;height:8px;border-radius:50%;background:var(--success);box-shadow:0 0 6px var(--success)} |
|
|
| @media(max-width:600px){ |
| .topbar{padding:12px 16px} |
| .tab-content{padding:16px} |
| .quick-grid{grid-template-columns:repeat(3,1fr)} |
| .stats-grid{grid-template-columns:repeat(2,1fr)} |
| .form-row{flex-direction:column} |
| } |
| </style> |
| </head> |
| <body> |
|
|
| <div id="lockScreen"> |
| <div class="lock-card"> |
| <div class="lock-logo">Titan Nexus · Secure Access</div> |
| <span class="lock-icon">👑</span> |
| <div class="lock-title">COMMAND CENTER</div> |
| <div class="lock-sub">Royal Admin Access Required</div> |
| <input class="lock-input" type="password" id="lockKey" placeholder="Admin Key" autocomplete="off"> |
| <button class="lock-btn" id="authBtn">AUTHENTICATE →</button> |
| <div class="lock-err" id="lockErr"></div> |
| </div> |
| </div> |
|
|
| <div id="mainApp"> |
| <div class="topbar"> |
| <div class="topbar-brand"> |
| <span class="crown">👑</span> |
| <span class="name">Titan Nexus</span> |
| <span class="version">v14.2</span> |
| </div> |
| <div class="topbar-right"> |
| <div class="live-dot"></div> |
| <span class="topbar-time" id="topbarTime">--:--</span> |
| <button class="logout-btn" id="logoutBtn">LOGOUT</button> |
| </div> |
| </div> |
|
|
| <div class="tabs"> |
| <button class="tab active" data-tab="overview">OVERVIEW</button> |
| <button class="tab" data-tab="terminal">TERMINAL</button> |
| <button class="tab" data-tab="broadcast">BROADCAST</button> |
| <button class="tab" data-tab="users">USERS</button> |
| <button class="tab" data-tab="market">MARKET</button> |
| <button class="tab" data-tab="email">EMAIL</button> |
| <button class="tab" data-tab="database">DATABASE</button> |
| <button class="tab" data-tab="history">HISTORY</button> |
| </div> |
|
|
| |
| <div class="tab-content active" id="tab-overview"> |
| <div class="stats-grid"> |
| <div class="stat-card" style="--accent-c:var(--accent)"><div class="stat-label">Uptime</div><div class="stat-value" id="s-uptime">β</div><div class="stat-sub">since restart</div></div> |
| <div class="stat-card" style="--accent-c:var(--success)"><div class="stat-label">Online Users</div><div class="stat-value" id="s-users">β</div><div class="stat-sub">registered</div></div> |
| <div class="stat-card" style="--accent-c:var(--accent2)"><div class="stat-label">Connections</div><div class="stat-value" id="s-conns">β</div><div class="stat-sub">total sockets</div></div> |
| <div class="stat-card" style="--accent-c:var(--warn)"><div class="stat-label">Memory</div><div class="stat-value" id="s-mem">β</div><div class="stat-sub" id="s-mem-sub">heap</div></div> |
| <div class="stat-card" style="--accent-c:var(--gold)"><div class="stat-label">CPU Load</div><div class="stat-value" id="s-cpu">β</div><div class="stat-sub">1-min avg</div></div> |
| <div class="stat-card" style="--accent-c:var(--success)"><div class="stat-label">Sentinel</div><div class="stat-value" style="font-size:14px;color:var(--success)">ACTIVE</div><div class="stat-sub">MK2 armed</div></div> |
| </div> |
| <div class="section-title">Quick Actions</div> |
| <div class="quick-grid"> |
| <div class="quick-btn" id="qStats"><div class="qicon">📊</div><div class="qlabel">Stats</div></div> |
| <div class="quick-btn" id="qBroadcast"><div class="qicon">📢</div><div class="qlabel">Broadcast</div></div> |
| <div class="quick-btn" id="qClear"><div class="qicon">🧹</div><div class="qlabel">Clear Logs</div></div> |
| <div class="quick-btn" id="qMarket"><div class="qicon">📈</div><div class="qlabel">Market</div></div> |
| <div class="quick-btn" id="qUsers"><div class="qicon">👤</div><div class="qlabel">Users</div></div> |
| <div class="quick-btn" id="qSweep"><div class="qicon">🧹</div><div class="qlabel">DB Sweep</div></div> |
| <div class="quick-btn" id="qRefresh"><div class="qicon">🔄</div><div class="qlabel">Refresh</div></div> |
| </div> |
| <div class="section-title">System Info</div> |
| <div class="terminal-wrap"><div class="terminal-output" id="overviewInfo">Loading...</div></div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-terminal"> |
| <div class="section-title">Command Terminal</div> |
| <div class="terminal-wrap"> |
| <div class="terminal-bar"> |
| <div class="terminal-dots"><span class="d1"></span><span class="d2"></span><span class="d3"></span></div> |
| <div class="terminal-title">titan-nexus@server:~$</div> |
| <div class="terminal-status ready" id="termStatus">READY</div> |
| </div> |
| <div class="terminal-output" id="termOutput">Welcome to Titan Nexus Command Center v14.2 |
| Type a command below or use the quick buttons. |
| ββββββββββββββββββββββββββββββββββββββββββ</div> |
| <div class="cmd-row"> |
| <span class="cmd-prompt">$</span> |
| <input class="cmd-input" id="termInput" type="text" placeholder="/stats /ban [uid] [reason] /broadcast [msg]" autocomplete="off"> |
| <button class="cmd-run" id="runBtn">RUN ▶</button> |
| </div> |
| </div> |
| <div style="margin-top:16px"> |
| <div class="section-title">Quick Commands</div> |
| <div class="quick-grid"> |
| <div class="quick-btn qcmd" data-cmd="/stats"><div class="qicon">📊</div><div class="qlabel">/stats</div></div> |
| <div class="quick-btn qcmd" data-cmd="/clear logs"><div class="qicon">🧹</div><div class="qlabel">/clear logs</div></div> |
| <div class="quick-btn qcmd" data-cmd="/broadcast "><div class="qicon">📢</div><div class="qlabel">/broadcast</div></div> |
| <div class="quick-btn qcmd" data-cmd="/notify "><div class="qicon">🔔</div><div class="qlabel">/notify</div></div> |
| <div class="quick-btn qcmd" data-cmd="/ban "><div class="qicon">🚫</div><div class="qlabel">/ban</div></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-broadcast"> |
| <div class="section-title">Broadcast Message</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-header">📢 Send to All Users</div> |
| <div class="broadcast-body"> |
| <textarea class="bcast-input" id="bcastMsg" placeholder="Ψ§ΩΨͺΨ¨ Ψ±Ψ³Ψ§ΩΨͺΩ ΩΩΨ§..."></textarea> |
| <button class="action-btn primary" id="sendBcastBtn">SEND BROADCAST →</button> |
| </div> |
| </div> |
| <div class="section-title" style="margin-top:24px">Notify Specific User</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-header">🔔 Direct Notification</div> |
| <div class="broadcast-body"> |
| <div class="form-row"> |
| <div class="form-field"><label>User UID</label><input type="text" id="notifyUid" placeholder="Firebase UID..."></div> |
| <div class="form-field"><label>Message</label><input type="text" id="notifyMsg" placeholder="Notification message..."></div> |
| </div> |
| <button class="action-btn primary" id="sendNotifyBtn">SEND NOTIFICATION →</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-users"> |
| <div class="section-title">User Actions</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-header">🚫 Ban User</div> |
| <div class="broadcast-body"> |
| <div class="form-row"> |
| <div class="form-field"><label>User UID</label><input type="text" id="banUid" placeholder="Firebase UID to ban..."></div> |
| <div class="form-field"><label>Reason</label><input type="text" id="banReason" placeholder="Ban reason..."></div> |
| </div> |
| <button class="action-btn danger" id="banBtn">BAN USER 🚫</button> |
| </div> |
| </div> |
| <div class="section-title" style="margin-top:24px">Royal Family</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-header" style="color:var(--gold)">👑 Protected Members</div> |
| <div class="broadcast-body" style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:2"> |
| Royal members cannot be banned.<br>Their messages are preserved by Janitor MK2.<br> |
| <span style="color:var(--gold)">2 members active.</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-market"> |
| <div class="market-trigger-card"> |
| <div class="m-icon">📈</div> |
| <div class="m-title">MARKET INTELLIGENCE ENGINE</div> |
| <div class="m-sub">Powered by Groq Compound β Live web search for gold, stocks & currencies</div> |
| <button class="action-btn market" id="marketBtn">FETCH & BROADCAST MARKET REPORT →</button> |
| </div> |
| <div class="section-title">Scheduled Reports</div> |
| <div class="schedule-list"> |
| <div class="schedule-item"><span class="s-time">08:00</span><span class="s-label">Morning Market Report</span><span class="s-dot"></span></div> |
| <div class="schedule-item"><span class="s-time">15:00</span><span class="s-label">Afternoon Update</span><span class="s-dot"></span></div> |
| <div class="schedule-item"><span class="s-time">23:59</span><span class="s-label">Royal Night Report</span><span class="s-dot"></span></div> |
| </div> |
| <div class="section-title" style="margin-top:20px">Coverage</div> |
| <div class="stats-grid"> |
| <div class="stat-card" style="--accent-c:var(--gold)"><div class="stat-label">Gold Markets</div><div class="stat-value" style="font-size:14px">EG SA AE</div><div class="stat-sub">EGP / SAR / AED / USD</div></div> |
| <div class="stat-card" style="--accent-c:var(--accent)"><div class="stat-label">Indices</div><div class="stat-value" style="font-size:14px">S&P NSDQ</div><div class="stat-sub">+ Dow + EGX30</div></div> |
| <div class="stat-card" style="--accent-c:var(--warn)"><div class="stat-label">Commodities</div><div class="stat-value" style="font-size:14px">Brent BTC</div><div class="stat-sub">Oil & Bitcoin</div></div> |
| <div class="stat-card" style="--accent-c:var(--accent2)"><div class="stat-label">FX Rates</div><div class="stat-value" style="font-size:14px">4 pairs</div><div class="stat-sub">vs EGP</div></div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-database"> |
| <div class="section-title">Database Maintenance</div> |
|
|
| |
| <div class="broadcast-box" style="border-color:rgba(252,92,101,.25)"> |
| <div class="broadcast-header" style="color:var(--warn)">🧹 Orphaned Data Sweeper</div> |
| <div class="broadcast-body"> |
| <div style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:2;margin-bottom:18px"> |
| • Paginates all Firebase Auth users to build a valid-UID set<br> |
| • Cross-references <span style="color:var(--accent)">titan_ghosts</span> to find records with no Auth account<br> |
| • Batch-deletes orphans from 5 collections: <span style="color:var(--accent)">titan_ghosts, titan_keys, users, user_reputation, welcome_sent</span><br> |
| • Safe chunking β commits every 450 ops to stay under Firestore's 500-op batch limit |
| </div> |
| <div style="display:flex;gap:12px;align-items:center;flex-wrap:wrap"> |
| <button class="action-btn danger" id="sweepBtn">🧹 RUN SWEEP</button> |
| <span id="sweepStatus" style="font-family:var(--mono);font-size:12px;color:var(--muted)"></span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="sweepResultPanel" style="display:none;margin-top:8px"> |
| <div class="section-title">Last Sweep Report</div> |
| <div class="terminal-wrap"> |
| <div class="terminal-bar"> |
| <div class="terminal-dots"><span class="d1"></span><span class="d2"></span><span class="d3"></span></div> |
| <div class="terminal-title">sweeper@titan-nexus:~$</div> |
| <div class="terminal-status ok" id="sweepTermStatus">DONE</div> |
| </div> |
| <div class="terminal-output" id="sweepOutput" style="min-height:140px"></div> |
| </div> |
|
|
| |
| <div class="stats-grid" style="margin-top:16px" id="sweepStats"> |
| <div class="stat-card" style="--accent-c:var(--danger)"> |
| <div class="stat-label">Orphans Found</div> |
| <div class="stat-value" id="sw-orphans">β</div> |
| <div class="stat-sub">missing from Auth</div> |
| </div> |
| <div class="stat-card" style="--accent-c:var(--warn)"> |
| <div class="stat-label">Docs Deleted</div> |
| <div class="stat-value" id="sw-docs">β</div> |
| <div class="stat-sub">across 5 collections</div> |
| </div> |
| <div class="stat-card" style="--accent-c:var(--accent)"> |
| <div class="stat-label">Batches</div> |
| <div class="stat-value" id="sw-batches">β</div> |
| <div class="stat-sub">committed</div> |
| </div> |
| <div class="stat-card" style="--accent-c:var(--success)"> |
| <div class="stat-label">Status</div> |
| <div class="stat-value" style="font-size:14px" id="sw-status">β</div> |
| <div class="stat-sub" id="sw-time">β</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
|
|
| <div class="tab-content" id="tab-history"> |
| <div class="section-title">Command History <span style="color:var(--muted);font-size:10px">(this session)</span></div> |
| <div class="history-list" id="historyList"> |
| <div style="color:var(--muted);font-family:var(--mono);font-size:13px;padding:20px 0;text-align:center">No commands run yet.</div> |
| </div> |
| </div> |
|
|
| |
| <div class="tab-content" id="tab-email"> |
| <div class="section-title">Send Email to Users</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-header">📧 Professional Email Broadcast</div> |
| <div class="broadcast-body"> |
| <div class="form-row"> |
| <div class="form-field"> |
| <label>Target (empty = ALL users)</label> |
| <input type="email" id="emailTarget" placeholder="specific@email.com or leave empty for all"> |
| </div> |
| <div class="form-field"> |
| <label>Accent Color</label> |
| <input type="text" id="emailAccent" placeholder="#63b3ed" value="#63b3ed" style="max-width:140px"> |
| </div> |
| </div> |
| <div class="form-field" style="margin-bottom:12px"> |
| <label>Subject</label> |
| <input type="text" id="emailSubject" placeholder="Email subject..."> |
| </div> |
| <div class="form-field" style="margin-bottom:12px"> |
| <label>Logo URL (optional)</label> |
| <input type="url" id="emailLogo" placeholder="https://example.com/logo.png"> |
| </div> |
| <div class="form-field" style="margin-bottom:12px"> |
| <label>Message Body</label> |
| <textarea class="bcast-input" id="emailBody" placeholder="Write your email here..." style="min-height:160px"></textarea> |
| </div> |
| <div class="form-row"> |
| <div class="form-field"> |
| <label>Button Text (optional)</label> |
| <input type="text" id="emailBtnText" placeholder="Open App"> |
| </div> |
| <div class="form-field"> |
| <label>Button URL (optional)</label> |
| <input type="url" id="emailBtnUrl" placeholder="https://..."> |
| </div> |
| </div> |
| <div class="form-field" style="margin-bottom:16px"> |
| <label>PDF URL (shown as download button)</label> |
| <input type="url" id="emailPdfUrl" placeholder="https://..."> |
| </div> |
| <div style="display:flex;gap:10px;align-items:center;flex-wrap:wrap"> |
| <button class="action-btn primary" id="sendEmailBtn">📧 SEND EMAIL</button> |
| <span id="emailStatus" style="font-family:var(--mono);font-size:12px;color:var(--muted)"></span> |
| </div> |
| </div> |
| </div> |
| <div class="section-title" style="margin-top:16px">Features</div> |
| <div class="broadcast-box"> |
| <div class="broadcast-body" style="font-family:var(--mono);font-size:12px;color:var(--muted);line-height:2"> |
| • Each user gets a private individual email<br> |
| • Professional dark HTML template with gradient<br> |
| • Optional logo, CTA button, PDF download link<br> |
| • Customizable accent color per campaign<br> |
| • Requires MAILER_URL + MAILER_TOKEN in HF Secrets |
| </div> |
| </div> |
| </div> |
|
|
| </div> |
|
|
| <div id="toast"></div> |
|
|
| <script> |
| var ADMIN_KEY = ''; |
| var history_log = []; |
| var toastTimer = null; |
| |
| |
| document.getElementById('authBtn').addEventListener('click', function() { doUnlock(); }); |
| document.getElementById('lockKey').addEventListener('keydown', function(e) { if (e.key === 'Enter') doUnlock(); }); |
| |
| function doUnlock() { |
| var k = document.getElementById('lockKey').value.trim(); |
| if (!k) { document.getElementById('lockErr').textContent = 'Enter admin key'; return; } |
| document.getElementById('lockErr').textContent = 'Authenticating...'; |
| document.getElementById('authBtn').disabled = true; |
| |
| fetch('/api/admin/command', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ adminKey: k, command: '/stats' }) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| if (d.success) { |
| ADMIN_KEY = k; |
| document.getElementById('lockScreen').style.display = 'none'; |
| document.getElementById('mainApp').style.display = 'block'; |
| startClock(); |
| loadStats(); |
| if (d.stats) showOverviewInfo(d.stats); |
| } else { |
| document.getElementById('lockErr').textContent = 'Invalid key β access denied'; |
| document.getElementById('authBtn').disabled = false; |
| } |
| }) |
| .catch(function(e) { |
| document.getElementById('lockErr').textContent = 'Server unreachable: ' + e.message; |
| document.getElementById('authBtn').disabled = false; |
| }); |
| } |
| |
| |
| document.getElementById('logoutBtn').addEventListener('click', function() { |
| ADMIN_KEY = ''; |
| document.getElementById('lockScreen').style.display = 'flex'; |
| document.getElementById('mainApp').style.display = 'none'; |
| document.getElementById('lockKey').value = ''; |
| document.getElementById('authBtn').disabled = false; |
| }); |
| |
| |
| function startClock() { |
| function tick() { |
| var now = new Date(); |
| document.getElementById('topbarTime').textContent = |
| now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); |
| } |
| tick(); |
| setInterval(tick, 1000); |
| } |
| |
| |
| document.querySelectorAll('.tab').forEach(function(btn) { |
| btn.addEventListener('click', function() { |
| document.querySelectorAll('.tab').forEach(function(t) { t.classList.remove('active'); }); |
| document.querySelectorAll('.tab-content').forEach(function(t) { t.classList.remove('active'); }); |
| btn.classList.add('active'); |
| document.getElementById('tab-' + btn.dataset.tab).classList.add('active'); |
| }); |
| }); |
| |
| function goTab(name) { |
| document.querySelectorAll('.tab').forEach(function(t) { |
| if (t.dataset.tab === name) t.click(); |
| }); |
| } |
| |
| |
| function loadStats() { |
| fetch('/stats') |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| document.getElementById('s-uptime').textContent = d.uptime || 'β'; |
| document.getElementById('s-users').textContent = d.connections ? d.connections.registered : 'β'; |
| document.getElementById('s-conns').textContent = d.connections ? d.connections.total : 'β'; |
| document.getElementById('s-mem').textContent = d.memory ? d.memory.percentage : 'β'; |
| document.getElementById('s-mem-sub').textContent = d.memory ? d.memory.heapUsed : ''; |
| document.getElementById('s-cpu').textContent = d.cpuLoad ? d.cpuLoad['1min'] : 'β'; |
| }) |
| .catch(function() {}); |
| } |
| setInterval(function() { if (ADMIN_KEY) loadStats(); }, 30000); |
| |
| function showOverviewInfo(s) { |
| var lines = [ |
| 'Platform : ' + s.platform, |
| 'Node.js : ' + s.nodeVersion, |
| 'Uptime : ' + s.uptime, |
| 'Memory : ' + s.memory.heapUsed + ' / ' + s.memory.heapTotal + ' (' + s.memory.percentage + ')', |
| 'CPU Load : ' + s.cpuLoad['1min'] + ' / ' + s.cpuLoad['5min'] + ' / ' + s.cpuLoad['15min'] |
| ]; |
| document.getElementById('overviewInfo').textContent = lines.join('\n'); |
| } |
| |
| |
| document.getElementById('qStats').addEventListener('click', function() { goTab('terminal'); setCmd('/stats'); }); |
| document.getElementById('qBroadcast').addEventListener('click', function() { goTab('broadcast'); }); |
| document.getElementById('qClear').addEventListener('click', function() { goTab('terminal'); setCmd('/clear logs'); }); |
| document.getElementById('qMarket').addEventListener('click', function() { goTab('market'); }); |
| document.getElementById('qUsers').addEventListener('click', function() { goTab('users'); }); |
| document.getElementById('qSweep').addEventListener('click', function() { goTab('database'); }); |
| document.getElementById('qRefresh').addEventListener('click', function() { loadStats(); showToast('Refreshed!', 'success'); }); |
| |
| |
| function setCmd(c) { |
| goTab('terminal'); |
| var inp = document.getElementById('termInput'); |
| inp.value = c; |
| inp.focus(); |
| } |
| |
| function appendTerm(text, cls) { |
| var el = document.getElementById('termOutput'); |
| var line = document.createElement('div'); |
| if (cls) line.className = cls; |
| line.textContent = text; |
| el.appendChild(line); |
| el.scrollTop = el.scrollHeight; |
| } |
| |
| function runCommand() { |
| var cmd = document.getElementById('termInput').value.trim(); |
| if (!cmd) return; |
| var btn = document.getElementById('runBtn'); |
| var statusEl = document.getElementById('termStatus'); |
| btn.disabled = true; |
| statusEl.textContent = 'RUNNING'; |
| statusEl.className = 'terminal-status running'; |
| appendTerm('$ ' + cmd, 'line-info'); |
| |
| fetch('/api/admin/command', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ adminKey: ADMIN_KEY, command: cmd }) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| if (d.success) { |
| var out = d.report || (d.stats ? JSON.stringify(d.stats, null, 2) : JSON.stringify(d, null, 2)); |
| appendTerm(out, 'line-success'); |
| statusEl.textContent = 'OK'; |
| statusEl.className = 'terminal-status ok'; |
| addHistory(cmd, true); |
| showToast('Done!', 'success'); |
| } else { |
| appendTerm('Error: ' + (d.error || 'Unknown'), 'line-error'); |
| statusEl.textContent = 'ERROR'; |
| statusEl.className = 'terminal-status error'; |
| addHistory(cmd, false); |
| showToast(d.error || 'Failed', 'error'); |
| } |
| btn.disabled = false; |
| document.getElementById('termInput').value = ''; |
| }) |
| .catch(function(e) { |
| appendTerm('Connection error: ' + e.message, 'line-error'); |
| statusEl.textContent = 'ERROR'; |
| statusEl.className = 'terminal-status error'; |
| btn.disabled = false; |
| showToast('Unreachable', 'error'); |
| }); |
| } |
| |
| document.getElementById('runBtn').addEventListener('click', runCommand); |
| document.getElementById('termInput').addEventListener('keydown', function(e) { if (e.key === 'Enter') runCommand(); }); |
| |
| |
| document.querySelectorAll('.qcmd').forEach(function(el) { |
| el.addEventListener('click', function() { setCmd(el.dataset.cmd); }); |
| }); |
| |
| |
| document.getElementById('sendBcastBtn').addEventListener('click', function() { |
| var msg = document.getElementById('bcastMsg').value.trim(); |
| if (!msg) { showToast('Write a message first', 'error'); return; } |
| execCmd('/broadcast ' + msg, function(ok) { |
| if (ok) { document.getElementById('bcastMsg').value = ''; showToast('Broadcast sent!', 'success'); } |
| }); |
| }); |
| |
| document.getElementById('sendNotifyBtn').addEventListener('click', function() { |
| var uid = document.getElementById('notifyUid').value.trim(); |
| var msg = document.getElementById('notifyMsg').value.trim(); |
| if (!uid || !msg) { showToast('Fill all fields', 'error'); return; } |
| execCmd('/notify ' + uid + ' ' + msg, function(ok) { |
| if (ok) showToast('Notification sent!', 'success'); |
| }); |
| }); |
| |
| |
| document.getElementById('banBtn').addEventListener('click', function() { |
| var uid = document.getElementById('banUid').value.trim(); |
| var reason = document.getElementById('banReason').value.trim(); |
| if (!uid || !reason) { showToast('Fill all fields', 'error'); return; } |
| if (!confirm('Ban user ' + uid + '?')) return; |
| execCmd('/ban ' + uid + ' ' + reason, function(ok) { |
| if (ok) showToast('User banned.', 'success'); |
| }); |
| }); |
| |
| |
| document.getElementById('marketBtn').addEventListener('click', function() { |
| showToast('Fetching market data... (30-45s)', 'success'); |
| fetch('/admin/market-now', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ adminKey: ADMIN_KEY }) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| if (d.success) showToast('Market report initiated!', 'success'); |
| else showToast(d.error || 'Failed', 'error'); |
| }) |
| .catch(function() { showToast('Server error', 'error'); }); |
| }); |
| |
| |
| function execCmd(cmd, cb) { |
| fetch('/api/admin/command', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ adminKey: ADMIN_KEY, command: cmd }) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| addHistory(cmd, d.success); |
| if (!d.success) showToast(d.error || 'Failed', 'error'); |
| if (cb) cb(d.success); |
| }) |
| .catch(function() { showToast('Unreachable', 'error'); if (cb) cb(false); }); |
| } |
| |
| function addHistory(cmd, ok) { |
| history_log.unshift({ cmd: cmd, ok: ok, time: new Date().toLocaleTimeString('en-US', { hour12: false }) }); |
| if (history_log.length > 50) history_log.pop(); |
| renderHistory(); |
| } |
| |
| function renderHistory() { |
| var el = document.getElementById('historyList'); |
| if (!history_log.length) return; |
| var html = ''; |
| for (var i = 0; i < history_log.length; i++) { |
| var h = history_log[i]; |
| html += '<div class="history-item">'; |
| html += '<span class="h-time">' + h.time + '</span>'; |
| html += '<span class="h-cmd">' + h.cmd + '</span>'; |
| html += '<span>' + (h.ok ? '✅' : '❌') + '</span>'; |
| html += '</div>'; |
| } |
| el.innerHTML = html; |
| } |
| |
| function showToast(msg, type) { |
| var el = document.getElementById('toast'); |
| el.textContent = (type === 'success' ? 'β ' : 'β ') + msg; |
| el.className = 'show t-' + type; |
| clearTimeout(toastTimer); |
| toastTimer = setTimeout(function() { el.className = ''; }, 3000); |
| } |
| |
| document.getElementById('sendEmailBtn').addEventListener('click', function() { |
| var subject = document.getElementById('emailSubject').value.trim(); |
| var body = document.getElementById('emailBody').value.trim(); |
| var target = document.getElementById('emailTarget').value.trim(); |
| var logo = document.getElementById('emailLogo').value.trim(); |
| var btnText = document.getElementById('emailBtnText').value.trim(); |
| var btnUrl = document.getElementById('emailBtnUrl').value.trim(); |
| var pdfUrl = document.getElementById('emailPdfUrl').value.trim(); |
| var accent = document.getElementById('emailAccent').value.trim(); |
| var statusEl = document.getElementById('emailStatus'); |
| if (!subject || !body) { showToast('Fill subject and message', 'error'); return; } |
| if (!confirm(target ? 'Send to ' + target + '?' : 'Send to ALL ' + recipients_count + ' users?')) return; |
| statusEl.textContent = 'Sending...'; |
| document.getElementById('sendEmailBtn').disabled = true; |
| var payload = { adminKey: ADMIN_KEY, subject: subject, body: body }; |
| if (target) payload.targetEmail = target; |
| if (logo) payload.logoUrl = logo; |
| if (btnText) payload.buttonText = btnText; |
| if (btnUrl) payload.buttonUrl = btnUrl; |
| if (pdfUrl) payload.pdfUrl = pdfUrl; |
| if (accent) payload.accentColor = accent; |
| fetch('/admin/send-email', { |
| method: 'POST', headers: {'Content-Type':'application/json'}, |
| body: JSON.stringify(payload) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| document.getElementById('sendEmailBtn').disabled = false; |
| if (d.success) { |
| statusEl.textContent = 'Sent: ' + d.sent + ' | Failed: ' + d.failed + ' | Total: ' + d.total; |
| showToast('Email sent!', 'success'); |
| } else { |
| statusEl.textContent = 'Error: ' + (d.error || 'Unknown'); |
| showToast(d.error || 'Failed', 'error'); |
| } |
| }) |
| .catch(function() { |
| document.getElementById('sendEmailBtn').disabled = false; |
| statusEl.textContent = 'Connection error'; |
| showToast('Server unreachable', 'error'); |
| }); |
| }); |
| |
| document.getElementById('sweepBtn').addEventListener('click', function() { |
| if (!confirm('Run orphaned data sweep? This will permanently delete records for users no longer in Firebase Auth.')) return; |
| |
| var btn = document.getElementById('sweepBtn'); |
| var statusEl = document.getElementById('sweepStatus'); |
| var panel = document.getElementById('sweepResultPanel'); |
| var termOut = document.getElementById('sweepOutput'); |
| var termSt = document.getElementById('sweepTermStatus'); |
| |
| btn.disabled = true; |
| statusEl.textContent = 'Sweeping⦠this may take a moment.'; |
| statusEl.style.color = 'var(--warn)'; |
| termSt.textContent = 'RUNNING'; |
| termSt.className = 'terminal-status running'; |
| panel.style.display = 'block'; |
| termOut.textContent = 'Paginating Firebase Auth users...\nFetching titan_ghosts collection...'; |
| |
| fetch('/api/admin/command', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ adminKey: ADMIN_KEY, command: '/sweep-orphans' }) |
| }) |
| .then(function(r) { return r.json(); }) |
| .then(function(d) { |
| btn.disabled = false; |
| |
| if (d.success) { |
| var det = d.details || {}; |
| var orphans = det.orphansFound !== undefined ? det.orphansFound : '?'; |
| var docs = det.docsDeleted !== undefined ? det.docsDeleted : '?'; |
| var batches = det.batchesCommitted !== undefined ? det.batchesCommitted : '?'; |
| var colls = det.collectionsSwept ? det.collectionsSwept.join(', ') : 'β'; |
| var uids = det.orphanedUids && det.orphanedUids.length ? det.orphanedUids.join('\n ') : 'none'; |
| var now = new Date().toLocaleTimeString('en-US', { hour12: false }); |
| |
| var lines = [ |
| 'β
' + (d.message || 'Sweep complete.'), |
| 'βββββββββββββββββββββββββββββββββ', |
| 'Orphaned UIDs : ' + orphans, |
| 'Docs deleted : ' + docs, |
| 'Batches : ' + batches, |
| 'Collections : ' + colls, |
| 'βββββββββββββββββββββββββββββββββ' |
| ]; |
| if (orphans > 0) { |
| lines.push('Orphaned UIDs (masked):'); |
| lines.push(' ' + uids); |
| } |
| lines.push('Completed at : ' + now); |
| |
| termOut.textContent = lines.join('\n'); |
| termSt.textContent = 'DONE'; |
| termSt.className = 'terminal-status ok'; |
| |
| document.getElementById('sw-orphans').textContent = orphans; |
| document.getElementById('sw-docs').textContent = docs; |
| document.getElementById('sw-batches').textContent = batches; |
| document.getElementById('sw-status').textContent = orphans === 0 ? 'CLEAN β
' : 'SWEPT β
'; |
| document.getElementById('sw-status').style.color = orphans === 0 ? 'var(--success)' : 'var(--warn)'; |
| document.getElementById('sw-time').textContent = 'at ' + now; |
| |
| statusEl.textContent = orphans === 0 ? 'β Database is clean.' : 'β Swept ' + orphans + ' orphan(s).'; |
| statusEl.style.color = 'var(--success)'; |
| addHistory('/sweep-orphans', true); |
| showToast(orphans === 0 ? 'DB is clean!' : 'Swept ' + orphans + ' orphan(s)!', 'success'); |
| } else { |
| termOut.textContent = 'β Error: ' + (d.error || 'Unknown'); |
| termSt.textContent = 'ERROR'; |
| termSt.className = 'terminal-status error'; |
| statusEl.textContent = 'Failed: ' + (d.error || 'Unknown'); |
| statusEl.style.color = 'var(--danger)'; |
| addHistory('/sweep-orphans', false); |
| showToast(d.error || 'Sweep failed', 'error'); |
| } |
| }) |
| .catch(function(e) { |
| btn.disabled = false; |
| termOut.textContent = 'β Connection error: ' + e.message; |
| termSt.textContent = 'ERROR'; |
| termSt.className = 'terminal-status error'; |
| statusEl.textContent = 'Server unreachable'; |
| statusEl.style.color = 'var(--danger)'; |
| showToast('Server unreachable', 'error'); |
| }); |
| }); |
| |
| var recipients_count = '?'; |
| </script> |
| </body> |
| </html> |