feat: Integrate SmartMetrics for enhanced CPU and memory monitoring in UI

This commit is contained in:
Juergen Kunz
2025-06-12 11:22:18 +00:00
parent facae93e4b
commit 8ce6c88d58
8 changed files with 154 additions and 170 deletions

View File

@ -20,7 +20,7 @@
"@git.zone/tsrun": "^1.3.3",
"@git.zone/tstest": "^2.3.1",
"@git.zone/tswatch": "^2.0.1",
"@types/node": "^24.0.0",
"@types/node": "^22.0.0",
"node-forge": "^1.3.1"
},
"dependencies": {
@ -29,7 +29,7 @@
"@api.global/typedserver": "^3.0.74",
"@api.global/typedsocket": "^3.0.0",
"@apiclient.xyz/cloudflare": "^6.4.1",
"@design.estate/dees-catalog": "^1.8.1",
"@design.estate/dees-catalog": "^1.8.4",
"@design.estate/dees-element": "^2.0.42",
"@push.rocks/projectinfo": "^5.0.1",
"@push.rocks/qenv": "^6.1.0",

90
pnpm-lock.yaml generated
View File

@ -24,8 +24,8 @@ importers:
specifier: ^6.4.1
version: 6.4.1
'@design.estate/dees-catalog':
specifier: ^1.8.1
version: 1.8.1
specifier: ^1.8.4
version: 1.8.4
'@design.estate/dees-element':
specifier: ^2.0.42
version: 2.0.42
@ -130,8 +130,8 @@ importers:
specifier: ^2.0.1
version: 2.1.0
'@types/node':
specifier: ^24.0.0
version: 24.0.0
specifier: ^22.0.0
version: 22.15.31
node-forge:
specifier: ^1.3.1
version: 1.3.1
@ -344,8 +344,8 @@ packages:
'@dabh/diagnostics@2.0.3':
resolution: {integrity: sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==}
'@design.estate/dees-catalog@1.8.1':
resolution: {integrity: sha512-luYrGMK5APzw7GgXSe4UpFE5oC3vwzKTTB0etH3o6Au1nxvllhDcWr6S5UkB+WL9lroslBcK8Icyhiq8kIxEHg==}
'@design.estate/dees-catalog@1.8.4':
resolution: {integrity: sha512-G5N9CUzhWhTt3NuHbix4yyB/Fz1fqO2gTO1OoBffu5DTA1keJlOCvyBaUbgwjmZWnBBMN1O6r8cUhYIHcf4SJg==}
'@design.estate/dees-comms@1.0.27':
resolution: {integrity: sha512-GvzTUwkV442LD60T08iqSoqvhA02Mou5lFvvqBPc4yBUiU7cZISqBx+76xvMgMIEI9Dx9JfTl4/2nW8MoVAanw==}
@ -1661,8 +1661,8 @@ packages:
'@types/node@18.19.111':
resolution: {integrity: sha512-90sGdgA+QLJr1F9X79tQuEut0gEYIfkX9pydI4XGRgvFo9g2JWswefI+WUSUHPYVBHYSEfTEqBxA5hQvAZB3Mw==}
'@types/node@24.0.0':
resolution: {integrity: sha512-yZQa2zm87aRVcqDyH5+4Hv9KYgSdgwX1rFnGvpbzMaC7YAljmhBET93TPiTd3ObwTL+gSpIzPKg5BqVxdCvxKg==}
'@types/node@22.15.31':
resolution: {integrity: sha512-jnVe5ULKl6tijxUhvQeNbQG/84fHfg+yMak02cT8QVhBx/F05rAVxCGBYYTh2EKz22D6JF5ktXuNwdx7b9iEGw==}
'@types/pidusage@2.0.5':
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
@ -3090,8 +3090,8 @@ packages:
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
engines: {node: '>=12'}
lucide@0.501.0:
resolution: {integrity: sha512-ufrFxsi7s5dcuDgR/z21CqP61jyMshh2BghKaEviz3nrFohrJWZtjvHvlamDaNzeJU1tGTxxPMbWqkmTPBijjA==}
lucide@0.514.0:
resolution: {integrity: sha512-GQ3Rzj1qFANBvTmhe8RM3vR491RGnSjHsivA5wSuEj5taLwH1Sv4N7n6ae3ENGsYRzdrkVXZo4ieUbD1WOXmoA==}
mailauth@4.8.6:
resolution: {integrity: sha512-Ler6XMLCrXyCf3kmNOMA/1aUJN6let/w9HBtjl+2KzXOUxKIl4WPJM1FwqC4IHdVJO8kmHUrvyFIKIiEGj6mvg==}
@ -4194,8 +4194,8 @@ packages:
undici-types@5.26.5:
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
undici-types@7.8.0:
resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==}
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@7.10.0:
resolution: {integrity: sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==}
@ -5075,7 +5075,7 @@ snapshots:
enabled: 2.0.0
kuler: 2.0.0
'@design.estate/dees-catalog@1.8.1':
'@design.estate/dees-catalog@1.8.4':
dependencies:
'@design.estate/dees-domtools': 2.3.2
'@design.estate/dees-element': 2.0.42
@ -5092,7 +5092,7 @@ snapshots:
apexcharts: 4.7.0
highlight.js: 11.11.1
ibantools: 4.5.1
lucide: 0.501.0
lucide: 0.514.0
monaco-editor: 0.52.2
pdfjs-dist: 4.10.38
xterm: 5.3.0
@ -7044,27 +7044,27 @@ snapshots:
'@types/bn.js@5.2.0':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
source-map: 0.6.1
'@types/connect@3.4.38':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/cors@2.8.18':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/debug@4.1.12':
dependencies:
@ -7076,7 +7076,7 @@ snapshots:
'@types/dns-packet@5.6.5':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/elliptic@6.4.18':
dependencies:
@ -7084,7 +7084,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@ -7101,30 +7101,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/fs-extra@9.0.13':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/gunzip-maybe@1.4.2':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/hast@3.0.4':
dependencies:
@ -7146,16 +7146,16 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/jsonwebtoken@9.0.9':
dependencies:
'@types/ms': 2.1.0
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/mailparser@3.4.6':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
iconv-lite: 0.6.3
'@types/mdast@4.0.4':
@ -7174,20 +7174,20 @@ snapshots:
'@types/node-fetch@2.6.12':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
form-data: 4.0.2
'@types/node-forge@1.3.11':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/node@18.19.111':
dependencies:
undici-types: 5.26.5
'@types/node@24.0.0':
'@types/node@22.15.31':
dependencies:
undici-types: 7.8.0
undici-types: 6.21.0
'@types/pidusage@2.0.5': {}
@ -7203,30 +7203,30 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/semver@7.7.0': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/send': 0.17.4
'@types/symbol-tree@3.2.5': {}
'@types/tar-stream@2.2.3':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/through2@2.0.41':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/triple-beam@1.3.5': {}
@ -7250,18 +7250,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
'@types/yauzl@2.10.3':
dependencies:
'@types/node': 24.0.0
'@types/node': 22.15.31
optional: true
'@ungap/structured-clone@1.3.0': {}
@ -7833,7 +7833,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.18
'@types/node': 24.0.0
'@types/node': 22.15.31
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@ -8776,7 +8776,7 @@ snapshots:
lru-cache@7.18.3: {}
lucide@0.501.0: {}
lucide@0.514.0: {}
mailauth@4.8.6:
dependencies:
@ -10166,7 +10166,7 @@ snapshots:
undici-types@5.26.5: {}
undici-types@7.8.0: {}
undici-types@6.21.0: {}
undici@7.10.0: {}

