From a513ba4670db4b239ab94d15e9314deed1631efe Mon Sep 17 00:00:00 2001 From: Snider Date: Thu, 11 Dec 2025 14:36:08 +0000 Subject: [PATCH] feat: Rename checkSystemState to forceRefreshState and refactor polling logic for improved clarity --- ui/src/app/app.html | 2 +- ui/src/app/app.ts | 4 +- ui/src/app/miner.service.ts | 243 +++++++++++++++++++----------------- 3 files changed, 132 insertions(+), 117 deletions(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 2605a78..8cf8c95 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -7,7 +7,7 @@ } @else if (!state().apiAvailable) {

API Not Available. Please ensure the mining service is running.

- + Retry diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index a4b71d7..bcb08cc 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -28,7 +28,7 @@ export class SniderMining { minerService = inject(MinerService); state = this.minerService.state; - checkSystemState() { - this.minerService.checkSystemState(); + forceRefreshState() { + this.minerService.forceRefreshState(); } } diff --git a/ui/src/app/miner.service.ts b/ui/src/app/miner.service.ts index e3d777e..8a6c8cc 100644 --- a/ui/src/app/miner.service.ts +++ b/ui/src/app/miner.service.ts @@ -1,9 +1,9 @@ import { Injectable, OnDestroy, signal, computed } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { of, forkJoin, Subscription, interval } from 'rxjs'; -import { switchMap, catchError, map, startWith, tap } from 'rxjs/operators'; +import { switchMap, catchError, map, tap } from 'rxjs/operators'; -// Define interfaces +// --- Interfaces --- export interface InstallationDetails { is_installed: boolean; version: string; @@ -46,8 +46,9 @@ export interface SystemState { }) export class MinerService implements OnDestroy { private apiBaseUrl = 'http://localhost:9090/api/v1/mining'; + private pollingSubscription?: Subscription; - // State Signals + // --- State Signals --- public state = signal({ needsSetup: false, apiAvailable: true, @@ -59,180 +60,194 @@ export class MinerService implements OnDestroy { profiles: [] }); + // Separate signal for hashrate history as it updates frequently public hashrateHistory = signal>(new Map()); - // Computed signals for convenience (optional, but helpful for components) + // --- Computed Signals for easy access in components --- public runningMiners = computed(() => this.state().runningMiners); public installedMiners = computed(() => this.state().installedMiners); public apiAvailable = computed(() => this.state().apiAvailable); public profiles = computed(() => this.state().profiles); - private pollingSubscription: Subscription | undefined; - constructor(private http: HttpClient) { - // Initial check - this.checkSystemState(); - // Start polling for system state every 5 seconds for chart updates - this.pollingSubscription = interval(5000).subscribe(() => this.checkSystemState()); + this.forceRefreshState(); + this.startPollingLive_Data(); } ngOnDestroy(): void { - if (this.pollingSubscription) { - this.pollingSubscription.unsubscribe(); - } + this.stopPolling(); } - checkSystemState() { + // --- Data Loading and Polling Logic --- + + /** + * Loads all system data. Can be called to force a full refresh. + */ + public forceRefreshState() { forkJoin({ available: this.getAvailableMiners().pipe(catchError(() => of([]))), info: this.getSystemInfo().pipe(catchError(() => of({ installed_miners_info: [] }))), - running: this.getRunningMiners().pipe(catchError(() => of([]))), // This endpoint contains the history + running: this.getRunningMiners().pipe(catchError(() => of([]))), profiles: this.getProfiles().pipe(catchError(() => of([]))) }).pipe( - map(({ available, info, running, profiles }) => { - const installedMap = new Map(); - - (info.installed_miners_info || []).forEach((m: InstallationDetails) => { - if (m.is_installed) { - const type = this.getMinerType(m); - installedMap.set(type, { ...m, type }); - } - }); - - running.forEach((miner: any) => { - const type = miner.name.split('-')[0]; - if (!installedMap.has(type)) { - installedMap.set(type, { - is_installed: true, - version: 'unknown (running)', - path: 'unknown (running)', - miner_binary: 'unknown (running)', - type: type, - } as InstallationDetails); - } - }); - - const allInstalledMiners = Array.from(installedMap.values()); - - // Populate hashrate history directly from the running miners data - const newHistory = new Map(); - running.forEach((miner: any) => { - if (miner.hashrateHistory) { - newHistory.set(miner.name, miner.hashrateHistory); - } - }); - this.hashrateHistory.set(newHistory); - - if (allInstalledMiners.length === 0) { - this.state.set({ - needsSetup: true, - apiAvailable: true, - error: null, - systemInfo: info, - manageableMiners: available.map(availMiner => ({ ...availMiner, is_installed: false })), - installedMiners: [], - runningMiners: [], - profiles: profiles - }); - return; - } - - const manageableMiners = available.map(availMiner => ({ - ...availMiner, - is_installed: installedMap.has(availMiner.name), - })); - - this.state.set({ - needsSetup: false, - apiAvailable: true, - error: null, - systemInfo: info, - manageableMiners, - installedMiners: allInstalledMiners, - runningMiners: running, - profiles: profiles - }); - }), - catchError(err => { - console.error('API not available or needs setup:', err); - this.hashrateHistory.set(new Map()); // Clear history on error - this.state.set({ - needsSetup: false, - apiAvailable: false, - error: 'Failed to connect to the mining API.', - systemInfo: {}, - manageableMiners: [], - installedMiners: [], - runningMiners: [], - profiles: [] - }); - return of(null); - }) - ).subscribe(); + map(({ available, info, running, profiles }) => this.processSystemState(available, info, running, profiles)), + catchError(err => this.handleApiError(err)) + ).subscribe(initialState => { + if (initialState) { + this.state.set(initialState); + this.updateHashrateHistory(initialState.runningMiners); + } + }); } - getAvailableMiners() { - return this.http.get(`${this.apiBaseUrl}/miners/available`); + /** + * Starts a polling interval to fetch only live data (running miners and hashrates). + */ + private startPollingLive_Data() { + this.pollingSubscription = interval(5000).pipe( + switchMap(() => this.getRunningMiners().pipe(catchError(() => of([])))) + ).subscribe(runningMiners => { + this.state.update(s => ({ ...s, runningMiners })); + this.updateHashrateHistory(runningMiners); + }); } - getSystemInfo() { - return this.http.get(`${this.apiBaseUrl}/info`); + private stopPolling() { + this.pollingSubscription?.unsubscribe(); } - getRunningMiners() { - return this.http.get(`${this.apiBaseUrl}/miners`); + /** + * Refreshes only the list of profiles. Called after create, update, or delete. + */ + private refreshProfiles() { + this.getProfiles().pipe(catchError(() => of(this.state().profiles))).subscribe(profiles => { + this.state.update(s => ({ ...s, profiles })); + }); } - getMinerHashrateHistory(minerName: string) { - return this.http.get(`${this.apiBaseUrl}/miners/${minerName}/hashrate-history`); + /** + * Refreshes system information, typically after installing or uninstalling a miner. + */ + private refreshSystemInfo() { + forkJoin({ + available: this.getAvailableMiners().pipe(catchError(() => of([]))), + info: this.getSystemInfo().pipe(catchError(() => of({ installed_miners_info: [] }))) + }).subscribe(({ available, info }) => { + const { manageableMiners, installedMiners: allInstalledMiners } = this.processStaticMinerInfo(available, info); + this.state.update(s => ({ ...s, manageableMiners, installedMiners: allInstalledMiners, systemInfo: info })); + }); } + // --- Public API Methods for Components --- + installMiner(minerType: string) { return this.http.post(`${this.apiBaseUrl}/miners/${minerType}/install`, {}).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + tap(() => setTimeout(() => this.refreshSystemInfo(), 1000)) ); } uninstallMiner(minerType: string) { return this.http.delete(`${this.apiBaseUrl}/miners/${minerType}/uninstall`).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + tap(() => setTimeout(() => this.refreshSystemInfo(), 1000)) ); } startMiner(profileId: string) { return this.http.post(`${this.apiBaseUrl}/profiles/${profileId}/start`, {}).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + // An immediate poll for running miners will be triggered by the interval soon enough ); } stopMiner(minerName: string) { return this.http.delete(`${this.apiBaseUrl}/miners/${minerName}`).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + // An immediate poll for running miners will be triggered by the interval soon enough ); } - getProfiles() { - return this.http.get(`${this.apiBaseUrl}/profiles`); - } - createProfile(profile: MiningProfile) { return this.http.post(`${this.apiBaseUrl}/profiles`, profile).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + tap(() => this.refreshProfiles()) ); } updateProfile(profile: MiningProfile) { return this.http.put(`${this.apiBaseUrl}/profiles/${profile.id}`, profile).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + tap(() => this.refreshProfiles()) ); } deleteProfile(profileId: string) { return this.http.delete(`${this.apiBaseUrl}/profiles/${profileId}`).pipe( - tap(() => setTimeout(() => this.checkSystemState(), 1000)) + tap(() => this.refreshProfiles()) ); } + // --- Private Endpoints and Helpers --- + + private getAvailableMiners = () => this.http.get(`${this.apiBaseUrl}/miners/available`); + private getSystemInfo = () => this.http.get(`${this.apiBaseUrl}/info`); + private getRunningMiners = () => this.http.get(`${this.apiBaseUrl}/miners`); + private getProfiles = () => this.http.get(`${this.apiBaseUrl}/profiles`); + + private updateHashrateHistory(runningMiners: any[]) { + const newHistory = new Map(); + runningMiners.forEach(miner => { + if (miner.hashrateHistory) { + newHistory.set(miner.name, miner.hashrateHistory); + } + }); + this.hashrateHistory.set(newHistory); + } + + private processStaticMinerInfo(available: AvailableMiner[], info: any) { + const installedMap = new Map(); + (info.installed_miners_info || []).forEach((m: InstallationDetails) => { + if (m.is_installed) { + const type = this.getMinerType(m); + installedMap.set(type, { ...m, type }); + } + }); + + const allInstalledMiners = Array.from(installedMap.values()); + const manageableMiners = available.map(availMiner => ({ + ...availMiner, + is_installed: installedMap.has(availMiner.name), + })); + + return { manageableMiners, installedMiners: allInstalledMiners }; + } + + private processSystemState(available: AvailableMiner[], info: any, running: any[], profiles: MiningProfile[]): SystemState { + const { manageableMiners, installedMiners } = this.processStaticMinerInfo(available, info); + + return { + needsSetup: installedMiners.length === 0, + apiAvailable: true, + error: null, + systemInfo: info, + manageableMiners, + installedMiners, + runningMiners: running, + profiles + }; + } + + private handleApiError(err: any) { + console.error('API not available or needs setup:', err); + this.hashrateHistory.set(new Map()); // Clear history on error + this.state.set({ + needsSetup: false, + apiAvailable: false, + error: 'Failed to connect to the mining API.', + systemInfo: {}, + manageableMiners: [], + installedMiners: [], + runningMiners: [], + profiles: [] + }); + return of(null); + } + private getMinerType(miner: any): string { if (!miner.path) return 'unknown'; const parts = miner.path.split('/').filter((p: string) => p);