feat: mining section rework, GPU filter, chart as full-width row, progress bars separate line, contrast fix
This commit is contained in:
+32
-15
@@ -143,12 +143,16 @@ type GPUInfo struct {
|
|||||||
MemClock float64 `json:"mem_clock_mhz"`
|
MemClock float64 `json:"mem_clock_mhz"`
|
||||||
Power float64 `json:"power"`
|
Power float64 `json:"power"`
|
||||||
PowerLimit float64 `json:"power_limit"`
|
PowerLimit float64 `json:"power_limit"`
|
||||||
|
PowerMax float64 `json:"power_max"`
|
||||||
FanSpeed float64 `json:"fan_speed"`
|
FanSpeed float64 `json:"fan_speed"`
|
||||||
PState string `json:"pstate"`
|
PState string `json:"pstate"`
|
||||||
Persistence string `json:"persistence_mode"`
|
|
||||||
ComputeMode string `json:"compute_mode"`
|
ComputeMode string `json:"compute_mode"`
|
||||||
DriverVersion string `json:"driver_version"`
|
DriverVersion string `json:"driver_version"`
|
||||||
CudaVersion string `json:"cuda_version"`
|
UtilMemory float64 `json:"util_memory"`
|
||||||
|
UtilEncoder float64 `json:"util_encoder"`
|
||||||
|
UtilDecoder float64 `json:"util_decoder"`
|
||||||
|
ClocksMaxGfx float64 `json:"clocks_max_graphics"`
|
||||||
|
ClocksMaxMem float64 `json:"clocks_max_memory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════
|
// ═══════════════════════════════════════════════
|
||||||
@@ -1220,20 +1224,33 @@ func tryNvidiaSMI() []GPUInfo {
|
|||||||
parts[i] = strings.Trim(parts[i], "[]")
|
parts[i] = strings.Trim(parts[i], "[]")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0:index 1:name 2:uuid 3:bus_id 4:driver 5:vbios
|
||||||
|
// 6:temp_gpu 7:temp_mem 8:fan 9:power 10:power_limit
|
||||||
|
// 11:power_default 12:power_min 13:power_max
|
||||||
|
// 14:clk_gfx 15:clk_sm 16:clk_mem 17:clk_max_gfx 18:clk_max_mem
|
||||||
|
// 19:util_gpu 20:util_mem 21:util_enc 22:util_dec
|
||||||
|
// 23:vram_total 24:vram_used 25:vram_free
|
||||||
|
// 26:ecc_corr 27:ecc_uncorr 28:compute_mode 29:pstate
|
||||||
gpus = append(gpus, GPUInfo{
|
gpus = append(gpus, GPUInfo{
|
||||||
Name: cleanStr(parts[1]),
|
Name: cleanStr(parts[1]),
|
||||||
VRAMTotalMB: parseFloat(parts[23]),
|
DriverVersion: cleanStr(parts[4]),
|
||||||
VRAMUsedMB: parseFloat(parts[24]),
|
Temperature: parseFloat(parts[6]),
|
||||||
Temperature: parseFloat(parts[6]),
|
FanSpeed: parseFloat(parts[8]),
|
||||||
LoadPercent: parseFloat(parts[19]),
|
Power: parseFloat(parts[9]),
|
||||||
CoreClock: parseFloat(parts[14]),
|
PowerLimit: parseFloat(parts[10]),
|
||||||
MemClock: parseFloat(parts[16]),
|
PowerMax: parseFloat(parts[13]),
|
||||||
Power: parseFloatSafe(parts[9]),
|
CoreClock: parseFloat(parts[14]),
|
||||||
PowerLimit: parseFloatSafe(parts[10]),
|
MemClock: parseFloat(parts[16]),
|
||||||
FanSpeed: parseFloatSafe(parts[8]),
|
ClocksMaxGfx: parseFloat(parts[17]),
|
||||||
PState: cleanStr(parts[29]),
|
ClocksMaxMem: parseFloat(parts[18]),
|
||||||
ComputeMode: cleanStr(parts[28]),
|
LoadPercent: parseFloat(parts[19]),
|
||||||
DriverVersion: cleanStr(parts[4]),
|
UtilMemory: parseFloat(parts[20]),
|
||||||
|
UtilEncoder: parseFloat(parts[21]),
|
||||||
|
UtilDecoder: parseFloat(parts[22]),
|
||||||
|
VRAMTotalMB: parseFloat(parts[23]),
|
||||||
|
VRAMUsedMB: parseFloat(parts[24]),
|
||||||
|
ComputeMode: cleanStr(parts[28]),
|
||||||
|
PState: cleanStr(parts[29]),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return gpus
|
return gpus
|
||||||
|
|||||||
+65
-15
@@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -374,6 +375,55 @@ func (h *Hub) saveYAML() {
|
|||||||
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
|
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func filterGPURaw(raw json.RawMessage) json.RawMessage {
|
||||||
|
var data map[string]interface{}
|
||||||
|
if json.Unmarshal(raw, &data) != nil {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
gpusRaw, ok := data["gpus"]
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
gpus, ok := gpusRaw.([]interface{})
|
||||||
|
if !ok {
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
var filtered []interface{}
|
||||||
|
for _, g := range gpus {
|
||||||
|
gpu, ok := g.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
gpuName, _ := gpu["name"].(string)
|
||||||
|
if isVirtualGPU(gpuName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, g)
|
||||||
|
}
|
||||||
|
data["gpus"] = filtered
|
||||||
|
out, _ := json.Marshal(data)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVirtualGPU(name string) bool {
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
blacklist := []string{
|
||||||
|
"microsoft",
|
||||||
|
"basic render",
|
||||||
|
"vmware",
|
||||||
|
"virtual",
|
||||||
|
"hyper-v",
|
||||||
|
"parallels",
|
||||||
|
"virtualbox",
|
||||||
|
}
|
||||||
|
for _, v := range blacklist {
|
||||||
|
if strings.Contains(name, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||||
var p AgentPayload
|
var p AgentPayload
|
||||||
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
|
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
|
||||||
@@ -398,7 +448,7 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
|||||||
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
|
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
|
||||||
}
|
}
|
||||||
cp := *rec
|
cp := *rec
|
||||||
h.live[name] = raw
|
h.live[name] = filterGPURaw(raw)
|
||||||
var md *MiningData
|
var md *MiningData
|
||||||
var hist []HashratePoint
|
var hist []HashratePoint
|
||||||
if m, ok := h.miningData[name]; ok {
|
if m, ok := h.miningData[name]; ok {
|
||||||
@@ -416,10 +466,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
|||||||
h.saveYAML()
|
h.saveYAML()
|
||||||
h.broadcast(BrowserMsg{
|
h.broadcast(BrowserMsg{
|
||||||
Type: "device", Machine: name,
|
Type: "device", Machine: name,
|
||||||
Device: &BrowserDevice{Info: &cp, Live: raw, Online: true, Mining: md, History: hist},
|
Device: &BrowserDevice{Info: &cp, Live: filterGPURaw(raw), Online: true, Mining: md, History: hist},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Hub) DeleteDevice(name string) {
|
||||||
|
h.mu.Lock()
|
||||||
|
_, ok := h.records[name]
|
||||||
|
delete(h.records, name)
|
||||||
|
delete(h.live, name)
|
||||||
|
h.mu.Unlock()
|
||||||
|
if ok {
|
||||||
|
h.saveYAML()
|
||||||
|
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
||||||
|
logInfo("delete", "%s", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Hub) MarkOffline(name string) {
|
func (h *Hub) MarkOffline(name string) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
_, had := h.live[name]
|
_, had := h.live[name]
|
||||||
@@ -449,19 +512,6 @@ func (h *Hub) MarkOffline(name string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hub) DeleteDevice(name string) {
|
|
||||||
h.mu.Lock()
|
|
||||||
_, ok := h.records[name]
|
|
||||||
delete(h.records, name)
|
|
||||||
delete(h.live, name)
|
|
||||||
h.mu.Unlock()
|
|
||||||
if ok {
|
|
||||||
h.saveYAML()
|
|
||||||
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
|
||||||
logInfo("delete", "%s", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Hub) AddMonitor(c *websocket.Conn) {
|
func (h *Hub) AddMonitor(c *websocket.Conn) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
h.monitors[c] = struct{}{}
|
h.monitors[c] = struct{}{}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Hardware Monitor</title>
|
<title>Hardware Monitor</title>
|
||||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.min.css">
|
<link rel="stylesheet" href="https://cdn.staticfile.net/ant-design-vue/1.7.8/antd.min.css">
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.16/vue.min.js"></script>
|
<script src="https://cdn.staticfile.net/vue/2.7.16/vue.min.js"></script>
|
||||||
<script src="https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.min.js"></script>
|
<script src="https://cdn.staticfile.net/ant-design-vue/1.7.8/antd.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0a0d13; --card: #111520; --sec: #161b28; --border: #1e253a;
|
--bg: #0a0d13; --card: #111520; --sec: #161b28; --border: #1e253a;
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
.pdot { position:absolute; top:50%; left:50%; width:8px; height:8px; margin:-4px; background:var(--accent); border-radius:50% }
|
.pdot { position:absolute; top:50%; left:50%; width:8px; height:8px; margin:-4px; background:var(--accent); border-radius:50% }
|
||||||
@keyframes pulse { 0%{transform:scale(.35);opacity:1} 100%{transform:scale(2.4);opacity:0} }
|
@keyframes pulse { 0%{transform:scale(.35);opacity:1} 100%{transform:scale(2.4);opacity:0} }
|
||||||
|
|
||||||
.device-grid { padding:12px 20px 32px; display:grid; grid-template-columns:repeat(auto-fill,minmax(600px,1fr)); gap:12px }
|
.device-grid { padding:12px 20px 32px; display:grid; grid-template-columns:repeat(auto-fill,minmax(450px,1fr)); gap:12px }
|
||||||
.device-grid > .device-card { min-width:0 }
|
.device-grid > .device-card { min-width:0 }
|
||||||
.device-card { background:var(--card); border:1px solid var(--border); border-radius:10px; transition:opacity .3s,border-color .2s,box-shadow .2s }
|
.device-card { background:var(--card); border:1px solid var(--border); border-radius:10px; transition:opacity .3s,border-color .2s,box-shadow .2s }
|
||||||
.device-card:hover { border-color:#28304a; box-shadow:0 4px 24px rgba(0,0,0,.2) }
|
.device-card:hover { border-color:#28304a; box-shadow:0 4px 24px rgba(0,0,0,.2) }
|
||||||
@@ -72,8 +72,11 @@
|
|||||||
.dc-bd { padding:5px 0 0 }
|
.dc-bd { padding:5px 0 0 }
|
||||||
.dc-col { padding:0 12px 8px; flex:1 1 0; min-width:0; display:flex; flex-direction:column }
|
.dc-col { padding:0 12px 8px; flex:1 1 0; min-width:0; display:flex; flex-direction:column }
|
||||||
.dc-left { border-right:1px solid var(--border) }
|
.dc-left { border-right:1px solid var(--border) }
|
||||||
|
.dc-gpu-full { padding:0 12px 8px }
|
||||||
|
.dc-mobile-mining { display:none }
|
||||||
|
|
||||||
.sec { background:var(--sec); border-radius:6px; padding:8px 12px }
|
.sec { background:var(--sec); border-radius:6px; padding:8px 12px }
|
||||||
|
.mining-sec { background:#1a2030; border:1px solid #2a3548 }
|
||||||
.sec-tt { font-size:10px; font-weight:600; letter-spacing:.8px; text-transform:uppercase; color:var(--dim); margin-bottom:4px }
|
.sec-tt { font-size:10px; font-weight:600; letter-spacing:.8px; text-transform:uppercase; color:var(--dim); margin-bottom:4px }
|
||||||
.sec-tt .hl { color:var(--text) }
|
.sec-tt .hl { color:var(--text) }
|
||||||
.hw-row { display:flex; align-items:center; gap:6px }
|
.hw-row { display:flex; align-items:center; gap:6px }
|
||||||
@@ -83,7 +86,8 @@
|
|||||||
.info-row { display:flex; justify-content:space-between; align-items:center; font-size:10px; line-height:1.8 }
|
.info-row { display:flex; justify-content:space-between; align-items:center; font-size:10px; line-height:1.8 }
|
||||||
.info-row .label { color:var(--dim) } .info-row .value { font-family:var(--mono); font-weight:500 }
|
.info-row .label { color:var(--dim) } .info-row .value { font-family:var(--mono); font-weight:500 }
|
||||||
|
|
||||||
.gpu-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px }
|
.gpu-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(90px,1fr)); gap:4px }
|
||||||
|
@media (max-width:767px) { .gpu-grid { grid-template-columns:repeat(auto-fill,minmax(80px,1fr)) } }
|
||||||
.gpu-block { background:rgba(0,0,0,.2); border-radius:5px; padding:6px; cursor:pointer; transition:background .2s,transform .15s }
|
.gpu-block { background:rgba(0,0,0,.2); border-radius:5px; padding:6px; cursor:pointer; transition:background .2s,transform .15s }
|
||||||
.gpu-block:hover { background:rgba(0,229,160,.06); transform:translateY(-1px) }
|
.gpu-block:hover { background:rgba(0,229,160,.06); transform:translateY(-1px) }
|
||||||
.gpu-active { background:rgba(0,229,160,.12); border:1px solid var(--accent); transform:translateY(-1px) }
|
.gpu-active { background:rgba(0,229,160,.12); border:1px solid var(--accent); transform:translateY(-1px) }
|
||||||
@@ -95,7 +99,6 @@
|
|||||||
.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-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:9px; font-family:var(--mono); margin-top:2px }
|
.gb-info { display:flex; justify-content:space-between; font-size:9px; font-family:var(--mono); margin-top:2px }
|
||||||
|
|
||||||
.gpu-popover .ant-popover { width:100%!important; left:0!important }
|
|
||||||
.gpu-popover .ant-popover-inner { width:100%!important }
|
.gpu-popover .ant-popover-inner { width:100%!important }
|
||||||
.gpu-popover .ant-popover-inner-content { background:var(--card)!important; border:1px solid var(--border); border-radius:10px; padding:10px 12px; box-shadow:0 8px 32px rgba(0,0,0,.5); box-sizing:border-box }
|
.gpu-popover .ant-popover-inner-content { background:var(--card)!important; border:1px solid var(--border); border-radius:10px; padding:10px 12px; box-shadow:0 8px 32px rgba(0,0,0,.5); box-sizing:border-box }
|
||||||
.gpu-popover .ant-popover-arrow { display:none }
|
.gpu-popover .ant-popover-arrow { display:none }
|
||||||
@@ -120,15 +123,15 @@
|
|||||||
.m-hr-lbl { width:22px; text-align:right; font-family:var(--mono); font-size:9px; color:var(--dim); flex-shrink:0 }
|
.m-hr-lbl { width:22px; text-align:right; font-family:var(--mono); font-size:9px; color:var(--dim); flex-shrink:0 }
|
||||||
.m-hr-bar { flex:1; height:6px; background:var(--dim-bar); border-radius:3px; overflow:hidden }
|
.m-hr-bar { flex:1; height:6px; background:var(--dim-bar); border-radius:3px; overflow:hidden }
|
||||||
.m-hr-fill { height:100%; border-radius:3px; transition:width .4s ease }
|
.m-hr-fill { height:100%; border-radius:3px; 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-hr-val { min-width:70px; text-align:right; font-family:var(--mono); font-size:10px; font-weight:500; flex-shrink:0; color:var(--text) }
|
||||||
.m-status { display:flex; align-items:center; gap:5px; font-size:10px; margin-bottom:5px }
|
.m-status { display:flex; align-items:center; gap:5px; font-size:10px; margin-bottom:5px; color:var(--text) }
|
||||||
.m-agent,.m-country { color:var(--dim); font-size:9px; font-family:var(--mono); margin-left:auto }
|
.m-agent,.m-country { color:var(--text); font-size:9px; font-family:var(--mono); margin-left:auto }
|
||||||
.m-shares { display:flex; justify-content:space-between; margin-top:5px; font-family:var(--mono); font-size:9px }
|
.m-shares { display:flex; justify-content:space-between; margin-top:5px; font-family:var(--mono); font-size:10px; color:var(--text) }
|
||||||
.m-sh.valid { color:var(--accent) } .m-sh.stale { color:var(--warn) } .m-sh.invalid { color:var(--danger) }
|
.m-sh.valid { color:var(--accent) } .m-sh.stale { color:var(--warn) } .m-sh.invalid { color:var(--danger) }
|
||||||
.m-times { display:flex; gap:10px; margin-top:5px; font-size:9px }
|
.m-times { display:flex; gap:10px; margin-top:5px; font-size:9px; color:var(--text) }
|
||||||
.m-times span { color:var(--dim) } .m-times .v { font-family:var(--mono) }
|
.m-times span { color:var(--text) } .m-times .v { font-family:var(--mono); color:var(--text) }
|
||||||
.chart-box { background:rgba(0,0,0,.15); border-radius:4px; padding:6px 8px; margin-top:6px; overflow:hidden }
|
.chart-box { background:rgba(0,0,0,.25); border-radius:4px; padding:6px 8px; margin-top:6px; overflow:hidden }
|
||||||
.chart-lbl { font-size:8px; color:var(--dim); font-family:var(--mono); margin-bottom:3px }
|
.chart-lbl { font-size:9px; color:var(--text); font-family:var(--mono); margin-bottom:3px }
|
||||||
.chart-empty { color:var(--dim); font-size:8px; text-align:center; padding:10px 0 }
|
.chart-empty { color:var(--dim); font-size:8px; text-align:center; padding:10px 0 }
|
||||||
|
|
||||||
.off-actions { display:flex; justify-content:flex-end; padding-top:0 }
|
.off-actions { display:flex; justify-content:flex-end; padding-top:0 }
|
||||||
@@ -229,13 +232,26 @@
|
|||||||
<div class="sec"><div class="hw-row"><span class="sec-tt">硬件状态</span><span class="dot on"></span><span class="tag-on">在线</span></div></div>
|
<div class="sec"><div class="hw-row"><span class="sec-tt">硬件状态</span><span class="dot on"></span><span class="tag-on">在线</span></div></div>
|
||||||
<cpu-section :cpu="getLive(name).cpu||{}" :info="devices[name].info" :is-on="true"></cpu-section>
|
<cpu-section :cpu="getLive(name).cpu||{}" :info="devices[name].info" :is-on="true"></cpu-section>
|
||||||
<mem-section :mem="getLive(name).memory||{}" :is-on="true"></mem-section>
|
<mem-section :mem="getLive(name).memory||{}" :is-on="true"></mem-section>
|
||||||
<gpu-section :gpus="getLive(name).gpus||[]" :expanded-gpu="expandedGpu" @toggle-gpu="toggleGpu"></gpu-section>
|
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="0" :sm="0" :md="12" class="dc-col dc-right" style="flex:1 1 0">
|
<a-col :xs="0" :sm="0" :md="12" class="dc-col dc-right" style="flex:1 1 0">
|
||||||
<div class="sec"><div class="hw-row"><span class="sec-tt">矿池状态</span><span class="dot on"></span><span class="tag-on">在线</span></div></div>
|
<div class="sec hw-status"><div class="hw-row"><span class="sec-tt">矿池状态</span><span class="dot on"></span><span class="tag-on">在线</span><span style="margin-left:auto;font-size:8px;color:var(--dim);font-family:var(--mono)">{{ miningRuntime(name) }} {{ miningCountry(name) }}</span></div></div>
|
||||||
<mining-section :mining="devices[name].mining" :history="devices[name].hash_history"></mining-section>
|
<mining-section :mining="devices[name].mining" :history="devices[name].hash_history"></mining-section>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
<div class="dc-gpu-full">
|
||||||
|
<gpu-section :gpus="getLive(name).gpus||[]" :expanded-gpu="expandedGpu" @toggle-gpu="toggleGpu"></gpu-section>
|
||||||
|
</div>
|
||||||
|
<div v-if="devices[name].hash_history && devices[name].hash_history.length > 1" class="dc-gpu-full">
|
||||||
|
<div class="sec mining-sec"><div class="chart-lbl">算力走势 (30m 均值)</div><div v-html="buildChartSvg(devices[name].hash_history)"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="dc-mobile-mining">
|
||||||
|
<a-popover trigger="click" placement="bottom" overlayClassName="gpu-popover">
|
||||||
|
<template slot="content"><div class="gpu-pop-body" style="min-width:280px">
|
||||||
|
<mining-section :mining="devices[name].mining" :history="devices[name].hash_history"></mining-section>
|
||||||
|
</div></template>
|
||||||
|
<a-button size="small" type="primary" ghost><a-icon type="bar-chart" /> 矿池详情</a-button>
|
||||||
|
</a-popover>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -299,12 +315,13 @@ Vue.component('cpu-section',{
|
|||||||
props:['cpu','info','isOn'],
|
props:['cpu','info','isOn'],
|
||||||
template:`<div class="sec">
|
template:`<div class="sec">
|
||||||
<div class="sec-tt">CPU · <span class="hl">{{ model }}</span></div>
|
<div class="sec-tt">CPU · <span class="hl">{{ model }}</span></div>
|
||||||
<div style="display:flex;align-items:center;gap:8px">
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||||
<a-progress :percent="pct" :stroke-color="barColor" :stroke-width="6" size="small" :format="fmtPct" style="flex:1;min-width:0" />
|
|
||||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ pc }}/{{ lc }}</span>
|
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ pc }}/{{ lc }}</span>
|
||||||
<span class="value" :style="{color:tempColor,fontSize:'10px',whiteSpace:'nowrap'}">{{ tmpStr }}</span>
|
<span class="value" :style="{color:tempColor,fontSize:'10px',whiteSpace:'nowrap'}">{{ tmpStr }}</span>
|
||||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ freq }}MHz</span>
|
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ freq }}MHz</span>
|
||||||
|
<span class="value" style="margin-left:auto;font-size:10px;font-family:var(--mono);font-weight:500" :style="{color:barColor}">{{ pct }}%</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:block"><a-progress :percent="pct" :stroke-color="barColor" :stroke-width="5" size="small" :format="fmtPct" /></div>
|
||||||
</div>`,
|
</div>`,
|
||||||
computed:{
|
computed:{
|
||||||
model:function(){return this.cpu.model||(this.info&&this.info.cpu_model)||'Unknown'},
|
model:function(){return this.cpu.model||(this.info&&this.info.cpu_model)||'Unknown'},
|
||||||
@@ -323,10 +340,12 @@ Vue.component('cpu-section',{
|
|||||||
Vue.component('mem-section',{
|
Vue.component('mem-section',{
|
||||||
props:['mem','isOn'],
|
props:['mem','isOn'],
|
||||||
template:`<div class="sec"><div class="sec-tt">MEMORY</div>
|
template:`<div class="sec"><div class="sec-tt">MEMORY</div>
|
||||||
<div v-if="mem.total_bytes" style="display:flex;align-items:center;gap:8px">
|
<div v-if="mem.total_bytes">
|
||||||
<a-progress :percent="pct" :stroke-color="barColor" :stroke-width="6" size="small" :format="fmtPct" style="flex:1;min-width:0" />
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ used }}/{{ total }}</span>
|
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ used }} / {{ total }}</span>
|
||||||
<span class="value" :style="{color:barColor,fontSize:'10px',whiteSpace:'nowrap'}">{{ pct }}%</span>
|
<span class="value" :style="{color:barColor,fontSize:'10px',whiteSpace:'nowrap',marginLeft:'auto'}">{{ pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:block"><a-progress :percent="pct" :stroke-color="barColor" :stroke-width="5" size="small" :format="fmtPct" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else style="font-size:10px;color:var(--dim)">无数据</div></div>`,
|
<div v-else style="font-size:10px;color:var(--dim)">无数据</div></div>`,
|
||||||
computed:{total:function(){return fmtB(this.mem.total_bytes)},used:function(){return fmtB(this.mem.used_bytes)},pct:function(){return Math.round(this.mem.percent||0)},barColor:function(){return pColor(this.pct)}},
|
computed:{total:function(){return fmtB(this.mem.total_bytes)},used:function(){return fmtB(this.mem.used_bytes)},pct:function(){return Math.round(this.mem.percent||0)},barColor:function(){return pColor(this.pct)}},
|
||||||
@@ -335,7 +354,7 @@ Vue.component('mem-section',{
|
|||||||
|
|
||||||
Vue.component('gpu-section',{
|
Vue.component('gpu-section',{
|
||||||
props:['gpus','expandedGpu'],
|
props:['gpus','expandedGpu'],
|
||||||
template:`<div class="sec"><div class="sec-tt">GPU · <span class="hl">{{ gpus.length }} 张</span><span style="margin-left:auto;font-size:9px;color:var(--text);font-family:var(--mono)">{{ totalPower }} W</span></div>
|
template:`<div class="sec"><div class="sec-tt">GPU · <span class="hl">{{ gpus.length }} 张</span><span style="margin-left:auto;font-size:9px;color:var(--text);font-family:var(--mono)">{{ totalPower }}W / {{ totalPowerMax }}W</span></div>
|
||||||
<div class="gpu-grid"><div class="gpu-block" :class="{'gpu-active':expandedGpu===i}" v-for="(g,i) in gpus" :key="i">
|
<div class="gpu-grid"><div class="gpu-block" :class="{'gpu-active':expandedGpu===i}" v-for="(g,i) in gpus" :key="i">
|
||||||
<a-popover trigger="click" placement="bottom" :visible="expandedGpu===i" @visibleChange="onVis(i,$event)" overlayClassName="gpu-popover" :getPopupContainer="getPopContainer">
|
<a-popover trigger="click" placement="bottom" :visible="expandedGpu===i" @visibleChange="onVis(i,$event)" overlayClassName="gpu-popover" :getPopupContainer="getPopContainer">
|
||||||
<template slot="content"><div class="gpu-pop-body">
|
<template slot="content"><div class="gpu-pop-body">
|
||||||
@@ -343,20 +362,23 @@ Vue.component('gpu-section',{
|
|||||||
<div class="gpu-pop-row-hd"><span class="gpu-pop-name">GPU{{ ii+1 }}</span><span class="gpu-pop-model">{{ gg.name||'N/A' }}</span></div>
|
<div class="gpu-pop-row-hd"><span class="gpu-pop-name">GPU{{ ii+1 }}</span><span class="gpu-pop-model">{{ gg.name||'N/A' }}</span></div>
|
||||||
<div class="gpu-pop-grid">
|
<div class="gpu-pop-grid">
|
||||||
<div class="gd-item"><span class="gd-l">负载</span><span class="gd-v" :style="{color:pColor(gg.load_percent||0)}">{{ (gg.load_percent||0).toFixed(1) }}%</span></div>
|
<div class="gd-item"><span class="gd-l">负载</span><span class="gd-v" :style="{color:pColor(gg.load_percent||0)}">{{ (gg.load_percent||0).toFixed(1) }}%</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">显存负载</span><span class="gd-v">{{ (gg.util_memory||0).toFixed(0) }}%</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">温度</span><span class="gd-v" :style="{color:tmpColor(gg)}">{{ tmpStr(gg) }}</span></div>
|
<div class="gd-item"><span class="gd-l">温度</span><span class="gd-v" :style="{color:tmpColor(gg)}">{{ tmpStr(gg) }}</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">功耗</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_limit||0) }}W</span></div>
|
<div class="gd-item"><span class="gd-l">功耗</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_limit||0) }}W</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
<div class="gd-item"><span class="gd-l">功耗范围</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_max||0) }}W</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
|
||||||
<div class="gd-item"><span class="gd-l">风扇</span><span class="gd-v">{{ fanStr(gg) }}</span></div>
|
<div class="gd-item"><span class="gd-l">风扇</span><span class="gd-v">{{ fanStr(gg) }}</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">最大核心</span><span class="gd-v">{{ Math.round(gg.clocks_max_graphics||0) }} MHz</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">编码器</span><span class="gd-v">{{ (gg.util_encoder||0).toFixed(0) }}%</span></div>
|
||||||
|
<div class="gd-item"><span class="gd-l">解码器</span><span class="gd-v">{{ (gg.util_decoder||0).toFixed(0) }}%</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">性能状态</span><span class="gd-v">{{ gg.pstate||'N/A' }}</span></div>
|
<div class="gd-item"><span class="gd-l">性能状态</span><span class="gd-v">{{ gg.pstate||'N/A' }}</span></div>
|
||||||
<div class="gd-item"><span class="gd-l">持久模式</span><span class="gd-v">{{ gg.persistence_mode||'N/A' }}</span></div>
|
|
||||||
<div class="gd-item"><span class="gd-l">计算模式</span><span class="gd-v">{{ gg.compute_mode||'N/A' }}</span></div>
|
<div class="gd-item"><span class="gd-l">计算模式</span><span class="gd-v">{{ gg.compute_mode||'N/A' }}</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="vramTotal(gg)>0" style="margin-top:2px"><div class="info-row" style="margin-bottom:1px"><span class="label">显存</span><span class="value">{{ vramUsed(gg) }} / {{ vramTotal(gg) }} GB</span></div>
|
<div v-if="vramTotal(gg)>0" style="margin-top:2px"><div class="info-row" style="margin-bottom:1px"><span class="label">显存</span><span class="value">{{ vramUsed(gg) }} / {{ vramTotal(gg) }} GB</span></div>
|
||||||
<a-progress :percent="Math.round(vramPct(gg))" :stroke-color="pColor(vramPct(gg))" :stroke-width="4" size="small" :format="fmtPct" /></div>
|
<a-progress :percent="Math.round(vramPct(gg))" :stroke-color="pColor(vramPct(gg))" :stroke-width="4" size="small" :format="fmtPct" /></div>
|
||||||
<div v-if="gg.driver_version" style="margin-top:2px;font-size:8px;color:var(--dim);display:flex;gap:8px">
|
<div v-if="gg.driver_version" style="margin-top:2px;font-size:8px;color:var(--dim)">
|
||||||
<span>驱动: {{ gg.driver_version }}</span>
|
<span>驱动: {{ gg.driver_version }}</span>
|
||||||
<span v-if="gg.cuda_version">CUDA: {{ gg.cuda_version }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></template>
|
</div></template>
|
||||||
@@ -369,7 +391,8 @@ Vue.component('gpu-section',{
|
|||||||
</a-popover>
|
</a-popover>
|
||||||
</div></div></div>`,
|
</div></div></div>`,
|
||||||
computed:{
|
computed:{
|
||||||
totalPower:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power||0;return Math.round(t)}
|
totalPower:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power||0;return Math.round(t)},
|
||||||
|
totalPowerMax:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power_max||this.gpus[i].power_limit||0;return Math.round(t)}
|
||||||
},
|
},
|
||||||
methods:{
|
methods:{
|
||||||
pColor:pColor, tmpStr:function(g){var t=(g||{}).temperature||0;return t>0?t.toFixed(0)+'°':'—'},
|
pColor:pColor, tmpStr:function(g){var t=(g||{}).temperature||0;return t>0?t.toFixed(0)+'°':'—'},
|
||||||
@@ -380,7 +403,17 @@ Vue.component('gpu-section',{
|
|||||||
vramStr:function(g){var t=(g||{}).vram_total||(g||{}).vram_total_mb||0,u=(g||{}).vram_used||(g||{}).vram_used_mb||0;return t>0?(u/1024).toFixed(1)+'/'+(t/1024).toFixed(0)+'G':'—'},
|
vramStr:function(g){var t=(g||{}).vram_total||(g||{}).vram_total_mb||0,u=(g||{}).vram_used||(g||{}).vram_used_mb||0;return t>0?(u/1024).toFixed(1)+'/'+(t/1024).toFixed(0)+'G':'—'},
|
||||||
fanStr:function(g){var f=(g||{}).fan_speed||0;return f>0?Math.round(f)+'%':'—'},
|
fanStr:function(g){var f=(g||{}).fan_speed||0;return f>0?Math.round(f)+'%':'—'},
|
||||||
fmtPct:function(v){return v+'%'},
|
fmtPct:function(v){return v+'%'},
|
||||||
getPopContainer:function(){return this.$el.closest('.device-card')||document.body},
|
getPopContainer:function(){
|
||||||
|
var card=this.$el.closest('.device-card');
|
||||||
|
if(card){
|
||||||
|
var w=card.offsetWidth;
|
||||||
|
this.$nextTick(function(){
|
||||||
|
var pop=document.querySelector('.gpu-popover .ant-popover');
|
||||||
|
if(pop) pop.style.width=w+'px';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return card||document.body;
|
||||||
|
},
|
||||||
toggle:function(i){var next=this.expandedGpu===i?null:i;this.$emit('toggle-gpu',next);
|
toggle:function(i){var next=this.expandedGpu===i?null:i;this.$emit('toggle-gpu',next);
|
||||||
if(next!==null){var self=this;this.$nextTick(function(){setTimeout(function(){var el=document.getElementById('gpu-pop-'+next);if(el){var c=document.querySelector('.gpu-pop-body');if(c){c.scrollTop=el.offsetTop-c.clientHeight/2+el.clientHeight/2}}},50)})}},
|
if(next!==null){var self=this;this.$nextTick(function(){setTimeout(function(){var el=document.getElementById('gpu-pop-'+next);if(el){var c=document.querySelector('.gpu-pop-body');if(c){c.scrollTop=el.offsetTop-c.clientHeight/2+el.clientHeight/2}}},50)})}},
|
||||||
onVis:function(i,vis){if(!vis&&this.expandedGpu===i)this.$emit('toggle-gpu',null)}
|
onVis:function(i,vis){if(!vis&&this.expandedGpu===i)this.$emit('toggle-gpu',null)}
|
||||||
@@ -389,21 +422,19 @@ Vue.component('gpu-section',{
|
|||||||
|
|
||||||
Vue.component('mining-section',{
|
Vue.component('mining-section',{
|
||||||
props:['mining','history'],
|
props:['mining','history'],
|
||||||
template:`<div v-if="mining"><div class="sec">
|
template:`<div v-if="mining"><div class="sec mining-sec">
|
||||||
<div class="sec-tt">MINING · <span class="hl">{{ coin }}</span></div>
|
<div class="sec-tt"><span class="hl">{{ coin }}</span><span style="margin-left:6px;font-size:8px;color:var(--dim)">{{ mining.agent||'' }}</span></div>
|
||||||
<div class="m-status"><span class="dot" :class="sOn?'on':'off'"></span>
|
<div style="display:flex;gap:8px;margin:6px 0">
|
||||||
<span :style="{color:sOn?'#00e5a0':'#ff4060',fontWeight:500,fontSize:'9px'}">{{ sOn?'在线':'离线' }}</span>
|
<div v-for="hr in hrs" :key="hr.l" style="flex:1;min-width:0;text-align:center">
|
||||||
<span class="m-agent">{{ mining.agent||'' }}</span><span class="m-country" v-if="mining.country">{{ mining.country }}</span></div>
|
<div style="font-size:8px;color:var(--dim);margin-bottom:2px">{{ hr.l }}</div>
|
||||||
<div v-for="hr in hrs" :key="hr.l" class="m-hr-row"><span class="m-hr-lbl">{{ hr.l }}</span>
|
<div style="font-size:12px;font-weight:500;font-family:var(--mono)" :style="{color:pColor(hr.p)}">{{ hr.v }}</div>
|
||||||
<div class="m-hr-bar"><div class="m-hr-fill" :style="{width:hr.p+'%',background:pColor(hr.p)}"></div></div>
|
</div>
|
||||||
<span class="m-hr-val" :style="{color:pColor(hr.p)}">{{ hr.v }}</span></div>
|
</div>
|
||||||
<div class="m-times"><span>活跃 <span class="v">{{ fmtMsTs(mining.last_active) }}</span></span>
|
<div class="m-times"><span>活跃 <span class="v">{{ fmtMsTs(mining.last_active) }}</span></span>
|
||||||
<span>开始 <span class="v">{{ fmtMsTs(mining.opened_at) }}</span></span>
|
<span>开始 <span class="v">{{ fmtMsTs(mining.opened_at) }}</span></span></div>
|
||||||
<span v-if="durH">运行 <span class="v">{{ durH }} h</span></span></div>
|
|
||||||
<div class="m-shares"><span class="m-sh valid">{{ mining.valid||0 }} 有效</span>
|
<div class="m-shares"><span class="m-sh valid">{{ mining.valid||0 }} 有效</span>
|
||||||
<span class="m-sh stale">{{ mining.stale||0 }} 过期</span><span class="m-sh invalid">{{ mining.invalid||0 }} 无效</span></div>
|
<span class="m-sh stale">{{ mining.stale||0 }} 过期</span><span class="m-sh invalid">{{ mining.invalid||0 }} 无效</span></div>
|
||||||
<div class="chart-box"><div class="chart-lbl">算力走势 (30m 均值)</div><div v-html="chartHtml"></div></div>
|
</div></div><div v-else class="sec mining-sec"><div class="sec-tt" style="color:var(--dim)">暂无矿池数据</div></div>`,
|
||||||
</div></div><div v-else class="sec"><div class="sec-tt">MINING</div><div style="color:var(--dim);font-size:9px;text-align:center;padding:10px 0">暂无矿池数据</div></div>`,
|
|
||||||
computed:{
|
computed:{
|
||||||
coin:function(){return((this.mining||{}).coin||'').toUpperCase()},
|
coin:function(){return((this.mining||{}).coin||'').toUpperCase()},
|
||||||
sOn:function(){return(this.mining||{}).status==='online'},
|
sOn:function(){return(this.mining||{}).status==='online'},
|
||||||
@@ -451,6 +482,8 @@ new Vue({
|
|||||||
methods:{
|
methods:{
|
||||||
getLive:function(name){var e=this.devices[name];if(!e||!e.live)return{};var l=e.live;if(typeof l==='string')try{l=JSON.parse(l)}catch(ex){}return l||{}},
|
getLive:function(name){var e=this.devices[name];if(!e||!e.live)return{};var l=e.live;if(typeof l==='string')try{l=JSON.parse(l)}catch(ex){}return l||{}},
|
||||||
isPoolOnline:function(name){var e=this.devices[name];return e&&e.mining&&e.mining.status==='online'},
|
isPoolOnline:function(name){var e=this.devices[name];return e&&e.mining&&e.mining.status==='online'},
|
||||||
|
miningRuntime(name){var e=this.devices[name];if(!e||!e.mining)return'';var m=e.mining;if(m.last_active>0&&m.opened_at>0)return((m.last_active-m.opened_at)/3600000).toFixed(1)+'h';return''},
|
||||||
|
miningCountry(name){var e=this.devices[name];return e&&e.mining?e.mining.country||'':''},
|
||||||
ago:ago,
|
ago:ago,
|
||||||
showRefresh:function(){var self=this;this.refreshing=true;setTimeout(function(){self.refreshing=false},500)},
|
showRefresh:function(){var self=this;this.refreshing=true;setTimeout(function(){self.refreshing=false},500)},
|
||||||
countdown:function(iso){
|
countdown:function(iso){
|
||||||
@@ -464,6 +497,7 @@ new Vue({
|
|||||||
return Math.floor(s/86400)+'天前';
|
return Math.floor(s/86400)+'天前';
|
||||||
},
|
},
|
||||||
toggleGpu:function(i){this.expandedGpu=i},
|
toggleGpu:function(i){this.expandedGpu=i},
|
||||||
|
buildChartSvg:buildChartSvg,
|
||||||
deleteDevice:function(n){var self=this;this.$confirm({title:'确定删除 '+n+' 的设备记录?',onOk:function(){if(self.ws&&self.ws.readyState===1)self.ws.send(JSON.stringify({action:'delete',machine:n}))}})},
|
deleteDevice:function(n){var self=this;this.$confirm({title:'确定删除 '+n+' 的设备记录?',onOk:function(){if(self.ws&&self.ws.readyState===1)self.ws.send(JSON.stringify({action:'delete',machine:n}))}})},
|
||||||
handleLogin:function(e){e.preventDefault();var self=this;this.loginForm.validateFields(function(err,values){if(err)return;self.loginLoading=true;self.loginErr='';self.loginConn='';localStorage.setItem('mm_id',values.miner_id);localStorage.setItem('mm_pw',values.password);self.connectWs(values.miner_id,values.password)})},
|
handleLogin:function(e){e.preventDefault();var self=this;this.loginForm.validateFields(function(err,values){if(err)return;self.loginLoading=true;self.loginErr='';self.loginConn='';localStorage.setItem('mm_id',values.miner_id);localStorage.setItem('mm_pw',values.password);self.connectWs(values.miner_id,values.password)})},
|
||||||
sendAuth:function(mid,pw){if(this.ws&&this.ws.readyState===WebSocket.OPEN)this.ws.send(JSON.stringify({type:'auth',miner_id:mid,password:pw}))},
|
sendAuth:function(mid,pw){if(this.ws&&this.ws.readyState===WebSocket.OPEN)this.ws.send(JSON.stringify({type:'auth',miner_id:mid,password:pw}))},
|
||||||
|
|||||||
Reference in New Issue
Block a user