feat: Refactor dashboard layout and integrate admin panel functionality
This commit is contained in:
parent
0d531032bf
commit
8f888a3749
4 changed files with 336 additions and 205 deletions
|
|
@ -17,7 +17,7 @@ import (
|
|||
"github.com/adrg/xdg"
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/shirou/gopsutil/v4/mem" // Import mem for memory stats
|
||||
"github.com/shirou/gopsutil/v4/mem"
|
||||
"github.com/swaggo/swag"
|
||||
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
|
|
@ -142,18 +142,26 @@ func (s *Service) handleGetInfo(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
systemInfo.Timestamp = time.Now()
|
||||
vMem, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, systemInfo)
|
||||
}
|
||||
|
||||
// updateInstallationCache performs a live check and updates the cache file.
|
||||
func (s *Service) updateInstallationCache() (*SystemInfo, error) {
|
||||
var allDetails []*InstallationDetails
|
||||
// Always create a complete SystemInfo object
|
||||
systemInfo := &SystemInfo{
|
||||
Timestamp: time.Now(),
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
AvailableCPUCores: runtime.NumCPU(),
|
||||
InstalledMinersInfo: []*InstallationDetails{}, // Initialize as empty slice
|
||||
}
|
||||
|
||||
vMem, err := mem.VirtualMemory()
|
||||
if err == nil {
|
||||
systemInfo.TotalSystemRAMGB = float64(vMem.Total) / (1024 * 1024 * 1024)
|
||||
}
|
||||
|
||||
for _, availableMiner := range s.Manager.ListAvailableMiners() {
|
||||
var miner Miner
|
||||
switch availableMiner.Name {
|
||||
|
|
@ -163,16 +171,7 @@ func (s *Service) updateInstallationCache() (*SystemInfo, error) {
|
|||
continue
|
||||
}
|
||||
details, _ := miner.CheckInstallation()
|
||||
allDetails = append(allDetails, details)
|
||||
}
|
||||
|
||||
systemInfo := &SystemInfo{
|
||||
Timestamp: time.Now(),
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
GoVersion: runtime.Version(),
|
||||
AvailableCPUCores: runtime.NumCPU(),
|
||||
InstalledMinersInfo: allDetails,
|
||||
systemInfo.InstalledMinersInfo = append(systemInfo.InstalledMinersInfo, details)
|
||||
}
|
||||
|
||||
configDir, err := xdg.ConfigFile("lethean-desktop/miners")
|
||||
|
|
|
|||
|
|
@ -38,26 +38,35 @@
|
|||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.miner-list {
|
||||
.dashboard-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
padding-bottom: 1rem; /* Add padding to separate from charts */
|
||||
border-bottom: 1px solid #e0e0e0; /* Visual separator */
|
||||
}
|
||||
|
||||
.miner-summary-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.dashboard-charts {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.miner-item-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
.miner-chart-item {
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.miner-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.miner-name {
|
||||
|
|
@ -92,3 +101,32 @@ wa-button {
|
|||
wa-spinner {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Admin Panel specific styles (moved from app.css) */
|
||||
.admin-panel {
|
||||
padding: 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin-top: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.admin-title {
|
||||
margin-top: 0;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.path-list ul {
|
||||
list-style: none;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
border-radius: 0.25rem;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.path-list li {
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,42 @@
|
|||
<div class="mining-dashboard">
|
||||
|
||||
<!-- Initial Loading State -->
|
||||
@if (systemInfo === null && !needsSetup) {
|
||||
<div class="centered-container">
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header">Loading...</div>
|
||||
<wa-spinner style="font-size: 3rem; margin-top: 1rem;"></wa-spinner>
|
||||
</wa-card>
|
||||
<!-- The main container card that is ALWAYS present -->
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header" class="card-header">
|
||||
<div class="header-title">
|
||||
<wa-icon name="cpu" style="font-size: 1.5rem;"></wa-icon>
|
||||
<!-- Title changes based on state -->
|
||||
@if (needsSetup) {
|
||||
<span>Setup Required</span>
|
||||
} @else {
|
||||
<span>Mining Control</span>
|
||||
}
|
||||
</div>
|
||||
@if (!needsSetup) {
|
||||
<wa-button variant="neutral" appearance="plain" (click)="toggleAdminPanel()">
|
||||
<wa-icon name="gear" label="Admin Panel"></wa-icon>
|
||||
</wa-button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Setup Wizard: Shown when needsSetup is true -->
|
||||
@if (needsSetup) {
|
||||
<div class="centered-container">
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header">
|
||||
<wa-icon name="wand-magic-sparkles" style="font-size: 2rem;"></wa-icon>
|
||||
Welcome! Let's Get Started
|
||||
</div>
|
||||
<!-- Initial Loading State -->
|
||||
@if (systemInfo === null && !needsSetup) {
|
||||
<div class="centered-container">
|
||||
<wa-spinner style="font-size: 3rem; margin-top: 1rem;"></wa-spinner>
|
||||
</div>
|
||||
} @else if (!apiAvailable) {
|
||||
<!-- API Not Available State -->
|
||||
<div class="centered-container">
|
||||
<p>API Not Available. Please ensure the mining service is running.</p>
|
||||
<wa-button (click)="checkSystemState()">
|
||||
<wa-icon name="arrow-clockwise" slot="prefix"></wa-icon>
|
||||
Retry
|
||||
</wa-button>
|
||||
</div>
|
||||
} @else if (apiAvailable && needsSetup) {
|
||||
<!-- Setup Wizard Content (re-integrated) -->
|
||||
<div class="setup-wizard">
|
||||
<p>To begin, please install a miner from the list below.</p>
|
||||
|
||||
<h4>Available Miners</h4>
|
||||
<div class="miner-list">
|
||||
@for (miner of manageableMiners; track miner.name) {
|
||||
|
|
@ -44,20 +61,9 @@
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div slot="footer">
|
||||
<wa-button (click)="checkSystemState()">
|
||||
<wa-icon name="arrow-clockwise" slot="prefix"></wa-icon>
|
||||
Refresh Status
|
||||
</wa-button>
|
||||
</div>
|
||||
</wa-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Main Content: API is Available and initial load is complete -->
|
||||
@if (systemInfo !== null && apiAvailable && !needsSetup) {
|
||||
<div>
|
||||
</div>
|
||||
} @else if (apiAvailable && !needsSetup) {
|
||||
<!-- Main Content (when not in setup mode) -->
|
||||
@if (error) {
|
||||
<wa-card class="card-error">
|
||||
<div slot="header">
|
||||
|
|
@ -68,147 +74,155 @@
|
|||
</wa-card>
|
||||
}
|
||||
|
||||
<wa-card class="card-overview">
|
||||
<div slot="header" class="card-header">
|
||||
<div class="header-title">
|
||||
<wa-icon name="cpu" style="font-size: 1.5rem;"></wa-icon>
|
||||
Mining Control
|
||||
</div>
|
||||
<wa-button variant="neutral" appearance="plain" (click)="toggleAdminPanel()">
|
||||
<wa-icon name="gear" label="Admin Panel"></wa-icon>
|
||||
</wa-button>
|
||||
</div>
|
||||
<!-- Admin Panel Content (re-integrated) -->
|
||||
@if (showAdminPanel) {
|
||||
<div class="admin-panel">
|
||||
<h3 class="admin-title">Admin Panel</h3>
|
||||
|
||||
<!-- Admin Panel -->
|
||||
@if (showAdminPanel) {
|
||||
<div class="admin-panel">
|
||||
<h3 class="admin-title">Admin Panel</h3>
|
||||
|
||||
<h4>Manage Miners</h4>
|
||||
<div class="miner-list">
|
||||
@for (miner of manageableMiners; track miner.name) {
|
||||
<div class="miner-item">
|
||||
<span>{{ miner.name }}</span>
|
||||
@if (miner.is_installed) {
|
||||
<wa-button
|
||||
variant="danger"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'uninstall-' + miner.name"
|
||||
(click)="uninstallMiner(miner.name)">
|
||||
@if (actionInProgress === 'uninstall-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="trash" slot="prefix"></wa-icon>
|
||||
Uninstall
|
||||
}
|
||||
</wa-button>
|
||||
} @else {
|
||||
<wa-button
|
||||
variant="success"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'install-' + miner.name"
|
||||
(click)="installMiner(miner.name)">
|
||||
@if (actionInProgress === 'install-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="download" slot="prefix"></wa-icon>
|
||||
Install
|
||||
}
|
||||
</wa-button>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="miner-item">
|
||||
<span>Could not load available miners.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h4>Antivirus Whitelist Paths</h4>
|
||||
<div class="path-list">
|
||||
<p>To prevent antivirus software from interfering, please add the following paths to your exclusion list:</p>
|
||||
<ul>
|
||||
@for (path of whitelistPaths; track path) {
|
||||
<li><code>{{ path }}</code></li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Miner Control List -->
|
||||
@if (installedMiners.length > 0) {
|
||||
<h4>Manage Miners</h4>
|
||||
<div class="miner-list">
|
||||
@for (miner of installedMiners; track miner.path) {
|
||||
<div class="miner-item-container">
|
||||
<div class="miner-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>
|
||||
@for (miner of manageableMiners; track miner.name) {
|
||||
<div class="miner-item">
|
||||
<span>{{ miner.name }}</span>
|
||||
@if (miner.is_installed) {
|
||||
<wa-button
|
||||
variant="danger"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'uninstall-' + miner.name"
|
||||
(click)="uninstallMiner(miner.name)">
|
||||
@if (actionInProgress === 'uninstall-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="trash" slot="prefix"></wa-icon>
|
||||
Uninstall
|
||||
}
|
||||
</wa-button>
|
||||
} @else {
|
||||
<wa-button
|
||||
variant="success"
|
||||
size="small"
|
||||
[disabled]="actionInProgress === 'install-' + miner.name"
|
||||
(click)="installMiner(miner.name)">
|
||||
@if (actionInProgress === 'install-' + miner.name) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="download" slot="prefix"></wa-icon>
|
||||
Install
|
||||
}
|
||||
</wa-button>
|
||||
}
|
||||
</div>
|
||||
} @empty {
|
||||
<div class="miner-item">
|
||||
<span>Could not load available miners.</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isMinerRunning(miner)) {
|
||||
<h4>Antivirus Whitelist Paths</h4>
|
||||
<div class="path-list">
|
||||
<p>To prevent antivirus software from interfering, please add the following paths to your exclusion list:</p>
|
||||
<ul>
|
||||
@for (path of whitelistPaths; track path) {
|
||||
<li><code>{{ path }}</code></li>
|
||||
} @empty {
|
||||
<li>No paths to display. Install a miner to see required paths.</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
} @else { <!-- Only show dashboard content if admin panel is not open -->
|
||||
<!-- Miner Summary and Controls -->
|
||||
@if (installedMiners.length > 0) {
|
||||
<div class="dashboard-summary">
|
||||
@for (miner of 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 {
|
||||
<div class="start-buttons">
|
||||
<wa-button
|
||||
variant="danger"
|
||||
[disabled]="actionInProgress === 'stop-' + miner.type"
|
||||
(click)="stopMiner(miner)">
|
||||
@if (actionInProgress === 'stop-' + miner.type) {
|
||||
variant="secondary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner, true)">
|
||||
@if (actionInProgress === 'start-' + miner.type && showStartOptionsFor !== miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="stop-circle" slot="prefix"></wa-icon>
|
||||
Stop
|
||||
<wa-icon name="play-circle" slot="prefix"></wa-icon>
|
||||
Start Last Config
|
||||
}
|
||||
</wa-button>
|
||||
} @else {
|
||||
<div class="start-buttons">
|
||||
<wa-button
|
||||
variant="secondary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner, true)">
|
||||
@if (actionInProgress === 'start-' + miner.type && showStartOptionsFor !== miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="play-circle" slot="prefix"></wa-icon>
|
||||
Start Last Config
|
||||
}
|
||||
</wa-button>
|
||||
<wa-button
|
||||
variant="primary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="toggleStartOptions(miner.type)">
|
||||
<wa-icon name="gear" slot="prefix"></wa-icon>
|
||||
New Config
|
||||
</wa-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showStartOptionsFor === miner.type) {
|
||||
<div class="start-options">
|
||||
<wa-input label="Pool Address" [(ngModel)]="poolAddress" name="poolAddress"></wa-input>
|
||||
<wa-input label="Wallet Address" [(ngModel)]="walletAddress" name="walletAddress"></wa-input>
|
||||
<wa-button
|
||||
variant="success"
|
||||
variant="primary"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner)">
|
||||
@if (actionInProgress === 'start-' + miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="rocket-launch" slot="prefix"></wa-icon>
|
||||
Confirm & Start
|
||||
}
|
||||
(click)="toggleStartOptions(miner.type)">
|
||||
<wa-icon name="gear" slot="prefix"></wa-icon>
|
||||
New Config
|
||||
</wa-button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showStartOptionsFor === miner.type) {
|
||||
<div class="start-options">
|
||||
<wa-input label="Pool Address" [(ngModel)]="poolAddress" name="poolAddress"></wa-input>
|
||||
<wa-input label="Wallet Address" [(ngModel)]="walletAddress" name="walletAddress"></wa-input>
|
||||
<wa-button
|
||||
variant="success"
|
||||
[disabled]="actionInProgress === 'start-' + miner.type"
|
||||
(click)="startMiner(miner)">
|
||||
@if (actionInProgress === 'start-' + miner.type) {
|
||||
<wa-spinner class="button-spinner"></wa-spinner>
|
||||
} @else {
|
||||
<wa-icon name="rocket-launch" slot="prefix"></wa-icon>
|
||||
Confirm & Start
|
||||
}
|
||||
</wa-button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</wa-card>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Charts for Running Miners -->
|
||||
<div class="dashboard-charts">
|
||||
@for (miner of installedMiners; track miner.path) {
|
||||
@if (isMinerRunning(miner) && chartOptionsMap.has(getRunningMinerInstance(miner).name)) {
|
||||
<div class="miner-chart-item">
|
||||
<highcharts-chart
|
||||
[constructorType]="chartConstructor"
|
||||
[options]="chartOptionsMap.get(getRunningMinerInstance(miner).name)!"
|
||||
[(update)]="updateFlag"
|
||||
[oneToOne]="oneToOneFlag"
|
||||
class="chart"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="centered-container">
|
||||
<p>No miners installed. Open the Admin Panel to install one.</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</wa-card>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
Component,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ElementRef,
|
||||
ViewEncapsulation,
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
|
|
@ -8,8 +9,10 @@ import {
|
|||
import { HttpClient, HttpClientModule, HttpErrorResponse } from '@angular/common/http';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { of, forkJoin } from 'rxjs';
|
||||
import { switchMap, catchError, map } from 'rxjs/operators';
|
||||
import { of, forkJoin, Subscription, interval } from 'rxjs';
|
||||
import { switchMap, catchError, map, startWith } from 'rxjs/operators';
|
||||
import { HighchartsChartComponent, ChartConstructorType } from 'highcharts-angular'; // Corrected import
|
||||
import * as Highcharts from 'highcharts';
|
||||
|
||||
// Import Web Awesome components
|
||||
import "@awesome.me/webawesome/dist/webawesome.js";
|
||||
|
|
@ -20,7 +23,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js';
|
|||
import '@awesome.me/webawesome/dist/components/spinner/spinner.js';
|
||||
import '@awesome.me/webawesome/dist/components/input/input.js';
|
||||
|
||||
// Define interfaces for our data structures
|
||||
// Define interfaces
|
||||
interface InstallationDetails {
|
||||
is_installed: boolean;
|
||||
version: string;
|
||||
|
|
@ -35,16 +38,21 @@ interface AvailableMiner {
|
|||
description: string;
|
||||
}
|
||||
|
||||
interface HashratePoint {
|
||||
timestamp: string;
|
||||
hashrate: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'snider-mining-dashboard',
|
||||
standalone: true,
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
imports: [CommonModule, HttpClientModule, FormsModule],
|
||||
imports: [CommonModule, HttpClientModule, FormsModule, HighchartsChartComponent], // Corrected import
|
||||
templateUrl: './app.html',
|
||||
styleUrls: ["app.css"],
|
||||
encapsulation: ViewEncapsulation.ShadowDom
|
||||
})
|
||||
export class MiningDashboardElementComponent implements OnInit {
|
||||
export class MiningDashboardElementComponent implements OnInit, OnDestroy {
|
||||
apiBaseUrl: string = 'http://localhost:9090/api/v1/mining';
|
||||
|
||||
// State management
|
||||
|
|
@ -60,6 +68,14 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
installedMiners: InstallationDetails[] = [];
|
||||
whitelistPaths: string[] = [];
|
||||
|
||||
// Charting
|
||||
Highcharts: typeof Highcharts = Highcharts;
|
||||
chartOptionsMap: Map<string, Highcharts.Options> = new Map();
|
||||
chartConstructor: ChartConstructorType = 'chart';
|
||||
updateFlag: boolean = false;
|
||||
oneToOneFlag: boolean = true;
|
||||
private statsSubscription: Subscription | undefined;
|
||||
|
||||
// Form inputs
|
||||
poolAddress: string = 'pool.hashvault.pro:80';
|
||||
walletAddress: string = '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H';
|
||||
|
|
@ -71,6 +87,10 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
this.checkSystemState();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stopStatsPolling();
|
||||
}
|
||||
|
||||
private handleError(err: HttpErrorResponse, defaultMessage: string) {
|
||||
console.error(err);
|
||||
this.actionInProgress = null;
|
||||
|
|
@ -92,7 +112,6 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
switchMap(({ available, info }) => {
|
||||
this.apiAvailable = true;
|
||||
this.systemInfo = info;
|
||||
|
||||
const trulyInstalledMiners = (info.installed_miners_info || []).filter((m: InstallationDetails) => m.is_installed);
|
||||
|
||||
if (trulyInstalledMiners.length === 0) {
|
||||
|
|
@ -100,21 +119,14 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
this.manageableMiners = available.map(availMiner => ({ ...availMiner, is_installed: false }));
|
||||
this.installedMiners = [];
|
||||
this.runningMiners = [];
|
||||
this.stopStatsPolling();
|
||||
return of(null);
|
||||
}
|
||||
|
||||
this.needsSetup = false;
|
||||
const installedMap = new Map<string, InstallationDetails>(
|
||||
(info.installed_miners_info || []).map((m: InstallationDetails) => [this.getMinerType(m), m])
|
||||
);
|
||||
|
||||
this.manageableMiners = available.map(availMiner => ({
|
||||
...availMiner,
|
||||
is_installed: installedMap.get(availMiner.name)?.is_installed ?? false,
|
||||
}));
|
||||
|
||||
const installedMap = new Map<string, InstallationDetails>((info.installed_miners_info || []).map((m: InstallationDetails) => [this.getMinerType(m), m]));
|
||||
this.manageableMiners = available.map(availMiner => ({ ...availMiner, is_installed: installedMap.get(availMiner.name)?.is_installed ?? false }));
|
||||
this.installedMiners = trulyInstalledMiners.map((m: InstallationDetails) => ({ ...m, type: this.getMinerType(m) }));
|
||||
|
||||
this.updateWhitelistPaths();
|
||||
return this.fetchRunningMiners();
|
||||
}),
|
||||
|
|
@ -144,7 +156,15 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
|
||||
fetchRunningMiners() {
|
||||
return this.http.get<any[]>(`${this.apiBaseUrl}/miners`).pipe(
|
||||
map(miners => { this.runningMiners = miners; this.updateWhitelistPaths(); }),
|
||||
map(miners => {
|
||||
this.runningMiners = miners;
|
||||
this.updateWhitelistPaths();
|
||||
if (this.runningMiners.length > 0 && !this.statsSubscription) {
|
||||
this.startStatsPolling();
|
||||
} else if (this.runningMiners.length === 0) {
|
||||
this.stopStatsPolling();
|
||||
}
|
||||
}),
|
||||
catchError(err => {
|
||||
this.handleError(err, 'Could not fetch running miners');
|
||||
this.runningMiners = [];
|
||||
|
|
@ -153,6 +173,66 @@ export class MiningDashboardElementComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
startStatsPolling(): void {
|
||||
this.stopStatsPolling();
|
||||
this.statsSubscription = interval(5000).pipe(
|
||||
startWith(0),
|
||||
switchMap(() => forkJoin(
|
||||
this.runningMiners.map(miner =>
|
||||
this.http.get<HashratePoint[]>(`${this.apiBaseUrl}/miners/${miner.name}/hashrate-history`).pipe(
|
||||
map(history => ({ name: miner.name, history })),
|
||||
catchError(() => of({ name: miner.name, history: [] }))
|
||||
)
|
||||
)
|
||||
))
|
||||
).subscribe(results => {
|
||||
results.forEach(result => {
|
||||
this.updateChart(result.name, result.history);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
stopStatsPolling(): void {
|
||||
if (this.statsSubscription) {
|
||||
this.statsSubscription.unsubscribe();
|
||||
this.statsSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
updateChart(minerName: string, history: HashratePoint[]): void {
|
||||
const chartData = history.map(point => [new Date(point.timestamp).getTime(), point.hashrate]);
|
||||
let options = this.chartOptionsMap.get(minerName);
|
||||
|
||||
if (!options) {
|
||||
options = this.createChartOptions(minerName);
|
||||
this.chartOptionsMap.set(minerName, options);
|
||||
}
|
||||
|
||||
// Directly update the data property of the series
|
||||
if (options.series && options.series.length > 0) {
|
||||
const series = options.series[0] as Highcharts.SeriesLineOptions;
|
||||
series.data = chartData;
|
||||
}
|
||||
|
||||
// Trigger change detection by creating a new options object reference
|
||||
// This is the correct way to make Highcharts detect updates when oneToOne is true
|
||||
this.chartOptionsMap.set(minerName, { ...options });
|
||||
|
||||
// Toggle updateFlag to force re-render
|
||||
this.updateFlag = !this.updateFlag;
|
||||
}
|
||||
|
||||
createChartOptions(minerName: string): Highcharts.Options {
|
||||
return {
|
||||
chart: { type: 'spline' },
|
||||
title: { text: `${minerName} Hashrate` },
|
||||
xAxis: { type: 'datetime', title: { text: 'Time' } },
|
||||
yAxis: { title: { text: 'Hashrate (H/s)' }, min: 0 },
|
||||
series: [{ name: 'Hashrate', type: 'line', data: [] }],
|
||||
credits: { enabled: false },
|
||||
};
|
||||
}
|
||||
|
||||
private updateWhitelistPaths() {
|
||||
const paths = new Set<string>();
|
||||
this.installedMiners.forEach(miner => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue