mirror of
https://github.com/everoddandeven/monerod-gui.git
synced 2024-12-22 19:49:27 +00:00
Add logs, version and other
This commit is contained in:
parent
5deb58f135
commit
91bb85e119
35 changed files with 549 additions and 99 deletions
|
@ -96,7 +96,7 @@ function execMoneroDaemon(configFilePath: string): ChildProcess {
|
|||
return monerodProcess;
|
||||
}
|
||||
|
||||
function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStreams {
|
||||
function startMoneroDaemon(commandOptions: string[], logHandler?: (message: string) => void): ChildProcessWithoutNullStreams {
|
||||
const monerodPath = path.resolve(__dirname, monerodFilePath);
|
||||
|
||||
console.log("Starting monerod daemon with options: " + commandOptions.join(" "));
|
||||
|
@ -107,17 +107,20 @@ function startMoneroDaemon(commandOptions: string[]): ChildProcessWithoutNullStr
|
|||
// Gestisci l'output di stdout in streaming
|
||||
monerodProcess.stdout.on('data', (data) => {
|
||||
console.log(`monerod stdout: ${data}`);
|
||||
win?.webContents.send('monero-stdout', `${data}`);
|
||||
// Puoi anche inviare i log all'interfaccia utente tramite IPC
|
||||
});
|
||||
|
||||
// Gestisci gli errori in stderr
|
||||
monerodProcess.stderr.on('data', (data) => {
|
||||
console.error(`monerod stderr: ${data}`);
|
||||
win?.webContents.send('monero-stderr', `${data}`);
|
||||
});
|
||||
|
||||
// Gestisci la chiusura del processo
|
||||
monerodProcess.on('close', (code) => {
|
||||
console.log(`monerod chiuso con codice: ${code}`);
|
||||
win?.webContents.send('monero-stdout', `monerod exited with code: ${code}`);
|
||||
});
|
||||
|
||||
return monerodProcess;
|
||||
|
@ -147,8 +150,8 @@ try {
|
|||
}
|
||||
});
|
||||
|
||||
ipcMain.on('start-monerod', (event, configFilePath) => {
|
||||
startMoneroDaemon(configFilePath);
|
||||
ipcMain.on('start-monerod', (event, configFilePath: string[], logHandler?: (message: string) => void) => {
|
||||
startMoneroDaemon(configFilePath, logHandler);
|
||||
})
|
||||
|
||||
} catch (e) {
|
||||
|
|
|
@ -23,6 +23,8 @@ import { TransactionsModule } from './pages/transactions/transactions.module';
|
|||
import { OutputsModule } from './pages/outputs/outputs.module';
|
||||
import { SidebarComponent } from './shared/components';
|
||||
import { SettingsModule } from './pages/settings/settings.module';
|
||||
import { LogsModule } from './pages/logs/logs.module';
|
||||
import { VersionModule } from './pages/version/version.module';
|
||||
|
||||
// AoT requires an exported function for factories
|
||||
const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new TranslateHttpLoader(http, './assets/i18n/', '.json');
|
||||
|
@ -41,7 +43,9 @@ const httpLoaderFactory = (http: HttpClient): TranslateHttpLoader => new Transl
|
|||
MiningModule,
|
||||
TransactionsModule,
|
||||
OutputsModule,
|
||||
LogsModule,
|
||||
SettingsModule,
|
||||
VersionModule,
|
||||
TranslateModule,
|
||||
AppRoutingModule,
|
||||
TranslateModule.forRoot({
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
||||
import { EventEmitter, Injectable } from '@angular/core';
|
||||
import { BlockCount } from '../../../../common/BlockCount';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
|
@ -181,23 +181,41 @@ export class DaemonService {
|
|||
|
||||
}
|
||||
|
||||
private async delay(ms: number = 0): Promise<void> {
|
||||
await new Promise<void>(f => setTimeout(f, ms));
|
||||
}
|
||||
|
||||
private async callRpc(request: RPCRequest): Promise<{ [key: string]: any }> {
|
||||
let method: string = '';
|
||||
try {
|
||||
let method: string = '';
|
||||
|
||||
if (request instanceof JsonRPCRequest) {
|
||||
method = 'json_rpc';
|
||||
if (request instanceof JsonRPCRequest) {
|
||||
method = 'json_rpc';
|
||||
}
|
||||
else {
|
||||
method = request.method;
|
||||
}
|
||||
|
||||
const response = await firstValueFrom<{ [key: string]: any }>(this.httpClient.post(`${this.url}/${method}`, request.toDictionary(), this.headers));
|
||||
|
||||
if (response.error) {
|
||||
this.raiseRpcError(response.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
else {
|
||||
method = request.method;
|
||||
catch (error) {
|
||||
if (error instanceof HttpErrorResponse && error.status == 0) {
|
||||
const wasRunning = this.daemonRunning;
|
||||
this.daemonRunning = false;
|
||||
|
||||
if (wasRunning) {
|
||||
this.onDaemonStart.emit(false);
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const response = await firstValueFrom<{ [key: string]: any }>(this.httpClient.post(`${this.url}/${method}`, request.toDictionary(), this.headers));
|
||||
|
||||
if (response.error) {
|
||||
this.raiseRpcError(response.error);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public async startDaemon(): Promise<void> {
|
||||
|
@ -215,7 +233,7 @@ export class DaemonService {
|
|||
const settings = await this.getSettings();
|
||||
this.electronService.ipcRenderer.send('start-monerod', settings.toCommandOptions());
|
||||
|
||||
await new Promise(f => setTimeout(f, 3000));
|
||||
await this.delay(3000);
|
||||
|
||||
if (await this.isRunning(true)) {
|
||||
console.log("Daemon started");
|
||||
|
@ -485,7 +503,20 @@ export class DaemonService {
|
|||
public async getTxPoolBacklog(): Promise<TxBacklogEntry[]> {
|
||||
const response = await this.callRpc(new GetTxPoolBacklogRequest());
|
||||
|
||||
return TxBacklogEntry.fromBinary(response.backlog);
|
||||
if (typeof response.status == 'string' && response.status != 'OK') {
|
||||
throw new Error(`Error code: ${response.status}`)
|
||||
}
|
||||
|
||||
if (!response.bakclog && !response.result) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (response.backlog) {
|
||||
return TxBacklogEntry.fromBinary(response.backlog);
|
||||
}
|
||||
else if (response.result.backlog) return TxBacklogEntry.fromBinary(response.result.backlog);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public async pruneBlockchain(check: boolean = false): Promise<BlockchainPruneInfo> {
|
||||
|
@ -577,11 +608,20 @@ export class DaemonService {
|
|||
}
|
||||
|
||||
public async stopDaemon(): Promise<void> {
|
||||
if (!this.daemonRunning) {
|
||||
console.warn("Daemon not running");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await this.callRpc(new StopDaemonRequest());
|
||||
console.log(response);
|
||||
|
||||
if (typeof response.status == 'string' && response.status != 'OK') {
|
||||
throw new Error(`Could not stop daemon: ${response.status}`);
|
||||
}
|
||||
|
||||
this.daemonRunning = false;
|
||||
this.onDaemonStart.emit(false);
|
||||
}
|
||||
|
||||
public async setLimit(limitDown: number, limitUp: number): Promise<{ limitDown: number, limitUp: number }> {
|
||||
|
|
|
@ -20,7 +20,7 @@ export class BansComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.removeNavbarLinks();
|
||||
this.navbarService.removeLinks();
|
||||
|
||||
console.log('BansComponent AFTER VIEW INIT');
|
||||
|
||||
|
@ -36,7 +36,7 @@ export class BansComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
private onNavigationEnd(): void {
|
||||
this.navbarService.removeNavbarLinks();
|
||||
this.navbarService.removeLinks();
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
|
|
|
@ -10,12 +10,27 @@
|
|||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Starting daemon
|
||||
</button>
|
||||
|
||||
<button routerLink="/settings" class="btn btn-outline-light" type="button" [disabled]="startingDaemon"><i class="bi bi-gear"></i> Configure</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="daemonRunning" class="card text-bg-dark m-3 text-center" style="max-width: 18rem;">
|
||||
<div class="card-header"><strong>Daemon running</strong></div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<button *ngIf="!stoppingDaemon" type="button" class="btn btn-danger btn-lg" (click)="stopDaemon()"><i class="bi bi-stop-circle"></i> Stop</button>
|
||||
<button *ngIf="stoppingDaemon" class="btn btn-danger-light" type="button" disabled>
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
Stopping daemon
|
||||
</button>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@for(card of cards; track card.header) {
|
||||
@if(card.loading) {
|
||||
<div class="card text-bg-dark m-3 text-center" style="max-width: 18rem;" aria-hidden="true">
|
||||
<div class="card-header">{{card.header}}</div>
|
||||
<div class="card-header"><strong>{{card.header}}</strong></div>
|
||||
<div class="card-body">
|
||||
|
||||
<p class="card-text placeholder-glow">
|
||||
|
@ -36,9 +51,11 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="pills-profile" role="tabpanel" aria-labelledby="pills-profile-tab" tabindex="0">
|
||||
|
|
|
@ -9,6 +9,7 @@ import { DaemonInfo } from '../../../common/DaemonInfo';
|
|||
|
||||
import * as $ from 'jquery';
|
||||
import * as bootstrapTable from 'bootstrap-table';
|
||||
import { LogsService } from '../logs/logs.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-detail',
|
||||
|
@ -19,6 +20,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
public daemonRunning: boolean;
|
||||
public startingDaemon: boolean;
|
||||
public stoppingDaemon: boolean;
|
||||
private syncInfo?: SyncInfo;
|
||||
private daemonInfo?: DaemonInfo;
|
||||
private readonly navbarLinks: NavbarLink[];
|
||||
|
@ -50,9 +52,10 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
public cards: Card[];
|
||||
|
||||
constructor(private router: Router,private daemonService: DaemonService, private navbarService: NavbarService, private ngZone: NgZone) {
|
||||
constructor(private router: Router,private daemonService: DaemonService, private navbarService: NavbarService, private logsService: LogsService) {
|
||||
this.daemonRunning = false;
|
||||
this.startingDaemon = false;
|
||||
this.stoppingDaemon = false;
|
||||
this.syncStatus = 'Not synced';
|
||||
this.height = 0;
|
||||
this.targetHeight = 0;
|
||||
|
@ -72,7 +75,9 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
this.navbarLinks = [
|
||||
new NavbarLink('pills-home-tab', '#pills-home', 'pills-home', true, 'Overview', true),
|
||||
new NavbarLink('pills-profile-tab', '#pills-profile', 'pills-profile', false, 'Peers', true)
|
||||
new NavbarLink('pills-profile-tab', '#pills-profile', 'pills-profile', false, 'Peers', true),
|
||||
new NavbarLink('pills-spans-tab', '#pills-spans', 'pills-spans', false, 'Spans', true),
|
||||
new NavbarLink('pills-save-bc-tab', '#pills-save-bc', 'pills-save-bc', false, 'Save Blockchain', true)
|
||||
];
|
||||
|
||||
this.cards = this.createLoadingCards();
|
||||
|
@ -91,8 +96,10 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
ngAfterViewInit(): void {
|
||||
console.log('DetailComponent AFTER VIEW INIT');
|
||||
this.navbarService.setNavbarLinks(this.navbarLinks);
|
||||
this.navbarService.setLinks(this.navbarLinks);
|
||||
|
||||
if (this.loadInterval != null) return;
|
||||
|
||||
this.load().then(() => {
|
||||
this.cards = this.createCards();
|
||||
});
|
||||
|
@ -106,6 +113,8 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
});
|
||||
*/
|
||||
|
||||
if (this.stoppingDaemon) return;
|
||||
|
||||
this.load().then(() => {
|
||||
this.cards = this.createCards();
|
||||
});
|
||||
|
@ -114,7 +123,7 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
|
||||
ngOnDestroy(): void {
|
||||
console.log("DetailComponent ON DESTROY");
|
||||
|
||||
|
||||
if(this.loadInterval != null) {
|
||||
clearInterval(this.loadInterval);
|
||||
}
|
||||
|
@ -125,6 +134,11 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
console.warn("Daemon already running");
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.startingDaemon || this.stoppingDaemon) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.startingDaemon = true;
|
||||
|
||||
setTimeout(async () => {
|
||||
|
@ -141,6 +155,27 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
}, 500);
|
||||
}
|
||||
|
||||
public async stopDaemon(): Promise<void> {
|
||||
if (this.stoppingDaemon || this.startingDaemon || !this.daemonRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.stoppingDaemon = true;
|
||||
|
||||
try {
|
||||
if (this.loadInterval) clearInterval(this.loadInterval);
|
||||
|
||||
await this.daemonService.stopDaemon();
|
||||
|
||||
this.daemonRunning = false;
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.stoppingDaemon = false;
|
||||
}
|
||||
|
||||
private onNavigationEnd(): void {
|
||||
this.load().then(() => {
|
||||
//this.cards = this.createCards();
|
||||
|
@ -194,12 +229,12 @@ export class DetailComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||
this.daemonRunning = await this.daemonService.isRunning();
|
||||
|
||||
if (!this.daemonRunning) {
|
||||
this.navbarService.disableNavbarLinks();
|
||||
this.navbarService.disableLinks();
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.navbarService.enableNavbarLinks();
|
||||
this.navbarService.enableLinks();
|
||||
|
||||
const $table = $('#table');
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export class HardForkInfoComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.removeNavbarLinks();
|
||||
this.navbarService.removeLinks();
|
||||
}
|
||||
|
||||
private onNavigationEnd(): void {
|
||||
|
|
14
src/app/pages/logs/logs-routing.module.ts
Normal file
14
src/app/pages/logs/logs-routing.module.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { LogsComponent } from './logs.component';
|
||||
|
||||
const routes: Routes = [{
|
||||
path: 'logs',
|
||||
component: LogsComponent
|
||||
}];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class LogsRoutingModule { }
|
8
src/app/pages/logs/logs.component.html
Normal file
8
src/app/pages/logs/logs.component.html
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="terminal bg-dark text-light p-3 m-4">
|
||||
<div class="terminal-output" id="terminalOutput">
|
||||
<ng-container *ngFor="let line of lines; trackBy: trackByFn">
|
||||
<div>{{ line }}</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
11
src/app/pages/logs/logs.component.scss
Normal file
11
src/app/pages/logs/logs.component.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.terminal {
|
||||
max-height: 700px;
|
||||
overflow-y: auto;
|
||||
font-family: monospace;
|
||||
border: 1px solid #333;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
white-space: pre-wrap;
|
||||
}
|
23
src/app/pages/logs/logs.component.spec.ts
Normal file
23
src/app/pages/logs/logs.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsComponent } from './logs.component';
|
||||
|
||||
describe('LogsComponent', () => {
|
||||
let component: LogsComponent;
|
||||
let fixture: ComponentFixture<LogsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [LogsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(LogsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
42
src/app/pages/logs/logs.component.ts
Normal file
42
src/app/pages/logs/logs.component.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { AfterViewInit, Component, NgZone } from '@angular/core';
|
||||
import { LogsService } from './logs.service';
|
||||
import { NavbarService } from '../../shared/components/navbar/navbar.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logs',
|
||||
templateUrl: './logs.component.html',
|
||||
styleUrl: './logs.component.scss'
|
||||
})
|
||||
export class LogsComponent implements AfterViewInit {
|
||||
|
||||
constructor(private navbarService: NavbarService, private logsService: LogsService, private ngZone: NgZone) {
|
||||
this.logsService.onLog.subscribe((message: string) => this.onLog());
|
||||
}
|
||||
|
||||
public get lines(): string[] {
|
||||
return this.logsService.lines;
|
||||
}
|
||||
|
||||
private onLog(): void {
|
||||
// Scorri automaticamente in basso
|
||||
setTimeout(() => {
|
||||
this.ngZone.run(() => {
|
||||
this.lines;
|
||||
const terminalOutput = document.getElementById('terminalOutput');
|
||||
if (terminalOutput) {
|
||||
terminalOutput.style.width = `${window.innerWidth}`;
|
||||
terminalOutput.scrollTop = terminalOutput.scrollHeight;
|
||||
}
|
||||
})
|
||||
|
||||
}, 100);
|
||||
}
|
||||
|
||||
public trackByFn(index: number, item: string): number {
|
||||
return index; // usa l'indice per tracciare gli elementi
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.removeLinks();
|
||||
}
|
||||
}
|
17
src/app/pages/logs/logs.module.ts
Normal file
17
src/app/pages/logs/logs.module.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { LogsRoutingModule } from './logs-routing.module';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { LogsComponent } from './logs.component';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [LogsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
LogsRoutingModule
|
||||
]
|
||||
})
|
||||
export class LogsModule { }
|
16
src/app/pages/logs/logs.service.spec.ts
Normal file
16
src/app/pages/logs/logs.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogsService } from './logs.service';
|
||||
|
||||
describe('LogsService', () => {
|
||||
let service: LogsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(LogsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
27
src/app/pages/logs/logs.service.ts
Normal file
27
src/app/pages/logs/logs.service.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { EventEmitter, Injectable, NgZone } from '@angular/core';
|
||||
import { ipcRenderer, webFrame } from 'electron';
|
||||
import { ElectronService } from '../../core/services';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class LogsService {
|
||||
public readonly onLog: EventEmitter<string> = new EventEmitter<string>();
|
||||
public readonly lines: string[] = [];
|
||||
|
||||
constructor(private electronService: ElectronService, private ngZone: NgZone) {
|
||||
if (this.electronService.isElectron) {
|
||||
this.electronService.ipcRenderer.on('monero-stdout', (event, message: string) => this.log(message));
|
||||
this.electronService.ipcRenderer.on('monero-stderr', (event, message: string) => this.log(message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public log(message: string): void {
|
||||
this.ngZone.run(() => {
|
||||
this.lines.push(message);
|
||||
this.onLog.emit(message);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@
|
|||
<div class="tab-pane fade" id="pills-disabled" role="tabpanel" aria-labelledby="pills-disabled-tab" tabindex="0">...</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="coreBusy" class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column text-center">
|
||||
<div *ngIf="coreBusy" class="cover-container d-flex w-100 h-100 p-3 mx-auto flex-column text-bg-dark rounded-3 m-4 text-center">
|
||||
|
||||
<main class="px-3">
|
||||
<h1><i class="bi bi-exclamation-diamond"></i> Core is busy.</h1>
|
||||
|
|
|
@ -7,7 +7,6 @@ import { NavbarLink } from '../../shared/components/navbar/navbar.model';
|
|||
import { MineableTxBacklog } from '../../../common/MineableTxBacklog';
|
||||
import { Chain } from '../../../common/Chain';
|
||||
import { CoreIsBusyError } from '../../../common/error';
|
||||
import { CommonModule, NgIf } from '@angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-mining',
|
||||
|
@ -64,7 +63,7 @@ export class MiningComponent implements AfterViewInit {
|
|||
|
||||
ngAfterViewInit(): void {
|
||||
console.log('DetailComponent AFTER VIEW INIT');
|
||||
this.navbarService.setNavbarLinks(this.navbarLinks);
|
||||
this.navbarService.setLinks(this.navbarLinks);
|
||||
|
||||
setTimeout(() => {
|
||||
const $table = $('#chainsTable');
|
||||
|
@ -106,10 +105,10 @@ export class MiningComponent implements AfterViewInit {
|
|||
const $table = $('#chainsTable');
|
||||
$table.bootstrapTable('load', this.getChains());
|
||||
this.coreBusy = false;
|
||||
this.navbarService.enableNavbarLinks();
|
||||
this.navbarService.enableLinks();
|
||||
}
|
||||
catch(error) {
|
||||
this.navbarService.disableNavbarLinks();
|
||||
this.navbarService.disableLinks();
|
||||
if (error instanceof CoreIsBusyError) {
|
||||
this.coreBusy = true;
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ export class OutputsComponent implements AfterViewInit {
|
|||
this.navbarLinks = [
|
||||
new NavbarLink('pills-outputs-overview-tab', '#pills-outputs-overview', 'outputs-overview', true, 'Overview'),
|
||||
new NavbarLink('pills-outputs-histogram-tab', '#pills-outputs-histogram', 'outputs-histogram', false, 'Histogram'),
|
||||
new NavbarLink('pills-outputs-distribution-tab', '#pills-outputs-distribution', 'outputs-distribution', false, 'Distribution')
|
||||
|
||||
new NavbarLink('pills-outputs-distribution-tab', '#pills-outputs-distribution', 'outputs-distribution', false, 'Distribution'),
|
||||
new NavbarLink('pills-is-key-image-spent-tab', '#pills-is-key-image-spent', 'is-key-image-spent', false, 'Is Key Image Spent')
|
||||
];
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.setNavbarLinks(this.navbarLinks);
|
||||
this.navbarService.setLinks(this.navbarLinks);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ export class SettingsComponent implements AfterViewInit {
|
|||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.setNavbarLinks(this.navbarLinks);
|
||||
this.navbarService.setLinks(this.navbarLinks);
|
||||
}
|
||||
|
||||
public OnOfflineChange() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div class="tab-pane fade show active" id="pills-rpc" role="tabpanel" aria-labelledby="pills-rpc-tab" tabindex="0">
|
||||
<div class="tab-content" id="pills-tabContent">
|
||||
<div class="tab-pane fade show active" id="pills-relay-tx" role="tabpanel" aria-labelledby="pills-relay-tx-tab" tabindex="0">
|
||||
<div class="row g-5 m-2">
|
||||
<div class="col-md-7 col-lg-10">
|
||||
<h4 class="mb-3">Relay a list of transaction IDs</h4>
|
||||
|
@ -7,7 +8,13 @@
|
|||
|
||||
<div class="col-12">
|
||||
<label for="tx_ids" class="form-label">Tx Ids</label>
|
||||
<textarea type="text" class="form-control" id="tx_ids" placeholder="" rows="15" cols="15"></textarea>
|
||||
<textarea type="text" class="form-control" id="tx_ids" placeholder="[
|
||||
'tx_hash_1',
|
||||
'tx_hash_2',
|
||||
... ,
|
||||
'tx_hash_n'
|
||||
]"
|
||||
rows="15" cols="15"></textarea>
|
||||
<small class="text-body-secondary">List of transaction IDs to relay</small>
|
||||
</div>
|
||||
|
||||
|
@ -18,5 +25,93 @@
|
|||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary btn-lg" type="button" [disabled]="!canRelay" (click)="onRelay()">Relay Tx</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-coinbase-tx-sum" role="tabpanel" aria-labelledby="pills-coinbase-tx-sum-tab" tabindex="0">
|
||||
<div class="row g-5 m-2">
|
||||
<div class="col-md-7 col-lg-10">
|
||||
<h4 class="mb-3">Get coinbase tx sum</h4>
|
||||
<form class="needs-validation" novalidate="">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="height" class="form-label">Height</label>
|
||||
<input type="number" class="form-control" id="height" placeholder="" [(ngModel)]="height" [ngModelOptions]="{standalone: true}">
|
||||
<small class="text-body-secondary">Block height from which getting the amounts</small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<label for="count" class="form-label">Count</label>
|
||||
<input type="number" class="form-control" id="count" placeholder="" [(ngModel)]="count" [ngModelOptions]="{standalone: true}">
|
||||
<small class="text-body-secondary">Number of blocks to include in the sum</small>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary btn-lg" type="button" (click)="onGetCoinbaseTxSum()">Get Coinbase Tx Sum</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-flush-tx-pool" role="tabpanel" aria-labelledby="pills-flush-tx-pool-tab" tabindex="0">
|
||||
<div class="row g-5 m-2">
|
||||
<div class="col-md-7 col-lg-10">
|
||||
<h4 class="mb-3">Flush a list of transaction IDs</h4>
|
||||
<form class="needs-validation" novalidate="">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-12">
|
||||
<label for="tx_ids" class="form-label">Tx Ids</label>
|
||||
<textarea type="text" class="form-control" id="tx_ids" placeholder="[
|
||||
'tx_hash_1',
|
||||
'tx_hash_2',
|
||||
... ,
|
||||
'tx_hash_n'
|
||||
]"
|
||||
rows="15" cols="15"></textarea>
|
||||
<small class="text-body-secondary">List of transaction IDs to flush in tx pool</small>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary btn-lg" type="button" [disabled]="!canRelay" (click)="onFlush()">Flush Tx Pool</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="pills-flush-cache" role="tabpanel" aria-labelledby="pills-flush-cache-tab" tabindex="0">
|
||||
<div class="row g-5 m-2">
|
||||
<div class="col-md-7 col-lg-10">
|
||||
<h4 class="mb-3">Flush a list of bad transaction IDs from cache</h4>
|
||||
<form class="needs-validation" novalidate="">
|
||||
<div class="row g-3">
|
||||
|
||||
<div class="col-12">
|
||||
<label for="tx_ids" class="form-label">Tx Ids</label>
|
||||
<textarea type="text" class="form-control" id="tx_ids" placeholder="[
|
||||
'tx_hash_1',
|
||||
'tx_hash_2',
|
||||
... ,
|
||||
'tx_hash_n'
|
||||
]"
|
||||
rows="15" cols="15"></textarea>
|
||||
<small class="text-body-secondary">List of bad transaction IDs to flush from cache</small>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button class="w-100 btn btn-primary btn-lg" type="button" [disabled]="!canRelay" (click)="onFlushFromCache()">Flush Bad Txs</button>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AfterViewInit, Component } from '@angular/core';
|
|||
import { DaemonService } from '../../core/services/daemon/daemon.service';
|
||||
import { NavbarService } from '../../shared/components/navbar/navbar.service';
|
||||
import { NavbarLink } from '../../shared/components/navbar/navbar.model';
|
||||
import { TxBacklogEntry } from '../../../common/TxBacklogEntry';
|
||||
|
||||
@Component({
|
||||
selector: 'app-transactions',
|
||||
|
@ -12,27 +13,58 @@ export class TransactionsComponent implements AfterViewInit {
|
|||
private readonly navbarLinks: NavbarLink[];
|
||||
|
||||
public canRelay: boolean;
|
||||
public txPoolBacklog: TxBacklogEntry[];
|
||||
public height: number;
|
||||
public count: number;
|
||||
|
||||
constructor(private daemonService: DaemonService, private navbarService: NavbarService) {
|
||||
this.navbarLinks = [
|
||||
new NavbarLink('pills-relay-tx-tab', '#pills-relay-tx', 'pills-relay-tx', true, 'Relay Tx', true),
|
||||
new NavbarLink('pills-tx-backlog', '#pills-tx-backlog', 'pills-tx-backlog', false, 'Tx Backlog', true),
|
||||
new NavbarLink('pills-tx-backlog-tab', '#pills-tx-backlog', 'pills-tx-backlog', false, 'Tx Backlog', true),
|
||||
new NavbarLink('pills-coinbase-tx-sum-tab', '#pills-coinbase-tx-sum', 'pills-coinbase-tx-sum', false, 'Coinbase Tx Sum', true),
|
||||
new NavbarLink('pills-flush-tx-pool-tab', '#pills-flush-tx-pool', 'pills-flush-tx-pool', false, 'Flush Tx Pool', true),
|
||||
new NavbarLink('pills-flush-cahe', '#pills-flush-cache', 'pills-flush-cache', false, 'Flush Cache', true)
|
||||
new NavbarLink('pills-flush-cahe-tab', '#pills-flush-cache', 'pills-flush-cache', false, 'Flush Cache', true)
|
||||
];
|
||||
this.height = 0;
|
||||
this.count = 0;
|
||||
this.txPoolBacklog = [];
|
||||
|
||||
this.canRelay = false;
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.setNavbarLinks(this.navbarLinks);
|
||||
this.navbarService.setLinks(this.navbarLinks);
|
||||
this.load().then(() => {
|
||||
this.navbarService.enableLinks();
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
this.navbarService.disableLinks();
|
||||
})
|
||||
}
|
||||
|
||||
private async load(): Promise<void> {
|
||||
|
||||
try {
|
||||
this.txPoolBacklog = await this.daemonService.getTxPoolBacklog();
|
||||
console.log(this.txPoolBacklog)
|
||||
}
|
||||
catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async onRelay(): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
public async onFlush(): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
public async onFlushFromCache(): Promise<void> {
|
||||
|
||||
}
|
||||
|
||||
public async onGetCoinbaseTxSum(): Promise<void> {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
16
src/app/pages/version/version-routing.module.ts
Normal file
16
src/app/pages/version/version-routing.module.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { VersionComponent } from './version.component';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'version',
|
||||
component: VersionComponent
|
||||
}
|
||||
];
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class VersionRoutingModule { }
|
1
src/app/pages/version/version.component.html
Normal file
1
src/app/pages/version/version.component.html
Normal file
|
@ -0,0 +1 @@
|
|||
<p>version works!</p>
|
0
src/app/pages/version/version.component.scss
Normal file
0
src/app/pages/version/version.component.scss
Normal file
23
src/app/pages/version/version.component.spec.ts
Normal file
23
src/app/pages/version/version.component.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { VersionComponent } from './version.component';
|
||||
|
||||
describe('VersionComponent', () => {
|
||||
let component: VersionComponent;
|
||||
let fixture: ComponentFixture<VersionComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [VersionComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(VersionComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
23
src/app/pages/version/version.component.ts
Normal file
23
src/app/pages/version/version.component.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { AfterViewInit, Component } from '@angular/core';
|
||||
import { NavbarService } from '../../shared/components/navbar/navbar.service';
|
||||
import { NavbarLink } from '../../shared/components/navbar/navbar.model';
|
||||
import { DaemonService } from '../../core/services/daemon/daemon.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-version',
|
||||
templateUrl: './version.component.html',
|
||||
styleUrl: './version.component.scss'
|
||||
})
|
||||
export class VersionComponent implements AfterViewInit {
|
||||
private readonly links: NavbarLink[];
|
||||
|
||||
constructor(private navbarService: NavbarService, private daemonService: DaemonService) {
|
||||
this.links = [
|
||||
new NavbarLink('pills-overview-tab', '#pills-overview', 'pills-overview', true, 'Overview')
|
||||
];
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.navbarService.setLinks(this.links);
|
||||
}
|
||||
}
|
17
src/app/pages/version/version.module.ts
Normal file
17
src/app/pages/version/version.module.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { VersionRoutingModule } from './version-routing.module';
|
||||
import { VersionComponent } from './version.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
|
||||
@NgModule({
|
||||
declarations: [VersionComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
VersionRoutingModule
|
||||
]
|
||||
})
|
||||
export class VersionModule { }
|
|
@ -1,7 +1,8 @@
|
|||
<nav class="navbar navbar-expand-lg d-flex justify-content-center py-3 text-bg-dark">
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||
<img src="/assets/icons/monero-symbol-on-white-480.png" width="40" height="40">
|
||||
<span class="fs-4">Monero Daemon</span>
|
||||
|
||||
<a href="/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto text-white text-decoration-none">
|
||||
<img class="m-1" src="/assets/icons/monero-symbol-on-white-480.png" width="40" height="40">
|
||||
<span class="fs-4"><strong>Monero Daemon</strong></span>
|
||||
</a>
|
||||
|
||||
<ul class="nav nav-pills" id="pills-tab" role="tablist">
|
||||
|
|
|
@ -12,7 +12,7 @@ import { NavbarLink } from './navbar.model';
|
|||
export class NavbarComponent {
|
||||
|
||||
public get navbarLinks(): NavbarLink[] {
|
||||
return this.navbarService.navbarLinks;
|
||||
return this.navbarService.links;
|
||||
}
|
||||
|
||||
constructor(private navbarService: NavbarService) {
|
||||
|
|
|
@ -7,29 +7,29 @@ import { NavbarLink } from './navbar.model';
|
|||
export class NavbarService {
|
||||
private _navbarLinks: NavbarLink[] = [];
|
||||
|
||||
public get navbarLinks(): NavbarLink[] {
|
||||
public get links(): NavbarLink[] {
|
||||
return this._navbarLinks;
|
||||
}
|
||||
|
||||
constructor() { }
|
||||
|
||||
public addNavbarLink(... navbarLinks: NavbarLink[]): void {
|
||||
public addLink(... navbarLinks: NavbarLink[]): void {
|
||||
navbarLinks.forEach((navLink: NavbarLink) => this._navbarLinks.push(navLink));
|
||||
}
|
||||
|
||||
public setNavbarLinks(navbarLinks: NavbarLink[]): void {
|
||||
public setLinks(navbarLinks: NavbarLink[]): void {
|
||||
this._navbarLinks = navbarLinks;
|
||||
}
|
||||
|
||||
public removeNavbarLinks(): void {
|
||||
this.setNavbarLinks([]);
|
||||
public removeLinks(): void {
|
||||
this.setLinks([]);
|
||||
}
|
||||
|
||||
public disableNavbarLinks(): void {
|
||||
public disableLinks(): void {
|
||||
this._navbarLinks.forEach((link) => link.disabled = true);
|
||||
}
|
||||
|
||||
public enableNavbarLinks(): void {
|
||||
public enableLinks(): void {
|
||||
this._navbarLinks.forEach((link) => link.disabled = false);
|
||||
}
|
||||
|
||||
|
|
|
@ -21,44 +21,41 @@ export class SidebarComponent implements OnChanges {
|
|||
this.errorMessage = '';
|
||||
this.daemonService.onDaemonStart.subscribe((started: boolean) => {
|
||||
if (!started) {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createLightLinks();
|
||||
}
|
||||
else {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Blockchain', '/blockchain', 'bi bi-bounding-box'),
|
||||
new NavLink('Transactions', '/transactions', 'bi bi-credit-card-2-front'),
|
||||
new NavLink('Outputs', '/outputs', 'bi bi-circle-fill'),
|
||||
new NavLink('Mining', '/mining', 'bi bi-minecart-loaded'),
|
||||
new NavLink('Hard Fork Info', '/hardforkinfo', 'bi bi-signpost-split'),
|
||||
new NavLink('Bans', '/bans', 'bi bi-ban'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createFullLinks();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createLightLinks(): NavLink[] {
|
||||
return [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
}
|
||||
|
||||
private createFullLinks(): NavLink[] {
|
||||
return this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Transactions', '/transactions', 'bi bi-credit-card-2-front'),
|
||||
new NavLink('Outputs', '/outputs', 'bi bi-circle-fill'),
|
||||
new NavLink('Mining', '/mining', 'bi bi-minecart-loaded'),
|
||||
new NavLink('Hard Fork Info', '/hardforkinfo', 'bi bi-signpost-split'),
|
||||
new NavLink('Bans', '/bans', 'bi bi-ban'),
|
||||
new NavLink('Logs', '/logs', 'bi bi-terminal'),
|
||||
new NavLink('Version', '/version', 'bi bi-git'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
}
|
||||
|
||||
private updateLinks(): void {
|
||||
if (!this.isDaemonRunning) {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createLightLinks();
|
||||
}
|
||||
else {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Blockchain', '/blockchain', 'bi bi-bounding-box'),
|
||||
new NavLink('Transactions', '/transactions', 'bi bi-credit-card-2-front'),
|
||||
new NavLink('Outputs', '/outputs', 'bi bi-circle-fill'),
|
||||
new NavLink('Mining', '/mining', 'bi bi-minecart-loaded'),
|
||||
new NavLink('Hard Fork Info', '/hardforkinfo', 'bi bi-signpost-split'),
|
||||
new NavLink('Bans', '/bans', 'bi bi-ban'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createFullLinks();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,22 +65,10 @@ export class SidebarComponent implements OnChanges {
|
|||
|
||||
public ngOnChanges(changes: SimpleChanges): void {
|
||||
if (!this.isDaemonRunning) {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createLightLinks();
|
||||
}
|
||||
else {
|
||||
this.navLinks = [
|
||||
new NavLink('Dashboard', '/detail', 'bi bi-speedometer2'),
|
||||
new NavLink('Blockchain', '/blockchain', 'bi bi-bounding-box'),
|
||||
new NavLink('Transactions', '/transactions', 'bi bi-credit-card-2-front'),
|
||||
new NavLink('Outputs', '/outputs', 'bi bi-circle-fill'),
|
||||
new NavLink('Mining', '/mining', 'bi bi-minecart-loaded'),
|
||||
new NavLink('Hard Fork Info', '/hardforkinfo', 'bi bi-signpost-split'),
|
||||
new NavLink('Bans', '/bans', 'bi bi-ban'),
|
||||
new NavLink('Settings', '/settings', 'bi bi-gear')
|
||||
];
|
||||
this.navLinks = this.createFullLinks();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 66 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 15 KiB |
|
@ -10,6 +10,7 @@ export class TxBacklogEntry {
|
|||
}
|
||||
|
||||
public static fromBinary(binary: string): TxBacklogEntry[] {
|
||||
|
||||
throw new Error("TxBacklogEntry.fromBinary(): not implemented");
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
<html data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Angular Electron</title>
|
||||
<title>Monero Daemon</title>
|
||||
<base href="/">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
|
Loading…
Reference in a new issue