import { Component, inject, computed, signal, effect, OnDestroy } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { MinerService } from '../../miner.service'; import { TerminalModalComponent } from '../../terminal-modal.component'; export interface WorkerStats { name: string; hashrate: number; shares: number; rejected: number; uptime: number; pool: string; algorithm: string; cpu?: string; threads?: number; } @Component({ selector: 'app-workers', standalone: true, imports: [CommonModule, FormsModule, TerminalModalComponent], template: `
@if (firewallWarning()) {
Miner running but no hashrate detected. This may indicate a firewall blocking the connection.
}
@if (workers().length > 0) { }
@if (workers().length > 0) {
@for (worker of workers(); track worker.name) { }
Worker Hashrate Shares Efficiency Uptime Pool Actions
{{ worker.name }} @if (worker.algorithm) { {{ worker.algorithm }} }
{{ formatHashrate(worker.hashrate) }} {{ getHashrateUnit(worker.hashrate) }}
{{ worker.shares }} @if (worker.rejected > 0) { / {{ worker.rejected }} } {{ getEfficiency(worker).toFixed(1) }}% {{ formatUptime(worker.uptime) }} {{ worker.pool }}
} @else {

No Active Workers

Select a profile and start mining to see workers here.

}
@if (terminalMinerName) { } `, styles: [` .workers-page { display: flex; flex-direction: column; gap: 1rem; } .warning-banner { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; background: rgb(245 158 11 / 0.1); border: 1px solid rgb(245 158 11 / 0.3); border-radius: 0.5rem; color: var(--color-warning-500); font-size: 0.875rem; } .warning-banner span { flex: 1; } .warning-actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; } .view-logs-btn { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem; background: rgb(0 212 255 / 0.15); border: 1px solid var(--color-accent-500); border-radius: 0.25rem; color: var(--color-accent-500); cursor: pointer; font-size: 0.75rem; transition: all 0.15s ease; } .view-logs-btn:hover { background: rgb(0 212 255 / 0.25); } .warning-banner .dismiss-btn { padding: 0.25rem 0.75rem; background: transparent; border: 1px solid currentColor; border-radius: 0.25rem; color: inherit; cursor: pointer; font-size: 0.75rem; } .actions-bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; } .profile-selector { display: flex; align-items: center; gap: 0.5rem; } .profile-select { min-width: 200px; padding: 0.5rem 0.75rem; background: var(--color-surface-200); border: 1px solid rgb(37 37 66 / 0.3); border-radius: 0.375rem; color: white; font-size: 0.875rem; } .profile-select:focus { outline: none; border-color: var(--color-accent-500); } .btn { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 1rem; border-radius: 0.375rem; font-size: 0.875rem; font-weight: 500; cursor: pointer; transition: all 0.15s ease; border: none; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: var(--color-accent-500); color: #0f0f1a; } .btn-primary:hover:not(:disabled) { background: rgb(0 212 255 / 0.8); } .btn-danger { background: rgb(239 68 68 / 0.2); color: var(--color-danger-500); } .btn-danger:hover:not(:disabled) { background: rgb(239 68 68 / 0.3); } .workers-table-container { background: var(--color-surface-100); border-radius: 0.5rem; border: 1px solid rgb(37 37 66 / 0.2); overflow: hidden; } .workers-table { width: 100%; border-collapse: collapse; } .workers-table th { padding: 0.75rem 1rem; text-align: left; font-size: 0.75rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; background: var(--color-surface-200); border-bottom: 1px solid rgb(37 37 66 / 0.2); } .workers-table td { padding: 0.75rem 1rem; font-size: 0.875rem; color: #e2e8f0; border-bottom: 1px solid rgb(37 37 66 / 0.1); } .workers-table tbody tr:hover { background: rgb(37 37 66 / 0.2); } .text-right { text-align: right; } .text-center { text-align: center; } .tabular-nums { font-family: var(--font-family-mono); font-variant-numeric: tabular-nums; } .worker-name { display: flex; align-items: center; gap: 0.5rem; } .status-dot { width: 8px; height: 8px; border-radius: 50%; } .status-dot.online { background: var(--color-success-500); box-shadow: 0 0 6px var(--color-success-500); } .algo-badge { padding: 0.125rem 0.375rem; background: rgb(0 212 255 / 0.1); border-radius: 0.25rem; font-size: 0.6875rem; color: var(--color-accent-500); text-transform: uppercase; } .hashrate-value { font-weight: 600; color: white; } .hashrate-unit { margin-left: 0.25rem; font-size: 0.75rem; color: #94a3b8; } .hashrate-bar { margin-top: 0.25rem; height: 3px; background: rgb(37 37 66 / 0.5); border-radius: 2px; overflow: hidden; } .hashrate-fill { height: 100%; background: var(--color-accent-500); transition: width 0.3s ease; } .shares-good { color: var(--color-success-500); } .shares-rejected { color: var(--color-danger-500); font-size: 0.75rem; } .pool-cell { max-width: 150px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .actions-cell { display: flex; align-items: center; justify-content: center; gap: 0.5rem; } .icon-btn { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: transparent; border: none; border-radius: 0.25rem; color: #94a3b8; cursor: pointer; transition: all 0.15s ease; } .icon-btn:hover { background: rgb(37 37 66 / 0.5); color: white; } .icon-btn-danger:hover { background: rgb(239 68 68 / 0.2); color: var(--color-danger-500); } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 4rem 2rem; text-align: center; } .empty-state h3 { margin-top: 1rem; font-size: 1.125rem; font-weight: 600; color: white; } .empty-state p { margin-top: 0.5rem; color: #64748b; font-size: 0.875rem; } .empty-icon { width: 64px; height: 64px; color: #475569; } `] }) export class WorkersComponent implements OnDestroy { private minerService = inject(MinerService); state = this.minerService.state; selectedProfileId = signal(null); terminalMinerName: string | null = null; firewallWarningDismissed = signal(false); firewallWarning = computed(() => { if (this.firewallWarningDismissed()) return false; const miners = this.state().runningMiners; if (miners.length === 0) return false; for (const miner of miners) { const uptime = miner.full_stats?.uptime || 0; const hashrate = miner.full_stats?.hashrate?.total?.[0] || 0; if (uptime >= 15 && hashrate === 0) return true; } return false; }); constructor() { effect(() => { if (this.state().runningMiners.length === 0) { this.firewallWarningDismissed.set(false); } }); } ngOnDestroy() {} workers = computed(() => { return this.state().runningMiners.map(miner => { const stats = miner.full_stats; return { name: miner.name, hashrate: stats?.hashrate?.total?.[0] || 0, shares: stats?.results?.shares_good || 0, rejected: (stats?.results?.shares_total || 0) - (stats?.results?.shares_good || 0), uptime: stats?.uptime || 0, pool: stats?.connection?.pool?.split(':')[0] || 'N/A', algorithm: stats?.algo || '', cpu: stats?.cpu?.brand, threads: stats?.cpu?.threads }; }); }); totalHashrate = computed(() => this.workers().reduce((sum, w) => sum + w.hashrate, 0)); dismissFirewallWarning() { this.firewallWarningDismissed.set(true); } viewAffectedMinerLogs() { // Find the first miner with 0 hashrate but uptime >= 15s (the firewall-affected one) const miners = this.state().runningMiners; for (const miner of miners) { const uptime = miner.full_stats?.uptime || 0; const hashrate = miner.full_stats?.hashrate?.total?.[0] || 0; if (uptime >= 15 && hashrate === 0) { this.openTerminal(miner.name); return; } } // Fallback: open first miner if any if (miners.length > 0) { this.openTerminal(miners[0].name); } } onProfileSelect(event: Event) { const select = event.target as HTMLSelectElement; this.selectedProfileId.set(select.value); } startMining() { const profileId = this.selectedProfileId(); if (profileId) { this.minerService.startMiner(profileId).subscribe({ error: (err) => console.error('Failed to start miner:', err) }); } } stopWorker(name: string) { this.minerService.stopMiner(name).subscribe({ error: (err) => console.error(`Failed to stop ${name}:`, err) }); } stopAllWorkers() { this.workers().forEach(w => { this.minerService.stopMiner(w.name).subscribe({ error: (err) => console.error(`Failed to stop ${w.name}:`, err) }); }); } formatHashrate(hashrate: number): string { if (hashrate >= 1000000000) return (hashrate / 1000000000).toFixed(2); if (hashrate >= 1000000) return (hashrate / 1000000).toFixed(2); if (hashrate >= 1000) return (hashrate / 1000).toFixed(2); return hashrate.toFixed(0); } getHashrateUnit(hashrate: number): string { if (hashrate >= 1000000000) return 'GH/s'; if (hashrate >= 1000000) return 'MH/s'; if (hashrate >= 1000) return 'kH/s'; return 'H/s'; } formatUptime(seconds: number): string { if (seconds < 60) return `${seconds}s`; if (seconds < 3600) { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return `${mins}m ${secs}s`; } const hours = Math.floor(seconds / 3600); const mins = Math.floor((seconds % 3600) / 60); return `${hours}h ${mins}m`; } getHashratePercent(worker: WorkerStats): number { const total = this.totalHashrate(); if (total === 0) return 0; return (worker.hashrate / total) * 100; } getEfficiency(worker: WorkerStats): number { const total = worker.shares + worker.rejected; if (total === 0) return 100; return (worker.shares / total) * 100; } openTerminal(minerName: string) { this.terminalMinerName = minerName; } closeTerminal() { this.terminalMinerName = null; } }