First version of the new WebUI

This commit is contained in:
nicolargo
2026-03-12 22:22:52 +01:00
parent ea54cffd1a
commit 643a8042ea
19 changed files with 2252 additions and 1057 deletions

View File

@@ -1,48 +1 @@
// Custom.scss
// Option A: Include all of Bootstrap
// ==================================
// Include any default variable overrides here (though functions won't be available)
// @import "../node_modules/bootstrap/scss/bootstrap";
// Then add additional custom code here
// // Option B: Include parts of Bootstrap
// // ====================================
// // 1. Include functions first (so you can manipulate colors, SVGs, calc, etc)
@import "../node_modules/bootstrap/scss/functions";
// // 2. Include any default variable overrides here
// $body-bg: black;
// // 3. Include remainder of required Bootstrap stylesheets (including any separate color mode stylesheets)
@import "../node_modules/bootstrap/scss/variables";
@import "../node_modules/bootstrap/scss/variables-dark";
// // 4. Include any default map overrides here
// // 5. Include remainder of required parts
@import "../node_modules/bootstrap/scss/maps";
@import "../node_modules/bootstrap/scss/mixins";
@import "../node_modules/bootstrap/scss/root";
// // 6. Optionally include any other parts as needed
@import "../node_modules/bootstrap/scss/utilities";
@import "../node_modules/bootstrap/scss/reboot";
@import "../node_modules/bootstrap/scss/type";
@import "../node_modules/bootstrap/scss/images";
@import "../node_modules/bootstrap/scss/containers";
@import "../node_modules/bootstrap/scss/grid";
@import "../node_modules/bootstrap/scss/helpers";
@import "../node_modules/bootstrap/scss/tables";
@import "../node_modules/bootstrap/scss/progress";
// // 7. Optionally include utilities API last to generate classes based on the Sass map in `_utilities.scss`
@import "../node_modules/bootstrap/scss/utilities/api";
// // 8. Add additional custom code here
// No Bootstrap - custom theme only

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,219 +1,86 @@
<template>
<div v-if="!dataLoaded" id="loading-page" class="container-fluid">
<div class="loader">Glances is loading...</div>
<div v-if="!dataLoaded" class="loading-page">
Glances is loading...
</div>
<glances-help v-else-if="args.help_tag"></glances-help>
<main v-else>
<!-- Display minimal header on low screen size (smarthphone) -->
<div class="d-sm-none">
<div class="header-small">
<div v-if="!args.disable_system"><glances-plugin-hostname :data="data"></glances-plugin-hostname></div>
<div v-if="!args.disable_uptime"><glances-plugin-uptime :data="data"></glances-plugin-uptime></div>
</div>
</div>
<!-- Display standard header on others screen sizes -->
<div class="d-none d-sm-block">
<div class="header d-flex justify-content-between flex-row">
<div v-if="!args.disable_system" class=""><glances-plugin-system :data="data"></glances-plugin-system>
</div>
<div v-if="!args.disable_ip" class="d-none d-lg-block"><glances-plugin-ip
:data="data"></glances-plugin-ip>
</div>
<div v-if="!args.disable_uptime" class="d-none d-md-block"><glances-plugin-uptime
:data="data"></glances-plugin-uptime></div>
<div v-if="!args.disable_now" class="d-none d-xl-block"><glances-plugin-now
:data="data"></glances-plugin-now></div>
</div>
</div>
<div class="d-flex d-none d-sm-block">
<div v-if="!args.disable_cloud">
<glances-plugin-cloud :data="data"></glances-plugin-cloud>
</div>
</div>
<!-- Display top menu with CPU, MEM, LOAD...-->
<div class="top d-flex justify-content-between flex-row">
<!-- Quicklook -->
<div v-if="!args.disable_quicklook" class="d-none d-md-block">
<glances-plugin-quicklook :data="data"></glances-plugin-quicklook>
</div>
<!-- CPU -->
<div v-if="!args.disable_cpu || !args.percpu" class="">
<glances-plugin-cpu :data="data"></glances-plugin-cpu>
</div>
<!-- TODO: percpu need to be refactor
<div class="col"
v-if="!args.disable_cpu && !args.percpu">
<glances-plugin-cpu :data="data"></glances-plugin-cpu>
</div>
<div class="col"
v-if="!args.disable_cpu && args.percpu">
<glances-plugin-percpu :data="data"></glances-plugin-percpu>
</div> -->
<template v-else>
<!-- HEADER -->
<header-section :data="data"></header-section>
<!-- NPU -->
<div v-if="!args.disable_npu && hasNpu" class="d-none d-xl-block">
<glances-plugin-npu :data="data"></glances-plugin-npu>
</div>
<!-- GPU -->
<div v-if="!args.disable_gpu && hasGpu" class="d-none d-xl-block">
<glances-plugin-gpu :data="data"></glances-plugin-gpu>
</div>
<!-- MEM -->
<div v-if="!args.disable_mem" class="">
<glances-plugin-mem :data="data"></glances-plugin-mem>
</div>
<!-- SWAP -->
<div v-if="!args.disable_memswap" class="d-none d-lg-block">
<glances-plugin-memswap :data="data"></glances-plugin-memswap>
</div>
<!-- LOAD -->
<div v-if="!args.disable_load" class="d-none d-sm-block">
<glances-plugin-load :data="data"></glances-plugin-load>
</div>
<!-- METRICS STRIP -->
<section id="metrics" :class="metricsClass">
<metrics-cpu v-if="!args.disable_cpu" :data="data"></metrics-cpu>
<metrics-mem v-if="!args.disable_mem" :data="data"></metrics-mem>
<metrics-load v-if="!args.disable_load" :data="data"></metrics-load>
<metrics-gpu v-if="!args.disable_gpu && hasGpu" :data="data"></metrics-gpu>
</section>
<!-- BODY: sidebar + main -->
<div id="body" :class="{ 'no-sidebar': args.disable_left_sidebar }">
<sidebar-section v-if="!args.disable_left_sidebar" :data="data"></sidebar-section>
<main-process :data="data"></main-process>
</div>
<!-- Display bottom of the screen with sidebar and processlist -->
<div class="bottom container-fluid">
<div class="row">
<div v-if="!args.disable_left_sidebar" class="col-3 d-none d-md-block"
:class="{ 'sidebar-min': !args.percpu, 'sidebar-max': args.percpu }">
<template v-for="plugin in leftMenu">
<component :is="`glances-plugin-${plugin}`" v-if="!args[`disable_${plugin}`]" :id="`${plugin}`"
:data="data">
</component>
</template>
</div>
<div class="col" :class="{ 'sidebar-min': !args.percpu, 'sidebar-max': args.percpu }">
<glances-plugin-vms v-if="!args.disable_vms" :data="data"></glances-plugin-vms>
<glances-plugin-containers v-if="!args.disable_containers" :data="data"></glances-plugin-containers>
<glances-plugin-process :data="data"></glances-plugin-process>
<glances-plugin-alert v-if="!args.disable_alert" :data="data"></glances-plugin-alert>
</div>
</div>
</div>
</main>
<!-- FOOTER ALERTS -->
<footer-alerts v-if="!args.disable_alert" :data="data"></footer-alerts>
</template>
</template>
<script>
import hotkeys from "hotkeys-js";
import GlancesHelp from "./components/help.vue";
import GlancesPluginAlert from "./components/plugin-alert.vue";
import GlancesPluginCloud from "./components/plugin-cloud.vue";
import GlancesPluginConnections from "./components/plugin-connections.vue";
import GlancesPluginContainers from "./components/plugin-containers.vue";
import GlancesPluginCpu from "./components/plugin-cpu.vue";
import GlancesPluginDiskio from "./components/plugin-diskio.vue";
import GlancesPluginFolders from "./components/plugin-folders.vue";
import GlancesPluginFs from "./components/plugin-fs.vue";
import GlancesPluginNpu from "./components/plugin-npu.vue";
import GlancesPluginGpu from "./components/plugin-gpu.vue";
import GlancesPluginHostname from "./components/plugin-hostname.vue";
import GlancesPluginIp from "./components/plugin-ip.vue";
import GlancesPluginIrq from "./components/plugin-irq.vue";
import GlancesPluginLoad from "./components/plugin-load.vue";
import GlancesPluginMem from "./components/plugin-mem.vue";
import GlancesPluginMemswap from "./components/plugin-memswap.vue";
import GlancesPluginNetwork from "./components/plugin-network.vue";
import GlancesPluginNow from "./components/plugin-now.vue";
import GlancesPluginPercpu from "./components/plugin-percpu.vue";
import GlancesPluginPorts from "./components/plugin-ports.vue";
import GlancesPluginProcess from "./components/plugin-process.vue";
import GlancesPluginQuicklook from "./components/plugin-quicklook.vue";
import GlancesPluginRaid from "./components/plugin-raid.vue";
import GlancesPluginSensors from "./components/plugin-sensors.vue";
import GlancesPluginSmart from "./components/plugin-smart.vue";
import GlancesPluginSystem from "./components/plugin-system.vue";
import GlancesPluginUptime from "./components/plugin-uptime.vue";
import GlancesPluginVms from "./components/plugin-vms.vue";
import GlancesPluginWifi from "./components/plugin-wifi.vue";
import HeaderSection from "./components/header-section.vue";
import MetricsCpu from "./components/metrics-cpu.vue";
import MetricsMem from "./components/metrics-mem.vue";
import MetricsLoad from "./components/metrics-load.vue";
import MetricsGpu from "./components/metrics-gpu.vue";
import SidebarSection from "./components/sidebar-section.vue";
import MainProcess from "./components/main-process.vue";
import FooterAlerts from "./components/footer-alerts.vue";
import { GlancesStats } from "./services.js";
import { store } from "./store.js";
import uiconfig from "./uiconfig.json";
export default {
components: {
GlancesHelp,
GlancesPluginAlert,
GlancesPluginCloud,
GlancesPluginConnections,
GlancesPluginCpu,
GlancesPluginDiskio,
GlancesPluginContainers,
GlancesPluginFolders,
GlancesPluginFs,
GlancesPluginNpu,
GlancesPluginGpu,
GlancesPluginHostname,
GlancesPluginIp,
GlancesPluginIrq,
GlancesPluginLoad,
GlancesPluginMem,
GlancesPluginMemswap,
GlancesPluginNetwork,
GlancesPluginNow,
GlancesPluginPercpu,
GlancesPluginPorts,
GlancesPluginProcess,
GlancesPluginQuicklook,
GlancesPluginRaid,
GlancesPluginSensors,
GlancesPluginSmart,
GlancesPluginSystem,
GlancesPluginUptime,
GlancesPluginVms,
GlancesPluginWifi,
HeaderSection,
MetricsCpu,
MetricsMem,
MetricsLoad,
MetricsGpu,
SidebarSection,
MainProcess,
FooterAlerts,
},
data() {
return {
store,
};
return { store };
},
computed: {
args() {
return this.store.args || {};
},
config() {
return this.store.config || {};
},
data() {
return this.store.data || {};
},
dataLoaded() {
return this.store.data !== undefined;
args() { return this.store.args || {}; },
config() { return this.store.config || {}; },
data() { return this.store.data || {}; },
dataLoaded() { return this.store.data !== undefined; },
hasGpu() {
return this.store.data?.stats?.gpu && this.store.data.stats.gpu.length > 0;
},
hasNpu() {
return this.store.data.stats.npu.length > 0;
return this.store.data?.stats?.npu && this.store.data.stats.npu.length > 0;
},
hasGpu() {
return this.store.data.stats.gpu.length > 0;
},
isLinux() {
return this.store.data.isLinux;
metricsClass() {
const classes = [];
if (this.args.disable_gpu || !this.hasGpu) classes.push('no-gpu');
if (this.args.disable_load) classes.push('no-load');
return classes.join(' ');
},
title() {
const { data } = this;
const title =
(data.stats && data.stats.system && data.stats.system.hostname) || "";
return title ? `${title} - Glances` : "Glances";
},
topMenu() {
return this.config.outputs !== undefined &&
this.config.outputs.top_menu !== undefined
? this.config.outputs.top_menu.split(",")
: uiconfig.topMenu;
},
leftMenu() {
return this.config.outputs !== undefined &&
this.config.outputs.left_menu !== undefined
? this.config.outputs.left_menu.split(",")
: uiconfig.leftMenu;
const title = (data.stats && data.stats.system && data.stats.system.hostname) || "";
return title ? `${title} \u2014 Glances` : "Glances";
},
},
watch: {
title() {
if (document) {
document.title = this.title;
}
if (document) document.title = this.title;
},
},
mounted() {
@@ -229,181 +96,48 @@ export default {
},
methods: {
setupHotKeys() {
// a => Sort processes/containers automatically
hotkeys("a", () => {
this.store.args.sort_processes_key = null;
});
// Sort keys
hotkeys("a", () => { this.store.args.sort_processes_key = null; });
hotkeys("c", () => { this.store.args.sort_processes_key = "cpu_percent"; });
hotkeys("m", () => { this.store.args.sort_processes_key = "memory_percent"; });
hotkeys("u", () => { this.store.args.sort_processes_key = "username"; });
hotkeys("p", () => { this.store.args.sort_processes_key = "name"; });
hotkeys("o", () => { this.store.args.sort_processes_key = "cpu_num"; });
hotkeys("i", () => { this.store.args.sort_processes_key = "io_counters"; });
hotkeys("t", () => { this.store.args.sort_processes_key = "timemillis"; });
// c => Sort processes/containers by CPU%
hotkeys("c", () => {
this.store.args.sort_processes_key = "cpu_percent";
});
// m => Sort processes/containers by MEM%
hotkeys("m", () => {
this.store.args.sort_processes_key = "memory_percent";
});
// u => Sort processes/containers by user
hotkeys("u", () => {
this.store.args.sort_processes_key = "username";
});
// p => Sort processes/containers by name
hotkeys("p", () => {
this.store.args.sort_processes_key = "name";
});
// o => Sort processes/containers by CPU core number
hotkeys("o", () => {
this.store.args.sort_processes_key = "cpu_num";
});
// i => Sort processes/containers by I/O rate
hotkeys("i", () => {
this.store.args.sort_processes_key = "io_counters";
});
// t => Sort processes/containers by time
hotkeys("t", () => {
this.store.args.sort_processes_key = "timemillis";
});
// A => Enable/disable AMPs
hotkeys("shift+A", () => {
this.store.args.disable_amps = !this.store.args.disable_amps;
});
// d => Show/hide disk I/O stats
hotkeys("d", () => {
this.store.args.disable_diskio = !this.store.args.disable_diskio;
});
// Q => Show/hide IRQ
hotkeys("shift+Q", () => {
this.store.args.enable_irq = !this.store.args.enable_irq;
});
// f => Show/hide filesystem stats
hotkeys("f", () => {
this.store.args.disable_fs = !this.store.args.disable_fs;
});
// j => Accumulate processes by program
hotkeys("j", () => {
this.store.args.programs = !this.store.args.programs;
});
// k => Show/hide connections stats
hotkeys("k", () => {
this.store.args.disable_connections =
!this.store.args.disable_connections;
});
// n => Show/hide network stats
hotkeys("n", () => {
this.store.args.disable_network = !this.store.args.disable_network;
});
// s => Show/hide sensors stats
hotkeys("s", () => {
this.store.args.disable_sensors = !this.store.args.disable_sensors;
});
// 2 => Show/hide left sidebar
hotkeys("2", () => {
this.store.args.disable_left_sidebar =
!this.store.args.disable_left_sidebar;
});
// z => Enable/disable processes stats
hotkeys("z", () => {
this.store.args.disable_process = !this.store.args.disable_process;
});
// S => Enable/disable short processes name
hotkeys("shift+S", () => {
this.store.args.process_short_name =
!this.store.args.process_short_name;
});
// D => Enable/disable containers stats
hotkeys("shift+D", () => {
this.store.args.disable_containers =
!this.store.args.disable_containers;
});
// b => Bytes or bits for network I/O
hotkeys("b", () => {
this.store.args.byte = !this.store.args.byte;
});
// 'B' => Switch between bit/s and IO/s for Disk IO
// Toggle keys
hotkeys("shift+A", () => { this.store.args.disable_amps = !this.store.args.disable_amps; });
hotkeys("d", () => { this.store.args.disable_diskio = !this.store.args.disable_diskio; });
hotkeys("shift+Q", () => { this.store.args.enable_irq = !this.store.args.enable_irq; });
hotkeys("f", () => { this.store.args.disable_fs = !this.store.args.disable_fs; });
hotkeys("j", () => { this.store.args.programs = !this.store.args.programs; });
hotkeys("k", () => { this.store.args.disable_connections = !this.store.args.disable_connections; });
hotkeys("n", () => { this.store.args.disable_network = !this.store.args.disable_network; });
hotkeys("s", () => { this.store.args.disable_sensors = !this.store.args.disable_sensors; });
hotkeys("2", () => { this.store.args.disable_left_sidebar = !this.store.args.disable_left_sidebar; });
hotkeys("z", () => { this.store.args.disable_process = !this.store.args.disable_process; });
hotkeys("shift+S", () => { this.store.args.process_short_name = !this.store.args.process_short_name; });
hotkeys("shift+D", () => { this.store.args.disable_containers = !this.store.args.disable_containers; });
hotkeys("b", () => { this.store.args.byte = !this.store.args.byte; });
hotkeys("shift+B", () => {
this.store.args.diskio_iops = !this.store.args.diskio_iops;
if (this.store.args.diskio_iops) {
this.store.args.diskio_latency = false;
}
if (this.store.args.diskio_iops) this.store.args.diskio_latency = false;
});
// 'L' => Switch to latency for Disk IO
hotkeys("shift+L", () => {
this.store.args.diskio_latency = !this.store.args.diskio_latency;
if (this.store.args.diskio_latency) {
this.store.args.diskio_iops = false;
}
if (this.store.args.diskio_latency) this.store.args.diskio_iops = false;
});
// l => Show/hide alert logs
hotkeys("l", () => {
this.store.args.disable_alert = !this.store.args.disable_alert;
});
// 1 => Global CPU or per-CPU stats
hotkeys("1", () => {
this.store.args.percpu = !this.store.args.percpu;
});
// h => Show/hide this help screen
hotkeys("h", () => {
this.store.args.help_tag = !this.store.args.help_tag;
});
// T => View network I/O as combination
hotkeys("shift+T", () => {
this.store.args.network_sum = !this.store.args.network_sum;
});
// U => View cumulative network I/O
hotkeys("shift+U", () => {
this.store.args.network_cumul = !this.store.args.network_cumul;
});
// F => Show filesystem free space
hotkeys("shift+F", () => {
this.store.args.fs_free_space = !this.store.args.fs_free_space;
});
// 3 => Enable/disable quick look plugin
hotkeys("3", () => {
this.store.args.disable_quicklook = !this.store.args.disable_quicklook;
});
// 6 => Enable/disable mean gpu
hotkeys("6", () => {
this.store.args.meangpu = !this.store.args.meangpu;
});
// 7 => Enable/disable mean gpu
hotkeys("7", () => {
this.store.args.disable_npu = !this.store.args.disable_npu;
});
// G => Enable/disable gpu
hotkeys("shift+G", () => {
this.store.args.disable_gpu = !this.store.args.disable_gpu;
});
hotkeys("l", () => { this.store.args.disable_alert = !this.store.args.disable_alert; });
hotkeys("1", () => { this.store.args.percpu = !this.store.args.percpu; });
hotkeys("h", () => { this.store.args.help_tag = !this.store.args.help_tag; });
hotkeys("shift+T", () => { this.store.args.network_sum = !this.store.args.network_sum; });
hotkeys("shift+U", () => { this.store.args.network_cumul = !this.store.args.network_cumul; });
hotkeys("shift+F", () => { this.store.args.fs_free_space = !this.store.args.fs_free_space; });
hotkeys("3", () => { this.store.args.disable_quicklook = !this.store.args.disable_quicklook; });
hotkeys("6", () => { this.store.args.meangpu = !this.store.args.meangpu; });
hotkeys("7", () => { this.store.args.disable_npu = !this.store.args.disable_npu; });
hotkeys("shift+G", () => { this.store.args.disable_gpu = !this.store.args.disable_gpu; });
hotkeys("5", () => {
this.store.args.disable_quicklook = !this.store.args.disable_quicklook;
this.store.args.disable_cpu = !this.store.args.disable_cpu;
@@ -413,32 +147,12 @@ export default {
this.store.args.disable_gpu = !this.store.args.disable_gpu;
this.store.args.disable_npu = !this.store.args.disable_npu;
});
// I => Show/hide IP module
hotkeys("shift+I", () => {
this.store.args.disable_ip = !this.store.args.disable_ip;
});
// P => Enable/disable ports module
hotkeys("shift+P", () => {
this.store.args.disable_ports = !this.store.args.disable_ports;
});
// V => Enable/disable VMs stats
hotkeys("shift+V", () => {
this.store.args.disable_vms = !this.store.args.disable_vms;
});
// 'W' > Enable/Disable Wifi plugin
hotkeys("shift+W", () => {
this.store.args.disable_wifi = !this.store.args.disable_wifi;
});
// 0 => Enable/disable IRIX mode (see issue #3158)
hotkeys("0", () => {
this.store.args.disable_irix = !this.store.args.disable_irix;
});
hotkeys("shift+I", () => { this.store.args.disable_ip = !this.store.args.disable_ip; });
hotkeys("shift+P", () => { this.store.args.disable_ports = !this.store.args.disable_ports; });
hotkeys("shift+V", () => { this.store.args.disable_vms = !this.store.args.disable_vms; });
hotkeys("shift+W", () => { this.store.args.disable_wifi = !this.store.args.disable_wifi; });
hotkeys("0", () => { this.store.args.disable_irix = !this.store.args.disable_irix; });
},
},
};
</script>
</script>

View File

@@ -3,11 +3,8 @@ if (module.hot) {
module.hot.accept();
}
import "../css/custom.scss";
import "../css/style.scss";
import * as bootstrap from "bootstrap";
import { createApp } from "vue";
import App from "./App.vue";
import * as filters from "./filters.js";

View File

@@ -0,0 +1,119 @@
<template>
<footer id="footer" v-if="hasAlerts && !cleared">
<div class="foot-header">
<span class="foot-label">Alerts</span>
<span class="foot-count">{{ alertCount }}</span>
<button class="foot-clear" @click="clearAlerts">[ CLEAR ]</button>
</div>
<div class="foot-alert" v-for="(alert, idx) in alerts" :key="idx">
<div class="foot-dot" :class="alert.dotClass"></div>
<span class="foot-time">{{ alert.time }}</span>
<template v-if="alert.ongoing">
<span class="foot-ongoing" :class="alert.ongoingClass">ongoing</span>
</template>
<template v-else>
<span class="foot-dur">{{ alert.duration }}</span>
</template>
<span class="foot-msg" :class="alert.msgClass">{{ alert.message }}</span>
</div>
</footer>
</template>
<script>
import { GlancesFavico } from "../services.js";
export default {
props: { data: { type: Object } },
data() {
return {
cleared: false,
};
},
watch: {
alertCount(newVal) {
// Reset cleared state when new alerts come in
if (newVal > 0) this.cleared = false;
// Update favicon badge
if (newVal > 0) {
GlancesFavico.badge(newVal);
} else {
GlancesFavico.reset();
}
},
},
computed: {
rawAlerts() {
return this.data?.stats?.alert || [];
},
hasAlerts() {
return this.rawAlerts.length > 0;
},
alertCount() {
return this.rawAlerts.length;
},
alerts() {
return this.rawAlerts.slice(0, 10).map(a => {
// a = [begin_timestamp, end_timestamp, state, type, min, avg, max, top, count]
// or {state, type, begin, end, min, avg, max, top}
const isArray = Array.isArray(a);
const state = isArray ? a[2] : a.state;
const type = isArray ? a[3] : a.type;
const begin = isArray ? a[0] * 1000 : (a.begin || 0) * 1000;
const end = isArray ? a[1] * 1000 : (a.end || 0) * 1000;
const maxVal = isArray ? a[6] : a.max;
const top = isArray ? a[7] : a.top;
const ongoing = end === -1000 || end < 0;
const stateLower = (state || '').toLowerCase();
// Time display
const beginDate = new Date(begin);
const pad = n => String(n).padStart(2, '0');
const time = `${pad(beginDate.getHours())}:${pad(beginDate.getMinutes())}:${pad(beginDate.getSeconds())}`;
// Duration
let duration = '';
if (!ongoing && end > begin) {
const diffSec = Math.round((end - begin) / 1000);
const m = Math.floor(diffSec / 60);
const s = diffSec % 60;
duration = `${m}m${pad(s)}s`;
}
// Dot class
let dotClass = 'ok';
if (stateLower === 'critical') dotClass = 'crit';
else if (stateLower === 'warning') dotClass = 'warn';
// Ongoing class
let ongoingClass = '';
if (stateLower === 'critical') ongoingClass = 'crit';
else if (stateLower === 'warning') ongoingClass = 'warn';
// Message class
let msgClass = 'info';
if (stateLower === 'critical') msgClass = 'critical';
else if (stateLower === 'warning') msgClass = 'warning';
// Build message text
let message = `${type}`;
if (maxVal != null) message += ` (${typeof maxVal === 'number' ? maxVal.toFixed(1) : maxVal})`;
if (top && top.length > 0) {
const topStr = Array.isArray(top)
? top.map(t => t.name || t).join(', ')
: top;
if (topStr) message += ` : ${topStr}`;
}
return { time, ongoing, duration, dotClass, ongoingClass, msgClass, message };
});
},
},
methods: {
clearAlerts() {
this.cleared = true;
GlancesFavico.reset();
},
},
};
</script>

View File

@@ -0,0 +1,82 @@
<template>
<header id="header">
<!-- System: hostname / OS -->
<div class="hdr-system">
<div class="hdr-hostname" :class="{ disconnected: isDisconnected }">
<template v-if="isDisconnected">Disconnected from </template>{{ hostname }}
</div>
<div class="hdr-os">{{ humanReadableName }}</div>
</div>
<!-- Right side blocks -->
<div class="hdr-right">
<!-- IP block -->
<div class="hdr-block" v-if="hasIp">
<div class="hdr-ip-row">
<span class="hdr-ip-label">LOCAL</span>
<span class="hdr-ip-val private">{{ address }}{{ maskCidr ? '/' + maskCidr : '' }}</span>
</div>
<div class="hdr-ip-row">
<span class="hdr-ip-label">PUBLIC</span>
<span class="hdr-ip-val">{{ publicAddress }}</span>
<span class="hdr-ip-location" v-if="publicInfo">&mdash; {{ publicInfo }}</span>
</div>
</div>
<!-- Uptime + clock block -->
<div class="hdr-block hdr-block--right">
<div>
<span class="pulse-dot" :class="{ disconnected: isDisconnected }"></span>
<span class="hdr-uptime">&uarr; {{ uptime }}</span>
</div>
<div class="hdr-clock">{{ clock }}</div>
</div>
</div>
</header>
</template>
<script>
import { store } from "../store.js";
export default {
props: {
data: { type: Object },
},
data() {
return {
clock: '',
clockInterval: null,
};
},
computed: {
stats() { return this.data.stats || {}; },
isDisconnected() { return store.status === 'FAILURE'; },
hostname() { return this.stats.system?.hostname || ''; },
humanReadableName() { return this.stats.system?.hr_name || ''; },
hasIp() { return this.stats.ip && this.stats.ip.address; },
address() { return this.stats.ip?.address || ''; },
maskCidr() { return this.stats.ip?.mask_cidr || ''; },
publicAddress() { return this.stats.ip?.public_address || ''; },
publicInfo() { return this.stats.ip?.public_info_human || ''; },
uptime() { return this.stats.uptime || ''; },
},
mounted() {
this.updateClock();
this.clockInterval = setInterval(() => this.updateClock(), 1000);
},
beforeUnmount() {
if (this.clockInterval) clearInterval(this.clockInterval);
},
methods: {
updateClock() {
if (this.stats.now?.custom) {
this.clock = this.stats.now.custom;
} else {
const n = new Date();
const p = x => String(x).padStart(2, '0');
this.clock = `${n.getFullYear()}-${p(n.getMonth()+1)}-${p(n.getDate())} ${p(n.getHours())}:${p(n.getMinutes())}:${p(n.getSeconds())}`;
}
},
},
};
</script>

View File

@@ -1,170 +1,143 @@
<template>
<div v-if="help">
<div class="container-fluid">
<div class="row">
<div class="col-sm-12 col-lg-24 title">{{ help.version }} {{ help.psutil_version }}</div>
</div>
<div class="row">&nbsp;</div>
<div class="row">
<div class="col-sm-12 col-lg-24">
{{ help.configuration_file }}
</div>
</div>
<div class="row">&nbsp;</div>
</div>
<table class="table table-sm table-borderless table-striped table-hover">
<thead>
<tr>
<th>{{ help.header_sort.replace(':', '') }}</th>
<th>{{ help.header_show_hide.replace(':', '') }}</th>
<th>{{ help.header_toggle.replace(':', '') }}</th>
<th>{{ help.header_miscellaneous.replace(':', '') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ help.sort_auto }}</td>
<td>{{ help.show_hide_application_monitoring }}</td>
<td>{{ help.toggle_bits_bytes }}</td>
<td>{{ help.misc_erase_process_filter }}</td>
</tr>
<tr>
<td>{{ help.sort_cpu }}</td>
<td>{{ help.show_hide_diskio }}</td>
<td>{{ help.toggle_count_rate }}</td>
<td>{{ help.misc_generate_history_graphs }}</td>
</tr>
<tr>
<td>{{ help.sort_io_rate }}</td>
<td>{{ help.show_hide_containers }}</td>
<td>{{ help.toggle_used_free }}</td>
<td>{{ help.misc_help }}</td>
</tr>
<tr>
<td>{{ help.sort_cpu_num }}</td>
<td>{{ help.show_hide_top_extended_stats }}</td>
<td>{{ help.toggle_bar_sparkline }}</td>
<td>{{ help.misc_accumulate_processes_by_program }}</td>
</tr>
<tr>
<td>{{ help.sort_mem }}</td>
<td>{{ help.show_hide_top_extended_stats }}</td>
<td>{{ help.toggle_bar_sparkline }}</td>
<td>{{ help.misc_accumulate_processes_by_program }}</td>
</tr>
<tr>
<td>{{ help.sort_process_name }}</td>
<td>{{ help.show_hide_filesystem }}</td>
<td>{{ help.toggle_separate_combined }}</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>{{ help.sort_cpu_times }}</td>
<td>{{ help.show_hide_gpu }}</td>
<td>{{ help.toggle_live_cumulative }}</td>
<td>{{ help.misc_reset_processes_summary_min_max }}</td>
</tr>
<tr>
<td>{{ help.sort_user }}</td>
<td>{{ help.show_hide_ip }}</td>
<td>{{ help.toggle_linux_percentage }}</td>
<td>{{ help.misc_quit }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_tcp_connection }}</td>
<td>{{ help.toggle_cpu_individual_combined }}</td>
<td>{{ help.misc_reset_history }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_alert }}</td>
<td>{{ help.toggle_gpu_individual_combined }}</td>
<td>{{ help.misc_delete_warning_alerts }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_network }}</td>
<td>{{ help.toggle_short_full }}</td>
<td>{{ help.misc_delete_warning_and_critical_alerts }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.sort_cpu_times }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_irq }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_raid_plugin }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_sensors }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_wifi_module }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_processes }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_left_sidebar }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_quick_look }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_cpu_mem_swap }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_all }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
<div class="row">&nbsp;</div>
<div>
<p>
For an exhaustive list of key bindings,
<a href="https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands">click here</a>.
</p>
</div>
<div>
<p><a href="/docs">API documentation</a> / <a href="/openapi.json">OpenAPI file</a></p>
</div>
<div class="row">&nbsp;</div>
<div>
<p>Press <b>h</b> to came back to Glances.</p>
</div>
</div>
<div class="help-screen" v-if="help">
<h2>{{ help.version }} {{ help.psutil_version }}</h2>
<p style="color:var(--fg2);margin-bottom:12px">{{ help.configuration_file }}</p>
<table>
<thead>
<tr>
<th>{{ help.header_sort.replace(':', '') }}</th>
<th>{{ help.header_show_hide.replace(':', '') }}</th>
<th>{{ help.header_toggle.replace(':', '') }}</th>
<th>{{ help.header_miscellaneous.replace(':', '') }}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{{ help.sort_auto }}</td>
<td>{{ help.show_hide_application_monitoring }}</td>
<td>{{ help.toggle_bits_bytes }}</td>
<td>{{ help.misc_erase_process_filter }}</td>
</tr>
<tr>
<td>{{ help.sort_cpu }}</td>
<td>{{ help.show_hide_diskio }}</td>
<td>{{ help.toggle_count_rate }}</td>
<td>{{ help.misc_generate_history_graphs }}</td>
</tr>
<tr>
<td>{{ help.sort_io_rate }}</td>
<td>{{ help.show_hide_containers }}</td>
<td>{{ help.toggle_used_free }}</td>
<td>{{ help.misc_help }}</td>
</tr>
<tr>
<td>{{ help.sort_cpu_num }}</td>
<td>{{ help.show_hide_top_extended_stats }}</td>
<td>{{ help.toggle_bar_sparkline }}</td>
<td>{{ help.misc_accumulate_processes_by_program }}</td>
</tr>
<tr>
<td>{{ help.sort_mem }}</td>
<td>{{ help.show_hide_filesystem }}</td>
<td>{{ help.toggle_separate_combined }}</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>{{ help.sort_process_name }}</td>
<td>{{ help.show_hide_gpu }}</td>
<td>{{ help.toggle_live_cumulative }}</td>
<td>{{ help.misc_reset_processes_summary_min_max }}</td>
</tr>
<tr>
<td>{{ help.sort_cpu_times }}</td>
<td>{{ help.show_hide_ip }}</td>
<td>{{ help.toggle_linux_percentage }}</td>
<td>{{ help.misc_quit }}</td>
</tr>
<tr>
<td>{{ help.sort_user }}</td>
<td>{{ help.show_hide_tcp_connection }}</td>
<td>{{ help.toggle_cpu_individual_combined }}</td>
<td>{{ help.misc_reset_history }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_alert }}</td>
<td>{{ help.toggle_gpu_individual_combined }}</td>
<td>{{ help.misc_delete_warning_alerts }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_network }}</td>
<td>{{ help.toggle_short_full }}</td>
<td>{{ help.misc_delete_warning_and_critical_alerts }}</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_irq }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_raid_plugin }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_sensors }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_wifi_module }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_processes }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_left_sidebar }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_quick_look }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_cpu_mem_swap }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
<tr>
<td>&nbsp;</td>
<td>{{ help.show_hide_all }}</td>
<td>&nbsp;</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
<p style="margin-top:12px">
For an exhaustive list of key bindings,
<a href="https://glances.readthedocs.io/en/latest/cmds.html#interactive-commands" style="color:var(--cyan)">click here</a>.
</p>
<p style="margin-top:6px">
<a href="/docs" style="color:var(--cyan)">API documentation</a> /
<a href="/openapi.json" style="color:var(--cyan)">OpenAPI file</a>
</p>
<p style="margin-top:12px;color:var(--fg2)">Press <strong style="color:var(--cyan)">h</strong> to go back to Glances.</p>
</div>
</template>
<script>
@@ -180,4 +153,4 @@ export default {
.then((response) => (this.help = response));
},
};
</script>
</script>

