Swarm Facelift (#1231)

* Action bar

* Add swarm table

* Add vrTemp check

* Reorder hostname & IP

* Optimize the refresh button to prevent flickering

* Prevent column width jumps on sorting

---------

Co-authored-by: duckaxe <>
This commit is contained in:
duckAxe
2025-09-22 19:03:26 +02:00
committed by GitHub
parent fa0409ef5a
commit f0539f9a8b
3 changed files with 179 additions and 250 deletions

View File

@@ -1,192 +1,154 @@
<div class="flex flex-column sm:flex-row w-full gap-2 xl:gap-4 mb-4"> <div class="card">
<div class="card mb-0 w-full text-center p-4 min-w-max flex sm:flex-column xl:flex-row justify-content-center gap-1" <h2>Swarm</h2>
*ngFor="let metric of [
{
label: 'Total Devices',
value: swarm.length
},
{
label: 'Total Hashrate',
value: totals.hashRate * 1000000000 | hashSuffix
},
{
label: 'Total Power',
value: (totals.power | number: '1.1-1') + ' W'
},
{
label: 'Best Diff',
value: totals.bestDiff
}
]"
>
{{metric.label}}: <span class="text-primary">{{metric.value}}</span>
</div>
</div>
<div class="table-container"> <ng-container *ngIf="swarm.length; else empty">
<table cellspacing="0" cellpadding="0" class="text-sm md:text-base"> <div class="grid">
<tr> <div class="col-6 xl:col-3" *ngFor="let metric of [
<th *ngFor="let field of [ { label: 'Total Devices', value: swarm.length },
{ { label: 'Total Hashrate', value: totals.hashRate * 1000000000 | hashSuffix},
name: 'IP', { label: 'Total Power', value: (totals.power | number: '1.1-1') + ' W' },
label: 'IP' { label: 'Best Diff', value: totals.bestDiff }
},
{
name: 'hostname',
label: 'Hostname'
},
{
name: 'hashRate',
label: 'Hashrate'
},
{
name: 'sharesAccepted',
label: 'Shares'
},
{
name: 'bestDiff',
label: 'Best Diff'
},
{
name: 'uptimeSeconds',
label: 'Uptime'
},
{
name: 'power',
label: 'Power'
},
{
name: 'temp',
label: 'Temp'
},
{
name: 'poolDifficulty',
label: 'Pool Diff'
},
{
name: 'version',
label: 'Version'
},
]"> ]">
<div class="flex align-items-center cursor-pointer select-none h-full" (click)="sortBy(field.name)"> <div class="card flex flex-column mb-0">
<span class="flex-1">{{field.label}}</span> <span class="text-500 font-medium mb-3">{{metric.label}}</span>
<i class="pi text-xs flex items-center" [ngClass]="{ <div class="text-900 font-medium text-2xl">{{metric.value}}</div>
'pi-sort-alt': sortField !== field.name,
'pi-sort-amount-up-alt': sortField === field.name && sortDirection === 'asc',
'pi-sort-amount-down': sortField === field.name && sortDirection === 'desc'
}"></i>
</div> </div>
</th>
<th>Edit</th>
<th>Restart</th>
<th>Remove</th>
</tr>
<ng-container *ngFor="let axe of swarm">
<tr>
<td>
<a
[ngClass]="'text-'+axe.swarmColor+'-500'"
[href]="'http://'+axe.IP"
target="_blank"
[pTooltip]="axe.deviceModel || 'Other'"
tooltipPosition="top">{{axe.IP}}</a>
</td>
<td>{{axe.hostname}}</td>
<td>{{axe.hashRate * 1000000000 | hashSuffix}}</td>
<td>
<div class="w-min cursor-pointer"
pTooltip="Shares Accepted"
tooltipPosition="top">
{{axe.sharesAccepted | number: '1.0-0'}}
</div>
<div class="text-sm w-min cursor-pointer"
pTooltip="Shares Rejected"
tooltipPosition="top">
{{axe.sharesRejected | number: '1.0-0'}}
</div>
</td>
<td>
<div class="w-min cursor-pointer"
pTooltip="Best Diff"
tooltipPosition="top">
{{axe.bestDiff}}
</div>
<div class="text-sm w-min cursor-pointer"
pTooltip="Best Session Diff"
tooltipPosition="top">
{{axe.bestSessionDiff}}
</div>
</td>
<td>{{axe.uptimeSeconds | dateAgo: {intervals: 2} }}</td>
<td>{{axe.power | number: '1.1-1'}} <small>W</small> </td>
<td>
<div
[ngClass]="{'text-orange-500': axe.temp > 68}"
pTooltip="ASIC Temperature"
tooltipPosition="top">
{{axe.temp | number: '1.0-1'}} °<small>C</small>
</div>
<div class="text-sm w-min cursor-pointer"
[ngClass]="{'text-orange-500': axe.vrTemp > 90}"
*ngIf="axe.vrTemp"
pTooltip="Voltage Regulator Temperature"
tooltipPosition="top">
{{axe.vrTemp | number: '1.0-1'}} °<small>C</small>
</div>
</td>
<td>{{axe.poolDifficulty}}</td>
<td>{{axe.version}}</td>
<td><p-button icon="pi pi-pencil" pp-button (click)="edit(axe)"></p-button></td>
<td><p-button icon="pi pi-sync" pp-button severity="danger" (click)="restart(axe)"></p-button></td>
<td><p-button icon="pi pi-trash" pp-button severity="secondary" (click)="remove(axe)"></p-button></td>
</tr>
</ng-container>
</table>
<div class="flex flex-wrap gap-2 mt-3 text-sm justify-content-center">
<div *ngFor="let item of getFamilies" class="flex align-items-center gap-1">
<span [class]="'text-' + item.swarmColor + '-500'"></span>
<span>{{ item.deviceModel || 'Other' }} ({{item.asicCount > 1 ? item.asicCount + 'x ' : ''}}{{item.ASICModel}})</span>
</div>
</div>
</div>
<div class="card p-3 mt-4">
<form [formGroup]="form">
<div class="field grid p-fluid mb-0">
<label htmlFor="ip" class="col-12 mb-2 md:col-4 md:mb-0">Manual Addition</label>
<div class="col-12 md:col-8">
<p-inputGroup>
<input pInputText id="manualAddIp" formControlName="manualAddIp" type="text" />
<button pButton [disabled]="form.invalid" (click)="add()">Add</button>
</p-inputGroup>
</div> </div>
</div> </div>
</form> <div class="card mt-2 overflow-x-auto">
</div> <table class="w-full">
<div class="flex flex-column sm:flex-row gap-4 justify-content-between"> <thead>
<div class="flex gap-1 sm:gap-3 text-sm md:text-base"> <tr>
<button pButton (click)="scanNetwork()" [disabled]="scanning">{{scanning ? 'Scanning...' : 'Automatic Scan'}}</button> <th *ngFor="let field of [
<button pButton severity="secondary" (click)="refreshList()" [disabled]="scanning || isRefreshing" class="refresh-button"> { label: 'Hostname', name: 'hostname' },
<span aria-hidden="true">Refresh List (30)</span> { label: 'IP', name: 'IP' },
<span>{{ isRefreshing ? 'Refreshing...' : 'Refresh List (' + refreshIntervalTime + ')' }}</span> { label: 'Hashrate', name: 'hashRate' },
</button> { label: 'Shares', name: 'sharesAccepted' },
</div> { label: 'Best Diff', name: 'bestDiff' },
<div class="flex align-items-center gap-2"> { label: 'Uptime', name: 'uptimeSeconds' },
<label for="refresh-interval" class="text-sm md:text-base">Refresh Interval:</label> { label: 'Power', name: 'power' },
<p-slider id="refresh-interval" class="pl-2 pr-2" { label: 'Temp', name: 'temp' },
[min]="5" { label: 'Pool Diff', name: 'poolDifficulty' },
[max]="30" { label: 'Version', name: 'version' },
[style]="{'width': '150px'}" ]" class="text-500 font-medium">
[formControl]="refreshIntervalControl"> <div class="flex align-items-center cursor-pointer select-none relative" (click)="sortBy(field.name)">
</p-slider> <span class="pr-3">{{field.label}}</span>
<span class="text-sm md:text-base">{{refreshTimeSet}} s</span> <i class="pi text-xs absolute right-0" [ngClass]="{
</div> 'pi-sort-up-fill': sortField === field.name && sortDirection === 'asc',
</div> 'pi-sort-down-fill': sortField === field.name && sortDirection === 'desc'
}"></i>
</div>
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let axe of swarm">
<td>
<a [href]="'http://' + axe.IP" target="_blank" [ngClass]="'text-' + axe.swarmColor + '-500'">
{{axe.hostname}}
</a>
</td>
<td>
<a [href]="'http://' + axe.IP" target="_blank" class="text-900">{{axe.IP}}</a>
</td>
<td>{{axe.hashRate * 1000000000 | hashSuffix}}</td>
<td>
<p class="cursor-pointer m-0" pTooltip="Shares Accepted" tooltipPosition="top">
{{axe.sharesAccepted | number: '1.0-0'}}
</p>
<p class="text-500 cursor-pointer m-0" pTooltip="Shares Rejected" tooltipPosition="top">
{{axe.sharesRejected | number: '1.0-0'}}
</p>
</td>
<td>
<p class="cursor-pointer m-0" pTooltip="Best Diff" tooltipPosition="top">
{{axe.bestDiff}}
</p>
<p class="text-500 cursor-pointer m-0" pTooltip="Best Session Diff" tooltipPosition="top">
{{axe.bestSessionDiff}}
</p>
</td>
<td class="cursor-pointer" pTooltip="{{axe.uptimeSeconds | dateAgo}}" tooltipPosition="top">
{{axe.uptimeSeconds | dateAgo: {intervals: 1} }}
</td>
<td>{{axe.power | number: '1.1-1'}} W</td>
<td>
<p class="cursor-pointer m-0" [ngClass]="{'text-orange-500': axe.temp > 68}" pTooltip="ASIC Temperature" tooltipPosition="top">
{{axe.temp | number: '1.0-1'}} °C
</p>
<p class="text-500 cursor-pointer m-0" *ngIf="axe.vrTemp" [ngClass]="{'text-orange-500': axe.vrTemp > 90}" pTooltip="Voltage Regulator Temperature" tooltipPosition="top">
{{axe.vrTemp | number: '1.0-1'}} °C
</p>
</td>
<td>{{axe.poolDifficulty}}</td>
<td>{{axe.version}}</td>
<td>
<p-button
icon="pi pi-pencil flex align-items-center justify-content-center"
pp-button
severity="secondary"
styleClass="button-icon"
(click)="edit(axe)" />
<p-button
icon="pi pi-refresh flex align-items-center justify-content-center"
pp-button
severity="secondary"
styleClass="button-icon"
class="mx-2"
(click)="restart(axe)" />
<p-button
icon="pi pi-trash flex align-items-center justify-content-center"
pp-button
severity="secondary"
styleClass="button-icon"
(click)="remove(axe)" />
</td>
</tr>
</tbody>
</table>
</div>
<app-modal [headline]="selectedAxeOs?.IP"> <div class="flex flex-wrap gap-2 mt-3 text-sm justify-content-center">
<app-edit *ngIf="selectedAxeOs" [uri]="'http://' + selectedAxeOs.IP"></app-edit> <div *ngFor="let item of deviceFamilies" class="flex align-items-center gap-1">
</app-modal> <span [class]="'text-' + item.swarmColor + '-500'"></span>
<span>{{ item.deviceModel || 'Other' }} ({{ item.asicCount > 1 ? item.asicCount + 'x ' : '' }}{{ item.ASICModel }})</span>
</div>
</div>
</ng-container>
<ng-template #empty>
<p class="text-500 text-center pt-5">
Scan your network for devices or add a device manually by using its IP address.
</p>
</ng-template>
<form *ngIf="form" [formGroup]="form" class="card flex flex-column md:flex-row gap-3 mt-5">
<button pButton (click)="scanNetwork()" [disabled]="scanning" class="white-space-nowrap w-full md:w-auto block text-center button-text">
{{scanning ? 'Scanning...' : 'Auto Scan'}}
</button>
<div class="flex align-items-center gap-3" *ngIf="this.swarm.length">
<button pButton (click)="refreshList()" class="flex justify-content-between button-text">
<span>{{isRefreshing ? 'Refreshing...' : 'Refresh'}}</span>
<span *ngIf="!isRefreshing" class="text-sm text-700">{{refreshIntervalTime}}</span>
</button>
<div class="flex flex-grow-1 align-items-center gap-3">
<p-slider class="w-full md:w-3rem xl:w-6rem" [min]="5" [max]="30" [formControl]="refreshIntervalControl"></p-slider>
<span class="text-sm white-space-nowrap">{{refreshTimeSet}} s</span>
</div>
</div>
<div class="md:ml-auto">
<p-inputGroup>
<input pInputText placeholder="Add Device by IP" formControlName="manualAddIp" type="text" class="xl:w-20rem" />
<button pButton [disabled]="form.invalid" (click)="add()" class="ml-1">Add</button>
</p-inputGroup>
</div>
</form>
<app-modal [headline]="selectedAxeOs?.IP">
<app-edit *ngIf="selectedAxeOs" [uri]="'http://' + selectedAxeOs.IP"></app-edit>
</app-modal>
</div>