View File

@ -903,4 +903,45 @@ The DNS functionality has been refactored from UnifiedEmailServer to a dedicated
- DNS functionality is now easily discoverable in DnsManager
- Clear separation between DNS management and email server logic
- UnifiedEmailServer is simpler and more focused
- All DNS-related tests pass successfully
- All DNS-related tests pass successfully
## SmartMetrics Integration (2025-06-12) - COMPLETED
### Overview
Fixed the UI metrics display to show accurate CPU and memory data from SmartMetrics.
### Key Findings
1. **CPU Metrics:**
- SmartMetrics provides `cpuUsageText` as a string percentage
- MetricsManager parses it as `cpuUsage.user` (system is always 0)
- UI was incorrectly dividing by 2, showing half the actual CPU usage
2. **Memory Metrics:**
- SmartMetrics calculates `maxMemoryMB` as minimum of:
- V8 heap size limit
- System total memory
- Docker memory limit (if available)
- Provides `memoryUsageBytes` (total process memory including children)
- Provides `memoryPercentage` (pre-calculated percentage)
- UI was only showing heap usage, missing actual memory constraints
### Changes Made
1. **MetricsManager Enhanced:**
- Added `maxMemoryMB` from SmartMetrics instance
- Added `actualUsageBytes` from SmartMetrics data
- Added `actualUsagePercentage` from SmartMetrics data
- Kept existing memory fields for compatibility
2. **Interface Updated:**
- Added optional fields to `IServerStats.memoryUsage`
- Fields are optional to maintain backward compatibility
3. **UI Fixed:**
- Removed incorrect CPU division by 2
- Uses `actualUsagePercentage` when available (falls back to heap percentage)
- Shows actual memory usage vs max memory limit (not just heap)
### Result
- CPU now shows accurate usage percentage
- Memory shows percentage of actual constraints (Docker/system/V8 limits)
- Better monitoring for containerized environments