View File

@@ -0,0 +1,340 @@
<template>
<main id="main">
<!-- Containers section -->
<template v-if="!args.disable_containers && hasContainers">
<div class="main-section-title">Containers ({{ containers.length }})</div>
<div class="proc-wrap" style="flex:none;max-height:120px">
<table class="proc-table container-table">
<thead>
<tr>
<th class="ct-name">Name</th>
<th class="ct-status">Status</th>
<th class="ct-cpu">CPU%</th>
<th class="ct-mem">MEM</th>
<th class="ct-cmd">Command</th>
</tr>
</thead>
<tbody>
<tr v-for="c in containers" :key="c.id">
<td class="ct-name" style="color:var(--fg)">{{ c.name }}</td>
<td class="ct-status" :class="c.statusClass">{{ c.status }}</td>
<td class="ct-cpu" :class="c.cpuClass">{{ c.cpu }}</td>
<td class="ct-mem">{{ c.mem }}</td>
<td class="ct-cmd cmd-cell">{{ c.command }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<!-- AMPs section -->
<template v-if="!args.disable_amps && hasAmps">
<div class="main-section-title">AMPs</div>
<div class="amps-wrap">
<div class="amps-row" v-for="(amp, idx) in amps" :key="idx">
<span class="amps-name" :class="amp.deco">{{ amp.name }}</span>
<span class="amps-count" v-if="amp.regex">{{ amp.count }}</span>
<span class="amps-result" v-html="amp.result"></span>
</div>
</div>
</template>
<!-- Process toolbar -->
<div class="main-toolbar">
<div class="toolbar-left">
<span class="toolbar-title">{{ args.programs ? 'Programs' : 'Processes' }}</span>
<span class="toolbar-count">
<strong>{{ processTotal }}</strong> tasks &middot;
<strong>{{ processThread }}</strong> thr &middot;
<strong style="color:var(--green)">{{ processRunning }}</strong> run &middot;
<strong>{{ processSleeping }}</strong> slp
</span>
<span class="toolbar-sort">sorted by <span class="active">{{ sortLabel }}</span></span>
</div>
<div style="display:flex;gap:5px">
<button class="t-pill" :class="{ active: isSort('io_counters') }" @click="setSort('io_counters')">IO</button>
<button class="t-pill" :class="{ active: isSort('memory_percent') }" @click="setSort('memory_percent')">MEM</button>
<button class="t-pill" :class="{ active: isSort('cpu_percent') }" @click="setSort('cpu_percent')">CPU &darr;</button>
<button class="t-pill" :class="{ active: isSort(null) }" @click="setSort(null)">AUTO</button>
</div>
</div>
<!-- Process table -->
<div class="proc-wrap">
<table class="proc-table">
<thead>
<tr>
<th class="col-cpu" :class="{ 'sort-active': isSort('cpu_percent') }" @click="setSort('cpu_percent')">CPU%</th>
<th class="col-mem" :class="{ 'sort-active': isSort('memory_percent') }" @click="setSort('memory_percent')">MEM%</th>
<th class="col-virt">VIRT</th>
<th class="col-res">RES</th>
<th class="col-pid">PID</th>
<th class="col-ni" v-if="!data.isWindows">NI</th>
<th class="col-s">S</th>
<th class="col-ior" :class="{ 'sort-active': isSort('io_counters') }" @click="setSort('io_counters')">IOR/s</th>
<th class="col-iow">IOW/s</th>
<th class="col-user" :class="{ 'sort-active': isSort('username') }" @click="setSort('username')">USER</th>
<th class="col-time" :class="{ 'sort-active': isSort('timemillis') }" @click="setSort('timemillis')">TIME+</th>
<th class="col-cmd" :class="{ 'sort-active': isSort('name') }" @click="setSort('name')" style="text-align:left">Command</th>
</tr>
</thead>
<tbody>
<tr v-for="proc in processList" :key="proc.pid" @click="toggleExtended(proc.pid)">
<td :class="proc.cpuClass">
<div class="cpu-bar-wrap">
<span style="min-width:32px;text-align:right">{{ proc.cpu }}</span>
<div class="cpu-ibar">
<div class="cpu-ibar-fill" :class="proc.cpuBarClass" :style="{ width: proc.cpuBarWidth + '%' }"></div>
</div>
</div>
</td>
<td :class="proc.memClass">{{ proc.mem }}</td>
<td>{{ proc.vms }}</td>
<td>{{ proc.rss }}</td>
<td style="color:var(--fg3)">{{ proc.pid }}</td>
<td v-if="!data.isWindows" style="color:var(--fg3)">{{ proc.nice }}</td>
<td><span class="proc-status" :class="proc.status">{{ proc.status }}</span></td>
<td style="color:var(--fg3)">{{ proc.ioRead }}</td>
<td style="color:var(--fg3)">{{ proc.ioWrite }}</td>
<td style="color:var(--fg2)">{{ proc.username }}</td>
<td style="color:var(--fg3)">{{ proc.timeStr }}</td>
<td>
<span class="cmd-cell" :class="proc.cmdClass" :title="proc.cmdFull">{{ proc.cmdDisplay }}</span>
</td>
</tr>
</tbody>
</table>
</div>
</main>
</template>
<script>
import { orderBy } from "lodash";
import { store } from "../store.js";
import { GlancesHelper } from "../services.js";
const REVERSE_COLUMNS = new Set([
'cpu_percent', 'memory_percent', 'io_counters', 'num_threads',
]);
export default {
props: { data: { type: Object } },
computed: {
args() { return store.args || {}; },
config() { return store.config || {}; },
// ── PROCESS COUNT ──
countStats() { return this.data?.stats?.processcount || {}; },
processTotal() { return this.countStats.total || 0; },
processThread() { return this.countStats.thread || 0; },
processRunning() { return this.countStats.running || 0; },
processSleeping() { return this.countStats.sleeping || 0; },
// ── SORT ──
currentSort() { return store.args?.sort_processes_key || null; },
sortLabel() {
const map = {
cpu_percent: 'CPU',
memory_percent: 'MEM',
username: 'USER',
name: 'NAME',
io_counters: 'IO',
timemillis: 'TIME',
cpu_num: 'CORE',
};
return map[this.currentSort] || 'AUTO';
},
// ── PROCESS LIST ──
limit() {
const cfgMax = this.config.outputs?.max_processes_display;
return cfgMax ? parseInt(cfgMax) : 50;
},
processList() {
const isPrograms = this.args.programs;
const rawList = isPrograms
? this.data?.stats?.programlist || []
: this.data?.stats?.processlist || [];
const cores = this.data?.stats?.core?.log || 1;
const isIrix = !this.args.disable_irix;
const isShort = this.args.process_short_name;
const fmt = this.$filters.bytes;
const fmtTd = this.$filters.timedelta;
// Sort raw data before slicing
const sortCol = this.currentSort || 'cpu_percent';
let sortKeys = [sortCol];
if (sortCol === 'io_counters') {
sortKeys = ['io_total_raw'];
} else if (sortCol === 'timemillis') {
sortKeys = ['cpu_times_total'];
}
const sortDir = REVERSE_COLUMNS.has(sortCol) ? 'desc' : 'asc';
// Pre-compute sortable values on raw items
const enriched = rawList.map(p => {
const tsu = p.time_since_update || 1;
p.io_read_raw = p.io_counters ? (p.io_counters[0] - p.io_counters[2]) / tsu : 0;
p.io_write_raw = p.io_counters ? (p.io_counters[1] - p.io_counters[3]) / tsu : 0;
p.io_total_raw = p.io_read_raw + p.io_write_raw;
p.cpu_times_total = p.cpu_times
? (p.cpu_times.user || p.cpu_times[0] || 0) + (p.cpu_times.system || p.cpu_times[1] || 0)
: 0;
return p;
});
const sorted = orderBy(enriched, sortKeys, sortKeys.map(() => sortDir));
return sorted.slice(0, this.limit).map(p => {
let cpuVal = p.cpu_percent || 0;
if (!isIrix && cores > 1) {
cpuVal = cpuVal / cores;
}
const cpuClass = this.getCpuClass(cpuVal);
const cpuBarWidth = Math.min(cpuVal * 2.5, 100);
const cpuBarClass = cpuClass || 'ok';
const memVal = p.memory_percent || 0;
const memClass = this.getMemClass(memVal);
// VIRT / RES from memory_info object
let memVirt = 0, memRes = 0;
if (p.memory_info) {
if (typeof p.memory_info === 'object' && !Array.isArray(p.memory_info)) {
memVirt = p.memory_info.vms || 0;
memRes = p.memory_info.rss || 0;
} else if (Array.isArray(p.memory_info)) {
memRes = p.memory_info[0] || 0;
memVirt = p.memory_info[1] || 0;
}
}
// I/O formatted
const ioRead = fmt(Math.max(0, p.io_read_raw));
const ioWrite = fmt(Math.max(0, p.io_write_raw));
// Time: timedelta expects array [user, system]
let timeStr = '?';
if (p.cpu_times) {
const timesArray = Array.isArray(p.cpu_times)
? p.cpu_times
: [p.cpu_times.user || 0, p.cpu_times.system || 0];
const td = fmtTd(timesArray);
if (td) {
timeStr = String(td.hours).padStart(2, '0') + ':' +
String(td.minutes).padStart(2, '0') + ':' +
String(td.seconds).padStart(2, '0');
}
}
// Command display
let cmdline = p.cmdline;
if (Array.isArray(cmdline)) {
cmdline = cmdline.join(' ').replace(/\n/g, ' ');
}
const name = isShort ? (p.name || '') : (cmdline || p.name || '');
const cmdFull = cmdline || p.name || '';
const maxLen = 80;
const cmdDisplay = name.length > maxLen ? name.substring(0, maxLen) + '\u2026' : name;
const cmdClass = '';
return {
pid: p.pid,
cpu: cpuVal.toFixed(1),
cpuVal,
cpuClass,
cpuBarWidth,
cpuBarClass,
mem: memVal.toFixed(1),
memClass,
vms: fmt(memVirt),
rss: fmt(memRes),
nice: p.nice ?? '',
status: p.status || 'S',
ioRead,
ioWrite,
username: p.username || '',
timeStr,
cmdDisplay,
cmdFull,
cmdClass,
};
});
},
// ── CONTAINERS ──
hasContainers() {
const c = this.data?.stats?.containers;
return c && c.length > 0;
},
containers() {
const list = this.data?.stats?.containers || [];
const fmt = this.$filters.bytes;
return list.slice(0, 10).map(c => {
const status = c.Status || c.status || 'unknown';
const statusClass = status.startsWith('Up') || status === 'running' ? 'ok' : 'critical';
return {
id: c.Id || c.id,
name: c.name || '',
status,
statusClass,
cpu: (c.cpu?.total || 0).toFixed(1),
cpuClass: (c.cpu?.total || 0) > 50 ? 'warning' : '',
mem: fmt(c.memory?.usage || 0),
command: c.Command || c.command || '',
};
});
},
// ── AMPS ──
hasAmps() {
const a = this.data?.stats?.amps;
return a && a.filter(p => p.result !== null).length > 0;
},
amps() {
const list = this.data?.stats?.amps || [];
const nl2br = this.$filters.nl2br;
return list.filter(p => p.result !== null).map(p => {
let deco = 'ok';
if (p.count > 0) {
if ((p.countmin !== null && p.count < p.countmin) ||
(p.countmax !== null && p.count > p.countmax)) {
deco = 'careful';
}
} else {
deco = p.countmin === null ? 'ok' : 'critical';
}
return {
name: p.name,
count: p.count,
regex: p.regex,
result: nl2br(p.result),
deco,
};
});
},
},
methods: {
isSort(key) {
return this.currentSort === key;
},
setSort(key) {
store.args.sort_processes_key = key;
},
toggleExtended(pid) {
fetch(`api/4/processes/extended/${pid}`, { method: 'POST' });
},
getCpuClass(val) {
const alert = GlancesHelper.getAlert('processlist', 'processlist_cpu_', val, 100);
return alert || '';
},
getMemClass(val) {
const alert = GlancesHelper.getAlert('processlist', 'processlist_mem_', val, 100);
return alert || '';
},
},
};
</script>

View File

@@ -0,0 +1,117 @@
<template>
<div class="m-tile" style="--t-color:var(--green)">
<div class="m-head">
<span class="m-id">CPU <span class="m-id-sub" v-if="cpuName">{{ cpuName }} &middot; {{ coreCount }}-core</span></span>
</div>
<div class="m-spark-row">
<span class="m-val" :class="decoration">{{ totalDisplay }}<span class="unit">%</span></span>
<sparkline :data="history" :max="100" :color="sparkColor" :height="22" />
</div>
<!-- 3-column extended stats grid -->
<div class="cpu-stats-grid">
<!-- col 1 -->
<div class="cpu-col">
<div class="cpu-row"><span class="k">user:</span><span class="v" :class="userDeco">{{ user }}%</span></div>
<div class="cpu-row"><span class="k">system:</span><span class="v" :class="systemDeco">{{ system }}%</span></div>
<div class="cpu-row"><span class="k">iowait:</span><span class="v" :class="iowaitDeco">{{ iowait }}%</span></div>
<div class="cpu-row" v-if="data.isLinux"><span class="k">steal:</span><span class="v dim">{{ steal }}%</span></div>
<div class="cpu-row" v-if="data.isWindows"><span class="k">dpc:</span><span class="v" :class="dpcDeco">{{ dpc }}%</span></div>
</div>
<!-- col 2 -->
<div class="cpu-col">
<div class="cpu-row"><span class="k">idle:</span><span class="v hi">{{ idle }}%</span></div>
<div class="cpu-row"><span class="k">irq:</span><span class="v dim">{{ irq }}%</span></div>
<div class="cpu-row"><span class="k">nice:</span><span class="v dim">{{ nice }}%</span></div>
<div class="cpu-row" v-if="data.isLinux"><span class="k">guest:</span><span class="v dim">{{ guest }}%</span></div>
</div>
<!-- col 3 -->
<div class="cpu-col">
<div class="cpu-row"><span class="k">ctx_sw:</span><span class="v" :class="ctxDeco">{{ ctxSwitches }}</span></div>
<div class="cpu-row"><span class="k">inter:</span><span class="v dim">{{ interrupts }}</span></div>
<div class="cpu-row" v-if="data.isLinux"><span class="k">sw_int:</span><span class="v dim">{{ softInterrupts }}</span></div>
</div>
</div>
</div>
</template>
<script>
import { store } from "../store.js";
import Sparkline from "./sparkline.vue";
function colorFor(pct) {
if (pct >= 90) return '#ff3355';
if (pct >= 75) return '#ffcc00';
if (pct >= 50) return '#4488ff';
return '#00ff88';
}
function decoClass(v) {
if (!v || !v.decoration) return 'dim';
const d = v.decoration.toLowerCase();
if (d === 'ok' || d === 'ok_log') return 'ok';
if (d === 'warning' || d === 'warning_log') return 'warn';
if (d === 'critical' || d === 'critical_log') return 'crit';
if (d === 'careful' || d === 'careful_log') return 'hi';
if (d === 'default') return 'dim';
return 'dim';
}
export default {
components: { Sparkline },
props: { data: { type: Object } },
computed: {
stats() { return this.data?.stats?.cpu || {}; },
view() { return this.data?.views?.cpu || {}; },
history() { return store.history.cpu; },
total() { return this.stats.total != null ? this.stats.total : 0; },
totalDisplay() { return this.total.toFixed(1); },
decoration() {
if (!this.view.total) return 'ok';
const d = this.view.total.decoration?.toLowerCase() || 'ok';
return d.replace('_log', '');
},
decorationLabel() {
const d = this.decoration;
if (d === 'ok') return 'OK';
if (d === 'critical') return 'CRIT';
if (d === 'warning') return 'WARN';
return d.toUpperCase();
},
sparkColor() { return colorFor(this.total); },
cpuName() { return this.data?.stats?.quicklook?.cpu_name || ''; },
coreCount() { return this.data?.stats?.core?.log || this.data?.stats?.core?.phys || '?'; },
user() { return (this.stats.user ?? 0).toFixed(1); },
system() { return (this.stats.system ?? 0).toFixed(1); },
idle() { return (this.stats.idle ?? 0).toFixed(1); },
nice() { return (this.stats.nice ?? 0).toFixed(1); },
irq() { return (this.stats.irq ?? 0).toFixed(1); },
iowait() { return (this.stats.iowait ?? 0).toFixed(1); },
steal() { return (this.stats.steal ?? 0).toFixed(1); },
guest() { return (this.stats.guest ?? 0).toFixed(1); },
dpc() { return (this.stats.dpc ?? 0).toFixed(1); },
ctxSwitches() {
const v = this.stats.ctx_switches;
if (v == null) return '0';
const tsu = this.stats.time_since_update || 1;
return Math.round(v / tsu).toLocaleString();
},
interrupts() {
const v = this.stats.interrupts;
if (v == null) return '0';
const tsu = this.stats.time_since_update || 1;
return Math.round(v / tsu).toLocaleString();
},
softInterrupts() {
const v = this.stats.soft_interrupts;
if (v == null) return '0';
const tsu = this.stats.time_since_update || 1;
return Math.round(v / tsu).toLocaleString();
},
userDeco() { return decoClass(this.view.user); },
systemDeco() { return decoClass(this.view.system); },
iowaitDeco() { return decoClass(this.view.iowait); },
dpcDeco() { return decoClass(this.view.dpc); },
ctxDeco() { return decoClass(this.view.ctx_switches); },
},
};
</script>

View File

@@ -0,0 +1,83 @@
<template>
<div class="m-tile tile-gpu" style="--t-color:var(--cyan)">
<div class="m-head">
<span class="m-id">GPU <template v-if="gpuCount > 1">&times;{{ gpuCount }}</template></span>
</div>
<div class="m-spark-row">
<span class="m-val" :class="decoration">{{ meanProcDisplay }}<span class="unit">%</span></span>
<sparkline :data="history" :max="100" :color="sparkColor" :height="26" />
</div>
<div class="m-sub-rows">
<div class="m-kv" v-for="gpu in gpus" :key="gpu.gpu_id">
{{ gpu.name }}
<span class="v" :class="gpuProcDeco(gpu)">{{ (gpu.proc || 0).toFixed(0) }}%</span>
</div>
<div class="m-kv" v-if="meanMem != null">
MEM <span class="v dim">{{ meanMem.toFixed(0) }}%</span>
</div>
</div>
</div>
</template>
<script>
import { store } from "../store.js";
import Sparkline from "./sparkline.vue";
function colorFor(pct) {
if (pct >= 90) return '#ff3355';
if (pct >= 75) return '#ffcc00';
if (pct >= 50) return '#4488ff';
return '#00ff88';
}
export default {
components: { Sparkline },
props: { data: { type: Object } },
computed: {
stats() { return this.data?.stats?.gpu || []; },
view() { return this.data?.views?.gpu || {}; },
history() { return store.history.gpu; },
gpus() { return this.stats; },
gpuCount() { return this.stats.length; },
meanProc() {
if (this.stats.length === 0) return 0;
return this.stats.reduce((s, g) => s + (g.proc || 0), 0) / this.stats.length;
},
meanProcDisplay() { return this.meanProc.toFixed(1); },
meanMem() {
if (this.stats.length === 0) return null;
return this.stats.reduce((s, g) => s + (g.mem || 0), 0) / this.stats.length;
},
decoration() {
// Use first GPU's decoration or derive from value
const first = this.stats[0];
if (first && this.view[first.gpu_id]?.proc?.decoration) {
return this.view[first.gpu_id].proc.decoration.toLowerCase();
}
if (this.meanProc >= 90) return 'critical';
if (this.meanProc >= 75) return 'warning';
if (this.meanProc >= 50) return 'careful';
return 'ok';
},
decorationLabel() {
const d = this.decoration;
if (d === 'ok') return 'OK';
if (d === 'critical') return 'CRIT';
if (d === 'warning') return 'WARN';
return d.toUpperCase();
},
sparkColor() { return colorFor(this.meanProc); },
},
methods: {
gpuProcDeco(gpu) {
if (this.view[gpu.gpu_id]?.proc?.decoration) {
const d = this.view[gpu.gpu_id].proc.decoration.toLowerCase();
if (d === 'ok') return 'ok';
if (d === 'warning') return 'warn';
if (d === 'critical') return 'crit';
}
return 'ok';
},
},
};
</script>

View File

@@ -0,0 +1,76 @@
<template>
<div class="m-tile tile-load" style="--t-color:var(--green)">
<div class="m-head">
<span class="m-id">LOAD</span>
</div>
<div class="m-spark-row">
<span class="m-val" :class="decoration">{{ min1Display }}</span>
<sparkline :data="history" :max="cpucore" :color="sparkColor" :height="26" />
</div>
<div class="m-sub-rows">
<div class="m-kv">1min <span class="v" :class="min1Deco">{{ min1Display }}</span></div>
<div class="m-kv">5min <span class="v" :class="min5Deco">{{ min5Display }}</span></div>
<div class="m-kv">15min <span class="v" :class="min15Deco">{{ min15Display }}</span></div>
<div class="m-kv">cores <span class="v hi">{{ cpucore }}</span></div>
</div>
</div>
</template>
<script>
import { store } from "../store.js";
import Sparkline from "./sparkline.vue";
function colorFor(pct) {
if (pct >= 90) return '#ff3355';
if (pct >= 75) return '#ffcc00';
if (pct >= 50) return '#4488ff';
return '#00ff88';
}
function decoStr(v) {
if (!v || !v.decoration) return '';
const d = v.decoration.toLowerCase();
if (d === 'ok' || d === 'ok_log') return 'ok';
if (d === 'warning' || d === 'warning_log') return 'warn';
if (d === 'critical' || d === 'critical_log') return 'crit';
if (d === 'careful' || d === 'careful_log') return 'hi';
if (d === 'default') return 'dim';
return 'dim';
}
export default {
components: { Sparkline },
props: { data: { type: Object } },
computed: {
stats() { return this.data?.stats?.load || {}; },
view() { return this.data?.views?.load || {}; },
history() { return store.history.load; },
cpucore() { return this.stats.cpucore || 1; },
min1() { return this.stats.min1 ?? 0; },
min5() { return this.stats.min5 ?? 0; },
min15() { return this.stats.min15 ?? 0; },
min1Display() { return this.min1.toFixed(2); },
min5Display() { return this.min5.toFixed(2); },
min15Display() { return this.min15.toFixed(2); },
decoration() {
if (!this.view.min1) return 'ok';
const d = this.view.min1.decoration?.toLowerCase() || 'ok';
return d.replace('_log', '');
},
decorationLabel() {
const d = this.decoration;
if (d === 'ok') return 'OK';
if (d === 'critical') return 'CRIT';
if (d === 'warning') return 'WARN';
return d.toUpperCase();
},
sparkColor() {
const pct = this.cpucore > 0 ? (this.min1 / this.cpucore) * 100 : 0;
return colorFor(pct);
},
min1Deco() { return decoStr(this.view.min1); },
min5Deco() { return decoStr(this.view.min5); },
min15Deco() { return decoStr(this.view.min15); },
},
};
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div class="m-tile tile-mem" style="--t-color:var(--yellow)">
<!-- header: MEM badge left, SWAP badge right -->
<div class="ms-heads">
<div class="ms-heads-left">
<span class="ms-id">MEM</span>
<span class="m-badge" :class="memDecoration">{{ memPercent }}%</span>
</div>
<div class="ms-heads-right" v-if="hasSwap">
<span class="ms-id">SWAP</span>
<span class="m-badge" :class="swapDecoration">{{ swapPercent }}%</span>
</div>
</div>
<!-- MEM sparkline row -->
<div class="ms-sparks">
<div class="m-spark-row">
<span class="ms-val" :class="memDecoration">{{ memPercent }}%</span>
<sparkline :data="history" :max="100" :color="sparkColor" :height="22" />
</div>
</div>
<!-- extended MEM stats in 2-column grid -->
<div class="ms-stats">
<div class="ms-stat-row"><span class="k">total:</span><span class="v">{{ total }}</span></div>
<div class="ms-stat-row"><span class="k">active:</span><span class="v">{{ active }}</span></div>
<div class="ms-stat-row"><span class="k">used:</span><span class="v" :class="usedDeco">{{ used }}</span></div>
<div class="ms-stat-row"><span class="k">inactive:</span><span class="v">{{ inactive }}</span></div>
<div class="ms-stat-row"><span class="k">free:</span><span class="v">{{ free }}</span></div>
<div class="ms-stat-row"><span class="k">buffers:</span><span class="v">{{ buffers }}</span></div>
<div class="ms-stat-row"><span class="k">avail:</span><span class="v">{{ available }}</span></div>
<div class="ms-stat-row"><span class="k">cached:</span><span class="v">{{ cached }}</span></div>
</div>
</div>
</template>
<script>
import { store } from "../store.js";
import Sparkline from "./sparkline.vue";
function colorFor(pct) {
if (pct >= 90) return '#ff3355';
if (pct >= 75) return '#ffcc00';
if (pct >= 50) return '#4488ff';
return '#00ff88';
}
export default {
components: { Sparkline },
props: { data: { type: Object } },
computed: {
memStats() { return this.data?.stats?.mem || {}; },
memView() { return this.data?.views?.mem || {}; },
swapStats() { return this.data?.stats?.memswap || {}; },
swapView() { return this.data?.views?.memswap || {}; },
history() { return store.history.mem; },
memPercent() { return (this.memStats.percent ?? 0).toFixed(1); },
memDecoration() {
if (!this.memView.percent) return 'ok';
const d = this.memView.percent.decoration?.toLowerCase() || 'ok';
return d.replace('_log', '');
},
hasSwap() { return this.swapStats.percent != null && this.swapStats.percent > 0; },
swapPercent() { return (this.swapStats.percent ?? 0).toFixed(1); },
swapDecoration() {
if (!this.swapView.percent) return 'ok';
const d = this.swapView.percent.decoration?.toLowerCase() || 'ok';
return d.replace('_log', '');
},
sparkColor() { return colorFor(this.memStats.percent || 0); },
total() { return this.$filters.bytes(this.memStats.total); },
used() { return this.$filters.bytes(this.memStats.used); },
available() { return this.$filters.bytes(this.memStats.available); },
free() { return this.$filters.bytes(this.memStats.free); },
active() { return this.$filters.bytes(this.memStats.active); },
inactive() { return this.$filters.bytes(this.memStats.inactive); },
buffers() { return this.$filters.bytes(this.memStats.buffers); },
cached() { return this.$filters.bytes(this.memStats.cached); },
usedDeco() {
if (!this.memView.used) return '';
const d = this.memView.used.decoration?.toLowerCase().replace('_log', '');
if (d === 'warning') return 'warn';
if (d === 'critical') return 'crit';
if (d === 'ok') return 'ok';
if (d === 'careful') return 'ok';
return '';
},
},
};
</script>

View File

@@ -0,0 +1,377 @@
<template>
<aside id="sidebar">
<!-- NETWORK -->
<div class="sb-section" v-if="!args.disable_network && hasNetworks">
<div class="sb-title">Network <span class="sub">Rx / Tx</span></div>
<div class="sb-row" v-for="net in networks" :key="net.ifname">
<span class="name">{{ net.alias || net.ifname }}</span>
<span class="vals">
<span class="v" :class="net.rxDeco">{{ net.rx }}</span>
<span class="dim">/</span>
<span class="v" :class="net.txDeco">{{ net.tx }}</span>
</span>
</div>
</div>
<!-- PORTS -->
<div class="sb-section" v-if="!args.disable_ports && hasPorts">
<template v-for="port in ports" :key="port.id">
<div class="sb-row">
<span class="name">{{ port.description || port.host }}</span>
<span class="vals">
<span class="v" :class="port.deco">{{ port.display }}</span>
</span>
</div>
</template>
</div>
<!-- WIFI -->
<div class="sb-section" v-if="!args.disable_wifi && hasWifi">
<template v-for="ap in wifiList" :key="ap.ssid">
<div class="sb-row">
<span class="name" style="font-size:10px;color:var(--fg3)">{{ ap.ssid }}</span>
<span class="vals">
<span class="dim">{{ ap.signal }} dBm</span>
</span>
</div>
</template>
</div>
<!-- DISK I/O -->
<div class="sb-section" v-if="!args.disable_diskio && hasDisks">
<div class="sb-title">Disk I/O <span class="sub">R / W</span></div>
<div class="sb-row" v-for="disk in disks" :key="disk.name">
<span class="name">{{ disk.alias || disk.name }}</span>
<span class="vals">
<span class="v" :class="disk.rDeco">{{ disk.read }}</span>
<span class="dim">/</span>
<span class="v" :class="disk.wDeco">{{ disk.write }}</span>
</span>
</div>
</div>
<!-- FILESYSTEM -->
<div class="sb-section" v-if="!args.disable_fs && hasFs">
<div class="sb-title">Filesystem <span class="sub">Used / Total</span></div>
<div class="sb-row" v-for="fs in fileSystems" :key="fs.mnt">
<span class="name">{{ fs.alias || fs.name }}</span>
<span class="vals">
<div class="sb-minibar">
<div class="sb-minibar-fill" :class="fs.deco" :style="{ width: fs.percent + '%' }"></div>
</div>
<span class="v neutral">{{ fs.used }}/{{ fs.size }}</span>
</span>
</div>
</div>
<!-- FOLDERS -->
<div class="sb-section" v-if="hasFolders">
<div class="sb-title">Folders <span class="sub">Size</span></div>
<div class="sb-row" v-for="folder in folders" :key="folder.path">
<span class="name" style="font-size:10px">{{ folder.path }}</span>
<span class="vals">
<span class="v" :class="folder.deco">{{ folder.size }}</span>
</span>
</div>
</div>
<!-- SENSORS -->
<div class="sb-section" v-if="!args.disable_sensors && hasSensors">
<div class="sb-title">Sensors</div>
<div class="sb-row" v-for="sensor in sensors" :key="sensor.label">
<span class="name">{{ sensor.label }}</span>
<span class="vals">
<template v-if="sensor.type === 'temperature_core' || sensor.type === 'temperature_hdd' || sensor.type === 'battery'">
<div class="sb-minibar">
<div class="sb-minibar-fill" :class="sensor.deco" :style="{ width: sensor.barPercent + '%' }"></div>
</div>
</template>
<span class="v" :class="sensor.deco">{{ sensor.display }}</span>
</span>
</div>
</div>
<!-- CONNECTIONS -->
<div class="sb-section" v-if="!args.disable_connections && hasConnections" style="border-bottom:none">
<div class="sb-title">Connections</div>
<div class="sb-row" v-if="connections.listen != null">
<span class="name">Listen</span>
<span class="vals"><span class="v neutral">{{ connections.listen }}</span></span>
</div>
<div class="sb-row" v-if="connections.initiated != null">
<span class="name">Initiated</span>
<span class="vals"><span class="v neutral">{{ connections.initiated }}</span></span>
</div>
<div class="sb-row" v-if="connections.established != null">
<span class="name">Established</span>
<span class="vals"><span class="v neutral">{{ connections.established }}</span></span>
</div>
<div class="sb-row" v-if="connections.terminated != null">
<span class="name">Terminated</span>
<span class="vals"><span class="v neutral">{{ connections.terminated }}</span></span>
</div>
<div class="sb-row" v-if="connections.tracked != null">
<span class="name">Tracked</span>
<span class="vals"><span class="v" :class="connections.trackedDeco">{{ connections.tracked }}</span></span>
</div>
</div>
</aside>
</template>
<script>
import { orderBy } from "lodash";
import { store } from "../store.js";
export default {
props: { data: { type: Object } },
computed: {
args() { return store.args || {}; },
// ── NETWORK ──
hasNetworks() {
const nets = this.data?.stats?.network;
return nets && nets.length > 0;
},
networks() {
const nets = this.data?.stats?.network || [];
const views = this.data?.views?.network || {};
const isByte = store.args?.byte;
const isCumul = store.args?.network_cumul;
const isSum = store.args?.network_sum;
const fmt = this.$filters.bytes;
const fmtBits = this.$filters.bits;
return nets
.filter(n => !n.is_up || n.is_up !== false)
.map(n => {
const name = n.interface_name;
let rx, tx, rxDeco = 'neutral', txDeco = 'neutral';
if (isSum) {
const val = isCumul ? n.bytes_all : n.bytes_all_rate_per_sec;
rx = isByte ? fmt(val) : fmtBits(val);
tx = '';
} else if (isCumul) {
rx = isByte ? fmt(n.bytes_recv) : fmtBits(n.bytes_recv);
tx = isByte ? fmt(n.bytes_sent) : fmtBits(n.bytes_sent);
} else {
rx = isByte ? fmt(n.bytes_recv_rate_per_sec) : fmtBits(n.bytes_recv_rate_per_sec);
tx = isByte ? fmt(n.bytes_sent_rate_per_sec) : fmtBits(n.bytes_sent_rate_per_sec);
}
if (views[name]) {
if (views[name].bytes_recv_rate_per_sec?.decoration)
rxDeco = views[name].bytes_recv_rate_per_sec.decoration.toLowerCase();
if (views[name].bytes_sent_rate_per_sec?.decoration)
txDeco = views[name].bytes_sent_rate_per_sec.decoration.toLowerCase();
}
return { ifname: name, alias: n.alias, rx, tx, rxDeco, txDeco };
});
},
// ── PORTS ──
hasPorts() {
const p = this.data?.stats?.ports;
return p && p.length > 0;
},
ports() {
const ports = this.data?.stats?.ports || [];
return ports.map((p, i) => {
let display, deco;
if (p.port != null) {
// TCP port
if (p.status === null) { deco = 'careful'; display = '?'; }
else if (p.status === false) { deco = 'critical'; display = 'Timeout'; }
else if (p.rtt_warning && p.status > p.rtt_warning) { deco = 'warning'; display = p.status.toFixed(0) + 'ms'; }
else { deco = 'ok'; display = p.status.toFixed(0) + 'ms'; }
} else if (p.url != null) {
// Web URL
if (p.status === null) { deco = 'careful'; display = '?'; }
else if (![200, 301, 302].includes(p.status)) { deco = 'critical'; display = p.status; }
else if (p.rtt_warning && p.elapsed > p.rtt_warning) { deco = 'warning'; display = p.elapsed?.toFixed(0) + 'ms'; }
else { deco = 'ok'; display = p.status; }
} else {
return null;
}
return { id: i, description: p.description || p.host || p.url, display, deco };
}).filter(Boolean);
},
// ── WIFI ──
hasWifi() {
const w = this.data?.stats?.wifi;
return w && w.length > 0;
},
wifiList() {
const w = this.data?.stats?.wifi || [];
return w.filter(h => h.ssid).map(h => ({
ssid: h.ssid,
signal: h.quality_level,
}));
},
// ── DISK I/O ──
hasDisks() {
const d = this.data?.stats?.diskio;
return d && d.length > 0;
},
disks() {
const disks = this.data?.stats?.diskio || [];
const views = this.data?.views?.diskio || {};
const fmt = this.$filters.bytes;
const isIops = store.args?.diskio_iops;
const isLatency = store.args?.diskio_latency;
return disks.map(d => {
const name = d.disk_name;
let read, write;
if (isLatency) {
read = (d.read_latency || 0).toFixed(1) + 'ms';
write = (d.write_latency || 0).toFixed(1) + 'ms';
} else if (isIops) {
read = Math.round(d.read_count_rate_per_sec || 0).toString();
write = Math.round(d.write_count_rate_per_sec || 0).toString();
} else {
read = fmt(d.read_bytes_rate_per_sec);
write = fmt(d.write_bytes_rate_per_sec);
}
let rDeco = 'neutral', wDeco = 'neutral';
if (views[name]) {
if (views[name].read_bytes_rate_per_sec?.decoration)
rDeco = views[name].read_bytes_rate_per_sec.decoration.toLowerCase();
if (views[name].write_bytes_rate_per_sec?.decoration)
wDeco = views[name].write_bytes_rate_per_sec.decoration.toLowerCase();
}
return { name, alias: d.alias, read, write, rDeco, wDeco };
});
},
// ── FILESYSTEM ──
hasFs() {
const f = this.data?.stats?.fs;
return f && f.length > 0;
},
fileSystems() {
const fsList = this.data?.stats?.fs || [];
const views = this.data?.views?.fs || {};
const fmt = this.$filters.bytes;
return fsList.map(f => {
const mnt = f.mnt_point;
let deco = 'ok';
if (views[mnt]?.percent?.decoration) {
deco = views[mnt].percent.decoration.toLowerCase();
}
const alias = f.alias || (mnt === '/' ? 'Root' : mnt.split('/').pop() || mnt);
return {
mnt,
name: alias,
alias: f.alias,
percent: f.percent || 0,
used: fmt(f.used),
size: fmt(f.size),
deco,
};
});
},
// ── FOLDERS ──
hasFolders() {
const f = this.data?.stats?.folders;
return f && f.length > 0;
},
folders() {
const folders = this.data?.stats?.folders || [];
const fmt = this.$filters.bytes;
return folders.map(f => {
let deco = 'ok';
if (f.errno && f.errno > 0) {
deco = 'critical';
} else if (f.critical && f.size > f.critical * 1000000) {
deco = 'critical';
} else if (f.warning && f.size > f.warning * 1000000) {
deco = 'warning';
} else if (f.careful && f.size > f.careful * 1000000) {
deco = 'careful';
}
return {
path: f.path,
size: f.errno && f.errno > 0 ? '? ' + f.errno : fmt(f.size),
deco,
};
});
},
// ── SENSORS ──
hasSensors() {
const s = this.data?.stats?.sensors;
return s && s.length > 0;
},
sensors() {
const sensors = this.data?.stats?.sensors || [];
const views = this.data?.views?.sensors || {};
const isFahrenheit = store.args?.fahrenheit;
return sensors.map(s => {
let value = s.value;
let unit = s.unit || '';
if (isFahrenheit && (s.type === 'temperature_core' || s.type === 'temperature_hdd')) {
value = value * 1.8 + 32;
unit = '\u00b0F';
}
let deco = 'neutral';
if (views[s.label]?.value?.decoration) {
deco = views[s.label].value.decoration.toLowerCase();
}
let barPercent = 0;
if (s.type === 'battery') {
barPercent = value;
} else if (s.type === 'temperature_core' || s.type === 'temperature_hdd') {
barPercent = Math.min(100, (value / 100) * 100);
}
const display = typeof value === 'number'
? value.toFixed(0) + unit
: value + unit;
return { label: s.label, type: s.type, display, deco, barPercent };
});
},
// ── CONNECTIONS ──
hasConnections() {
return this.data?.stats?.connections != null;
},
connections() {
const c = this.data?.stats?.connections || {};
const views = this.data?.views?.connections || {};
let trackedDeco = 'neutral';
if (views.nf_conntrack_percent?.decoration) {
trackedDeco = views.nf_conntrack_percent.decoration.toLowerCase();
}
const tracked = c.nf_conntrack_count != null && c.nf_conntrack_max
? `${c.nf_conntrack_count}/${c.nf_conntrack_max}`
: null;
return {
listen: c.LISTEN,
initiated: c.initiated,
established: c.ESTABLISHED,
terminated: c.terminated,
tracked,
trackedDeco,
};
},
},
};
</script>

View File

@@ -0,0 +1,94 @@
<template>
<canvas ref="canvas" :height="height" style="display:block;flex:1"></canvas>
</template>
<script>
export default {
props: {
data: { type: Array, default: () => [] },
max: { type: Number, default: 100 },
height: { type: Number, default: 22 },
color: { type: String, default: '#00ff88' },
},
mounted() {
this.resizeObserver = new ResizeObserver(() => this.draw());
this.resizeObserver.observe(this.$refs.canvas.parentElement);
this.$nextTick(() => this.draw());
},
beforeUnmount() {
if (this.resizeObserver) this.resizeObserver.disconnect();
},
watch: {
data: { handler() { this.draw(); }, deep: true },
color() { this.draw(); },
},
methods: {
draw() {
const c = this.$refs.canvas;
if (!c) return;
const parent = c.parentElement;
if (!parent) return;
// Size canvas to fill available flex space
const siblings = [...parent.children].filter(el => el !== c);
const gap = parseFloat(getComputedStyle(parent).gap) || 8;
const usedW = siblings.reduce((s, el) => s + el.offsetWidth, 0);
const w = Math.max(40, Math.floor(parent.offsetWidth - usedW - gap * siblings.length));
c.width = w;
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
ctx.clearRect(0, 0, W, H);
const pts = this.data.slice(-60);
if (pts.length < 2) return;
const sx = W / (pts.length - 1);
const pad = 2;
const uh = H - pad * 2;
const maxVal = this.max || 1;
const tx = i => i * sx;
const ty = v => pad + uh - Math.min(v, maxVal) / maxVal * uh;
const color = this.color;
// gradient fill
const g = ctx.createLinearGradient(0, 0, 0, H);
g.addColorStop(0, color + '22');
g.addColorStop(1, color + '03');
ctx.beginPath();
ctx.moveTo(tx(0), ty(pts[0]));
for (let i = 1; i < pts.length; i++) ctx.lineTo(tx(i), ty(pts[i]));
ctx.lineTo(tx(pts.length - 1), H);
ctx.lineTo(0, H);
ctx.closePath();
ctx.fillStyle = g;
ctx.fill();
// line
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = 5;
ctx.beginPath();
ctx.moveTo(tx(0), ty(pts[0]));
for (let i = 1; i < pts.length; i++) ctx.lineTo(tx(i), ty(pts[i]));
ctx.strokeStyle = color;
ctx.lineWidth = 1.5;
ctx.lineJoin = 'round';
ctx.stroke();
ctx.restore();
// tip dot
const lx = tx(pts.length - 1);
const ly = ty(pts[pts.length - 1]);
ctx.save();
ctx.shadowColor = color;
ctx.shadowBlur = 8;
ctx.beginPath();
ctx.arc(lx, ly, 2.5, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
},
},
};
</script>

View File

@@ -1,5 +1,5 @@
import Favico from "favico.js";
import { store } from "./store.js";
import { store, pushHistory } from "./store.js";
// prettier-ignore
const fetchAll = () =>
@@ -89,6 +89,7 @@ class GlancesStatsService {
this.data = data;
store.data = data;
store.status = "SUCCESS";
pushHistory(data.stats);
})
.catch((error) => {
console.log(error);

View File

@@ -1,8 +1,48 @@
import { reactive } from "vue";
const MAX_HISTORY = 120;
export const store = reactive({
args: undefined,
config: undefined,
data: undefined,
status: "IDLE",
history: {
cpu: [],
mem: [],
swap: [],
load: [],
gpu: [],
},
});
export function pushHistory(stats) {
if (!stats) return;
if (stats.cpu != null && stats.cpu.total != null) {
store.history.cpu.push(stats.cpu.total);
if (store.history.cpu.length > MAX_HISTORY) store.history.cpu.shift();
}
if (stats.mem != null && stats.mem.percent != null) {
store.history.mem.push(stats.mem.percent);
if (store.history.mem.length > MAX_HISTORY) store.history.mem.shift();
}
if (stats.memswap != null && stats.memswap.percent != null) {
store.history.swap.push(stats.memswap.percent);
if (store.history.swap.length > MAX_HISTORY) store.history.swap.shift();
}
if (stats.load != null && stats.load.min1 != null) {
store.history.load.push(stats.load.min1);
if (store.history.load.length > MAX_HISTORY) store.history.load.shift();
}
if (stats.gpu != null && Array.isArray(stats.gpu) && stats.gpu.length > 0) {
const meanProc =
stats.gpu.reduce((s, g) => s + (g.proc || 0), 0) / stats.gpu.length;
store.history.gpu.push(meanProc);
if (store.history.gpu.length > MAX_HISTORY) store.history.gpu.shift();
}
}

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -1,12 +1,15 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Glances</title>
<link rel="icon" type="image/x-icon" href="static/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
<script>
window.__GLANCES__ = {
'refresh-time': '{{ refresh_time }}'