View File

@@ -1,75 +1,41 @@
table { table {
width: 100%; border-collapse: collapse;
border: 1px solid #304562;
} }
th {
text-align: left;
background-color: #1f2d40;
padding: 0;
> div {
height: 100%;
padding: 0.5rem 1rem;
gap: 0.75rem;
}
.pi {
opacity: 0.7;
height: 1em;
line-height: 1;
&.pi-sort-amount-up-alt,
&.pi-sort-amount-down {
opacity: 1;
}
}
}
th, th,
td { td {
border-bottom: 1px solid #304562; padding: 0 0.5rem 0.5rem;
} white-space: nowrap;
text-align: left;
vertical-align: top;
&:first-child {
padding-left: 0;
}
&:last-child {
padding-right: 0;
text-align: right;
}
}
td { td {
padding: 1rem; padding-top: 0.5rem;
p {
line-height: 1.4;
}
} }
tbody tr {
border-top: 1px solid var(--surface-border);
th > div { &:last-child td {
min-width: 100px; padding-bottom: 0;
padding-right: 0.5rem;
}
a {
color: white;
}
.table-container {
width: 100%;
overflow-x: auto;
margin: 0;
table {
min-width: 100%;
white-space: nowrap;
th, td {
padding: 0.5rem;
text-align: left;
}
} }
} }
.refresh-button { .button-text {
display: grid; min-width: 8rem;
place-items: center;
> span:first-child {
visibility: hidden;
grid-area: 1 / 1;
}
> span:last-child {
grid-area: 1 / 1;
}
} }
::ng-deep .p-button.button-icon {
width: 2.2rem;
height: 2.2rem;
margin-top: 0.25rem;
}

