Mining/ui/src/app/dashboard.component.html

283 lines
17 KiB
HTML

<div class="mining-dashboard flex flex-col gap-6 h-full p-4">
@if (error()) {
<div class="card bg-danger-50 border-danger-500">
<div class="flex items-center gap-2 p-4 font-semibold text-danger-600">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
Error
</div>
<p class="px-4 pb-4">{{ error() }}</p>
</div>
}
@if (firewallWarning()) {
<div class="flex items-start gap-3 p-4 bg-warning-50 border border-warning-500 rounded-lg text-warning-600">
<svg class="w-6 h-6 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
<div class="flex-1">
<strong class="block text-sm mb-1">Connection Issues Detected</strong>
<p class="text-sm">Unable to connect to the mining pool. Please check your firewall settings or try a different port.</p>
</div>
<button class="btn btn-ghost btn-xs" (click)="dismissFirewallWarning()">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
}
@if (state().runningMiners.length > 0) {
<!-- Quick Stats Bar -->
<div class="grid grid-cols-4 gap-3 max-sm:grid-cols-2">
<div class="card flex flex-col items-center p-4 hover:bg-surface-50/50 transition-colors">
<svg class="w-6 h-6 mb-2 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
<div class="stat-value">{{ formatHashrate(currentHashrate()) }} {{ getHashrateUnit(currentHashrate()) }}</div>
<div class="stat-label">
@if (minerCount() > 1) {
Total ({{ minerCount() }} workers)
} @else {
Hashrate
}
</div>
</div>
<div class="card flex flex-col items-center p-4 hover:bg-surface-50/50 transition-colors">
<svg class="w-6 h-6 mb-2 text-success-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="stat-value flex items-baseline gap-1">
<span class="text-success-500">{{ acceptedShares() }}</span>
@if (rejectedShares() > 0) {
<span class="text-danger-500 text-sm">/ {{ rejectedShares() }}</span>
}
</div>
<div class="stat-label">Shares{{ rejectedShares() > 0 ? ' (rej)' : '' }}</div>
</div>
<div class="card flex flex-col items-center p-4 hover:bg-surface-50/50 transition-colors">
<svg class="w-6 h-6 mb-2 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<div class="stat-value">{{ formatUptime(uptime()) }}</div>
<div class="stat-label">Uptime</div>
</div>
<div class="card flex flex-col items-center p-4 hover:bg-surface-50/50 transition-colors">
<svg class="w-6 h-6 mb-2 text-warning-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
<div class="stat-value text-sm font-semibold truncate max-w-full">{{ poolName() }}</div>
<div class="stat-label">Pool ({{ poolPing() }}ms)</div>
</div>
</div>
<!-- Chart Section -->
<div class="card flex-1 min-h-[250px] p-4">
<snider-mining-chart></snider-mining-chart>
</div>
<!-- Workers Table (shown when multiple miners are running) -->
@if (minerCount() > 1) {
<div class="card overflow-hidden">
<div class="flex justify-between items-center px-4 py-3 bg-surface-50 border-b border-surface-50/20">
<h3 class="flex items-center gap-2 text-sm font-semibold">
<svg class="w-5 h-5" 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"/>
</svg>
Workers
</h3>
<button class="btn btn-danger btn-sm" (click)="stopAllWorkers()">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>
Stop All
</button>
</div>
<div class="overflow-x-auto">
<table class="table-dense w-full">
<thead>
<tr>
<th>Worker</th>
<th>Hashrate</th>
<th>Shares</th>
<th>Efficiency</th>
<th>Pool</th>
<th>Uptime</th>
<th></th>
</tr>
</thead>
<tbody>
@for (worker of workers(); track worker.name) {
<tr class="worker-row">
<td>
<div class="flex items-center gap-3">
<svg class="w-5 h-5 text-accent-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
</svg>
<div class="flex flex-col">
<span class="font-semibold">{{ worker.name }}</span>
<span class="text-xs text-slate-500">{{ worker.algorithm }}</span>
</div>
</div>
</td>
<td class="min-w-[150px]">
<div class="relative h-6 bg-surface-50 rounded overflow-hidden">
<div class="absolute top-0 left-0 h-full bg-gradient-to-r from-accent-500 to-accent-600 rounded transition-all"
[style.width.%]="getHashratePercent(worker)"></div>
<span class="relative flex items-center justify-center h-full text-xs font-semibold tabular-nums">
{{ formatHashrate(worker.hashrate) }} {{ getHashrateUnit(worker.hashrate) }}
</span>
</div>
</td>
<td class="tabular-nums">
<span class="text-success-500 font-semibold">{{ worker.shares }}</span>
@if (worker.rejected > 0) {
<span class="text-danger-500 text-xs ml-1">({{ worker.rejected }} rej)</span>
}
</td>
<td>
<span class="font-semibold px-2 py-0.5 rounded text-xs"
[class.bg-success-500/20]="getEfficiency(worker) >= 99"
[class.text-success-500]="getEfficiency(worker) >= 99"
[class.bg-warning-500/20]="getEfficiency(worker) < 99 && getEfficiency(worker) >= 95"
[class.text-warning-500]="getEfficiency(worker) < 99 && getEfficiency(worker) >= 95"
[class.bg-danger-500/20]="getEfficiency(worker) < 95"
[class.text-danger-500]="getEfficiency(worker) < 95">
{{ getEfficiency(worker) | number:'1.1-1' }}%
</span>
</td>
<td class="text-slate-400 text-sm truncate max-w-[150px]">{{ worker.pool }}</td>
<td class="tabular-nums text-slate-400">{{ formatUptime(worker.uptime) }}</td>
<td class="text-right">
<button class="btn btn-ghost btn-xs mr-1" (click)="openTerminal(worker.name)" title="View logs">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</button>
<button class="btn btn-ghost btn-xs hover:text-danger-500" (click)="stopWorker(worker.name)" title="Stop worker">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- Controls & Details -->
<div class="flex justify-between items-center px-4 py-3 bg-surface-50 rounded-lg flex-wrap gap-3">
<div class="flex items-center gap-3 flex-wrap">
<span class="badge badge-success flex items-center gap-1">
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
</svg>
{{ minerName() }}
</span>
<span class="px-3 py-1 bg-surface-100 rounded-full text-sm font-medium text-slate-300">{{ algorithm() }}</span>
<span class="px-3 py-1 bg-surface-100 rounded-full text-sm font-medium text-slate-300">Diff: {{ difficulty() | number }}</span>
</div>
<div class="flex gap-2">
@if (minerCount() === 1) {
<button class="btn btn-danger btn-sm" (click)="stopMiner()">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 10a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z"/>
</svg>
Stop Mining
</button>
}
</div>
</div>
<!-- Detailed Stats (collapsible) -->
<details class="card">
<summary class="px-4 py-3 cursor-pointer font-medium hover:bg-surface-50/50 transition-colors">
Detailed Statistics
</summary>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 p-4 border-t border-surface-50/20">
<div>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wide mb-3 pb-2 border-b-2 border-accent-500">Hashrate</h4>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dt class="text-slate-400">10s Average</dt><dd class="text-right font-mono">{{ stats()?.hashrate?.total[0] | number:'1.0-2' }} H/s</dd>
<dt class="text-slate-400">60s Average</dt><dd class="text-right font-mono">{{ stats()?.hashrate?.total[1] | number:'1.0-2' }} H/s</dd>
<dt class="text-slate-400">15m Average</dt><dd class="text-right font-mono">{{ stats()?.hashrate?.total[2] | number:'1.0-2' }} H/s</dd>
</dl>
</div>
<div>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wide mb-3 pb-2 border-b-2 border-accent-500">Shares</h4>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dt class="text-slate-400">Good Shares</dt><dd class="text-right font-mono">{{ stats()?.results?.shares_good }}</dd>
<dt class="text-slate-400">Total Shares</dt><dd class="text-right font-mono">{{ stats()?.results?.shares_total }}</dd>
<dt class="text-slate-400">Avg Time</dt><dd class="text-right font-mono">{{ stats()?.results?.avg_time }}s</dd>
<dt class="text-slate-400">Total Hashes</dt><dd class="text-right font-mono">{{ stats()?.results?.hashes_total | number }}</dd>
</dl>
</div>
<div>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wide mb-3 pb-2 border-b-2 border-accent-500">Connection</h4>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dt class="text-slate-400">Pool</dt><dd class="text-right font-mono truncate">{{ stats()?.connection?.pool }}</dd>
<dt class="text-slate-400">IP</dt><dd class="text-right font-mono">{{ stats()?.connection?.ip }}</dd>
<dt class="text-slate-400">Ping</dt><dd class="text-right font-mono">{{ stats()?.connection?.ping }}ms</dd>
<dt class="text-slate-400">Difficulty</dt><dd class="text-right font-mono">{{ stats()?.connection?.diff | number }}</dd>
</dl>
</div>
<div>
<h4 class="text-sm font-semibold text-slate-400 uppercase tracking-wide mb-3 pb-2 border-b-2 border-accent-500">System</h4>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dt class="text-slate-400">CPU</dt><dd class="text-right font-mono truncate">{{ stats()?.cpu?.brand }}</dd>
<dt class="text-slate-400">Threads</dt><dd class="text-right font-mono">{{ stats()?.cpu?.threads }}</dd>
<dt class="text-slate-400">Algorithm</dt><dd class="text-right font-mono">{{ stats()?.algo }}</dd>
<dt class="text-slate-400">Version</dt><dd class="text-right font-mono">{{ stats()?.version }}</dd>
</dl>
</div>
</div>
</details>
} @else {
<!-- Idle State -->
<div class="flex flex-col items-center justify-center text-center py-12 px-8 flex-1">
<div class="w-20 h-20 flex items-center justify-center bg-surface-50 rounded-full mb-6">
<svg class="w-10 h-10 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
</svg>
</div>
<h3 class="text-2xl font-semibold mb-2">No Miners Running</h3>
<p class="text-slate-400 mb-6">Select a profile and start mining to see real-time statistics.</p>
@if (state().profiles.length > 0) {
<div class="flex flex-col items-center gap-4 w-full max-w-xs">
<select class="select w-full" (change)="onProfileSelect($event)">
<option value="" disabled selected>Select Profile</option>
@for (profile of state().profiles; track profile.id) {
<option [value]="profile.id">{{ profile.name }} ({{ profile.minerType }})</option>
}
</select>
<button class="btn btn-primary w-full" [disabled]="!selectedProfileId()" (click)="startMining()">
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Start Mining
</button>
</div>
} @else {
<p class="text-sm text-slate-500 italic">No profiles configured. Create one in the Profiles section.</p>
}
</div>
}
</div>
<!-- Terminal Modal -->
@if (terminalMinerName) {
<app-terminal-modal
[minerName]="terminalMinerName"
(close)="closeTerminal()">
</app-terminal-modal>
}