Files
BitMonitor/cmd/server/templates/index.html
T

1656 lines
57 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Hardware Monitor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet">
<style>
:root {
--bg: #0a0d13;
--card: #111520;
--sec: #161b28;
--border: #1e253a;
--border-l: #28304a;
--accent: #00e5a0;
--warn: #ffb020;
--danger: #ff4060;
--info: #4090ff;
--text: #e2e6ef;
--dim: #5a627a;
--dim-bar: #1e2336;
--radius: 14px;
--sec-r: 10px;
--sec-pad: 14px 16px;
--gap: 10px;
--font: 'Outfit', sans-serif;
--mono: 'JetBrains Mono', monospace;
--circ: 263.89;
}
*, *::before, *::after {
margin: 0;
padding: 0;
box-sizing: border-box
}
body {
font-family: var(--font);
background: var(--bg);
color: var(--text);
min-height: 100vh;
line-height: 1.5
}
body::before {
content: '';
position: fixed;
inset: 0;
pointer-events: none;
z-index: 0;
background: radial-gradient(ellipse at 10% 50%, rgba(0, 229, 160, .03) 0%, transparent 55%),
radial-gradient(ellipse at 90% 10%, rgba(64, 144, 255, .02) 0%, transparent 45%)
}
/* ═══ Login ═══ */
.login-wrap {
position: fixed;
inset: 0;
z-index: 200;
display: flex;
align-items: center;
justify-content: center;
background: rgba(10, 13, 19, .97);
backdrop-filter: blur(30px)
}
.login-box {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 40px 36px;
width: 380px;
max-width: 90vw;
animation: fadeIn .4s ease
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(16px)
}
to {
opacity: 1;
transform: none
}
}
.login-title {
font-size: 14px;
font-weight: 700;
letter-spacing: 2.5px;
color: var(--accent);
text-align: center;
margin-bottom: 4px
}
.login-sub {
font-size: 13px;
color: var(--dim);
text-align: center;
margin-bottom: 28px
}
.login-label {
font-size: 12px;
color: var(--dim);
margin-bottom: 6px;
display: block
}
.login-input {
width: 100%;
padding: 10px 14px;
background: var(--sec);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-family: var(--mono);
font-size: 13px;
outline: none;
transition: border-color .2s
}
.login-input:focus {
border-color: var(--accent)
}
.login-input::placeholder {
color: var(--dim)
}
.login-field + .login-field {
margin-top: 16px
}
.login-btn {
width: 100%;
padding: 11px 0;
margin-top: 24px;
background: var(--accent);
color: var(--bg);
border: 0;
border-radius: 8px;
font-family: var(--font);
font-size: 13px;
font-weight: 600;
letter-spacing: 1.5px;
cursor: pointer;
transition: opacity .2s
}
.login-btn:hover {
opacity: .85
}
.login-btn:disabled {
opacity: .4;
cursor: not-allowed
}
.login-err {
color: var(--danger);
font-size: 12px;
text-align: center;
margin-top: 14px;
min-height: 18px
}
.login-conn {
color: var(--dim);
font-size: 11px;
text-align: center;
margin-top: 12px
}
/* ═══ Header ═══ */
.header {
position: sticky;
top: 0;
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 32px;
height: 52px;
background: rgba(10, 13, 19, .92);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border)
}
.header h1 {
font-size: 14px;
font-weight: 700;
letter-spacing: 2.5px;
color: var(--accent)
}
.hmid {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: var(--dim)
}
.dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
transition: background .3s, box-shadow .3s
}
.dot.on {
background: var(--accent);
box-shadow: 0 0 6px var(--accent)
}
.dot.off {
background: var(--danger);
box-shadow: 0 0 6px var(--danger)
}
.sep {
color: var(--border-l)
}
#clock {
font-family: var(--mono);
font-size: 12px;
color: var(--dim)
}
/* ═══ Dashboard ═══ */
.dashboard {
position: relative;
z-index: 1;
display: flex;
gap: 16px;
padding: 20px 28px 0
}
.dash-card {
flex: 1;
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 16px 20px
}
.dash-title {
font-size: 10px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 12px
}
.dash-body {
display: flex;
align-items: center;
gap: 20px
}
.dash-donut {
width: 72px;
height: 72px;
flex-shrink: 0
}
.dash-d-track {
fill: none;
stroke: var(--dim-bar);
stroke-width: 10
}
.dash-d-fill {
fill: none;
stroke-width: 10;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset .6s ease, stroke .3s
}
.dash-d-val {
font-family: var(--mono);
font-size: 14px;
font-weight: 700;
fill: var(--text)
}
.dash-stats {
display: flex;
gap: 18px
}
.dash-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px
}
.dash-num {
font-family: var(--mono);
font-size: 20px;
font-weight: 700
}
.dash-num.on {
color: var(--accent)
}
.dash-num.off {
color: var(--danger)
}
.dash-label {
font-size: 10px;
color: var(--dim)
}
@media (max-width: 700px) {
.dashboard {
flex-direction: column;
padding: 12px
}
}
/* ═══ Empty ═══ */
.empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 50vh;
gap: 16px;
color: var(--dim);
position: relative;
z-index: 1
}
.pbox {
position: relative;
width: 72px;
height: 72px
}
.ring {
position: absolute;
inset: 0;
border: 2px solid var(--accent);
border-radius: 50%;
animation: pulse 2.4s ease-out infinite
}
.ring:nth-child(2) {
animation-delay: .7s
}
.ring:nth-child(3) {
animation-delay: 1.4s
}
.pdot {
position: absolute;
top: 50%;
left: 50%;
width: 10px;
height: 10px;
margin: -5px;
background: var(--accent);
border-radius: 50%
}
@keyframes pulse {
0% {
transform: scale(.35);
opacity: 1
}
100% {
transform: scale(2.4);
opacity: 0
}
}
/* ═══ Grid — full width adaptive ═══ */
.grid {
position: relative;
z-index: 1;
display: grid;
grid-template-columns:repeat(auto-fill, minmax(760px, 1fr));
align-items: start;
gap: 14px;
padding: 16px 20px 36px
}
/* ═══ Card ═══ */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: opacity .3s, border-color .25s, box-shadow .25s
}
.card:hover {
border-color: var(--border-l);
box-shadow: 0 4px 32px rgba(0, 0, 0, .25)
}
.card.offline {
opacity: .55
}
.card.offline:hover {
opacity: .75
}
.card.birth {
animation: cardIn .4s ease both
}
@keyframes cardIn {
from {
opacity: 0;
transform: translateY(12px)
}
to {
opacity: 1;
transform: none
}
}
.card-hd {
padding: 8px 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px
}
.card-id {
display: flex;
align-items: center;
gap: 6px;
min-width: 0
}
.card-id h3 {
font-size: 12px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis
}
.card-tm {
font-size: 9px;
color: var(--dim);
font-family: var(--mono);
flex-shrink: 0
}
.card-sub {
padding: 1px 12px 0;
font-size: 9px;
color: var(--dim)
}
.badge {
font-size: 7px;
font-weight: 700;
letter-spacing: .8px;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 10px;
flex-shrink: 0;
transition: background .3s, color .3s
}
.badge-on {
background: rgba(0, 229, 160, .12);
color: var(--accent)
}
.badge-off {
background: rgba(255, 64, 96, .1);
color: var(--danger)
}
/* ═══ Card body — horizontal flex ═══ */
.card-bd {
display: flex;
flex-wrap: wrap;
gap: 0;
padding: 5px 0 0
}
.card-col {
flex: 1 1 320px;
min-width: 0;
display: flex;
flex-direction: column;
gap: 3px;
padding: 0 12px 8px;
align-self: stretch
}
.card-left {
border-right: 1px solid var(--border)
}
@media (max-width: 700px) {
.card-bd {
flex-direction: column
}
.card-left {
border-right: none;
border-bottom: 1px solid var(--border);
padding-bottom: 14px
}
}
/* ═══ Section ═══ */
.sec {
background: var(--sec);
border-radius: 6px;
padding: 6px 10px
}
.sec-tt {
font-size: 8px;
font-weight: 600;
letter-spacing: .8px;
text-transform: uppercase;
color: var(--dim);
margin-bottom: 3px
}
.sec-tt .hl {
color: var(--text)
}
.sr {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px
}
.sr .sl {
color: var(--dim);
font-size: 9px;
flex-shrink: 0
}
.sr .sv {
font-family: var(--mono);
font-size: 9px;
font-weight: 500
}
.bar {
height: 5px;
background: var(--dim-bar);
border-radius: 3px;
overflow: hidden
}
.bar .bf {
height: 100%;
border-radius: 3px;
transition: width .5s ease;
min-width: 2px
}
.bar.mini {
height: 3px;
border-radius: 2px
}
.bar.mini .bf {
border-radius: 2px
}
/* ═══ CPU ═══ */
.cpu-top {
display: flex;
gap: 10px;
align-items: center
}
.gauge {
width: 44px;
height: 44px;
flex-shrink: 0
}
.g-track {
fill: none;
stroke: var(--dim-bar);
stroke-width: 8
}
.g-fill {
fill: none;
stroke-width: 8;
stroke-dasharray: var(--circ);
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: center;
transition: stroke-dashoffset .5s ease, stroke .3s
}
.g-val {
font-family: var(--mono);
font-size: 11px;
font-weight: 700;
fill: var(--text)
}
.g-lbl {
font-size: 5px;
fill: var(--dim);
letter-spacing: .6px
}
.cpu-inf {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px
}
.cores {
display: flex;
gap: 1px;
flex-wrap: wrap;
margin-top: 4px
}
.cbar {
flex: 1;
min-width: 4px;
height: 8px;
background: var(--dim-bar);
border-radius: 1px;
position: relative;
overflow: hidden
}
.cfill {
position: absolute;
bottom: 0;
left: 0;
right: 0;
border-radius: 2px;
transition: height .4s ease, background .3s
}
/* ═══ Memory ═══ */
.mem-hd {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 3px
}
.mem-hd .mp {
font-family: var(--mono);
font-size: 12px;
font-weight: 700
}
/* ═══ GPU ═══ */
.gpu-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
gap: 5px
}
.gpu-block {
background: rgba(0, 0, 0, .2);
border-radius: 6px;
padding: 6px;
cursor: pointer;
transition: background .2s, transform .15s
}
.gpu-block:hover {
background: rgba(0, 229, 160, .06);
transform: translateY(-1px)
}
.gb-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px
}
.gb-name {
font-size: 8px;
color: var(--dim);
font-family: var(--mono)
}
.gb-temp {
font-size: 11px;
font-weight: 700;
font-family: var(--mono)
}
.gb-bar {
position: relative;
height: 14px;
background: var(--dim-bar);
border-radius: 3px;
overflow: hidden;
margin-bottom: 3px
}
.gb-fill {
height: 100%;
border-radius: 3px;
transition: width .5s ease
}
.gb-pct {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 8px;
font-weight: 700;
font-family: var(--mono);
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,.6)
}
.gb-info {
display: flex;
justify-content: space-between;
font-size: 7px;
font-family: var(--mono)
}
.gpu-detail-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px 10px
}
.gd-item {
display: flex;
justify-content: space-between;
align-items: center
}
.gd-l {
color: var(--dim);
font-size: 9px
}
.gd-v {
font-family: var(--mono);
font-size: 9px;
font-weight: 500
}
/* ═══ Mining ═══ */
.m-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
margin-bottom: 6px
}
.m-agent {
color: var(--dim);
font-size: 9px;
font-family: var(--mono);
margin-left: auto
}
.m-country {
color: var(--dim);
font-size: 9px;
font-family: var(--mono);
margin-left: auto
}
.m-hr-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px
}
.m-hr-row:last-of-type {
margin-bottom: 0
}
.m-hr-lbl {
width: 22px;
text-align: right;
font-family: var(--mono);
font-size: 8px;
color: var(--dim);
flex-shrink: 0
}
.m-hr-bar {
flex: 1;
height: 6px;
background: var(--dim-bar);
border-radius: 3px;
overflow: hidden
}
.m-hr-fill {
height: 100%;
border-radius: 4px;
transition: width .4s ease
}
.m-hr-val {
min-width: 70px;
text-align: right;
font-family: var(--mono);
font-size: 9px;
font-weight: 500;
flex-shrink: 0
}
.m-shares {
display: flex;
justify-content: space-between;
margin-top: 6px;
font-family: var(--mono);
font-size: 9px
}
.m-sh.valid {
color: var(--accent)
}
.m-sh.stale {
color: var(--warn)
}
.m-sh.invalid {
color: var(--danger)
}
.m-chart {
background: rgba(0, 0, 0, .15);
border-radius: 5px;
padding: 6px 8px;
margin-top: 6px;
overflow: hidden
}
.chart-lbl {
font-size: 8px;
color: var(--dim);
font-family: var(--mono);
margin-bottom: 4px
}
.chart-empty {
color: var(--dim);
font-size: 9px;
text-align: center;
padding: 14px 0
}
/* ═══ Misc ═══ */
.hw-status {
padding: 6px 10px
}
.hw-status-row {
display: flex;
align-items: center;
gap: 8px
}
.hw-status-row .sec-tt {
margin-bottom: 0
}
.sec-div {
grid-column: 1/-1;
display: flex;
align-items: center;
gap: 14px;
padding: 16px 0 2px;
font-size: 11px;
font-weight: 600;
letter-spacing: 1.5px;
text-transform: uppercase;
color: var(--dim)
}
.sec-div::before, .sec-div::after {
content: '';
flex: 1;
height: 1px;
background: linear-gradient(to right, transparent, var(--border), transparent)
}
.off-actions {
display: flex;
justify-content: flex-end;
padding-top: 0
}
.btn-del {
padding: 2px 8px;
border: 1px solid var(--border);
border-radius: 4px;
background: 0;
color: var(--dim);
font-family: var(--font);
font-size: 9px;
cursor: pointer;
transition: all .2s
}
.btn-del:hover {
border-color: var(--danger);
color: var(--danger);
background: rgba(255, 64, 96, .06)
}
::-webkit-scrollbar {
width: 6px
}
::-webkit-scrollbar-track {
background: var(--bg)
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px
}
@media (max-width: 520px) {
.grid {
grid-template-columns:1fr;
padding: 12px;
gap: 14px
}
.header {
padding: 0 14px
}
}
</style>
</head>
<body>
<!-- Login -->
<div class="login-wrap" id="login-wrap">
<div class="login-box">
<div class="login-title">SYSTEM MONITOR</div>
<div class="login-sub">输入设备ID和密码以登录</div>
<form id="login-form" autocomplete="off">
<div class="login-field">
<label class="login-label">设备 ID</label>
<input class="login-input" id="login-id" type="text" placeholder="miner_id" autocomplete="off"
autofocus>
</div>
<div class="login-field">
<label class="login-label">密码</label>
<input class="login-input" id="login-pw" type="password" placeholder="password" autocomplete="off">
</div>
<button class="login-btn" id="login-btn" type="submit">登 录</button>
</form>
<div class="login-err" id="login-err"></div>
<div class="login-conn" id="login-conn"></div>
</div>
</div>
<!-- App -->
<header class="header" id="app-header" style="display:none">
<h1>SYSTEM MONITOR</h1>
<div class="hmid">
<span class="dot off" id="ws-dot"></span>
<span id="ws-txt">连接中...</span>
<span class="sep"></span>
<span id="cnt">0 台设备</span>
</div>
<span id="clock"></span>
</header>
<div class="dashboard" id="dashboard" style="display:none">
<div class="dash-card">
<div class="dash-title">硬件状态</div>
<div class="dash-body">
<svg class="dash-donut" id="hw-donut" viewBox="0 0 100 100"></svg>
<div class="dash-stats">
<div class="dash-stat"><span class="dash-num" id="hw-total">0</span><span class="dash-label">总计</span></div>
<div class="dash-stat"><span class="dash-num on" id="hw-on">0</span><span class="dash-label">在线</span></div>
<div class="dash-stat"><span class="dash-num off" id="hw-off">0</span><span class="dash-label">离线</span></div>
</div>
</div>
</div>
<div class="dash-card">
<div class="dash-title">矿池状态</div>
<div class="dash-body">
<svg class="dash-donut" id="pool-donut" viewBox="0 0 100 100"></svg>
<div class="dash-stats">
<div class="dash-stat"><span class="dash-num" id="pool-total">0</span><span class="dash-label">总计</span></div>
<div class="dash-stat"><span class="dash-num on" id="pool-on">0</span><span class="dash-label">在线</span></div>
<div class="dash-stat"><span class="dash-num off" id="pool-off">0</span><span class="dash-label">离线</span></div>
</div>
</div>
</div>
</div>
<div class="empty" id="empty" style="display:none">
<div class="pbox">
<div class="ring"></div>
<div class="ring"></div>
<div class="ring"></div>
<div class="pdot"></div>
</div>
<h2 style="font-weight:500">等待设备连接</h2>
<p style="font-size:13px">启动客户端程序以开始监控</p>
</div>
<main class="grid" id="grid" style="display:none"></main>
<script>
(function () {
"use strict";
var C = 263.89, devices = {}, cards = {}, sepNode = null, ws, rt, pending = false, authed = false;
function $(id) {
return document.getElementById(id)
}
/* ═══ Utils ═══ */
function esc(s) {
if (s == null) return '';
var d = document.createElement('div');
d.textContent = String(s);
return d.innerHTML
}
function fmtB(b) {
if (!b || b <= 0) return '0 B';
var u = ['B', 'KB', 'MB', 'GB', 'TB'], i = 0, v = b;
while (v >= 1024 && i < u.length - 1) {
v /= 1024;
i++
}
return v.toFixed(1) + ' ' + u[i]
}
function fmtHR(v) {
if (!v || v <= 0) return '0 H/s';
if (v >= 1e15) return (v / 1e15).toFixed(2) + ' PH/s';
if (v >= 1e12) return (v / 1e12).toFixed(2) + ' TH/s';
if (v >= 1e9) return (v / 1e9).toFixed(2) + ' GH/s';
if (v >= 1e6) return (v / 1e6).toFixed(2) + ' MH/s';
if (v >= 1e3) return (v / 1e3).toFixed(2) + ' KH/s';
return v.toFixed(0) + ' H/s'
}
function pC(p) {
return p >= 80 ? 'var(--danger)' : p >= 60 ? 'var(--warn)' : 'var(--accent)'
}
function tT(t) {
if (t == null || t <= 0) return '<span style="color:var(--dim)">N/A</span>';
var c = t >= 85 ? 'var(--danger)' : t >= 70 ? 'var(--warn)' : 'var(--text)';
return '<span style="color:' + c + '">' + t.toFixed(1) + '°C</span>'
}
function ago(iso) {
if (!iso) return '\u2014';
var d = new Date(iso);
if (isNaN(d)) return '\u2014';
var s = (Date.now() - d.getTime()) / 1000;
if (s < 0) return '刚刚';
if (s < 60) return Math.floor(s) + ' 秒前';
if (s < 3600) return Math.floor(s / 60) + ' 分钟前';
if (s < 86400) return Math.floor(s / 3600) + ' 小时前';
return Math.floor(s / 86400) + ' 天前'
}
function fTm(iso) {
if (!iso) return '\u2014';
var d = new Date(iso);
if (isNaN(d)) return '\u2014';
function p(n) {
return String(n).padStart(2, '0')
}
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes())
}
function sr(l, v) {
return '<div class="sr"><span class="sl">' + l + '</span><span class="sv">' + v + '</span></div>'
}
/* ═══ Login ═══ */
var loginForm = $('login-form'), loginIdInput = $('login-id'), loginPwInput = $('login-pw');
var loginBtn = $('login-btn'), loginErr = $('login-err'), loginConn = $('login-conn');
var savedId = localStorage.getItem('mm_id') || '';
var savedPw = localStorage.getItem('mm_pw') || '';
if (savedId) loginIdInput.value = savedId;
if (savedPw) loginPwInput.value = savedPw;
loginForm.addEventListener('submit', function (e) {
e.preventDefault();
loginErr.textContent = '';
var mid = loginIdInput.value.trim(), pw = loginPwInput.value;
if (!mid) {
loginErr.textContent = '请输入设备ID';
return
}
if (!pw) {
loginErr.textContent = '请输入密码';
return
}
loginBtn.disabled = true;
loginBtn.textContent = '登录中...';
loginConn.textContent = '';
localStorage.setItem('mm_id', mid);
localStorage.setItem('mm_pw', pw);
if (!ws || ws.readyState !== WebSocket.OPEN) wsConnect(mid, pw); else sendAuth(mid, pw);
});
function sendAuth(mid, pw) {
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
type: 'auth',
miner_id: mid,
password: pw
}))
}
function loginFailed(msg) {
loginBtn.disabled = false;
loginBtn.textContent = '登 录';
loginErr.textContent = msg || '认证失败';
localStorage.removeItem('mm_pw');
loginPwInput.value = '';
loginPwInput.focus()
}
function loginSuccess() {
authed = true;
localStorage.setItem('mm_id', loginIdInput.value.trim());
localStorage.setItem('mm_pw', loginPwInput.value);
$('login-wrap').style.display = 'none';
$('app-header').style.display = '';
$('dashboard').style.display = '';
$('empty').style.display = '';
$('grid').style.display = '';
}
/* ═══ WebSocket ═══ */
function wsConnect() {
var p = location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(p + '//' + location.host + '/ws/monitor');
ws.onopen = function () {
loginConn.textContent = '';
setTimeout(function () {
if (!authed) sendAuth(loginIdInput.value.trim(), loginPwInput.value)
}, 1500);
};
ws.onmessage = function (e) {
try {
handleMsg(JSON.parse(e.data))
} catch (err) {
console.error(err)
}
};
ws.onclose = function () {
if (authed) {
$('ws-dot').className = 'dot off';
$('ws-txt').textContent = '已断开';
rt = setTimeout(wsConnect, 3000)
} else {
loginConn.textContent = '连接已断开';
loginBtn.disabled = false;
loginBtn.textContent = '登 录'
}
};
ws.onerror = function () {
ws.close()
};
}
/* ═══ Messages ═══ */
function handleMsg(msg) {
switch (msg.type) {
case'auth_required':
loginConn.textContent = '服务已连接,正在认证...';
sendAuth(loginIdInput.value.trim(), loginPwInput.value);
break;
case'auth_ok':
loginSuccess();
break;
case'auth_fail':
loginFailed(msg.msg || '设备ID或密码错误');
break;
case'snapshot':
var k;
for (k in devices) delete devices[k];
if (msg.devices) for (k in msg.devices) devices[k] = msg.devices[k];
for (k in cards) {
cards[k].remove();
delete cards[k]
}
if (sepNode) {
sepNode.remove();
sepNode = null
}
if (authed) {
$('ws-dot').className = 'dot on';
$('ws-txt').textContent = '已连接'
}
scheduleRender();
break;
case'device':
var prev = devices[msg.machine];
devices[msg.machine] = msg.device;
if (prev && prev.hash_history && (!msg.device.hash_history || !msg.device.hash_history.length)) devices[msg.machine].hash_history = prev.hash_history;
scheduleRender();
break;
case'mining':
var mi = msg.mining || {};
var oAt = mi.opened_at > 0 ? new Date(mi.opened_at).toISOString() : null;
var lAc = mi.last_active > 0 ? new Date(mi.last_active).toISOString() : null;
if (devices[msg.machine]) {
devices[msg.machine].mining = mi;
if (msg.hash_history) devices[msg.machine].hash_history = msg.hash_history;
var dInf = devices[msg.machine].info || {};
if (!dInf.first_seen && oAt) dInf.first_seen = oAt;
if (!dInf.last_seen && lAc) dInf.last_seen = lAc;
devices[msg.machine].info = dInf;
} else devices[msg.machine] = {
info: {machine_name: msg.machine, first_seen: oAt, last_seen: lAc},
online: false,
mining: mi,
hash_history: msg.hash_history
};
scheduleRender();
break;
case'remove':
if (cards[msg.machine]) {
cards[msg.machine].remove();
delete cards[msg.machine]
}
delete devices[msg.machine];
scheduleRender();
break;
}
}
/* ═══ Render ═══ */
function scheduleRender() {
if (pending) return;
pending = true;
requestAnimationFrame(function () {
pending = false;
render()
})
}
function renderDashboard(hwOn, hwOff, poolOn, poolOff) {
$('dashboard').style.display = '';
$('hw-total').textContent = hwOn + hwOff;
$('hw-on').textContent = hwOn;
$('hw-off').textContent = hwOff;
$('pool-total').textContent = poolOn + poolOff;
$('pool-on').textContent = poolOn;
$('pool-off').textContent = poolOff;
donut('hw-donut', hwOn, hwOff);
donut('pool-donut', poolOn, poolOff);
}
function donut(id, a, b) {
var el = $(id), C = 263.89, total = a + b;
var pctA = total > 0 ? a / total : 0;
var pctB = total > 0 ? b / total : 0;
var offA = C * (1 - pctA);
var offB = C * (1 - pctA - pctB);
var val = total > 0 ? Math.round(pctA * 100) : 0;
el.innerHTML = '<circle class="dash-d-track" cx="50" cy="50" r="42"/>'
+ '<circle class="dash-d-fill" cx="50" cy="50" r="42" style="stroke-dasharray:' + C + ';stroke-dashoffset:' + offA.toFixed(1) + ';stroke:var(--accent)"/>'
+ '<circle class="dash-d-fill" cx="50" cy="50" r="42" style="stroke-dasharray:' + C + ';stroke-dashoffset:' + offB.toFixed(1) + ';stroke:var(--danger)"/>'
+ '<text class="dash-d-val" x="50" y="48" text-anchor="middle" dominant-baseline="middle">' + val + '%</text>'
+ '<text x="50" y="62" text-anchor="middle" fill="var(--dim)" font-size="8" font-family="var(--mono)">在线率</text>';
}
function render() {
if (!authed) return;
var names = Object.keys(devices), on = [], off = [], i, n;
var poolOn = 0, poolOff = 0;
for (i = 0; i < names.length; i++) {
n = names[i];
(devices[n] && devices[n].online ? on : off).push(n)
}
on.sort();
off.sort(function (a, b) {
var ta = (devices[b] && devices[b].info && devices[b].info.last_seen) || '';
var tb = (devices[a] && devices[a].info && devices[a].info.last_seen) || '';
return ta.localeCompare(tb)
});
$('cnt').textContent = names.length ? names.length + ' 台设备' + (on.length ? ' · ' + on.length + ' 在线' : '') : '0 台设备';
if (!names.length) {
$('empty').style.display = 'flex';
$('grid').style.display = 'none';
$('dashboard').style.display = 'none';
return
}
$('empty').style.display = 'none';
$('grid').style.display = 'grid';
for (i = 0; i < names.length; i++) {
n = names[i];
var d = devices[n];
if (d && d.mining && d.mining.status === 'online') poolOn++; else poolOff++
}
renderDashboard(on.length, off.length, poolOn, poolOff);
var grid = $('grid'), liveSet = {};
for (i = 0; i < on.length; i++) liveSet[on[i]] = true;
for (i = 0; i < off.length; i++) liveSet[off[i]] = true;
for (n in cards) if (!liveSet[n]) {
cards[n].remove();
delete cards[n]
}
for (i = 0; i < on.length; i++) placeCard(grid, on[i], true);
if (on.length > 0 && off.length > 0) {
if (!sepNode) {
sepNode = document.createElement('div');
sepNode.className = 'sec-div'
}
sepNode.textContent = off.length + ' 台离线设备';
grid.appendChild(sepNode);
} else if (sepNode) {
sepNode.remove();
sepNode = null
}
for (i = 0; i < off.length; i++) placeCard(grid, off[i], false);
}
/* ═══ Card ═══ */
function placeCard(grid, name, isOn) {
var e = devices[name];
if (!e) return;
var el = cards[name];
if (!el) {
el = document.createElement('div');
el.className = 'card birth' + (isOn ? '' : ' offline');
el.innerHTML = buildShell(name, e, isOn);
el.addEventListener('animationend', function () {
el.classList.remove('birth')
}, {once: true});
cards[name] = el;
}
el.classList.toggle('offline', !isOn);
var tm = el.querySelector('.card-tm');
var info = e.info || {};
if (tm) tm.textContent = ago(info.last_seen);
var hwDot = el.querySelector('.hw-dot');
if (hwDot) hwDot.className = 'dot ' + (isOn ? 'on' : 'off');
var hwBadge = el.querySelector('.hw-badge');
if (hwBadge) {
hwBadge.className = 'badge ' + (isOn ? 'badge-on' : 'badge-off');
hwBadge.textContent = isOn ? '在线' : '离线'
}
var mining = e.mining || {};
var mOn = mining.status === 'online';
var mDot = el.querySelector('.m-dot');
if (mDot) mDot.className = 'dot ' + (mOn ? 'on' : 'off');
var mBadge = el.querySelector('.m-badge');
if (mBadge) {
mBadge.className = 'badge ' + (mOn ? 'badge-on' : 'badge-off');
mBadge.textContent = mOn ? '在线' : '离线'
}
var live = e.live || {};
var left = el.querySelector('.card-left');
if (left) left.innerHTML = buildLeft(live.cpu || {}, live.memory || {}, live.gpus || [], info.gpus, info, isOn);
var right = el.querySelector('.card-right');
if (right) right.innerHTML = buildRight(name, e.mining, e.hash_history, info, isOn);
grid.appendChild(el);
}
function buildShell(name, e, isOn) {
var info = e.info || {};
var mb = info.motherboard || {};
var h = '<div class="card-hd"><h3>' + esc(name) + '</h3><span class="card-tm">' + ago(info.last_seen) + '</span></div>';
if (mb.manufacturer || mb.product) h += '<div class="card-sub">' + esc(mb.manufacturer) + ' ' + esc(mb.product) + '</div>';
h += '<div class="card-bd"><div class="card-col card-left"></div><div class="card-col card-right"></div></div>';
return h;
}
/* ═══ Left Column: Hardware Status + CPU + Memory + GPU ═══ */
function buildLeft(cpu, mem, gpus, infoGPUs, info, isOn) {
var h = '<div class="sec hw-status"><div class="hw-status-row"><span class="sec-tt" style="margin-bottom:0">硬件状态</span><span class="dot ' + (isOn ? 'on' : 'off') + ' hw-dot"></span><span class="badge ' + (isOn ? 'badge-on' : 'badge-off') + ' hw-badge">' + (isOn ? '在线' : '离线') + '</span></div></div>';
return h + buildCPU(cpu, info, isOn) + buildMem(mem, isOn) + buildGPU(gpus, infoGPUs, isOn);
}
function buildCPU(cpu, info, isOn) {
var model = cpu.model || info.cpu_model || 'Unknown';
var pc = cpu.cores_physical || info.cpu_cores_physical || '?';
var lc = cpu.cores_logical || info.cpu_cores_logical || '?';
var freq = cpu.freq_current_mhz || 0, pct = cpu.usage_total || 0, cores = cpu.usage_per_core || [],
tmp = cpu.temperature;
var h = '<div class="sec"><div class="sec-tt">CPU \u00B7 <span class="hl">' + esc(model) + '</span></div>';
if (isOn && (pct > 0 || cores.length > 0)) {
var uo = C * (1 - pct / 100);
h += '<div class="cpu-top"><svg class="gauge" viewBox="0 0 100 100"><circle class="g-track" cx="50" cy="50" r="42"/><circle class="g-fill" cx="50" cy="50" r="42" style="stroke-dashoffset:' + uo.toFixed(1) + ';stroke:' + pC(pct) + '"/><text class="g-val" x="50" y="46" text-anchor="middle" dominant-baseline="middle">' + pct.toFixed(1) + '%</text><text class="g-lbl" x="50" y="62" text-anchor="middle">CPU</text></svg><div class="cpu-inf">' + sr('核心/线程', pc + ' / ' + lc) + sr('频率', Math.round(freq) + ' MHz') + sr('温度', tT(tmp)) + '</div></div>';
if (cores.length) {
h += '<div class="cores">';
for (var i = 0; i < cores.length; i++) h += '<div class="cbar" title="Core ' + i + ': ' + cores[i].toFixed(1) + '%"><div class="cfill" style="height:' + cores[i] + '%;background:' + pC(cores[i]) + '"></div></div>';
h += '</div>'
}
} else {
h += sr('核心/线程', pc + ' / ' + lc);
if (freq > 0) h += sr('频率', Math.round(freq) + ' MHz')
}
return h + '</div>';
}
function buildMem(mem, isOn) {
var h = '<div class="sec"><div class="sec-tt">MEMORY</div>';
if (isOn && mem.total_bytes) {
var pct = mem.percent || 0;
h += '<div class="mem-hd"><span style="font-size:10px">' + fmtB(mem.used_bytes) + ' / ' + fmtB(mem.total_bytes) + '</span><span class="mp" style="color:' + pC(pct) + '">' + pct.toFixed(1) + '%</span></div><div class="bar"><div class="bf" style="width:' + pct + '%;background:' + pC(pct) + '"></div></div>'
} else if (mem.total_bytes) h += '<div style="font-size:10px;color:var(--dim)">总计 ' + fmtB(mem.total_bytes) + '</div>';
else h += '<div style="font-size:10px;color:var(--dim)">无数据</div>';
return h + '</div>';
}
function buildGPU(gpus, infoGPUs, isOn) {
var list = isOn ? gpus : (infoGPUs || []);
if (!list.length) return '<div class="sec"><div class="sec-tt">GPU</div><div style="color:var(--dim);font-size:9px">未检测到</div></div>';
var h = '<div class="sec"><div class="sec-tt">GPU \u00B7 <span class="hl">' + list.length + ' \u5F20</span></div>';
h += '<div class="gpu-grid">';
for (var i = 0; i < list.length; i++) {
var g = list[i], gl = g.load_percent || 0, tmp = g.temperature || 0,
vt = g.vram_total || g.vram_total_mb || 0,
vu = g.vram_used || g.vram_used_mb || 0;
var vp = vt > 0 ? (vu / vt * 100) : 0;
var tc = tmp >= 85 ? 'var(--danger)' : tmp >= 70 ? 'var(--warn)' : 'var(--text)';
h += '<div class="gpu-block" onclick="window.__gpuDetail(' + i + ')" title="点击查看详细信息">';
h += '<div class="gb-top"><span class="gb-name">GPU' + (i + 1) + '</span><span class="gb-temp" style="color:' + tc + '">' + (tmp > 0 ? tmp.toFixed(0) + '\u00B0' : '\u2014') + '</span></div>';
h += '<div class="gb-bar"><div class="gb-fill" style="width:' + gl.toFixed(0) + '%;background:' + pC(gl) + '"></div><span class="gb-pct">' + gl.toFixed(0) + '%</span></div>';
h += '<div class="gb-info"><span style="color:' + pC(vp) + '">' + vp.toFixed(0) + '%</span><span style="color:var(--dim)">' + (vt > 0 ? (vu / 1024).toFixed(1) + '/' + (vt / 1024).toFixed(0) + 'G' : '\u2014') + '</span></div>';
h += '</div>';
}
h += '</div>';
h += '<div class="gpu-detail" id="gpu-detail" style="display:none"></div>';
h += '</div>';
return h + '</div>';
}
window.__gpuDetail = function (idx) {
var el = document.getElementById('gpu-detail');
if (!el) return;
if (el.dataset.idx == idx && el.style.display !== 'none') {
el.style.display = 'none';
return;
}
el.dataset.idx = idx;
var names = Object.keys(devices), name = names[0], e = devices[name];
if (!e) return;
var live = e.live || {}, gpus = live.gpus || [], g = gpus[idx];
if (!g) return;
var gl = g.load_percent || 0, tmp = g.temperature || 0,
pwr = g.power || g.power_watts || 0, cc = g.core_clock || g.core_clock_mhz || 0,
mc = g.mem_clock || g.mem_clock_mhz || 0, vt = g.vram_total || g.vram_total_mb || 0,
vu = g.vram_used || g.vram_used_mb || 0, fan = g.fan_speed || 0;
var h = '<div class="sec" style="margin-top:6px"><div class="sec-tt">GPU' + (idx + 1) + ' \u00B7 <span class="hl">' + esc(g.name || 'N/A') + '</span></div>';
h += '<div class="gpu-detail-grid">';
h += '<div class="gd-item"><span class="gd-l">\u8D1F\u8F7D</span><span class="gd-v" style="color:' + pC(gl) + '">' + gl.toFixed(1) + '%</span></div>';
h += '<div class="gd-item"><span class="gd-l">\u6E29\u5EA6</span><span class="gd-v">' + tT(tmp > 0 ? tmp : null) + '</span></div>';
h += '<div class="gd-item"><span class="gd-l">\u529F\u8017</span><span class="gd-v">' + Math.round(pwr) + ' W</span></div>';
h += '<div class="gd-item"><span class="gd-l">\u6838\u5FC3\u9891\u7387</span><span class="gd-v">' + Math.round(cc) + ' MHz</span></div>';
h += '<div class="gd-item"><span class="gd-l">\u663E\u5B58\u9891\u7387</span><span class="gd-v">' + Math.round(mc) + ' MHz</span></div>';
h += '<div class="gd-item"><span class="gd-l">\u98CE\u6247</span><span class="gd-v">' + (fan > 0 ? Math.round(fan) + '%' : '\u2014') + '</span></div>';
h += '</div>';
if (vt > 0) {
var vp = vu / vt * 100;
h += '<div style="margin-top:6px"><div class="sr" style="margin-bottom:3px"><span class="sl">\u663E\u5B58</span><span class="sv">' + (vu / 1024).toFixed(1) + ' / ' + (vt / 1024).toFixed(1) + ' GB</span></div><div class="bar"><div class="bf" style="width:' + vp + '%;background:' + pC(vp) + '"></div></div></div>';
}
h += '</div>';
el.innerHTML = h;
el.style.display = '';
};
/* ═══ Right Column: Mining Status + Mining ═══ */
function buildRight(name, mining, hist, info, isOn) {
var h = '';
if (mining) {
var mOn = mining.status === 'online';
h += '<div class="sec hw-status"><div class="hw-status-row"><span class="sec-tt" style="margin-bottom:0">矿池状态</span><span class="dot ' + (mOn ? 'on' : 'off') + ' m-dot"></span><span class="badge ' + (mOn ? 'badge-on' : 'badge-off') + ' m-badge">' + (mOn ? '在线' : '离线') + '</span></div></div>';
h += buildMining(mining, hist);
} else {
h += '<div class="sec hw-status"><div class="hw-status-row"><span class="sec-tt" style="margin-bottom:0">矿池状态</span><span class="dot off m-dot"></span><span class="badge badge-off m-badge">离线</span></div></div>';
}
if (!isOn) {
h += '<div class="off-actions"><button class="btn-del" onclick="window.__del(\'' + esc(name) + '\')">删除记录</button></div>';
}
return h;
}
function buildMining(m, hist) {
if (!m) return '';
var sOn = m.status === 'online';
var maxHr = Math.max(m.hashrate_30m || 0, m.hashrate_3h || 0, m.hashrate_24h || 0) || 1;
var h = '<div class="sec"><div class="sec-tt">MINING \u00B7 <span class="hl">' + (m.coin || '').toUpperCase() + '</span></div>';
/* 状态 */
h += '<div class="m-status"><span class="dot ' + (sOn ? 'on' : 'off') + '"></span>'
+ '<span style="color:' + (sOn ? 'var(--accent)' : 'var(--danger)') + ';font-weight:500">' + (sOn ? '\u5728\u7EBF' : '\u79BB\u7EBF') + '</span>';
if (m.agent) h += '<span class="m-agent">' + esc(m.agent) + '</span>';
if (m.country) h += '<span class="m-country">' + esc(m.country) + '</span>';
h += '</div>';
/* 算力条 */
h += hrBar('30m', m.hashrate_30m || 0, maxHr) + hrBar('3h', m.hashrate_3h || 0, maxHr) + hrBar('24h', m.hashrate_24h || 0, maxHr);
/* 时间信息 */
h += '<div class="m-times" style="display:flex;gap:12px;margin-top:6px;font-size:9px">';
h += '<span><span style="color:var(--dim)">\u6D3B\u8DC3 </span><span style="font-family:var(--mono)">' + fmtMsTs(m.last_active) + '</span></span>';
h += '<span><span style="color:var(--dim)">\u5F00\u59CB </span><span style="font-family:var(--mono)">' + fmtMsTs(m.opened_at) + '</span></span>';
if (m.last_active > 0 && m.opened_at > 0) {
var durH = ((m.last_active - m.opened_at) / 3600000).toFixed(1);
h += '<span><span style="color:var(--dim)">\u8FD0\u884C </span><span style="font-family:var(--mono)">' + durH + ' h</span></span>';
}
h += '</div>';
/* Shares */
h += '<div class="m-shares"><span class="m-sh valid">' + (m.valid || 0) + ' \u6709\u6548</span>'
+ '<span class="m-sh stale">' + (m.stale || 0) + ' \u8FC7\u671F</span>'
+ '<span class="m-sh invalid">' + (m.invalid || 0) + ' \u65E0\u6548</span></div>';
/* 走势图 */
h += '<div class="m-chart"><div class="chart-lbl">\u7B97\u529B\u8D70\u52BF (30m \u5747\u503C)</div>' + buildChart(hist) + '</div>';
return h + '</div>';
}
/* ★ 毫秒时间戳 → 可读文本 */
function fmtMsTs(ms) {
if (!ms || ms <= 0) return '<span style="color:var(--dim)">\u2014</span>';
var d = new Date(ms);
if (isNaN(d)) return '<span style="color:var(--dim)">\u2014</span>';
function p(n) {
return String(n).padStart(2, '0')
}
return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes());
}
function hrBar(l, v, mx) {
var p = Math.min(100, v / mx * 100);
return '<div class="m-hr-row"><span class="m-hr-lbl">' + l + '</span><div class="m-hr-bar"><div class="m-hr-fill" style="width:' + p.toFixed(1) + '%;background:' + pC(p) + '"></div></div><span class="m-hr-val" style="color:' + pC(p) + '">' + fmtHR(v) + '</span></div>'
}
/* ═══ SVG Chart ═══ */
function buildChart(hist) {
if (!hist || hist.length < 2) return '<div class="chart-empty">数据采集中 (' + ((hist && hist.length) || 0) + '/2)...</div>';
var W = 420, H = 56, pad = {t: 8, b: 10, l: 40, r: 6}, cW = W - pad.l - pad.r, cH = H - pad.t - pad.b;
var vals = [], i;
for (i = 0; i < hist.length; i++) vals.push(hist[i].v);
var lo = vals[0], hi = vals[0];
for (i = 1; i < vals.length; i++) {
if (vals[i] < lo) lo = vals[i];
if (vals[i] > hi) hi = vals[i]
}
var rng = hi - lo || 1;
lo -= rng * .1;
hi += rng * .1;
if (hi <= lo) {
lo -= 1;
hi += 1
}
var step = cW / (hist.length - 1), pts = [];
for (i = 0; i < vals.length; i++) pts.push({
x: pad.l + i * step,
y: pad.t + cH * (1 - (vals[i] - lo) / (hi - lo))
});
var line = '';
for (i = 0; i < pts.length; i++) line += (i ? 'L' : 'M') + pts[i].x.toFixed(1) + ',' + pts[i].y.toFixed(1) + ' ';
var area = line + 'L' + pts[pts.length - 1].x.toFixed(1) + ',' + H + ' L' + pts[0].x.toFixed(1) + ',' + H + ' Z';
var gid = 'g' + (Math.random() * 1e6 | 0), last = pts[pts.length - 1];
function fT(d) {
return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0')
}
var t0 = new Date(hist[0].t), tN = new Date(hist[hist.length - 1].t);
var svg = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;height:' + H + 'px;display:block"><defs><linearGradient id="' + gid + '" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#00e5a0" stop-opacity=".2"/><stop offset="100%" stop-color="#00e5a0" stop-opacity=".01"/></linearGradient></defs>';
var gs = [.25, .5, .75];
for (i = 0; i < gs.length; i++) {
var gy = (pad.t + cH * (1 - gs[i])).toFixed(1);
svg += '<line x1="' + pad.l + '" y1="' + gy + '" x2="' + (W - pad.r) + '" y2="' + gy + '" stroke="#1a1f30" stroke-width="1"/>'
}
svg += '<path d="' + area + '" fill="url(#' + gid + ')"/><path d="' + line + '" fill="none" stroke="#00e5a0" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/><circle cx="' + last.x + '" cy="' + last.y + '" r="3" fill="#00e5a0" stroke="#111520" stroke-width="1.5"/>';
svg += '<text x="' + (pad.l - 3) + '" y="' + (pad.t + 2) + '" fill="#5a627a" font-size="7" font-family="JetBrains Mono,monospace" text-anchor="end">' + fmtHR(hi) + '</text>';
svg += '<text x="' + (pad.l - 3) + '" y="' + (H - pad.b + 2) + '" fill="#5a627a" font-size="7" font-family="JetBrains Mono,monospace" text-anchor="end">' + fmtHR(lo) + '</text>';
svg += '<text x="' + pad.l + '" y="' + (H - 1) + '" fill="#5a627a" font-size="7" font-family="JetBrains Mono,monospace">' + fT(t0) + '</text>';
svg += '<text x="' + (W - pad.r) + '" y="' + (H - 1) + '" fill="#5a627a" font-size="7" font-family="JetBrains Mono,monospace" text-anchor="end">' + fT(tN) + '</text></svg>';
return svg;
}
/* ═══ Delete + Clock ═══ */
window.__del = function (n) {
if (!confirm('确定删除 ' + n + ' 的设备记录?')) return;
if (ws && ws.readyState === 1) ws.send(JSON.stringify({action: 'delete', machine: n}))
};
setInterval(function () {
$('clock').textContent = new Date().toLocaleTimeString('zh-CN', {hour12: false});
for (var n in cards) {
var el = cards[n], tm = el.querySelector('.card-tm');
if (tm && devices[n] && devices[n].info) tm.textContent = ago(devices[n].info.last_seen)
}
}, 1000);
wsConnect();
if (savedId && savedPw) {
loginBtn.disabled = true;
loginBtn.textContent = '自动登录中...';
loginConn.textContent = '';
}
})();
</script>
</body>
</html>