View File

@@ -83,7 +83,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
} }
this.refreshIntervalRef = window.setInterval(() => { this.refreshIntervalRef = window.setInterval(() => {
if (!this.scanning && !this.isRefreshing) { if (!this.scanning && !this.isRefreshing && this.swarm.length) {
this.refreshIntervalTime--; this.refreshIntervalTime--;
if (this.refreshIntervalTime <= 0) { if (this.refreshIntervalTime <= 0) {
this.refreshList(false); this.refreshList(false);
@@ -132,6 +132,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
}, },
complete: () => { complete: () => {
this.scanning = false; this.scanning = false;
this.refreshIntervalTime = this.refreshTimeSet;
} }
}); });
} }
@@ -316,7 +317,7 @@ export class SwarmComponent implements OnInit, OnDestroy {
.reduce((max, curr) => this.compareBestDiff(max, curr), '0'); .reduce((max, curr) => this.compareBestDiff(max, curr), '0');
} }
get getFamilies(): SwarmDevice[] { get deviceFamilies(): SwarmDevice[] {
return this.swarm.filter((v, i, a) => return this.swarm.filter((v, i, a) =>
a.findIndex(({ deviceModel, ASICModel, asicCount }) => a.findIndex(({ deviceModel, ASICModel, asicCount }) =>
v.deviceModel === deviceModel && v.deviceModel === deviceModel &&