import { Component, inject, computed, signal, OnInit, OnDestroy, ElementRef, ViewChild, AfterViewChecked } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { MinerService } from '../../miner.service';
import { interval, Subscription, switchMap } from 'rxjs';
@Component({
selector: 'app-console',
standalone: true,
imports: [CommonModule],
template: `
@if (logs().length > 0) {
@for (line of logs(); track $index) {
}
} @else if (activeMiner()) {
Waiting for logs from {{ activeMiner() }}...
} @else {
Start a miner to see console output
}
>
`,
styles: [`
.console-page {
display: flex;
flex-direction: column;
height: calc(100vh - 120px);
gap: 0;
}
.console-header {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 0.75rem;
background: var(--color-surface-200);
border-radius: 0.5rem 0.5rem 0 0;
border: 1px solid rgb(37 37 66 / 0.2);
border-bottom: none;
}
.worker-chooser {
display: flex;
align-items: center;
gap: 0.5rem;
}
.chooser-label {
font-size: 0.75rem;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.worker-select {
padding: 0.375rem 0.625rem;
background: var(--color-surface-100);
border: 1px solid rgb(37 37 66 / 0.3);
border-radius: 0.375rem;
color: white;
font-size: 0.8125rem;
cursor: pointer;
min-width: 140px;
}
.worker-select:hover,
.worker-select:focus {
border-color: var(--color-accent-500);
outline: none;
}
.single-miner-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.625rem;
background: rgb(0 212 255 / 0.1);
border-radius: 0.375rem;
color: var(--color-accent-400);
font-size: 0.8125rem;
font-weight: 500;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #64748b;
}
.status-dot.online {
background: var(--color-success-500);
box-shadow: 0 0 6px var(--color-success-500);
}
.no-miners-msg {
padding: 0.375rem 0;
color: #64748b;
font-size: 0.8125rem;
}
.console-tabs {
display: flex;
align-items: center;
gap: 0.25rem;
margin-left: auto;
}
.tab-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.625rem;
background: transparent;
border: none;
border-radius: 0.375rem;
color: #94a3b8;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
}
.tab-btn:hover {
background: rgb(37 37 66 / 0.3);
color: white;
}
.tab-btn.active {
background: var(--color-surface-100);
color: white;
}
.tab-status {
width: 6px;
height: 6px;
border-radius: 50%;
background: #64748b;
}
.tab-status.online {
background: var(--color-success-500);
box-shadow: 0 0 4px var(--color-success-500);
}
.console-output {
flex: 1;
overflow-y: auto;
background: #0a0a12;
border-left: 1px solid rgb(37 37 66 / 0.2);
border-right: 1px solid rgb(37 37 66 / 0.2);
font-family: var(--font-family-mono);
font-size: 0.8125rem;
line-height: 1.5;
}
.log-line {
padding: 0.125rem 0.75rem;
border-bottom: 1px solid rgb(37 37 66 / 0.05);
}
.log-line:hover {
background: rgb(37 37 66 / 0.2);
}
.log-text {
color: #a3e635;
word-break: break-all;
}
.log-line.error .log-text {
color: var(--color-danger-500);
}
.log-line.warning .log-text {
color: var(--color-warning-500);
}
.console-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #64748b;
}
.console-empty p {
margin-top: 0.75rem;
font-size: 0.875rem;
}
.console-input-wrapper {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background: rgba(10, 10, 18, 0.6);
backdrop-filter: blur(4px);
border-left: 1px solid rgb(37 37 66 / 0.2);
border-right: 1px solid rgb(37 37 66 / 0.2);
}
.input-prompt {
color: var(--color-accent-500);
font-family: var(--font-family-mono);
font-size: 0.875rem;
margin-right: 0.5rem;
opacity: 0.7;
}
.console-input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: rgba(163, 230, 53, 0.8);
font-family: var(--font-family-mono);
font-size: 0.8125rem;
caret-color: var(--color-accent-500);
}
.console-input::placeholder {
color: rgba(100, 116, 139, 0.4);
font-style: italic;
}
.console-input:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.console-input:focus {
color: #a3e635;
}
.console-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.5rem 0.75rem;
background: var(--color-surface-200);
border-radius: 0 0 0.5rem 0.5rem;
border: 1px solid rgb(37 37 66 / 0.2);
border-top: none;
}
.control-checkbox {
display: flex;
align-items: center;
gap: 0.5rem;
color: #94a3b8;
font-size: 0.8125rem;
cursor: pointer;
}
.control-checkbox input {
width: 14px;
height: 14px;
accent-color: var(--color-accent-500);
}
.control-btn {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.625rem;
background: transparent;
border: 1px solid rgb(37 37 66 / 0.3);
border-radius: 0.25rem;
color: #94a3b8;
font-size: 0.75rem;
cursor: pointer;
transition: all 0.15s ease;
}
.control-btn:hover:not(:disabled) {
background: rgb(37 37 66 / 0.3);
color: white;
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
`]
})
export class ConsoleComponent implements OnInit, OnDestroy, AfterViewChecked {
@ViewChild('consoleOutput') consoleOutput!: ElementRef;
private minerService = inject(MinerService);
private sanitizer = inject(DomSanitizer);
private state = this.minerService.state;
private pollSub?: Subscription;
runningMiners = computed(() => this.state().runningMiners);
viewMode = this.minerService.viewMode;
globalSelectedMiner = this.minerService.selectedMinerName;
// Local console selection (used when in "all" mode to pick which logs to show)
consoleSelectedMiner = signal