Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions main/http_server/axe-os/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import { HashSuffixPipe } from './pipes/hash-suffix.pipe';
import { DiffSuffixPipe } from './pipes/diff-suffix.pipe';
import { AddressPipe } from './pipes/address.pipe';
import { SatsPipe } from './pipes/sats.pipe';
import { HeatmapLightnessPipe } from './pipes/heatmap-lightness.pipe';
import { DialogService, DialogListComponent } from './services/dialog.service';

const components = [
Expand Down Expand Up @@ -98,8 +97,7 @@ const components = [
HashSuffixPipe,
DiffSuffixPipe,
AddressPipe,
SatsPipe,
HeatmapLightnessPipe,
SatsPipe
],
providers: [
{ provide: LocationStrategy, useClass: HashLocationStrategy },
Expand Down
87 changes: 36 additions & 51 deletions main/http_server/axe-os/src/app/components/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ <h4 class="text-center font-medium">Fan</h4>

<div class="mb-4">
<h6 class="flex justify-between mb-1" tooltipPosition="top"
appTooltip="{{info.fan2rpm > 0 ? 'Fan 1: ' : ''}}{{info.fanrpm}} RPM{{info.fan2rpm > 0 ? '<br>Fan 2: ' + info.fan2rpm + ' RPM' : ''}}">
appTooltip="{{info.fan2rpm > 0 ? 'Fan 1: ' : ''}}{{info.fanrpm}} RPM{{info.fan2rpm > 0 ? '\nFan 2: ' + info.fan2rpm + ' RPM' : ''}}">

@if (info.fan2rpm > 0) {
Fan Speeds
Expand Down Expand Up @@ -592,59 +592,44 @@ <h4 class="whitespace-nowrap overflow-hidden text-ellipsis font-medium">
<!-- Hashrate Registers -->
<div class="grid-stack-item" gs-id="registers">
<div class="grid-stack-item-content">
<div class="card">
<h4 class="whitespace-nowrap overflow-hidden text-ellipsis font-medium">Hashrate Registers</h4>

@if (asicsAmount; as asicAmount) {
<table class="heatmap text-{{asicAmount <= 2 ? 'sm' : 'xs'}} w-full">
<thead>
<tr>
@if (asicAmount > 1) {
<th class="text-left">ASIC</th>
<div class="card flex flex-col">
<h4 class="whitespace-nowrap overflow-hidden text-ellipsis font-medium mb-2">Hashrate Registers</h4>

@if (sparklineAsicRows.length > 0) {
<div class="w-full flex-1 min-h-0 flex flex-col gap-1 overflow-hidden">
<div class="flex items-center gap-2 shrink-0 pb-0.5">
@if (sparklineAsicRows.length > 1) {
<span class="w-8 text-xs text-secondary font-medium shrink-0 text-center">ASIC</span>
}
<div class="flex gap-1.5 flex-1 min-w-0">
@for (cell of sparklineAsicRows[0].cells; track cell.id) {
<span class="flex-1 min-w-0 text-center text-xs text-secondary font-medium truncate">Domain {{ cell.domainNumber }}</span>
}
@if (asicDomainsAmount; as asicDomainsAmount) {
@if (asicDomainsAmount > 0) {
<th [attr.colspan]="asicDomainsAmount">
@if (asicDomainsAmount === 1) {
Domain
} @else if (asicDomainsAmount <= 4) {
Domains
} @else {
Domains ({{ domainUnit }})
}
</th>
}
</div>
</div>
@for (row of sparklineAsicRows; track row.id) {
<div class="flex items-stretch gap-2 flex-1 min-h-0">
@if (sparklineAsicRows.length > 1) {
<span class="w-8 text-xs text-secondary font-medium shrink-0 flex items-center justify-center">{{ row.label }}</span>
}
</tr>
</thead>
<tbody>
@for (asic of info.hashrateMonitor?.asics; track trackByIndex($index, asic); let i = $index) {
<tr class="cursor-pointer"
[appTooltip]="(asicAmount > 1 ? 'ASIC ' + (i + 1) + ' - ' : '') + 'Error count: ' + asic.errorCount"
tooltipPosition="bottom">
@if (asicAmount > 1) {
<td class="text-secondary text-left">
{{ i + 1 }}
</td>
<div class="flex gap-1.5 flex-1 h-full min-w-0">
@for (cell of row.cells; track cell.id) {
<div class="sparkline-cell flex-1 min-w-0 h-full" [appTooltip]="cell.tooltip" tooltipPosition="top">
<div class="absolute bottom-0.5 left-1 z-0 text-[10px] leading-none font-medium text-secondary truncate select-none pointer-events-none max-w-[calc(100%-8px)]">
{{ cell.hashrate }}
</div>
<svg class="sparkline-svg w-full h-full" [attr.viewBox]="sparklineViewBox" preserveAspectRatio="none">
<line x1="0" [attr.y1]="cell.nominalY" x2="100" [attr.y2]="cell.nominalY" class="nominal-line" />
@if (cell.path) {
<path [attr.d]="cell.path" class="sparkline-path" />
}
</svg>
</div>
}
@for (domain of asic?.domains; track trackByIndex($index, domain); let dIndex = $index) {
<td
[style.--lightness]="getHeatmapLightness(domain, info.expectedHashrate)"
[style.height]="(asicAmount <= 2 ? '2rem' : null)"
class="heatmap-cell"
[appTooltip]="asicDomainsAmount > 4 ? ((asicAmount > 1 ? 'ASIC ' + (i + 1) + ' - ' : '') + 'Domain ' + (dIndex + 1) + ': ' + (domain | hashSuffix)) : ''"
tooltipPosition="bottom">
@if (asicDomainsAmount <= 4) {
<span class="text-xs">{{ domain | hashSuffix }}</span>
} @else {
<span class="text-xs">{{ domain | hashSuffix: { power: domainPower, hideUnit: true } }}</span>
}
</td>
}
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
}
</div>
</div>
Expand Down
62 changes: 45 additions & 17 deletions main/http_server/axe-os/src/app/components/home/home.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,47 @@
word-break: break-all;
}

.heatmap {
width: 100%;
border-collapse: separate;
border-spacing: 1px;
table-layout: fixed;

th {
padding: 0;
.sparkline-cell {
position: relative;
background-color: var(--color-surface);
border: 1px solid var(--color-border-content);
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: border-color 0.15s ease;

&:hover {
border-color: var(--color-primary);
}

td {
&.heatmap-cell {
text-align: center;
padding: 0 2px;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 2s ease-in-out;
background-color: oklch(from var(--color-primary) var(--lightness, 0.5) c h);
color: contrast-color(oklch(from var(--color-primary) var(--lightness, 0.5) c h));
.sparkline-svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
transform: scaleY(-1);

.nominal-line {
stroke: var(--color-text-secondary);
stroke-dasharray: 2 2;
stroke-width: 1;
opacity: 0.35;
vector-effect: non-scaling-stroke;
}

.sparkline-path {
fill: none;
stroke: var(--color-primary);
stroke-width: 1.5;
vector-effect: non-scaling-stroke;
}
}
}



// Gridstack layout
.grid-stack {
margin: -8px;
Expand Down Expand Up @@ -92,6 +110,16 @@
}
}

// Registers widget: tighter padding to fit domain rows cleanly without scrollbar
.grid-stack-item[gs-id="registers"] {
.grid-stack-item-content {
> .card {
padding: 0.6rem 0.85rem;
overflow: hidden;
}
}
}

// Hide resize handles by default
.ui-resizable-handle,
.gs-resizable-handle {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import { DateAgoPipe } from 'src/app/pipes/date-ago.pipe';
import { AddressPipe } from 'src/app/pipes/address.pipe';
import { SatsPipe } from 'src/app/pipes/sats.pipe';
import { ByteSuffixPipe } from 'src/app/pipes/byte-suffix.pipe';
import { HeatmapLightnessPipe } from 'src/app/pipes/heatmap-lightness.pipe';

import { TooltipTextIconComponent } from 'src/app/components/tooltip-text-icon/tooltip-text-icon.component';
import { TooltipIconComponent } from 'src/app/components/tooltip-icon/tooltip-icon.component';
Expand Down Expand Up @@ -168,7 +167,8 @@ const mockSystemStatistics: ISystemStatistics = {

const mockLiveDataService = {
info$: new BehaviorSubject<ISystemInfo>(mockSystemInfo),
connected$: of(true)
connected$: of(true),
domainHistory: []
};

const mockSystemApiService = {
Expand Down Expand Up @@ -219,8 +219,7 @@ describe('HomeComponent', () => {
DateAgoPipe,
AddressPipe,
SatsPipe,
ByteSuffixPipe,
HeatmapLightnessPipe
ByteSuffixPipe
],
providers: [
provideRouter([]),
Expand Down
114 changes: 82 additions & 32 deletions main/http_server/axe-os/src/app/components/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ interface ISystemInfoError {
startTime: number | null;
}

export interface SparklineDomainCell {
id: string;
domainNumber: number;
hashrate: string;
nominalY: number;
path: string;
tooltip: string;
}

export interface SparklineAsicRow {
id: string;
label: string;
cells: SparklineDomainCell[];
}

const HOME_CHART_DATA_SOURCES = 'HOME_CHART_DATA_SOURCES';
const HOME_CHART_HIDDEN_SENSORS = 'HOME_CHART_HIDDEN_SENSORS';
const DASHBOARD_LAYOUT_KEY = 'DASHBOARD_LAYOUT_V1';
Expand Down Expand Up @@ -179,11 +194,8 @@ export class HomeComponent implements OnInit, OnDestroy {

// Performance optimization cache properties
private primaryColorRgb: { r: number, g: number, b: number } = { r: 0, g: 0, b: 0 };
private isHardwareConfigInitialized = false;
public asicsAmount: number = 0;
public asicDomainsAmount: number = 0;
public domainUnit: string = 'GH/s';
public domainPower: number = 3;
public sparklineAsicRows: SparklineAsicRow[] = [];
public sparklineViewBox: string = '0 0 100 30';
public efficiency: number = 0;
public efficiencyAverage: number = 0;
public expectedEfficiency: number = 0;
Expand Down Expand Up @@ -942,14 +954,8 @@ export class HomeComponent implements OnInit, OnDestroy {
this.maxFrequency = Math.max(800, info.actualFrequency || info.frequency || 0);
this.statsLimit = info.statsLimit || 720;

// Pre-compute values for template performance
if (!this.isHardwareConfigInitialized && info.hashrateMonitor?.asics?.length) {
this.isHardwareConfigInitialized = true;
this.asicsAmount = info.hashrateMonitor.asics.length;
this.asicDomainsAmount = info.hashrateMonitor.asics[0]?.domains?.length ?? 0;
}

this.updateDomainUnit(info);
this.updateDomainSparklines(info);

this.updateChartDataSources(info);

Expand Down Expand Up @@ -1208,9 +1214,6 @@ export class HomeComponent implements OnInit, OnDestroy {
hasCoinbaseVisibility(info: ISystemInfo): boolean {
return info.blockHeight > 0;
}
trackByIndex(index: number, _item: any) {
return index;
}

// Pools that pay miners directly from the coinbase can push the user's own output far down
// the list, so lift it to the top. Outputs beyond the firmware's capacity are not in this
Expand Down Expand Up @@ -1287,28 +1290,75 @@ export class HomeComponent implements OnInit, OnDestroy {
return percentage < 10 ? percentage.toPrecision(2) : percentage.toFixed(1);
}

public getHeatmapLightness(domainHashrate: number, expectedHashrate: number): string {
const expected = expectedHashrate || 1;
const ratio = Math.max(0, Math.min(2, (domainHashrate / expected) * this.asicsAmount) * this.asicDomainsAmount);
const deviation = isNaN(ratio) ? 1 : Math.abs(ratio - 1); // 0 = perfect, 1 = 100% off
const t = 1 - Math.pow(1 - deviation, 1.5); // Exponent controls graduality
private updateDomainSparklines(info: ISystemInfo): void {
const asics = info.hashrateMonitor?.asics;
if (!asics || !asics.length) {
return;
}

const direction = ratio > 1 ? 1 : -1;
const amount = direction * t * 0.4;
const lightness = 0.5 + amount;
const asicsCount = asics.length;
const domainsCount = asics[0]?.domains?.length ?? 0;
if (domainsCount === 0) {
return;
}

return lightness.toFixed(3);
}
const expected = info.expectedHashrate || 0;
const nominal = (expected > 0 && asicsCount > 0 && domainsCount > 0)
? expected / (asicsCount * domainsCount)
: 1;
const power = HashSuffixPipe.getPower(nominal);
const suffix = HashSuffixPipe.getSuffix(power);
const viewportHeight = nominal * 1.5;
this.sparklineViewBox = `0 0 100 ${viewportHeight.toFixed(2)}`;

const nominalY = nominal;
const rows: SparklineAsicRow[] = [];

for (let a = 0; a < asicsCount; a++) {
const domains = asics[a].domains || [];
const errorCount = asics[a].errorCount ?? 0;
const cells: SparklineDomainCell[] = [];

for (let d = 0; d < domainsCount; d++) {
const history = this.liveDataService?.domainHistory?.[a]?.[d] ?? [domains[d] ?? 0];
const currentVal = domains[d] ?? 0;

let path = '';
const pts = history.length === 1 ? [history[0], history[0]] : history;
const nPts = pts.length;
if (nPts >= 2) {
for (let k = 0; k < nPts; k++) {
const px = (k / (nPts - 1)) * 100;
const py = (pts[k] != null && !isNaN(pts[k])) ? pts[k] : 0;
path += (k === 0 ? 'M ' : ' L ') + px.toFixed(1) + ' ' + py.toFixed(2);
}
}

private updateDomainUnit(info: ISystemInfo) {
const totalDomains = this.asicsAmount * this.asicDomainsAmount;
if (totalDomains <= 0) return;
const pct = nominal > 0 ? Math.round((currentVal / nominal) * 100) : 0;
const valStr = HashSuffixPipe.transform(currentVal, { hideUnit: true, power });
const title = asicsCount > 1 ? `ASIC ${a + 1} • Domain ${d + 1}` : `Domain ${d + 1}`;
const errorLine = errorCount > 0 ? `\nErrors: ${errorCount}` : '';
const tooltip = `${title}\n${valStr} ${suffix} (${pct}% target)${errorLine}`;

cells.push({
id: `${a}-${d}`,
domainNumber: d + 1,
hashrate: valStr,
nominalY,
path,
tooltip
});
}

const totalHashrate = info.expectedHashrate || info.hashRate || 0;
if (totalHashrate <= 0) return;
rows.push({
id: `asic-${a}`,
label: `${a + 1}`,
cells
});
}

this.domainPower = HashSuffixPipe.getPower(totalHashrate / totalDomains);
this.domainUnit = HashSuffixPipe.getSuffix(this.domainPower);
this.sparklineAsicRows = rows;
this.cd.markForCheck();
}

private updateChartUnitGroups() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
standalone: true,
template: `{{ text }}`,
host: {
'class': 'fixed z-[99999] bg-[#2b2b2b] text-[#ffffff] app-tooltip-text px-2.5 py-1.5 rounded shadow-xl pointer-events-none border border-neutral-700 max-w-[250px] whitespace-normal text-center'
'class': 'fixed z-[99999] bg-[#2b2b2b] text-[#ffffff] app-tooltip-text px-2.5 py-1.5 rounded shadow-xl pointer-events-none border border-neutral-700 max-w-[250px] whitespace-pre-line text-center'
}
})
export class TooltipContentComponent {
Expand Down
Loading
Loading