бла бла бла
This commit is contained in:
Generated
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PortForwardingSettings">
|
||||||
|
<ports>
|
||||||
|
<entry key="8080">
|
||||||
|
<ForwardedPortInfo>
|
||||||
|
<option name="hostPort" value="8080" />
|
||||||
|
<option name="readOnly" value="false" />
|
||||||
|
</ForwardedPortInfo>
|
||||||
|
</entry>
|
||||||
|
</ports>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
<option name="externalProjectPath" value="$PROJECT_DIR$/backend" />
|
<option name="externalProjectPath" value="$PROJECT_DIR$/backend" />
|
||||||
<option name="gradleJvm" value="#JAVA_HOME" />
|
<option name="gradleJvm" value="21" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$/backend" />
|
<option value="$PROJECT_DIR$/backend" />
|
||||||
|
|||||||
Generated
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
|
<file type="web" url="file://$PROJECT_DIR$/backend" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -18,6 +18,7 @@ repositories {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||||
|
|
||||||
testImplementation 'org.springframework.boot:spring-boot-starter-webflux-test'
|
testImplementation 'org.springframework.boot:spring-boot-starter-webflux-test'
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
|
|
||||||
package com.htopweb.system;
|
package com.htopweb.system;
|
||||||
|
|
||||||
import com.htopweb.system.linux.LinuxDetailsResponse;
|
import com.htopweb.system.linux.*;
|
||||||
import com.htopweb.system.linux.LinuxDetailsService;
|
|
||||||
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -15,12 +13,25 @@ public class SystemMetricsController {
|
|||||||
private final SystemMetricsService _systemMetricsService;
|
private final SystemMetricsService _systemMetricsService;
|
||||||
private final LinuxDetailsService _linuxDetailsService;
|
private final LinuxDetailsService _linuxDetailsService;
|
||||||
|
|
||||||
|
private final CpuDetailsService _cpuDetailsService;
|
||||||
|
|
||||||
|
private final LinuxMemoryService _linuxMemoryService;
|
||||||
|
private final LinuxDiskService _linuxDiskService;
|
||||||
|
private final LinuxNetworkService _linuxNetworkService;
|
||||||
|
|
||||||
public SystemMetricsController(
|
public SystemMetricsController(
|
||||||
SystemMetricsService systemMetricsService,
|
SystemMetricsService systemMetricsService,
|
||||||
LinuxDetailsService linuxDetailsService
|
LinuxDetailsService linuxDetailsService,
|
||||||
|
CpuDetailsService cpuDetailsService,
|
||||||
|
LinuxMemoryService linuxMemoryService, LinuxDiskService linuxDiskService, LinuxNetworkService linuxNetworkService
|
||||||
|
|
||||||
) {
|
) {
|
||||||
_systemMetricsService = systemMetricsService;
|
_systemMetricsService = systemMetricsService;
|
||||||
_linuxDetailsService = linuxDetailsService;
|
_linuxDetailsService = linuxDetailsService;
|
||||||
|
_cpuDetailsService = cpuDetailsService;
|
||||||
|
_linuxMemoryService = linuxMemoryService;
|
||||||
|
_linuxDiskService = linuxDiskService;
|
||||||
|
_linuxNetworkService = linuxNetworkService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/metrics")
|
@GetMapping("/metrics")
|
||||||
@@ -32,4 +43,24 @@ public class SystemMetricsController {
|
|||||||
public LinuxDetailsResponse getLinuxDetails() {
|
public LinuxDetailsResponse getLinuxDetails() {
|
||||||
return _linuxDetailsService.getDetails();
|
return _linuxDetailsService.getDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/cpu")
|
||||||
|
public CpuDetailsResponse getCpuDetails() {
|
||||||
|
return _cpuDetailsService.getDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/linux/memory")
|
||||||
|
public LinuxMemoryResponse getLinuxMemory() {
|
||||||
|
return _linuxMemoryService.getDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/linux/disks")
|
||||||
|
public java.util.List<LinuxDiskResponse> getLinuxDisks() {
|
||||||
|
return _linuxDiskService.getDisks();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/linux/network")
|
||||||
|
public java.util.List<LinuxNetworkResponse> getLinuxNetwork() {
|
||||||
|
return _linuxNetworkService.getInterfaces();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record CpuDetailsResponse(
|
||||||
|
double totalUsagePercent,
|
||||||
|
int logicalProcessors,
|
||||||
|
List<CpuCoreResponse> cores
|
||||||
|
) {
|
||||||
|
public record CpuCoreResponse(
|
||||||
|
int core,
|
||||||
|
double usagePercent
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import oshi.SystemInfo;
|
||||||
|
import oshi.hardware.CentralProcessor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CpuDetailsService {
|
||||||
|
|
||||||
|
private final CentralProcessor _processor;
|
||||||
|
private long[][] _previousCoreTicks;
|
||||||
|
private long[] _previousSystemTicks;
|
||||||
|
|
||||||
|
public CpuDetailsService() {
|
||||||
|
SystemInfo systemInfo = new SystemInfo();
|
||||||
|
_processor = systemInfo.getHardware().getProcessor();
|
||||||
|
|
||||||
|
_previousCoreTicks = _processor.getProcessorCpuLoadTicks();
|
||||||
|
_previousSystemTicks = _processor.getSystemCpuLoadTicks();
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized CpuDetailsResponse getDetails() {
|
||||||
|
long[][] currentCoreTicks = _processor.getProcessorCpuLoadTicks();
|
||||||
|
long[] currentSystemTicks = _processor.getSystemCpuLoadTicks();
|
||||||
|
|
||||||
|
double[] coreLoads = _processor.getProcessorCpuLoadBetweenTicks(
|
||||||
|
_previousCoreTicks,
|
||||||
|
currentCoreTicks
|
||||||
|
);
|
||||||
|
|
||||||
|
double systemLoad = _processor.getSystemCpuLoadBetweenTicks(
|
||||||
|
_previousSystemTicks,
|
||||||
|
currentSystemTicks
|
||||||
|
);
|
||||||
|
|
||||||
|
_previousCoreTicks = currentCoreTicks;
|
||||||
|
_previousSystemTicks = currentSystemTicks;
|
||||||
|
|
||||||
|
List<CpuDetailsResponse.CpuCoreResponse> cores =
|
||||||
|
new ArrayList<>(coreLoads.length);
|
||||||
|
|
||||||
|
for (int i = 0; i < coreLoads.length; i++) {
|
||||||
|
cores.add(new CpuDetailsResponse.CpuCoreResponse(
|
||||||
|
i,
|
||||||
|
toPercent(coreLoads[i] * 100.0)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CpuDetailsResponse(
|
||||||
|
toPercent(systemLoad * 100.0),
|
||||||
|
_processor.getLogicalProcessorCount(),
|
||||||
|
List.copyOf(cores)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double toPercent(double value) {
|
||||||
|
if (!Double.isFinite(value)) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(
|
||||||
|
Math.max(0.0, Math.min(100.0, value)) * 10.0
|
||||||
|
) / 10.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
public record LinuxDiskResponse(
|
||||||
|
String name,
|
||||||
|
String mountPoint,
|
||||||
|
String fileSystem,
|
||||||
|
long totalBytes,
|
||||||
|
long usableBytes,
|
||||||
|
long usedBytes,
|
||||||
|
double usagePercent
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import oshi.SystemInfo;
|
||||||
|
import oshi.software.os.OSFileStore;
|
||||||
|
import oshi.software.os.OperatingSystem;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LinuxDiskService {
|
||||||
|
|
||||||
|
private final OperatingSystem _operatingSystem;
|
||||||
|
|
||||||
|
public LinuxDiskService() {
|
||||||
|
SystemInfo systemInfo = new SystemInfo();
|
||||||
|
_operatingSystem = systemInfo.getOperatingSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LinuxDiskResponse> getDisks() {
|
||||||
|
return _operatingSystem.getFileSystem()
|
||||||
|
.getFileStores()
|
||||||
|
.stream()
|
||||||
|
.filter(store -> store.getMount().startsWith("/"))
|
||||||
|
.filter(store -> store.getTotalSpace() > 0)
|
||||||
|
.map(this::toResponse)
|
||||||
|
.sorted(Comparator.comparing(LinuxDiskResponse::mountPoint))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinuxDiskResponse toResponse(OSFileStore store) {
|
||||||
|
long total = Math.max(0L, store.getTotalSpace());
|
||||||
|
long usable = Math.max(
|
||||||
|
0L,
|
||||||
|
Math.min(total, store.getUsableSpace())
|
||||||
|
);
|
||||||
|
long used = Math.max(0L, total - usable);
|
||||||
|
|
||||||
|
double usage = total > 0
|
||||||
|
? Math.round((double) used / total * 1000.0) / 10.0
|
||||||
|
: 0.0;
|
||||||
|
|
||||||
|
return new LinuxDiskResponse(
|
||||||
|
store.getName(),
|
||||||
|
store.getMount(),
|
||||||
|
store.getType(),
|
||||||
|
total,
|
||||||
|
usable,
|
||||||
|
used,
|
||||||
|
usage
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
public record LinuxMemoryResponse(
|
||||||
|
long totalRamBytes,
|
||||||
|
long usedRamBytes,
|
||||||
|
long availableRamBytes,
|
||||||
|
double ramUsagePercent,
|
||||||
|
long totalSwapBytes,
|
||||||
|
long usedSwapBytes,
|
||||||
|
long freeSwapBytes,
|
||||||
|
double swapUsagePercent
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import oshi.SystemInfo;
|
||||||
|
import oshi.hardware.GlobalMemory;
|
||||||
|
import oshi.hardware.VirtualMemory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LinuxMemoryService {
|
||||||
|
|
||||||
|
private final GlobalMemory _memory;
|
||||||
|
private final VirtualMemory _virtualMemory;
|
||||||
|
|
||||||
|
|
||||||
|
public LinuxMemoryService() {
|
||||||
|
SystemInfo systemInfo = new SystemInfo();
|
||||||
|
_memory = systemInfo.getHardware().getMemory();
|
||||||
|
_virtualMemory = _memory.getVirtualMemory();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public LinuxMemoryResponse getDetails() {
|
||||||
|
long totalRam = Math.max(0L, _memory.getTotal());
|
||||||
|
long availableRam = Math.max(0L, Math.min(totalRam, _memory.getAvailable()));
|
||||||
|
long usedRam = Math.max(0L, totalRam - availableRam);
|
||||||
|
|
||||||
|
long totalSwap = Math.max(0L, _virtualMemory.getSwapTotal());
|
||||||
|
long usedSwap = Math.max(
|
||||||
|
0L,
|
||||||
|
Math.min(totalSwap, _virtualMemory.getSwapUsed())
|
||||||
|
);
|
||||||
|
long freeSwap = Math.max(0L, totalSwap - usedSwap);
|
||||||
|
|
||||||
|
return new LinuxMemoryResponse(
|
||||||
|
totalRam,
|
||||||
|
usedRam,
|
||||||
|
availableRam,
|
||||||
|
percentage(usedRam, totalRam),
|
||||||
|
totalSwap,
|
||||||
|
usedSwap,
|
||||||
|
freeSwap,
|
||||||
|
percentage(usedSwap, totalSwap)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double percentage(long used, long total) {
|
||||||
|
if (total <= 0) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round((double) used / total * 1000.0) / 10.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
|
||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public record LinuxNetworkResponse(
|
||||||
|
String name,
|
||||||
|
String displayName,
|
||||||
|
String macAddress,
|
||||||
|
List<String> ipv4Addresses,
|
||||||
|
List<String> ipv6Addresses,
|
||||||
|
long bytesReceived,
|
||||||
|
long bytesSent,
|
||||||
|
long packetsReceived,
|
||||||
|
long packetsSent,
|
||||||
|
long inboundErrors,
|
||||||
|
long outboundErrors,
|
||||||
|
double receiveBytesPerSecond,
|
||||||
|
double sendBytesPerSecond,
|
||||||
|
double receiveMbps,
|
||||||
|
double sendMbps
|
||||||
|
) {}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
|
||||||
|
package com.htopweb.system.linux;
|
||||||
|
|
||||||
|
import oshi.SystemInfo;
|
||||||
|
import oshi.hardware.NetworkIF;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ConcurrentMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class LinuxNetworkService {
|
||||||
|
|
||||||
|
private final SystemInfo _systemInfo;
|
||||||
|
|
||||||
|
private final ConcurrentMap<String, NetworkSnapshot> _previousSnapshots =
|
||||||
|
new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public LinuxNetworkService() {
|
||||||
|
_systemInfo = new SystemInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<LinuxNetworkResponse> getInterfaces() {
|
||||||
|
return _systemInfo.getHardware()
|
||||||
|
.getNetworkIFs()
|
||||||
|
.stream()
|
||||||
|
.map(this::toResponse)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private LinuxNetworkResponse toResponse(NetworkIF networkIF) {
|
||||||
|
networkIF.updateAttributes();
|
||||||
|
|
||||||
|
String name = networkIF.getName();
|
||||||
|
long received = Math.max(0L, networkIF.getBytesRecv());
|
||||||
|
long sent = Math.max(0L, networkIF.getBytesSent());
|
||||||
|
long now = System.nanoTime();
|
||||||
|
|
||||||
|
NetworkSnapshot current = new NetworkSnapshot(received, sent, now);
|
||||||
|
NetworkSnapshot previous = _previousSnapshots.put(name, current);
|
||||||
|
|
||||||
|
double receiveBytesPerSecond = 0.0;
|
||||||
|
double sendBytesPerSecond = 0.0;
|
||||||
|
|
||||||
|
if (previous != null && now > previous.timestampNanos()) {
|
||||||
|
double elapsedSeconds =
|
||||||
|
(now - previous.timestampNanos()) / 1_000_000_000.0;
|
||||||
|
|
||||||
|
if (received >= previous.bytesReceived()) {
|
||||||
|
receiveBytesPerSecond =
|
||||||
|
(received - previous.bytesReceived()) / elapsedSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sent >= previous.bytesSent()) {
|
||||||
|
sendBytesPerSecond =
|
||||||
|
(sent - previous.bytesSent()) / elapsedSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double receiveMbps = receiveBytesPerSecond * 8.0 / 1_000_000.0;
|
||||||
|
double sendMbps = sendBytesPerSecond * 8.0 / 1_000_000.0;
|
||||||
|
|
||||||
|
return new LinuxNetworkResponse(
|
||||||
|
name,
|
||||||
|
networkIF.getDisplayName(),
|
||||||
|
networkIF.getMacaddr(),
|
||||||
|
Arrays.asList(networkIF.getIPv4addr()),
|
||||||
|
Arrays.asList(networkIF.getIPv6addr()),
|
||||||
|
received,
|
||||||
|
sent,
|
||||||
|
Math.max(0L, networkIF.getPacketsRecv()),
|
||||||
|
Math.max(0L, networkIF.getPacketsSent()),
|
||||||
|
Math.max(0L, networkIF.getInErrors()),
|
||||||
|
Math.max(0L, networkIF.getOutErrors()),
|
||||||
|
roundOne(receiveBytesPerSecond),
|
||||||
|
roundOne(sendBytesPerSecond),
|
||||||
|
roundOne(receiveMbps),
|
||||||
|
roundOne(sendMbps)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private double roundOne(double value) {
|
||||||
|
return Math.round(value * 10.0) / 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private record NetworkSnapshot(
|
||||||
|
long bytesReceived,
|
||||||
|
long bytesSent,
|
||||||
|
long timestampNanos
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -9,5 +9,5 @@
|
|||||||
<link rel="stylesheet" href="styles-5INURTSO.css"></head>
|
<link rel="stylesheet" href="styles-5INURTSO.css"></head>
|
||||||
<body>
|
<body>
|
||||||
<app-root></app-root>
|
<app-root></app-root>
|
||||||
<script src="main-7OF6PXJ5.js" type="module"></script></body>
|
<script src="main-Y67327X6.js" type="module"></script></body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "10kb",
|
"maximumWarning": "10kb",
|
||||||
"maximumError": "12kb"
|
"maximumError": "30kb"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all"
|
"outputHashing": "all"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export interface CpuCore {
|
||||||
|
core: number;
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CpuDetails {
|
||||||
|
totalUsagePercent: number;
|
||||||
|
logicalProcessors: number;
|
||||||
|
cores: CpuCore[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export interface LinuxDetails {
|
||||||
|
linux: boolean;
|
||||||
|
hostname: string;
|
||||||
|
distribution: string;
|
||||||
|
kernelVersion: string;
|
||||||
|
uptimeSeconds: number;
|
||||||
|
loadAverage1Min: number;
|
||||||
|
loadAverage5Min: number;
|
||||||
|
loadAverage15Min: number;
|
||||||
|
logicalProcessors: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface LinuxDisk {
|
||||||
|
name: string;
|
||||||
|
mountPoint: string;
|
||||||
|
fileSystem: string;
|
||||||
|
totalBytes: number;
|
||||||
|
usableBytes: number;
|
||||||
|
usedBytes: number;
|
||||||
|
usagePercent: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export interface LinuxMemory {
|
||||||
|
totalRamBytes: number;
|
||||||
|
usedRamBytes: number;
|
||||||
|
availableRamBytes: number;
|
||||||
|
ramUsagePercent: number;
|
||||||
|
totalSwapBytes: number;
|
||||||
|
usedSwapBytes: number;
|
||||||
|
freeSwapBytes: number;
|
||||||
|
swapUsagePercent: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export interface LinuxNetwork {
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
macAddress: string;
|
||||||
|
ipv4Addresses: string[];
|
||||||
|
ipv6Addresses: string[];
|
||||||
|
bytesReceived: number;
|
||||||
|
bytesSent: number;
|
||||||
|
packetsReceived: number;
|
||||||
|
packetsSent: number;
|
||||||
|
inboundErrors: number;
|
||||||
|
outboundErrors: number;
|
||||||
|
receiveBytesPerSecond: number;
|
||||||
|
sendBytesPerSecond: number;
|
||||||
|
receiveMbps: number;
|
||||||
|
sendMbps: number;
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable, timer, switchMap, shareReplay, catchError, of } from 'rxjs';
|
import { Observable, timer, switchMap, shareReplay, catchError, of, map } from 'rxjs';
|
||||||
|
|
||||||
import { SystemMetrics } from '../models/system-metrics.model';
|
import { SystemMetrics } from '../models/system-metrics.model';
|
||||||
import { SystemProcess } from '../models/system-process.model';
|
import { SystemProcess } from '../models/system-process.model';
|
||||||
|
import { LinuxDetails } from '../models/linux-details.model';
|
||||||
|
import { CpuDetails } from '../models/cpu-details.model';
|
||||||
|
import { LinuxMemory } from '../models/linux-memory.model';
|
||||||
|
import { LinuxDisk } from '../models/linux-disk.model';
|
||||||
|
import { LinuxNetwork } from '../models/linux-network.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -11,22 +16,95 @@ import { SystemProcess } from '../models/system-process.model';
|
|||||||
export class SystemMetricsService {
|
export class SystemMetricsService {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
|
|
||||||
readonly metrics$: Observable<SystemMetrics> = timer(0, 1000).pipe(
|
private readonly refreshInterval = 1000;
|
||||||
switchMap(() => this.http.get<SystemMetrics>('/api/system/metrics')),
|
|
||||||
|
readonly metrics$: Observable<SystemMetrics> = timer(0, this.refreshInterval).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<SystemMetrics>('/api/system/metrics').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load system metrics', error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Ignore failed requests without terminating the stream.
|
||||||
|
map((value) => value),
|
||||||
|
// Keep the last successful response for subscribers.
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
// The public type remains SystemMetrics below via the typed getter.
|
||||||
|
map((value) => value as SystemMetrics),
|
||||||
);
|
);
|
||||||
|
|
||||||
readonly processes$: Observable<SystemProcess[]> = timer(0, 1000).pipe(
|
readonly processes$: Observable<SystemProcess[]> = timer(0, this.refreshInterval).pipe(
|
||||||
switchMap(() =>
|
switchMap(() =>
|
||||||
this.http.get<SystemProcess[] | { value: SystemProcess[] }>('/api/system/processes').pipe(
|
this.http.get<SystemProcess[] | { value: SystemProcess[] }>('/api/system/processes').pipe(
|
||||||
|
map((response) => (Array.isArray(response) ? response : (response.value ?? []))),
|
||||||
catchError((error: unknown) => {
|
catchError((error: unknown) => {
|
||||||
console.error('Failed to load system processes', error);
|
console.error('Failed to load system processes', error);
|
||||||
return of([] as SystemProcess[]);
|
return of([] as SystemProcess[]);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Normalize either a plain array or a response containing "value".
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
switchMap((response) => of(Array.isArray(response) ? response : response.value)),
|
);
|
||||||
|
|
||||||
|
readonly linux$: Observable<LinuxDetails | null> = timer(0, this.refreshInterval).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<LinuxDetails>('/api/system/linux').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load Linux details', error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly cpu$: Observable<CpuDetails | null> = timer(0, this.refreshInterval).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<CpuDetails>('/api/system/cpu').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load CPU details', error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly memory$: Observable<LinuxMemory | null> = timer(0, this.refreshInterval).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<LinuxMemory>('/api/system/linux/memory').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load memory details', error);
|
||||||
|
return of(null);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly disks$: Observable<LinuxDisk[]> = timer(0, 10_000).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<LinuxDisk[]>('/api/system/linux/disks').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load disk details', error);
|
||||||
|
return of([] as LinuxDisk[]);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly network$: Observable<LinuxNetwork[]> = timer(0, this.refreshInterval).pipe(
|
||||||
|
switchMap(() =>
|
||||||
|
this.http.get<LinuxNetwork[]>('/api/system/linux/network').pipe(
|
||||||
|
catchError((error: unknown) => {
|
||||||
|
console.error('Failed to load network details', error);
|
||||||
|
return of([] as LinuxNetwork[]);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
shareReplay({ bufferSize: 1, refCount: true }),
|
shareReplay({ bufferSize: 1, refCount: true }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,297 +1,406 @@
|
|||||||
|
|
||||||
<main class="dashboard" [class.htop-mode]="viewMode === 'htop'">
|
<main
|
||||||
<header class="page-header">
|
class="dashboard"
|
||||||
<div>
|
[class.htop-mode]="viewMode === 'htop'"
|
||||||
<h1>htop-web</h1>
|
[class.htop-original-mode]="viewMode === 'htop-original'"
|
||||||
<p>System monitoring dashboard</p>
|
>
|
||||||
</div>
|
<nav class="view-switcher">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
[class.view-active]="viewMode === 'monitor'"
|
||||||
|
(click)="setViewMode('monitor')"
|
||||||
|
>Monitor</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
[class.view-active]="viewMode === 'htop'"
|
||||||
|
(click)="setViewMode('htop')"
|
||||||
|
>htop</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
[class.view-active]="viewMode === 'htop-original'"
|
||||||
|
(click)="setViewMode('htop-original')"
|
||||||
|
>htop-original</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<div class="header-actions">
|
@if (viewMode === 'monitor') {
|
||||||
<div class="view-switcher" role="group" aria-label="Dashboard view">
|
<section class="monitor-view">
|
||||||
<button
|
@if (metrics$ | async; as metrics) {
|
||||||
type="button"
|
<header class="monitor-heading">
|
||||||
[class.active]="viewMode === 'monitor'"
|
|
||||||
[attr.aria-pressed]="viewMode === 'monitor'"
|
|
||||||
(click)="setViewMode('monitor')">
|
|
||||||
<span>▦</span>
|
|
||||||
Monitor
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
[class.active]="viewMode === 'htop'"
|
|
||||||
[attr.aria-pressed]="viewMode === 'htop'"
|
|
||||||
(click)="setViewMode('htop')">
|
|
||||||
<span>▤</span>
|
|
||||||
htop
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="live-indicator">
|
|
||||||
<span class="live-dot"></span>
|
|
||||||
Live monitoring
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
@if (metrics$ | async; as metrics) {
|
|
||||||
@if (viewMode === 'monitor') {
|
|
||||||
<section class="system-info">
|
|
||||||
<div>
|
|
||||||
<span class="label">HOSTNAME</span>
|
|
||||||
<strong>{{ metrics.hostname }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span class="label">OPERATING SYSTEM</span>
|
|
||||||
<strong>{{ metrics.operatingSystem }} {{ metrics.osVersion }}</strong>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<span class="label">PROCESSOR</span>
|
|
||||||
<strong>{{ metrics.processor }}</strong>
|
|
||||||
<span class="muted">
|
|
||||||
{{ metrics.logicalProcessors }} logical processors
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="metrics-grid">
|
|
||||||
<article class="metric-card">
|
|
||||||
<div class="card-heading">
|
|
||||||
<span class="metric-icon cpu-icon">CPU</span>
|
|
||||||
<span class="muted">Processor load</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-value">
|
|
||||||
{{ metrics.cpuUsagePercent | number:'1.1-1' }}<span>%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="progress-track"
|
|
||||||
role="progressbar"
|
|
||||||
aria-label="CPU usage"
|
|
||||||
[attr.aria-valuenow]="metrics.cpuUsagePercent"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
<div
|
|
||||||
class="progress-fill cpu-fill"
|
|
||||||
[style.width.%]="clampPercent(metrics.cpuUsagePercent)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="card-footer">Total CPU utilization</p>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="metric-card">
|
|
||||||
<div class="card-heading">
|
|
||||||
<span class="metric-icon memory-icon">RAM</span>
|
|
||||||
<span class="muted">Physical memory</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="metric-value">
|
|
||||||
{{ metrics.memoryUsagePercent | number:'1.1-1' }}<span>%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="progress-track"
|
|
||||||
role="progressbar"
|
|
||||||
aria-label="Memory usage"
|
|
||||||
[attr.aria-valuenow]="metrics.memoryUsagePercent"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
<div
|
|
||||||
class="progress-fill memory-fill"
|
|
||||||
[style.width.%]="clampPercent(metrics.memoryUsagePercent)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="memory-details">
|
|
||||||
<span>Used: {{ formatBytes(metrics.usedMemoryBytes) }}</span>
|
|
||||||
<span>Total: {{ formatBytes(metrics.totalMemoryBytes) }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="card-footer">
|
|
||||||
Available: {{ formatBytes(metrics.availableMemoryBytes) }}
|
|
||||||
</p>
|
|
||||||
</article>
|
|
||||||
</section>
|
|
||||||
} @else {
|
|
||||||
<section class="htop-system-panel">
|
|
||||||
<div class="htop-system-heading">
|
|
||||||
<div>
|
<div>
|
||||||
<span class="htop-brand">HTOP-WEB</span>
|
<span class="eyebrow">SYSTEM MONITOR</span>
|
||||||
<h2>{{ metrics.hostname }}</h2>
|
<h1>{{ metrics.hostname }}</h1>
|
||||||
|
<p>{{ metrics.operatingSystem }} · {{ metrics.osVersion }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="status-indicator">
|
||||||
<div class="htop-os">
|
<span></span> Live
|
||||||
{{ metrics.operatingSystem }} {{ metrics.osVersion }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div class="htop-resource">
|
<div class="monitor-cards">
|
||||||
<div class="htop-resource-label">
|
<article class="monitor-card">
|
||||||
<span>CPU</span>
|
<span class="card-label">CPU USAGE</span>
|
||||||
<strong>{{ metrics.cpuUsagePercent | number:'1.1-1' }}%</strong>
|
<strong>{{ metrics.cpuUsagePercent | number:'1.1-1' }}%</strong>
|
||||||
</div>
|
<div class="progress-track">
|
||||||
|
<div
|
||||||
<div
|
class="progress-fill cpu-fill"
|
||||||
class="htop-bar"
|
[style.width.%]="clampPercent(metrics.cpuUsagePercent)"
|
||||||
role="progressbar"
|
></div>
|
||||||
aria-label="CPU usage"
|
|
||||||
[attr.aria-valuenow]="metrics.cpuUsagePercent"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
<div
|
|
||||||
class="htop-bar-fill htop-cpu-fill"
|
|
||||||
[style.width.%]="clampPercent(metrics.cpuUsagePercent)">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<span class="card-caption">{{ metrics.logicalProcessors }} logical processors</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
<div class="htop-resource-meta">
|
<article class="monitor-card">
|
||||||
{{ metrics.processor }}
|
<span class="card-label">MEMORY USAGE</span>
|
||||||
· {{ metrics.logicalProcessors }} logical processors
|
<strong>{{ metrics.memoryUsagePercent | number:'1.1-1' }}%</strong>
|
||||||
</div>
|
<div class="progress-track">
|
||||||
|
<div
|
||||||
|
class="progress-fill memory-fill"
|
||||||
|
[style.width.%]="clampPercent(metrics.memoryUsagePercent)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="card-caption">
|
||||||
|
{{ formatBytes(metrics.usedMemoryBytes) }} / {{ formatBytes(metrics.totalMemoryBytes) }}
|
||||||
|
</span>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="monitor-card">
|
||||||
|
<span class="card-label">AVAILABLE MEMORY</span>
|
||||||
|
<strong>{{ formatBytes(metrics.availableMemoryBytes) }}</strong>
|
||||||
|
<span class="card-caption">Currently available</span>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="htop-resource">
|
@if (cpu$ | async; as cpu) {
|
||||||
<div class="htop-resource-label">
|
<section class="monitor-section">
|
||||||
<span>Mem</span>
|
<h2>CPU cores</h2>
|
||||||
<strong>
|
<div class="core-cards">
|
||||||
{{ formatBytes(metrics.usedMemoryBytes) }}
|
@for (core of cpu.cores; track core.core) {
|
||||||
/ {{ formatBytes(metrics.totalMemoryBytes) }}
|
<article class="core-card">
|
||||||
</strong>
|
<div class="core-card-heading">
|
||||||
</div>
|
<span>Core {{ core.core }}</span>
|
||||||
|
<strong>{{ core.usagePercent | number:'1.1-1' }}%</strong>
|
||||||
<div
|
</div>
|
||||||
class="htop-bar"
|
<div class="progress-track">
|
||||||
role="progressbar"
|
<div
|
||||||
aria-label="Memory usage"
|
class="progress-fill cpu-fill"
|
||||||
[attr.aria-valuenow]="metrics.memoryUsagePercent"
|
[style.width.%]="clampPercent(core.usagePercent)"
|
||||||
aria-valuemin="0"
|
></div>
|
||||||
aria-valuemax="100">
|
</div>
|
||||||
<div
|
</article>
|
||||||
class="htop-bar-fill htop-memory-fill"
|
}
|
||||||
[style.width.%]="clampPercent(metrics.memoryUsagePercent)">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
}
|
||||||
|
} @else {
|
||||||
|
<p class="loading-state">Waiting for system metrics…</p>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="htop-resource-meta">
|
@if (linux$ | async; as linux) {
|
||||||
{{ metrics.memoryUsagePercent | number:'1.1-1' }}% used
|
<section class="monitor-section system-info">
|
||||||
· {{ formatBytes(metrics.availableMemoryBytes) }} available
|
<h2>System information</h2>
|
||||||
|
<div class="system-info-grid">
|
||||||
|
<div><span>Hostname</span><strong>{{ linux.hostname }}</strong></div>
|
||||||
|
<div><span>Distribution</span><strong>{{ linux.distribution }}</strong></div>
|
||||||
|
<div><span>Kernel</span><strong>{{ linux.kernelVersion }}</strong></div>
|
||||||
|
<div><span>Uptime</span><strong>{{ formatUptime(linux.uptimeSeconds) }}</strong></div>
|
||||||
|
<div><span>Load 1 min</span><strong>{{ linux.loadAverage1Min | number:'1.2-2' }}</strong></div>
|
||||||
|
<div><span>Load 5 min</span><strong>{{ linux.loadAverage5Min | number:'1.2-2' }}</strong></div>
|
||||||
|
<div><span>Load 15 min</span><strong>{{ linux.loadAverage15Min | number:'1.2-2' }}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
} @else if (viewMode === 'htop') {
|
||||||
|
<section class="classic-htop">
|
||||||
|
@if (cpu$ | async; as cpu) {
|
||||||
|
<div class="classic-cpu-grid">
|
||||||
|
@for (core of cpu.cores; track core.core) {
|
||||||
|
<div class="classic-meter-row">
|
||||||
|
<span class="classic-label">{{ core.core }}</span>
|
||||||
|
<div class="classic-meter-track">
|
||||||
|
<div
|
||||||
|
class="classic-meter-fill"
|
||||||
|
[style.width.%]="clampPercent(core.usagePercent)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<strong>{{ core.usagePercent | number:'1.0-0' }}%</strong>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (memory$ | async; as memory) {
|
||||||
|
<div class="classic-memory-row">
|
||||||
|
<span>Mem</span>
|
||||||
|
<div class="classic-meter-track">
|
||||||
|
<div
|
||||||
|
class="classic-memory-fill"
|
||||||
|
[style.width.%]="clampPercent(memory.ramUsagePercent)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<strong>{{ formatBytes(memory.usedRamBytes) }} / {{ formatBytes(memory.totalRamBytes) }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="classic-memory-row">
|
||||||
|
<span>Swp</span>
|
||||||
|
<div class="classic-meter-track">
|
||||||
|
<div
|
||||||
|
class="classic-swap-fill"
|
||||||
|
[style.width.%]="clampPercent(memory.swapUsagePercent)"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<strong>{{ formatBytes(memory.usedSwapBytes) }} / {{ formatBytes(memory.totalSwapBytes) }}</strong>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (linux$ | async; as linux) {
|
||||||
|
<div class="classic-system-line">
|
||||||
|
<span>Load average</span>
|
||||||
|
{{ linux.loadAverage1Min | number:'1.2-2' }}
|
||||||
|
{{ linux.loadAverage5Min | number:'1.2-2' }}
|
||||||
|
{{ linux.loadAverage15Min | number:'1.2-2' }}
|
||||||
|
<span>Uptime</span>
|
||||||
|
{{ formatUptime(linux.uptimeSeconds) }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<section class="classic-processes">
|
||||||
|
<header class="classic-process-heading">
|
||||||
|
<span>PID</span><span>Command</span><span>CPU%</span><span>MEMORY</span><span>STATE</span>
|
||||||
|
</header>
|
||||||
|
@if (processes$ | async; as processes) {
|
||||||
|
@for (process of getSortedProcesses(processes); track process.pid) {
|
||||||
|
<div class="classic-process-row">
|
||||||
|
<span>{{ process.pid }}</span>
|
||||||
|
<span class="classic-command">{{ process.name }}</span>
|
||||||
|
<span>{{ process.cpuUsagePercent | number:'1.1-1' }}</span>
|
||||||
|
<span>{{ formatBytes(process.residentMemoryBytes) }}</span>
|
||||||
|
<span>{{ process.state }}</span>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<p class="loading-state">No processes available</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
</section>
|
||||||
} @else {
|
} @else {
|
||||||
<section class="loading">
|
<!-- htop-original: styling follows the supplied reference -->
|
||||||
<div class="spinner"></div>
|
<section class="htop-original">
|
||||||
<p>Connecting to system metrics...</p>
|
@if (cpu$ | async; as cpu) {
|
||||||
|
@if (memory$ | async; as memory) {
|
||||||
|
@if (linux$ | async; as linux) {
|
||||||
|
<header class="original-header">
|
||||||
|
<div class="original-meters">
|
||||||
|
<div class="original-cpu-grid">
|
||||||
|
@for (core of cpu.cores; track core.core) {
|
||||||
|
<div class="original-meter-row">
|
||||||
|
<span class="original-cpu-number">{{ core.core }}</span>
|
||||||
|
<span class="meter-bracket">[</span>
|
||||||
|
<span class="original-meter-bar">
|
||||||
|
@for (segment of meterSegments20; track segment) {
|
||||||
|
<span
|
||||||
|
[class.pipe-used]="segment < core.usagePercent / 5"
|
||||||
|
[class.pipe-bg]="segment >= core.usagePercent / 5"
|
||||||
|
>|</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="meter-bracket">]</span>
|
||||||
|
<span class="original-meter-value">{{ core.usagePercent | number:'1.1-1' }}%</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-meter-row">
|
||||||
|
<span class="original-meter-label">Mem</span>
|
||||||
|
<span class="meter-bracket">[</span>
|
||||||
|
<span class="original-meter-bar">
|
||||||
|
@for (segment of meterSegments35; track segment) {
|
||||||
|
<span
|
||||||
|
[class.pipe-used]="segment < memory.ramUsagePercent / 100 * 35"
|
||||||
|
[class.pipe-bg]="segment >= memory.ramUsagePercent / 100 * 35"
|
||||||
|
>|</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="meter-bracket">]</span>
|
||||||
|
<span class="original-meter-value">
|
||||||
|
{{ formatBytes(memory.usedRamBytes) }}/{{ formatBytes(memory.totalRamBytes) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-meter-row">
|
||||||
|
<span class="original-meter-label">Swp</span>
|
||||||
|
<span class="meter-bracket">[</span>
|
||||||
|
<span class="original-meter-bar">
|
||||||
|
@for (segment of meterSegments15; track segment) {
|
||||||
|
<span
|
||||||
|
[class.pipe-used]="memory.totalSwapBytes > 0 && segment < memory.swapUsagePercent / 100 * 15"
|
||||||
|
[class.pipe-bg]="memory.totalSwapBytes <= 0 || segment >= memory.swapUsagePercent / 100 * 15"
|
||||||
|
>|</span>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<span class="meter-bracket">]</span>
|
||||||
|
<span class="original-meter-value">
|
||||||
|
{{ formatBytes(memory.usedSwapBytes) }}/{{ formatBytes(memory.totalSwapBytes) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-system-summary">
|
||||||
|
@if (processes$ | async; as processes) {
|
||||||
|
<div>
|
||||||
|
<span class="original-label">Tasks:</span>
|
||||||
|
<span class="original-white">{{ processes.length }}</span>,
|
||||||
|
<span class="original-white">—</span>
|
||||||
|
<span class="original-dim">thr</span>;
|
||||||
|
<span class="original-white">—</span>
|
||||||
|
<span class="original-dim">kthr</span>;
|
||||||
|
<span class="original-green">{{ countRunningProcesses(processes) }} running</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="original-summary-row">
|
||||||
|
<span class="original-label">Load average:</span>
|
||||||
|
<span class="original-white">{{ linux.loadAverage1Min | number:'1.2-2' }}</span>
|
||||||
|
<span class="original-white">{{ linux.loadAverage5Min | number:'1.2-2' }}</span>
|
||||||
|
<span class="original-white">{{ linux.loadAverage15Min | number:'1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-summary-row">
|
||||||
|
<span class="original-label">Uptime:</span>
|
||||||
|
<span class="original-green">{{ formatUptime(linux.uptimeSeconds) }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="original-navigation-hint">
|
||||||
|
<span>Navigation:</span> ↑/↓ (or J/K) Select |
|
||||||
|
<span>/</span> Filter |
|
||||||
|
<span>K</span> Kill |
|
||||||
|
<span>P</span> CPU Sort |
|
||||||
|
<span>M</span> MEM Sort
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<nav class="original-tabs" role="tablist" aria-label="htop views">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
[attr.aria-selected]="htopOriginalTab === 'main'"
|
||||||
|
[class.original-tab-active]="htopOriginalTab === 'main'"
|
||||||
|
[class.original-tab-inactive]="htopOriginalTab !== 'main'"
|
||||||
|
(click)="setHtopOriginalTab('main')"
|
||||||
|
>Main</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
[attr.aria-selected]="htopOriginalTab === 'io'"
|
||||||
|
[class.original-tab-active]="htopOriginalTab === 'io'"
|
||||||
|
[class.original-tab-inactive]="htopOriginalTab !== 'io'"
|
||||||
|
(click)="setHtopOriginalTab('io')"
|
||||||
|
>I/O</button>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="original-process-filter"
|
||||||
|
class="original-filter-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Type to filter..."
|
||||||
|
[value]="processFilter"
|
||||||
|
(input)="onOriginalFilterInput($event)"
|
||||||
|
/>
|
||||||
|
@if (processFilter) {
|
||||||
|
<button type="button" class="original-clear-filter" (click)="clearProcessFilter()">✕</button>
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
@if (htopOriginalTab === 'main') {
|
||||||
|
<section class="original-process-panel">
|
||||||
|
<div class="original-process-grid original-process-heading">
|
||||||
|
<button type="button" (click)="sortBy('pid')">PID</button>
|
||||||
|
<span>USER</span>
|
||||||
|
<span>PRI</span>
|
||||||
|
<span>NI</span>
|
||||||
|
<span>VIRT</span>
|
||||||
|
<span>RES</span>
|
||||||
|
<span>SHR</span>
|
||||||
|
<span>S</span>
|
||||||
|
<button type="button" (click)="sortBy('cpu')">
|
||||||
|
CPU% {{ processSort === 'cpu' ? (sortDescending ? '▼' : '▲') : '' }}
|
||||||
|
</button>
|
||||||
|
<button type="button" (click)="sortBy('memory')">
|
||||||
|
MEM% {{ processSort === 'memory' ? (sortDescending ? '▼' : '▲') : '' }}
|
||||||
|
</button>
|
||||||
|
<span>TIME+</span>
|
||||||
|
<span>Command</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="original-process-list"
|
||||||
|
tabindex="0"
|
||||||
|
(keydown)="onOriginalProcessKeydown($event)"
|
||||||
|
>
|
||||||
|
@if (memory$ | async; as memory) {
|
||||||
|
@if (processes$ | async; as processes) {
|
||||||
|
@for (process of getSortedProcessesForOriginal(processes); track process.pid; let i = $index) {
|
||||||
|
<div
|
||||||
|
class="original-process-grid original-process-row"
|
||||||
|
[class.original-process-selected]="i === selectedOriginalProcessIndex"
|
||||||
|
(click)="selectOriginalProcess(i)"
|
||||||
|
>
|
||||||
|
<span class="original-teal">{{ process.pid }}</span>
|
||||||
|
<span class="original-dim">—</span>
|
||||||
|
<span class="original-dim">—</span>
|
||||||
|
<span class="original-dim">—</span>
|
||||||
|
<span class="original-teal">{{ formatBytes(process.virtualMemoryBytes) }}</span>
|
||||||
|
<span class="original-green">{{ formatBytes(process.residentMemoryBytes) }}</span>
|
||||||
|
<span class="original-teal">—</span>
|
||||||
|
<span [class.original-green]="process.state === 'R'">{{ process.state }}</span>
|
||||||
|
<span
|
||||||
|
[class.original-red]="process.cpuUsagePercent > 10"
|
||||||
|
[class.original-yellow]="process.cpuUsagePercent > 5 && process.cpuUsagePercent <= 10"
|
||||||
|
[class.original-green]="process.cpuUsagePercent <= 5"
|
||||||
|
>{{ process.cpuUsagePercent | number:'1.1-1' }}</span>
|
||||||
|
<span>{{ getProcessMemoryPercent(process, memory.totalRamBytes) | number:'1.1-1' }}</span>
|
||||||
|
<span class="original-teal">{{ formatUptime(process.uptimeSeconds) }}</span>
|
||||||
|
<span class="original-command" [title]="process.name">{{ process.name }}</span>
|
||||||
|
</div>
|
||||||
|
} @empty {
|
||||||
|
<div class="original-empty">No matching processes found</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<section class="original-io-panel">
|
||||||
|
<h3>Disks</h3>
|
||||||
|
@if (disks$ | async; as disks) {
|
||||||
|
@for (disk of disks; track $index) {
|
||||||
|
<pre class="original-json-row">{{ disk | json }}</pre>
|
||||||
|
} @empty {
|
||||||
|
<p class="original-dim">No disk information available</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<h3>Network</h3>
|
||||||
|
@if (network$ | async; as networks) {
|
||||||
|
@for (network of networks; track $index) {
|
||||||
|
<pre class="original-json-row">{{ network | json }}</pre>
|
||||||
|
} @empty {
|
||||||
|
<p class="original-dim">No network information available</p>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<footer class="original-function-keys">
|
||||||
|
@for (key of originalFunctionKeys; track key.key) {
|
||||||
|
<button type="button" (click)="handleOriginalFunctionKey(key.key)">
|
||||||
|
<strong>{{ key.key }}</strong>
|
||||||
|
<span>{{ key.label }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="processes-section">
|
|
||||||
<header class="processes-header">
|
|
||||||
<div>
|
|
||||||
<h2>{{ viewMode === 'htop' ? 'Tasks / Processes' : 'Processes' }}</h2>
|
|
||||||
<p class="muted">
|
|
||||||
{{ viewMode === 'htop'
|
|
||||||
? 'Interactive process overview'
|
|
||||||
: 'Live system process monitor' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (processes$ | async; as processes) {
|
|
||||||
<span class="process-count">
|
|
||||||
{{ getSortedProcesses(processes).length }} processes
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<label class="process-search">
|
|
||||||
<span>Search</span>
|
|
||||||
<input
|
|
||||||
#processSearch
|
|
||||||
type="search"
|
|
||||||
placeholder="Process name or PID..."
|
|
||||||
[value]="processFilter"
|
|
||||||
(input)="processFilter = processSearch.value">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
@if (processes$ | async; as processes) {
|
|
||||||
<div class="table-scroll">
|
|
||||||
<table class="process-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>
|
|
||||||
<button type="button" (click)="sortBy('pid')">PID</button>
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
<button type="button" (click)="sortBy('name')">Process</button>
|
|
||||||
</th>
|
|
||||||
<th>State</th>
|
|
||||||
<th class="numeric">
|
|
||||||
<button type="button" (click)="sortBy('cpu')">CPU %</button>
|
|
||||||
</th>
|
|
||||||
<th class="numeric">
|
|
||||||
<button type="button" (click)="sortBy('memory')">Memory</button>
|
|
||||||
</th>
|
|
||||||
<th class="numeric">Virtual memory</th>
|
|
||||||
<th class="numeric">Threads</th>
|
|
||||||
<th>Uptime</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
@for (process of getSortedProcesses(processes); track process.pid) {
|
|
||||||
<tr>
|
|
||||||
<td class="pid-cell">{{ process.pid }}</td>
|
|
||||||
<td class="process-name">{{ process.name }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="process-state">{{ process.state }}</span>
|
|
||||||
</td>
|
|
||||||
<td class="numeric">
|
|
||||||
<span
|
|
||||||
class="cpu-value"
|
|
||||||
[class.cpu-high]="process.cpuUsagePercent >= 80">
|
|
||||||
{{ process.cpuUsagePercent | number:'1.1-1' }}%
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div class="mini-progress">
|
|
||||||
<div
|
|
||||||
class="mini-progress-fill"
|
|
||||||
[style.width.%]="clampPercent(process.cpuUsagePercent)">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td class="numeric">
|
|
||||||
{{ formatBytes(process.residentMemoryBytes) }}
|
|
||||||
</td>
|
|
||||||
<td class="numeric">
|
|
||||||
{{ formatBytes(process.virtualMemoryBytes) }}
|
|
||||||
</td>
|
|
||||||
<td class="numeric">{{ process.threadCount }}</td>
|
|
||||||
<td>{{ formatUptime(process.uptimeSeconds) }}</td>
|
|
||||||
</tr>
|
|
||||||
} @empty {
|
|
||||||
<tr>
|
|
||||||
<td colspan="8" class="empty-processes">
|
|
||||||
No matching processes found.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
} @else {
|
|
||||||
<div class="loading">
|
|
||||||
<p>Loading processes...</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<footer class="dashboard-footer">
|
|
||||||
<span>htop-web</span>
|
|
||||||
<span>Refresh interval: 500 ms</span>
|
|
||||||
</footer>
|
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,17 @@
|
|||||||
import { AsyncPipe, DecimalPipe } from '@angular/common';
|
import { AsyncPipe, DecimalPipe, JsonPipe } from '@angular/common';
|
||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
|
|
||||||
import { SystemMetricsService } from '../core/services/system-metrics.service';
|
import { SystemMetricsService } from '../core/services/system-metrics.service';
|
||||||
import { SystemProcess } from '../core/models/system-process.model';
|
import { SystemProcess } from '../core/models/system-process.model';
|
||||||
|
|
||||||
type ProcessSort = 'cpu' | 'memory' | 'name' | 'pid';
|
type ProcessSort = 'cpu' | 'memory' | 'name' | 'pid';
|
||||||
type DashboardView = 'monitor' | 'htop';
|
type DashboardView = 'monitor' | 'htop' | 'htop-original';
|
||||||
|
type OriginalTab = 'main' | 'io';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-dashboard',
|
selector: 'app-dashboard',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [AsyncPipe, DecimalPipe],
|
imports: [AsyncPipe, DecimalPipe, JsonPipe],
|
||||||
templateUrl: './dashboard.html',
|
templateUrl: './dashboard.html',
|
||||||
styleUrl: './dashboard.scss',
|
styleUrl: './dashboard.scss',
|
||||||
})
|
})
|
||||||
@@ -18,66 +20,117 @@ export class Dashboard {
|
|||||||
|
|
||||||
readonly metrics$ = this._systemMetricsService.metrics$;
|
readonly metrics$ = this._systemMetricsService.metrics$;
|
||||||
readonly processes$ = this._systemMetricsService.processes$;
|
readonly processes$ = this._systemMetricsService.processes$;
|
||||||
|
readonly linux$ = this._systemMetricsService.linux$;
|
||||||
|
readonly cpu$ = this._systemMetricsService.cpu$;
|
||||||
|
readonly memory$ = this._systemMetricsService.memory$;
|
||||||
|
readonly disks$ = this._systemMetricsService.disks$;
|
||||||
|
readonly network$ = this._systemMetricsService.network$;
|
||||||
|
|
||||||
|
readonly meterSegments20 = Array.from({ length: 20 }, (_, index) => index);
|
||||||
|
readonly meterSegments35 = Array.from({ length: 35 }, (_, index) => index);
|
||||||
|
readonly meterSegments15 = Array.from({ length: 15 }, (_, index) => index);
|
||||||
|
|
||||||
|
readonly originalFunctionKeys = [
|
||||||
|
{ key: 'F1', label: 'Help' },
|
||||||
|
{ key: 'F2', label: 'Setup' },
|
||||||
|
{ key: 'F3', label: 'Search' },
|
||||||
|
{ key: 'F4', label: 'Filter' },
|
||||||
|
{ key: 'F5', label: 'Tree' },
|
||||||
|
{ key: 'F6', label: 'SortBy' },
|
||||||
|
{ key: 'F7', label: 'Nice-' },
|
||||||
|
{ key: 'F8', label: 'Nice+' },
|
||||||
|
{ key: 'F9', label: 'Kill' },
|
||||||
|
{ key: 'F10', label: 'Quit' },
|
||||||
|
];
|
||||||
|
|
||||||
|
viewMode: DashboardView = 'monitor';
|
||||||
|
htopOriginalTab: OriginalTab = 'main';
|
||||||
|
|
||||||
processFilter = '';
|
processFilter = '';
|
||||||
processSort: ProcessSort = 'cpu';
|
processSort: ProcessSort = 'cpu';
|
||||||
sortDescending = true;
|
sortDescending = true;
|
||||||
|
selectedOriginalProcessIndex = 0;
|
||||||
viewMode: DashboardView = 'monitor';
|
|
||||||
|
|
||||||
setViewMode(mode: DashboardView): void {
|
setViewMode(mode: DashboardView): void {
|
||||||
this.viewMode = mode;
|
this.viewMode = mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setHtopOriginalTab(tab: OriginalTab): void {
|
||||||
|
this.htopOriginalTab = tab;
|
||||||
|
}
|
||||||
|
|
||||||
getSortedProcesses(processes: SystemProcess[]): SystemProcess[] {
|
getSortedProcesses(processes: SystemProcess[]): SystemProcess[] {
|
||||||
const query = this.processFilter.trim().toLowerCase();
|
const query = this.processFilter.trim().toLowerCase();
|
||||||
|
|
||||||
const filtered = processes.filter(
|
return processes
|
||||||
(process) =>
|
.filter(
|
||||||
process.name.toLowerCase().includes(query) || String(process.pid).includes(query),
|
(process) =>
|
||||||
);
|
process.name.toLowerCase().includes(query) || String(process.pid).includes(query),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
let comparison: number;
|
||||||
|
|
||||||
return [...filtered].sort((a, b) => {
|
switch (this.processSort) {
|
||||||
let comparison: number;
|
case 'cpu':
|
||||||
|
comparison = a.cpuUsagePercent - b.cpuUsagePercent;
|
||||||
|
break;
|
||||||
|
case 'memory':
|
||||||
|
comparison = a.residentMemoryBytes - b.residentMemoryBytes;
|
||||||
|
break;
|
||||||
|
case 'name':
|
||||||
|
comparison = a.name.localeCompare(b.name);
|
||||||
|
break;
|
||||||
|
case 'pid':
|
||||||
|
comparison = a.pid - b.pid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch (this.processSort) {
|
return this.sortDescending ? -comparison : comparison;
|
||||||
case 'cpu':
|
});
|
||||||
comparison = a.cpuUsagePercent - b.cpuUsagePercent;
|
}
|
||||||
break;
|
|
||||||
case 'memory':
|
|
||||||
comparison = a.residentMemoryBytes - b.residentMemoryBytes;
|
|
||||||
break;
|
|
||||||
case 'name':
|
|
||||||
comparison = a.name.localeCompare(b.name);
|
|
||||||
break;
|
|
||||||
case 'pid':
|
|
||||||
comparison = a.pid - b.pid;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.sortDescending ? -comparison : comparison;
|
getSortedProcessesForOriginal(processes: SystemProcess[]): SystemProcess[] {
|
||||||
});
|
return this.getSortedProcesses(processes);
|
||||||
}
|
}
|
||||||
|
|
||||||
sortBy(column: ProcessSort): void {
|
sortBy(column: ProcessSort): void {
|
||||||
if (this.processSort === column) {
|
if (this.processSort === column) {
|
||||||
this.sortDescending = !this.sortDescending;
|
this.sortDescending = !this.sortDescending;
|
||||||
return;
|
} else {
|
||||||
|
this.processSort = column;
|
||||||
|
this.sortDescending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
countRunningProcesses(processes: SystemProcess[]): number {
|
||||||
|
return processes.filter((process) => process.state === 'R').length;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectOriginalProcess(index: number): void {
|
||||||
|
this.selectedOriginalProcessIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProcessMemoryPercent(process: SystemProcess, totalMemoryBytes: number): number {
|
||||||
|
if (totalMemoryBytes <= 0) {
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processSort = column;
|
return (process.residentMemoryBytes / totalMemoryBytes) * 100;
|
||||||
this.sortDescending = true;
|
}
|
||||||
|
|
||||||
|
clampPercent(value: number): number {
|
||||||
|
return Math.max(0, Math.min(100, value));
|
||||||
}
|
}
|
||||||
|
|
||||||
formatBytes(bytes: number): string {
|
formatBytes(bytes: number): string {
|
||||||
if (bytes <= 0) {
|
if (!Number.isFinite(bytes) || bytes <= 0) {
|
||||||
return '0 B';
|
return '0 B';
|
||||||
}
|
}
|
||||||
|
|
||||||
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
||||||
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
const index = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||||
|
|
||||||
return `${(bytes / Math.pow(1024, index)).toFixed(2)} ${units[index]}`;
|
return `${(bytes / Math.pow(1024, index)).toFixed(1)}${units[index]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatUptime(seconds: number): string {
|
formatUptime(seconds: number): string {
|
||||||
@@ -85,19 +138,65 @@ export class Dashboard {
|
|||||||
const days = Math.floor(safeSeconds / 86400);
|
const days = Math.floor(safeSeconds / 86400);
|
||||||
const hours = Math.floor((safeSeconds % 86400) / 3600);
|
const hours = Math.floor((safeSeconds % 86400) / 3600);
|
||||||
const minutes = Math.floor((safeSeconds % 3600) / 60);
|
const minutes = Math.floor((safeSeconds % 3600) / 60);
|
||||||
|
const remainingSeconds = safeSeconds % 60;
|
||||||
|
|
||||||
if (days > 0) {
|
const time = [
|
||||||
return `${days}d ${hours}h`;
|
String(hours).padStart(2, '0'),
|
||||||
}
|
String(minutes).padStart(2, '0'),
|
||||||
|
String(remainingSeconds).padStart(2, '0'),
|
||||||
|
].join(':');
|
||||||
|
|
||||||
if (hours > 0) {
|
return days > 0 ? `${days} days, ${time}` : time;
|
||||||
return `${hours}h ${minutes}m`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${minutes}m`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clampPercent(value: number): number {
|
handleOriginalFunctionKey(key: string): void {
|
||||||
return Math.max(0, Math.min(100, value));
|
switch (key) {
|
||||||
|
case 'F3':
|
||||||
|
case 'F4':
|
||||||
|
document.getElementById('original-process-filter')?.focus();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'F6':
|
||||||
|
this.sortBy('cpu');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'F10':
|
||||||
|
this.setViewMode('htop');
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onOriginalFilterInput(event: Event): void {
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
if (target instanceof HTMLInputElement) {
|
||||||
|
this.processFilter = target.value;
|
||||||
|
this.selectedOriginalProcessIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearProcessFilter(): void {
|
||||||
|
this.processFilter = '';
|
||||||
|
this.selectedOriginalProcessIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
onOriginalProcessKeydown(event: KeyboardEvent): void {
|
||||||
|
if (event.key === 'ArrowDown' || event.key.toLowerCase() === 'j') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.selectedOriginalProcessIndex++;
|
||||||
|
} else if (event.key === 'ArrowUp' || event.key.toLowerCase() === 'k') {
|
||||||
|
event.preventDefault();
|
||||||
|
this.selectedOriginalProcessIndex = Math.max(0, this.selectedOriginalProcessIndex - 1);
|
||||||
|
} else if (event.key === '/') {
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById('original-process-filter')?.focus();
|
||||||
|
} else if (event.key.toLowerCase() === 'p') {
|
||||||
|
this.sortBy('cpu');
|
||||||
|
} else if (event.key.toLowerCase() === 'm') {
|
||||||
|
this.sortBy('memory');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user