View File

@ -115,6 +115,10 @@ export class MetricsManager {
heapTotal: process.memoryUsage().heapTotal,
external: process.memoryUsage().external,
rss: process.memoryUsage().rss,
// Add SmartMetrics memory data
maxMemoryMB: this.smartMetrics.maxMemoryMB,
actualUsageBytes: smartMetricsData.memoryUsageBytes,
actualUsagePercentage: smartMetricsData.memoryPercentage,
},
cpuUsage: {
user: parseFloat(smartMetricsData.cpuUsageText || '0'),

View File

@ -6,6 +6,10 @@ export interface IServerStats {
heapTotal: number;
external: number;
rss: number;
// SmartMetrics memory data
maxMemoryMB?: number;
actualUsageBytes?: number;
actualUsagePercentage?: number;
};
cpuUsage: {
user: number;

View File

@ -1,5 +1,6 @@
import { DeesElement, property, html, customElement, type TemplateResult, css, state, cssManager } from '@design.estate/dees-element';
import * as appstate from '../appstate.js';
import * as shared from './shared/index.js';
declare global {
interface HTMLElementTagNameMap {
@ -56,95 +57,39 @@ export class OpsViewEmails extends DeesElement {
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
:host {
display: block;
height: 100%;
padding: 24px;
}
.emailContainer {
display: grid;
grid-template-columns: 250px 1fr;
gap: 24px;
height: calc(100vh - 200px);
grid-template-columns: 280px 1fr;
gap: 16px;
height: 100%;
min-height: 600px;
}
.sidebar {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.folderList {
display: flex;
flex-direction: column;
gap: 8px;
}
.folderItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.folderItem:hover {
background: #f5f5f5;
}
.folderItem.selected {
background: #e3f2fd;
color: #1976d2;
}
.folderIcon {
font-size: 18px;
}
.folderLabel {
flex: 1;
font-weight: 500;
}
.folderCount {
background: #e0e0e0;
color: #666;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.folderItem.selected .folderCount {
background: #1976d2;
color: white;
}
.mainContent {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 16px;
overflow: hidden;
}
.emailToolbar {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
border-bottom: 1px solid #e9ecef;
background: #fafafa;
gap: 12px;
flex-wrap: wrap;
}
.searchBox {

View File

@ -100,18 +100,8 @@ export class OpsViewNetwork extends DeesElement {
margin-bottom: 24px;
}
.chartSection {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 24px;
}
.tableSection {
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
dees-chart-area {
margin-bottom: 24px;
}
.protocolBadge {
@ -224,51 +214,47 @@ export class OpsViewNetwork extends DeesElement {
${this.renderNetworkStats()}
<!-- Traffic Chart -->
<div class="chartSection">
<dees-chart-area
.label=${'Network Traffic'}
.series=${[
{
name: 'Requests/min',
data: this.trafficData,
}
]}
></dees-chart-area>
</div>
<dees-chart-area
.label=${'Network Traffic'}
.series=${[
{
name: 'Requests/min',
data: this.trafficData,
}
]}
></dees-chart-area>
<!-- Requests Table -->
<div class="tableSection">
<dees-table
.data=${this.getFilteredRequests()}
.displayFunction=${(req: INetworkRequest) => ({
Time: new Date(req.timestamp).toLocaleTimeString(),
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
Method: req.method,
'Host:Port': `${req.hostname}:${req.port}`,
Path: this.truncateUrl(req.url),
Status: this.renderStatus(req.statusCode),
Duration: `${req.duration}ms`,
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
'Remote IP': req.remoteIp,
})}
.dataActions=${[
{
name: 'View Details',
iconName: 'magnifyingGlass',
type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item);
}
<dees-table
.data=${this.getFilteredRequests()}
.displayFunction=${(req: INetworkRequest) => ({
Time: new Date(req.timestamp).toLocaleTimeString(),
Protocol: html`<span class="protocolBadge ${req.protocol}">${req.protocol.toUpperCase()}</span>`,
Method: req.method,
'Host:Port': `${req.hostname}:${req.port}`,
Path: this.truncateUrl(req.url),
Status: this.renderStatus(req.statusCode),
Duration: `${req.duration}ms`,
'In/Out': `${this.formatBytes(req.bytesIn)} / ${this.formatBytes(req.bytesOut)}`,
'Remote IP': req.remoteIp,
})}
.dataActions=${[
{
name: 'View Details',
iconName: 'magnifyingGlass',
type: ['inRow', 'doubleClick', 'contextmenu'],
actionFunc: async (actionData) => {
await this.showRequestDetails(actionData.item);
}
]}
heading1="Recent Network Activity"
heading2="Last ${this.selectedTimeRange} of network requests"
searchable
.pagination=${true}
.paginationSize=${50}
dataName="request"
></dees-table>
</div>
}
]}
heading1="Recent Network Activity"
heading2="Last ${this.selectedTimeRange} of network requests"
searchable
.pagination=${true}
.paginationSize=${50}
dataName="request"
></dees-table>
</div>
`;
}

View File

@ -138,8 +138,10 @@ export class OpsViewOverview extends DeesElement {
private renderServerStats(): TemplateResult {
if (!this.statsState.serverStats) return html``;
const cpuUsage = Math.round((this.statsState.serverStats.cpuUsage.user + this.statsState.serverStats.cpuUsage.system) / 2);
const memoryUsage = Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100);
const cpuUsage = Math.round(this.statsState.serverStats.cpuUsage.user);
const memoryUsage = this.statsState.serverStats.memoryUsage.actualUsagePercentage !== undefined
? Math.round(this.statsState.serverStats.memoryUsage.actualUsagePercentage)
: Math.round((this.statsState.serverStats.memoryUsage.heapUsed / this.statsState.serverStats.memoryUsage.heapTotal) * 100);
const tiles: IStatsTile[] = [
{
@ -183,7 +185,9 @@ export class OpsViewOverview extends DeesElement {
type: 'percentage',
icon: 'memory',
color: memoryUsage > 80 ? '#ef4444' : memoryUsage > 60 ? '#f59e0b' : '#22c55e',
description: `${this.formatBytes(this.statsState.serverStats.memoryUsage.heapUsed)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`,
description: this.statsState.serverStats.memoryUsage.actualUsageBytes !== undefined && this.statsState.serverStats.memoryUsage.maxMemoryMB !== undefined
? `${this.formatBytes(this.statsState.serverStats.memoryUsage.actualUsageBytes)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.maxMemoryMB * 1024 * 1024)}`
: `${this.formatBytes(this.statsState.serverStats.memoryUsage.rss)} / ${this.formatBytes(this.statsState.serverStats.memoryUsage.heapTotal)}`,
},
];