308 lines
9.4 KiB
TypeScript
308 lines
9.4 KiB
TypeScript
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, tap } from 'rxjs/operators';
|
|
|
|
// --- Interfaces ---
|
|
export interface InstallationDetails {
|
|
is_installed: boolean;
|
|
version: string;
|
|
path: string;
|
|
miner_binary: string;
|
|
config_path?: string;
|
|
type?: string;
|
|
}
|
|
|
|
export interface AvailableMiner {
|
|
name: string;
|
|
description: string;
|
|
}
|
|
|
|
export interface HashratePoint {
|
|
timestamp: string;
|
|
hashrate: number;
|
|
}
|
|
|
|
export interface MiningProfile {
|
|
id: string;
|
|
name: string;
|
|
minerType: string;
|
|
config: any;
|
|
}
|
|
|
|
export interface SystemState {
|
|
needsSetup: boolean;
|
|
apiAvailable: boolean;
|
|
error: string | null;
|
|
systemInfo: any;
|
|
manageableMiners: any[];
|
|
installedMiners: InstallationDetails[];
|
|
runningMiners: any[];
|
|
profiles: MiningProfile[];
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class MinerService implements OnDestroy {
|
|
private apiBaseUrl = 'http://localhost:9090/api/v1/mining';
|
|
private pollingSubscription?: Subscription;
|
|
|
|
// --- State Signals ---
|
|
public state = signal<SystemState>({
|
|
needsSetup: false,
|
|
apiAvailable: true,
|
|
error: null,
|
|
systemInfo: {},
|
|
manageableMiners: [],
|
|
installedMiners: [],
|
|
runningMiners: [],
|
|
profiles: []
|
|
});
|
|
|
|
// Separate signal for hashrate history as it updates frequently
|
|
public hashrateHistory = signal<Map<string, HashratePoint[]>>(new Map());
|
|
|
|
// --- View Mode Signals (single/multi miner view) ---
|
|
public viewMode = signal<'all' | 'single'>('all');
|
|
public selectedMinerName = signal<string | null>(null);
|
|
|
|
// --- 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);
|
|
|
|
// Selected miner object (when in single mode)
|
|
public selectedMiner = computed(() => {
|
|
const name = this.selectedMinerName();
|
|
if (!name) return null;
|
|
return this.runningMiners().find(m => m.name === name) || null;
|
|
});
|
|
|
|
// Displayed miners based on view mode
|
|
public displayedMiners = computed(() => {
|
|
if (this.viewMode() === 'all') {
|
|
return this.runningMiners();
|
|
}
|
|
const selected = this.selectedMiner();
|
|
return selected ? [selected] : [];
|
|
});
|
|
|
|
constructor(private http: HttpClient) {
|
|
this.forceRefreshState();
|
|
this.startPollingLive_Data();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.stopPolling();
|
|
}
|
|
|
|
// --- 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([]))),
|
|
profiles: this.getProfiles().pipe(catchError(() => of([])))
|
|
}).pipe(
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
});
|
|
}
|
|
|
|
private stopPolling() {
|
|
this.pollingSubscription?.unsubscribe();
|
|
}
|
|
|
|
/**
|
|
* 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 }));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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.refreshSystemInfo(), 1000))
|
|
);
|
|
}
|
|
|
|
uninstallMiner(minerType: string) {
|
|
return this.http.delete(`${this.apiBaseUrl}/miners/${minerType}/uninstall`).pipe(
|
|
tap(() => setTimeout(() => this.refreshSystemInfo(), 1000))
|
|
);
|
|
}
|
|
|
|
startMiner(profileId: string) {
|
|
return this.http.post(`${this.apiBaseUrl}/profiles/${profileId}/start`, {}).pipe(
|
|
// 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(
|
|
// An immediate poll for running miners will be triggered by the interval soon enough
|
|
);
|
|
}
|
|
|
|
getMinerLogs(minerName: string) {
|
|
return this.http.get<string[]>(`${this.apiBaseUrl}/miners/${minerName}/logs`);
|
|
}
|
|
|
|
createProfile(profile: MiningProfile) {
|
|
return this.http.post(`${this.apiBaseUrl}/profiles`, profile).pipe(
|
|
tap(() => this.refreshProfiles())
|
|
);
|
|
}
|
|
|
|
updateProfile(profile: MiningProfile) {
|
|
return this.http.put(`${this.apiBaseUrl}/profiles/${profile.id}`, profile).pipe(
|
|
tap(() => this.refreshProfiles())
|
|
);
|
|
}
|
|
|
|
deleteProfile(profileId: string) {
|
|
return this.http.delete(`${this.apiBaseUrl}/profiles/${profileId}`).pipe(
|
|
tap(() => this.refreshProfiles())
|
|
);
|
|
}
|
|
|
|
// --- View Mode Methods ---
|
|
|
|
/**
|
|
* Select a specific miner for single-miner view
|
|
*/
|
|
selectMiner(minerName: string) {
|
|
this.selectedMinerName.set(minerName);
|
|
this.viewMode.set('single');
|
|
}
|
|
|
|
/**
|
|
* Switch to all-miners view
|
|
*/
|
|
selectAllMiners() {
|
|
this.selectedMinerName.set(null);
|
|
this.viewMode.set('all');
|
|
}
|
|
|
|
/**
|
|
* Find the profile associated with a running miner
|
|
*/
|
|
getProfileForMiner(minerName: string): MiningProfile | null {
|
|
// Extract miner type from the name (e.g., "xmrig-123" -> "xmrig")
|
|
const minerType = minerName.split('-')[0];
|
|
// Find matching profile by miner type
|
|
return this.profiles().find(p => p.minerType === minerType) || null;
|
|
}
|
|
|
|
// --- Private Endpoints and Helpers ---
|
|
|
|
private getAvailableMiners = () => this.http.get<AvailableMiner[]>(`${this.apiBaseUrl}/miners/available`);
|
|
private getSystemInfo = () => this.http.get<any>(`${this.apiBaseUrl}/info`);
|
|
private getRunningMiners = () => this.http.get<any[]>(`${this.apiBaseUrl}/miners`);
|
|
private getProfiles = () => this.http.get<MiningProfile[]>(`${this.apiBaseUrl}/profiles`);
|
|
|
|
private updateHashrateHistory(runningMiners: any[]) {
|
|
const newHistory = new Map<string, HashratePoint[]>();
|
|
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<string, InstallationDetails>();
|
|
(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);
|
|
return parts.length > 1 ? parts[parts.length - 2] : parts[parts.length - 1] || 'unknown';
|
|
}
|
|
}
|