feat: Add stats bar component to display miner performance metrics and update dashboard layout

This commit is contained in:
Snider 2025-12-11 15:22:58 +00:00
parent aefe328254
commit 7d6e6e9c42
10 changed files with 824 additions and 90 deletions

View file

@ -615,6 +615,9 @@ const docTemplate = `{
"configPath": {
"type": "string"
},
"full_stats": {
"$ref": "#/definitions/mining.XMRigSummary"
},
"hashrateHistory": {
"type": "array",
"items": {
@ -646,6 +649,248 @@ const docTemplate = `{
"type": "string"
}
}
},
"mining.XMRigSummary": {
"type": "object",
"properties": {
"algo": {
"type": "string"
},
"algorithms": {
"type": "array",
"items": {
"type": "string"
}
},
"connection": {
"type": "object",
"properties": {
"accepted": {
"type": "integer"
},
"algo": {
"type": "string"
},
"avg_time": {
"type": "integer"
},
"avg_time_ms": {
"type": "integer"
},
"diff": {
"type": "integer"
},
"failures": {
"type": "integer"
},
"hashes_total": {
"type": "integer"
},
"ip": {
"type": "string"
},
"ping": {
"type": "integer"
},
"pool": {
"type": "string"
},
"rejected": {
"type": "integer"
},
"tls": {
"type": "string"
},
"tls-fingerprint": {
"type": "string"
},
"uptime": {
"type": "integer"
},
"uptime_ms": {
"type": "integer"
}
}
},
"cpu": {
"type": "object",
"properties": {
"64_bit": {
"type": "boolean"
},
"aes": {
"type": "boolean"
},
"arch": {
"type": "string"
},
"assembly": {
"type": "string"
},
"avx2": {
"type": "boolean"
},
"backend": {
"type": "string"
},
"brand": {
"type": "string"
},
"cores": {
"type": "integer"
},
"family": {
"type": "integer"
},
"flags": {
"type": "array",
"items": {
"type": "string"
}
},
"l2": {
"type": "integer"
},
"l3": {
"type": "integer"
},
"model": {
"type": "integer"
},
"msr": {
"type": "string"
},
"nodes": {
"type": "integer"
},
"packages": {
"type": "integer"
},
"proc_info": {
"type": "integer"
},
"stepping": {
"type": "integer"
},
"threads": {
"type": "integer"
},
"x64": {
"type": "boolean"
}
}
},
"donate_level": {
"type": "integer"
},
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"hashrate": {
"type": "object",
"properties": {
"highest": {
"type": "number"
},
"total": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"hugepages": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"kind": {
"type": "string"
},
"paused": {
"type": "boolean"
},
"resources": {
"type": "object",
"properties": {
"hardware_concurrency": {
"type": "integer"
},
"load_average": {
"type": "array",
"items": {
"type": "number"
}
},
"memory": {
"type": "object",
"properties": {
"free": {
"type": "integer"
},
"resident_set_memory": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
}
}
},
"restricted": {
"type": "boolean"
},
"results": {
"type": "object",
"properties": {
"avg_time": {
"type": "integer"
},
"avg_time_ms": {
"type": "integer"
},
"best": {
"type": "array",
"items": {
"type": "integer"
}
},
"diff_current": {
"type": "integer"
},
"hashes_total": {
"type": "integer"
},
"shares_good": {
"type": "integer"
},
"shares_total": {
"type": "integer"
}
}
},
"ua": {
"type": "string"
},
"uptime": {
"type": "integer"
},
"version": {
"type": "string"
},
"worker_id": {
"type": "string"
}
}
}
}
}`

View file

@ -609,6 +609,9 @@
"configPath": {
"type": "string"
},
"full_stats": {
"$ref": "#/definitions/mining.XMRigSummary"
},
"hashrateHistory": {
"type": "array",
"items": {
@ -640,6 +643,248 @@
"type": "string"
}
}
},
"mining.XMRigSummary": {
"type": "object",
"properties": {
"algo": {
"type": "string"
},
"algorithms": {
"type": "array",
"items": {
"type": "string"
}
},
"connection": {
"type": "object",
"properties": {
"accepted": {
"type": "integer"
},
"algo": {
"type": "string"
},
"avg_time": {
"type": "integer"
},
"avg_time_ms": {
"type": "integer"
},
"diff": {
"type": "integer"
},
"failures": {
"type": "integer"
},
"hashes_total": {
"type": "integer"
},
"ip": {
"type": "string"
},
"ping": {
"type": "integer"
},
"pool": {
"type": "string"
},
"rejected": {
"type": "integer"
},
"tls": {
"type": "string"
},
"tls-fingerprint": {
"type": "string"
},
"uptime": {
"type": "integer"
},
"uptime_ms": {
"type": "integer"
}
}
},
"cpu": {
"type": "object",
"properties": {
"64_bit": {
"type": "boolean"
},
"aes": {
"type": "boolean"
},
"arch": {
"type": "string"
},
"assembly": {
"type": "string"
},
"avx2": {
"type": "boolean"
},
"backend": {
"type": "string"
},
"brand": {
"type": "string"
},
"cores": {
"type": "integer"
},
"family": {
"type": "integer"
},
"flags": {
"type": "array",
"items": {
"type": "string"
}
},
"l2": {
"type": "integer"
},
"l3": {
"type": "integer"
},
"model": {
"type": "integer"
},
"msr": {
"type": "string"
},
"nodes": {
"type": "integer"
},
"packages": {
"type": "integer"
},
"proc_info": {
"type": "integer"
},
"stepping": {
"type": "integer"
},
"threads": {
"type": "integer"
},
"x64": {
"type": "boolean"
}
}
},
"donate_level": {
"type": "integer"
},
"features": {
"type": "array",
"items": {
"type": "string"
}
},
"hashrate": {
"type": "object",
"properties": {
"highest": {
"type": "number"
},
"total": {
"type": "array",
"items": {
"type": "number"
}
}
}
},
"hugepages": {
"type": "array",
"items": {
"type": "integer"
}
},
"id": {
"type": "string"
},
"kind": {
"type": "string"
},
"paused": {
"type": "boolean"
},
"resources": {
"type": "object",
"properties": {
"hardware_concurrency": {
"type": "integer"
},
"load_average": {
"type": "array",
"items": {
"type": "number"
}
},
"memory": {
"type": "object",
"properties": {
"free": {
"type": "integer"
},
"resident_set_memory": {
"type": "integer"
},
"total": {
"type": "integer"
}
}
}
}
},
"restricted": {
"type": "boolean"
},
"results": {
"type": "object",
"properties": {
"avg_time": {
"type": "integer"
},
"avg_time_ms": {
"type": "integer"
},
"best": {
"type": "array",
"items": {
"type": "integer"
}
},
"diff_current": {
"type": "integer"
},
"hashes_total": {
"type": "integer"
},
"shares_good": {
"type": "integer"
},
"shares_total": {
"type": "integer"
}
}
},
"ua": {
"type": "string"
},
"uptime": {
"type": "integer"
},
"version": {
"type": "string"
},
"worker_id": {
"type": "string"
}
}
}
}
}

View file

@ -93,6 +93,8 @@ definitions:
$ref: '#/definitions/mining.API'
configPath:
type: string
full_stats:
$ref: '#/definitions/mining.XMRigSummary'
hashrateHistory:
items:
$ref: '#/definitions/mining.HashratePoint'
@ -114,6 +116,165 @@ definitions:
version:
type: string
type: object
mining.XMRigSummary:
properties:
algo:
type: string
algorithms:
items:
type: string
type: array
connection:
properties:
accepted:
type: integer
algo:
type: string
avg_time:
type: integer
avg_time_ms:
type: integer
diff:
type: integer
failures:
type: integer
hashes_total:
type: integer
ip:
type: string
ping:
type: integer
pool:
type: string
rejected:
type: integer
tls:
type: string
tls-fingerprint:
type: string
uptime:
type: integer
uptime_ms:
type: integer
type: object
cpu:
properties:
64_bit:
type: boolean
aes:
type: boolean
arch:
type: string
assembly:
type: string
avx2:
type: boolean
backend:
type: string
brand:
type: string
cores:
type: integer
family:
type: integer
flags:
items:
type: string
type: array
l2:
type: integer
l3:
type: integer
model:
type: integer
msr:
type: string
nodes:
type: integer
packages:
type: integer
proc_info:
type: integer
stepping:
type: integer
threads:
type: integer
x64:
type: boolean
type: object
donate_level:
type: integer
features:
items:
type: string
type: array
hashrate:
properties:
highest:
type: number
total:
items:
type: number
type: array
type: object
hugepages:
items:
type: integer
type: array
id:
type: string
kind:
type: string
paused:
type: boolean
resources:
properties:
hardware_concurrency:
type: integer
load_average:
items:
type: number
type: array
memory:
properties:
free:
type: integer
resident_set_memory:
type: integer
total:
type: integer
type: object
type: object
restricted:
type: boolean
results:
properties:
avg_time:
type: integer
avg_time_ms:
type: integer
best:
items:
type: integer
type: array
diff_current:
type: integer
hashes_total:
type: integer
shares_good:
type: integer
shares_total:
type: integer
type: object
ua:
type: string
uptime:
type: integer
version:
type: string
worker_id:
type: string
type: object
host: localhost:8080
info:
contact: {}

View file

@ -139,17 +139,82 @@ type API struct {
ListenPort int `json:"listenPort"`
}
// XMRigSummary represents the summary of an XMRig miner's performance.
// XMRigSummary represents the full JSON response from the XMRig API.
type XMRigSummary struct {
Hashrate struct {
Total []float64 `json:"total"`
} `json:"hashrate"`
Results struct {
SharesGood uint64 `json:"shares_good"`
SharesTotal uint64 `json:"shares_total"`
ID string `json:"id"`
WorkerID string `json:"worker_id"`
Uptime int `json:"uptime"`
Restricted bool `json:"restricted"`
Resources struct {
Memory struct {
Free int64 `json:"free"`
Total int64 `json:"total"`
ResidentSetMemory int64 `json:"resident_set_memory"`
} `json:"memory"`
LoadAverage []float64 `json:"load_average"`
HardwareConcurrency int `json:"hardware_concurrency"`
} `json:"resources"`
Features []string `json:"features"`
Results struct {
DiffCurrent int `json:"diff_current"`
SharesGood int `json:"shares_good"`
SharesTotal int `json:"shares_total"`
AvgTime int `json:"avg_time"`
AvgTimeMS int `json:"avg_time_ms"`
HashesTotal int `json:"hashes_total"`
Best []int `json:"best"`
} `json:"results"`
Uptime uint64 `json:"uptime"`
Algorithm string `json:"algorithm"`
Algo string `json:"algo"`
Connection struct {
Pool string `json:"pool"`
IP string `json:"ip"`
Uptime int `json:"uptime"`
UptimeMS int `json:"uptime_ms"`
Ping int `json:"ping"`
Failures int `json:"failures"`
TLS string `json:"tls"`
TLSFingerprint string `json:"tls-fingerprint"`
Algo string `json:"algo"`
Diff int `json:"diff"`
Accepted int `json:"accepted"`
Rejected int `json:"rejected"`
AvgTime int `json:"avg_time"`
AvgTimeMS int `json:"avg_time_ms"`
HashesTotal int `json:"hashes_total"`
} `json:"connection"`
Version string `json:"version"`
Kind string `json:"kind"`
UA string `json:"ua"`
CPU struct {
Brand string `json:"brand"`
Family int `json:"family"`
Model int `json:"model"`
Stepping int `json:"stepping"`
ProcInfo int `json:"proc_info"`
AES bool `json:"aes"`
AVX2 bool `json:"avx2"`
X64 bool `json:"x64"`
Is64Bit bool `json:"64_bit"`
L2 int `json:"l2"`
L3 int `json:"l3"`
Cores int `json:"cores"`
Threads int `json:"threads"`
Packages int `json:"packages"`
Nodes int `json:"nodes"`
Backend string `json:"backend"`
MSR string `json:"msr"`
Assembly string `json:"assembly"`
Arch string `json:"arch"`
Flags []string `json:"flags"`
} `json:"cpu"`
DonateLevel int `json:"donate_level"`
Paused bool `json:"paused"`
Algorithms []string `json:"algorithms"`
Hashrate struct {
Total []float64 `json:"total"`
Highest float64 `json:"highest"`
} `json:"hashrate"`
Hugepages []int `json:"hugepages"`
}
// AvailableMiner represents a miner that is available for use.

View file

@ -19,6 +19,7 @@ import (
// XMRigMiner represents an XMRig miner, embedding the BaseMiner for common functionality.
type XMRigMiner struct {
BaseMiner
FullStats *XMRigSummary `json:"full_stats,omitempty"`
}
var httpClient = &http.Client{

View file

@ -100,6 +100,7 @@ func addCliArgs(config *Config, args *[]string) {
if config.TLS {
*args = append(*args, "--tls")
}
*args = append(*args, "--donate-level", "1")
}
// createConfig creates a JSON configuration file for the XMRig miner.

View file

@ -34,6 +34,9 @@ func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) {
return nil, err
}
// Store the full summary in the miner struct
m.FullStats = &summary
var hashrate int
if len(summary.Hashrate.Total) > 0 {
hashrate = int(summary.Hashrate.Total[0])
@ -41,9 +44,9 @@ func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) {
return &PerformanceMetrics{
Hashrate: hashrate,
Shares: int(summary.Results.SharesGood),
Rejected: int(summary.Results.SharesTotal - summary.Results.SharesGood),
Uptime: int(summary.Uptime),
Algorithm: summary.Algorithm,
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
}, nil
}

View file

@ -1,12 +1,4 @@
<div class="dashboard-view">
<div class="header-title">
<wa-icon name="cpu" style="font-size: 1.5rem;"></wa-icon>
<span>Mining Control</span>
<wa-button variant="neutral" appearance="plain" (click)="toggleProfileManager()">
<wa-icon name="list-alt" label="Profile Manager"></wa-icon>
</wa-button>
</div>
@if (error()) {
<wa-card class="card-error">
<div slot="header">
@ -17,75 +9,15 @@
</wa-card>
}
@if(showProfileManager()) {
<div class="profile-manager-view">
<snider-mining-profile-list></snider-mining-profile-list>
<snider-mining-profile-create></snider-mining-profile-create>
<!-- Consolidated Chart for All Running Miners -->
@if (state().runningMiners.length > 0) {
<div class="dashboard-charts">
<snider-mining-stats-bar [stats]="state().runningMiners[0]?.full_stats"></snider-mining-stats-bar>
<snider-mining-chart></snider-mining-chart>
</div>
} @else {
<!-- Miner Summary and Controls -->
@if (state().installedMiners.length > 0) {
<div class="dashboard-summary">
@for (miner of state().installedMiners; track miner.path) {
<div class="miner-summary-item">
<span class="miner-name">
{{ miner.type }}
<wa-tooltip>
<div slot="content">Version: {{ miner.version }}</div>
<wa-icon name="info-circle"></wa-icon>
</wa-tooltip>
</span>
@if (isMinerRunning(miner)) {
<wa-button
variant="danger"
[disabled]="actionInProgress() === 'stop-' + miner.type"
(click)="stopMiner(miner)">
@if (actionInProgress() === 'stop-' + miner.type) {
<wa-spinner class="button-spinner"></wa-spinner>
} @else {
<wa-icon name="stop-circle" slot="prefix"></wa-icon>
Stop
}
</wa-button>
} @else {
@if(miner.type) {
<div class="start-buttons">
<wa-select
[value]="selectedProfileIds().get(miner.type) ?? ''"
(change)="handleProfileSelection(miner.type, $event)">
<span slot="label">Profile</span>
@for(profile of state().profiles; track profile.id) {
@if(profile.minerType === miner.type) {
<wa-option [value]="profile.id">{{ profile.name }}</wa-option>
}
}
</wa-select>
<wa-button
variant="primary"
[disabled]="!selectedProfileIds().get(miner.type) || actionInProgress()?.startsWith('start-')"
(click)="startMiner(miner.type)">
<wa-icon name="play-circle" slot="prefix"></wa-icon>
Start
</wa-button>
</div>
}
}
</div>
}
</div>
<!-- Consolidated Chart for All Running Miners -->
@if (state().runningMiners.length > 0) {
<div class="dashboard-charts">
<snider-mining-chart></snider-mining-chart>
</div>
}
} @else {
<div class="centered-container">
<p>No miners installed. Open the Admin Panel to install one.</p>
</div>
}
<div class="centered-container">
<p>No miners running.</p>
</div>
}
</div>

View file

@ -6,6 +6,7 @@ import { MinerService } from './miner.service';
import { ChartComponent } from './chart.component';
import { ProfileListComponent } from './profile-list.component';
import { ProfileCreateComponent } from './profile-create.component';
import { StatsBarComponent } from './stats-bar.component';
// Import Web Awesome components
import "@awesome.me/webawesome/dist/webawesome.js";
@ -21,7 +22,7 @@ import '@awesome.me/webawesome/dist/components/select/select.js';
selector: 'snider-mining-dashboard',
standalone: true,
schemas: [CUSTOM_ELEMENTS_SCHEMA],
imports: [CommonModule, FormsModule, ChartComponent, ProfileListComponent, ProfileCreateComponent],
imports: [CommonModule, FormsModule, ChartComponent, ProfileListComponent, ProfileCreateComponent, StatsBarComponent],
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.css']
})

View file

@ -0,0 +1,80 @@
import { Component, Input, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'snider-mining-stats-bar',
standalone: true,
imports: [CommonModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
@if(stats) {
<div class="stats-bar">
<div class="stat-item">
<span class="label">Hashrate:</span>
<span class="value">{{ stats.hashrate?.total[0] | number:'1.0-2' }} H/s</span>
</div>
<div class="stat-item">
<span class="label">Algorithm:</span>
<span class="value">{{ stats.algo }}</span>
</div>
<div class="stat-item">
<span class="label">Difficulty:</span>
<span class="value">{{ stats.connection?.diff | number }}</span>
</div>
<div class="stat-item">
<span class="label">Accepted:</span>
<span class="value">{{ stats.results?.shares_good }}</span>
</div>
<div class="stat-item">
<span class="label">Rejected:</span>
<span class="value">{{ stats.connection?.rejected }}</span>
</div>
<div class="stat-item">
<span class="label">Avg Time:</span>
<span class="value">{{ stats.results?.avg_time | number }}s</span>
</div>
<div class="stat-item">
<span class="label">Uptime:</span>
<span class="value">{{ stats.uptime | number }}s</span>
</div>
<div class="stat-item">
<span class="label">Pool Uptime:</span>
<span class="value">{{ stats.connection?.uptime | number }}s</span>
</div>
</div>
}
`,
styles: [`
.stats-bar {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
padding: 0.5rem;
background-color: #f9f9f9;
border: 1px solid #eee;
border-radius: 8px;
margin-bottom: 1rem;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
background-color: #fff;
padding: 0.5rem;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.label {
font-size: 0.8rem;
color: #666;
margin-bottom: 0.25rem;
}
.value {
font-weight: bold;
font-size: 1.1rem;
}
`]
})
export class StatsBarComponent {
@Input() stats: any;
}