feat: Add avg difficulty per share statistic to dashboard

- Add AvgDifficulty and DiffCurrent fields to PerformanceMetrics
- Calculate avg difficulty as HashesTotal/SharesGood in XMRig stats
- Add difficulty data to TT-Miner stats using pool difficulty
- Display "Avg Diff" stat in stats panel with k/M/G/T formatting
- Add WriteStdin to MockMiner for test interface compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
snider 2025-12-29 23:41:26 +00:00
parent f10e7a16e2
commit b9f9143336
5 changed files with 71 additions and 17 deletions

View file

@ -123,13 +123,15 @@ type Config struct {
// PerformanceMetrics represents the performance metrics for a miner.
type PerformanceMetrics struct {
Hashrate int `json:"hashrate"`
Shares int `json:"shares"`
Rejected int `json:"rejected"`
Uptime int `json:"uptime"`
LastShare int64 `json:"lastShare"`
Algorithm string `json:"algorithm"`
ExtraData map[string]interface{} `json:"extraData,omitempty"`
Hashrate int `json:"hashrate"`
Shares int `json:"shares"`
Rejected int `json:"rejected"`
Uptime int `json:"uptime"`
LastShare int64 `json:"lastShare"`
Algorithm string `json:"algorithm"`
AvgDifficulty int `json:"avgDifficulty"` // Average difficulty per accepted share (HashesTotal/SharesGood)
DiffCurrent int `json:"diffCurrent"` // Current job difficulty from pool
ExtraData map[string]interface{} `json:"extraData,omitempty"`
}
// HashratePoint represents a single hashrate measurement at a specific time.

View file

@ -25,6 +25,7 @@ type MockMiner struct {
AddHashratePointFunc func(point HashratePoint)
ReduceHashrateHistoryFunc func(now time.Time)
GetLogsFunc func() []string
WriteStdinFunc func(input string) error
}
func (m *MockMiner) Install() error { return m.InstallFunc() }
@ -43,6 +44,7 @@ func (m *MockMiner) GetHashrateHistory() []HashratePoint { return m.GetHashrate
func (m *MockMiner) AddHashratePoint(point HashratePoint) { m.AddHashratePointFunc(point) }
func (m *MockMiner) ReduceHashrateHistory(now time.Time) { m.ReduceHashrateHistoryFunc(now) }
func (m *MockMiner) GetLogs() []string { return m.GetLogsFunc() }
func (m *MockMiner) WriteStdin(input string) error { return m.WriteStdinFunc(input) }
// MockManager is a mock implementation of the Manager for testing.
type MockManager struct {

View file

@ -49,11 +49,17 @@ func (m *TTMiner) GetStats() (*PerformanceMetrics, error) {
}
}
// For TT-Miner, we use the connection difficulty as both current and avg
// since TT-Miner doesn't expose per-share difficulty data
diffCurrent := summary.Connection.Diff
return &PerformanceMetrics{
Hashrate: int(totalHashrate),
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
Hashrate: int(totalHashrate),
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
AvgDifficulty: diffCurrent, // Use pool diff as approximation
DiffCurrent: diffCurrent,
}, nil
}

View file

@ -42,11 +42,19 @@ func (m *XMRigMiner) GetStats() (*PerformanceMetrics, error) {
hashrate = int(summary.Hashrate.Total[0])
}
// Calculate average difficulty per accepted share
var avgDifficulty int
if summary.Results.SharesGood > 0 {
avgDifficulty = summary.Results.HashesTotal / summary.Results.SharesGood
}
return &PerformanceMetrics{
Hashrate: hashrate,
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
Hashrate: hashrate,
Shares: summary.Results.SharesGood,
Rejected: summary.Results.SharesTotal - summary.Results.SharesGood,
Uptime: summary.Uptime,
Algorithm: summary.Algo,
AvgDifficulty: avgDifficulty,
DiffCurrent: summary.Results.DiffCurrent,
}, nil
}

View file

@ -60,6 +60,18 @@ import { MinerService } from '../../miner.service';
<div class="stat-divider"></div>
<div class="stat-item">
<svg class="stat-icon text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
<div class="stat-content">
<span class="stat-value tabular-nums">{{ formatDifficulty(avgDifficulty()) }}</span>
</div>
<span class="stat-label">Avg Diff</span>
</div>
<div class="stat-divider"></div>
<div class="stat-item workers">
<svg class="stat-icon" [class.text-success-500]="minerCount() > 0" [class.text-slate-500]="minerCount() === 0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/>
@ -149,6 +161,10 @@ import { MinerService } from '../../miner.service';
background: rgb(37 37 66 / 0.3);
}
.text-purple-500 {
color: #a855f7;
}
@media (max-width: 768px) {
.stats-panel {
gap: 0.75rem;
@ -202,6 +218,17 @@ export class StatsPanelComponent {
minerCount = computed(() => this.miners().length);
// Calculate average difficulty per accepted share (HashesTotal / SharesGood)
avgDifficulty = computed(() => {
let totalHashes = 0;
let totalShares = 0;
this.miners().forEach(m => {
totalHashes += m.full_stats?.results?.hashes_total || 0;
totalShares += m.full_stats?.results?.shares_good || 0;
});
return totalShares > 0 ? Math.floor(totalHashes / totalShares) : 0;
});
formatHashrate(hashrate: number): string {
if (hashrate >= 1000000000) return (hashrate / 1000000000).toFixed(2);
if (hashrate >= 1000000) return (hashrate / 1000000).toFixed(2);
@ -227,4 +254,13 @@ export class StatsPanelComponent {
const mins = Math.floor((seconds % 3600) / 60);
return `${hours}h ${mins}m`;
}
formatDifficulty(diff: number): string {
if (diff === 0) return '-';
if (diff >= 1000000000000) return (diff / 1000000000000).toFixed(2) + 'T';
if (diff >= 1000000000) return (diff / 1000000000).toFixed(2) + 'G';
if (diff >= 1000000) return (diff / 1000000).toFixed(2) + 'M';
if (diff >= 1000) return (diff / 1000).toFixed(2) + 'k';
return diff.toString();
}
}