add node_exporter host vitals + quickshell HUD

Prometheus node_exporter enabled on every host, plus a quickshell widget
(SUPER+CTRL+V on terra) to view live CPU/mem/disk/net/uptime without a
separate dashboard.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FHr5ug9pu8q4XPrRkFnzJ
This commit is contained in:
2026-08-22 20:50:16 +02:00
co-authored by Claude Sonnet 5
parent 60bef752cb
commit dc83e8c156
11 changed files with 978 additions and 0 deletions
@@ -0,0 +1,543 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import QtQuick
import QtQuick.Layouts
import QtQuick.Shapes
// Host vitals HUD — the "Slant" language (chamfered panel, trapezoid bevels,
// outward corner chunks, floating cap, slanted dividers) carried over from the
// sidebar and launcher V6, wrapped around this box's own node_exporter feed.
// Toggled with SUPER CTRL V.
Scope {
id: root
property bool active: false
function toggle() {
root.active = !root.active;
}
GlobalShortcut {
name: "vitals"
description: "Toggle the host vitals panel"
onPressed: root.toggle()
}
VitalsData {
id: vitals
active: root.active
}
PanelWindow {
id: win
visible: root.active
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
// Ignore the bars' exclusive zones so the backdrop covers the screen.
exclusionMode: ExclusionMode.Ignore
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
onVisibleChanged: {
if (visible)
keys.forceActiveFocus();
}
// Dim backdrop — click anywhere to dismiss.
Rectangle {
anchors.fill: parent
color: "#0A0A0C"
opacity: 0.5
MouseArea {
anchors.fill: parent
onClicked: root.active = false
}
}
Item {
id: keys
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
root.active = false;
event.accepted = true;
}
}
}
Item {
id: frame
anchors.centerIn: parent
width: 620
height: content.implicitHeight + 2 * 26
readonly property int chamfer: 18
readonly property int bevel: 4 // inward thickness of the bevel borders
readonly property int chunkThick: 8 // outward thickness of the heavy chunks
readonly property int chunkSlant: 12 // slant of their end pieces
readonly property int capSize: 10 // floating triangle cap
readonly property int smallChamfer: 6
// Chamfered panel — top-left / bottom-right cut, accent edge.
Shape {
id: panelShape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
fillColor: "#0F1012"
strokeColor: "#FFD063"
strokeWidth: 2
startX: frame.chamfer
startY: 0
PathLine { x: panelShape.width - frame.smallChamfer; y: 0 }
PathLine { x: panelShape.width; y: frame.smallChamfer }
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer }
PathLine { x: panelShape.width - frame.chamfer; y: panelShape.height }
PathLine { x: frame.smallChamfer; y: panelShape.height }
PathLine { x: 0; y: panelShape.height - frame.smallChamfer }
PathLine { x: 0; y: frame.chamfer }
PathLine { x: frame.chamfer; y: 0 }
}
// Thick top-left bevel accent.
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: 0
startY: frame.chamfer
PathLine { x: frame.chamfer; y: 0 }
PathLine { x: frame.chamfer + 2 * frame.bevel; y: 0 }
PathLine { x: 0; y: frame.chamfer + 2 * frame.bevel }
PathLine { x: 0; y: frame.chamfer }
}
// Thick bottom-right bevel accent.
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: panelShape.width
startY: panelShape.height - frame.chamfer
PathLine { x: panelShape.width - frame.chamfer; y: panelShape.height }
PathLine { x: panelShape.width - frame.chamfer - 2 * frame.bevel; y: panelShape.height }
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer - 2 * frame.bevel }
PathLine { x: panelShape.width; y: panelShape.height - frame.chamfer }
}
// Floating triangle cap in the bottom-right notch.
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: panelShape.width
startY: panelShape.height
PathLine { x: panelShape.width; y: panelShape.height - frame.capSize }
PathLine { x: panelShape.width - frame.capSize; y: panelShape.height }
PathLine { x: panelShape.width; y: panelShape.height }
}
// Heavy outward chunk wrapping the bottom-left corner.
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: panelShape.width / 3
startY: panelShape.height
PathLine { x: panelShape.width / 3 - frame.chunkSlant; y: panelShape.height + frame.chunkThick }
PathLine { x: frame.smallChamfer; y: panelShape.height + frame.chunkThick }
PathLine { x: -frame.chunkThick; y: panelShape.height - frame.smallChamfer }
PathLine { x: -frame.chunkThick; y: panelShape.height - panelShape.height / 7 + frame.chunkSlant }
PathLine { x: 0; y: panelShape.height - panelShape.height / 7 }
PathLine { x: 0; y: panelShape.height - frame.smallChamfer }
PathLine { x: frame.smallChamfer; y: panelShape.height }
PathLine { x: panelShape.width / 3; y: panelShape.height }
}
// Heavy outward chunk wrapping the top-right corner.
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: panelShape.width
startY: panelShape.height / 3
PathLine { x: panelShape.width + frame.chunkThick; y: panelShape.height / 3 - frame.chunkSlant }
PathLine { x: panelShape.width + frame.chunkThick; y: frame.smallChamfer }
PathLine { x: panelShape.width - frame.smallChamfer; y: -frame.chunkThick }
PathLine { x: panelShape.width - panelShape.width / 5 + frame.chunkSlant; y: -frame.chunkThick }
PathLine { x: panelShape.width - panelShape.width / 5; y: 0 }
PathLine { x: panelShape.width - frame.smallChamfer; y: 0 }
PathLine { x: panelShape.width; y: frame.smallChamfer }
PathLine { x: panelShape.width; y: panelShape.height / 3 }
}
}
// ── Content ────────────────────────────────────────────────────
ColumnLayout {
id: content
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: 26
anchors.leftMargin: 30
anchors.rightMargin: 26
spacing: 12
// Header — slash trio, host, uptime.
RowLayout {
Layout.fillWidth: true
spacing: 12
Shape {
id: slashes
implicitWidth: 26
implicitHeight: 22
Layout.alignment: Qt.AlignVCenter
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: 6
startY: 0
PathLine { x: 10; y: 0 }
PathLine { x: 4; y: slashes.height }
PathLine { x: 0; y: slashes.height }
PathLine { x: 6; y: 0 }
}
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: 15
startY: 0
PathLine { x: 19; y: 0 }
PathLine { x: 13; y: slashes.height }
PathLine { x: 9; y: slashes.height }
PathLine { x: 15; y: 0 }
}
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: 24
startY: 0
PathLine { x: 28; y: 0 }
PathLine { x: 22; y: slashes.height }
PathLine { x: 18; y: slashes.height }
PathLine { x: 24; y: 0 }
}
}
Text {
text: (vitals.host || "vitals").toUpperCase()
color: "#EEEEEE"
font.family: "Digital-7 Mono"
font.pointSize: 20
font.letterSpacing: 2
}
Item {
Layout.fillWidth: true
}
Text {
text: "UP"
color: "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
Text {
text: vitals.fmtUptime(vitals.uptime)
color: "#FFD063"
font.family: "Digital-7 Mono"
font.pointSize: 16
}
}
Slant {}
// Unreachable exporter — say so rather than drawing zeroes.
Text {
Layout.fillWidth: true
visible: vitals.failed
text: "NODE_EXPORTER UNREACHABLE ON :" + vitals.port
color: "#FF6B4A"
font.family: "Digital-7 Mono"
font.pointSize: 13
}
ColumnLayout {
Layout.fillWidth: true
visible: !vitals.failed
spacing: 12
Metric {
label: "CPU"
value: vitals.cpu
unknown: !vitals.ratesReady
warn: 0.9
readout: vitals.ratesReady ? Math.round(vitals.cpu * 100) + "%" : "--"
detail: "LOAD " + vitals.load1.toFixed(2) + " / " + vitals.load5.toFixed(2) + " / " + vitals.load15.toFixed(2)
aside: vitals.cpuThreads + "T " + vitals.fmtTemp(vitals.cpuTemp)
asideHot: vitals.cpuTemp >= 85
}
Metric {
label: "MEM"
value: vitals.memTotal > 0 ? vitals.memUsed / vitals.memTotal : 0
unknown: !vitals.ready
readout: vitals.memTotal > 0 ? Math.round(vitals.memUsed / vitals.memTotal * 100) + "%" : "--"
detail: vitals.fmtBytes(vitals.memUsed) + " / " + vitals.fmtBytes(vitals.memTotal)
aside: ""
}
// No GPU-busy counter exists in node_exporter, so the bar
// tracks power draw against the card's cap — a real
// reading, labelled for what it is rather than faked as
// utilisation.
Metric {
label: "GPU"
visible: isFinite(vitals.gpuTemp)
value: isFinite(vitals.gpuPower) && vitals.gpuPowerCap > 0 ? vitals.gpuPower / vitals.gpuPowerCap : 0
unknown: !isFinite(vitals.gpuPower)
warn: 0.9
readout: isFinite(vitals.gpuPower) ? Math.round(vitals.gpuPower) + "W" : "--"
detail: (vitals.gpuPowerCap > 0 ? "CAP " + Math.round(vitals.gpuPowerCap) + "W" : "") + (isFinite(vitals.gpuClock) ? " SCLK " + Math.round(vitals.gpuClock) + " MHZ" : "")
aside: vitals.fmtTemp(vitals.gpuTemp) + (isFinite(vitals.gpuHotspot) ? " / " + vitals.fmtTemp(vitals.gpuHotspot) : "")
asideHot: vitals.gpuHotspot >= 95
}
}
Slant {
visible: !vitals.failed
}
// Disks.
ColumnLayout {
Layout.fillWidth: true
visible: !vitals.failed
spacing: 6
Repeater {
model: vitals.disks
delegate: RowLayout {
id: diskRow
required property var modelData
readonly property real frac: diskRow.modelData.size > 0 ? diskRow.modelData.used / diskRow.modelData.size : 0
Layout.fillWidth: true
spacing: 12
Text {
Layout.preferredWidth: 96
text: diskRow.modelData.mount
color: "#7A7B7D"
font.pointSize: 9
elide: Text.ElideMiddle
}
VitalBar {
Layout.fillWidth: true
implicitHeight: 11
value: diskRow.frac
warn: 0.9
}
Text {
Layout.preferredWidth: 48
horizontalAlignment: Text.AlignRight
text: Math.round(diskRow.frac * 100) + "%"
color: diskRow.frac >= 0.9 ? "#FF6B4A" : "#EEEEEE"
font.family: "Digital-7 Mono"
font.pointSize: 13
}
Text {
Layout.preferredWidth: 104
horizontalAlignment: Text.AlignRight
text: vitals.fmtBytes(diskRow.modelData.size - diskRow.modelData.used) + " FREE"
color: "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
}
}
}
Slant {
visible: !vitals.failed
}
// Network.
RowLayout {
Layout.fillWidth: true
visible: !vitals.failed
spacing: 12
Text {
Layout.preferredWidth: 96
text: vitals.netIface || "NET"
color: "#7A7B7D"
font.pointSize: 9
}
Text {
text: "RX"
color: "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
Text {
text: vitals.fmtRate(vitals.netRx)
color: "#FFD063"
font.family: "Digital-7 Mono"
font.pointSize: 14
}
Item {
Layout.fillWidth: true
}
Text {
text: "TX"
color: "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
Text {
text: vitals.fmtRate(vitals.netTx)
color: "#FFD063"
font.family: "Digital-7 Mono"
font.pointSize: 14
// Right-align the TX readout against the panel edge the
// disk rows' "FREE" column already lines up with.
Layout.preferredWidth: 104
horizontalAlignment: Text.AlignRight
}
}
}
}
}
// ── Local pieces ───────────────────────────────────────────────────────
// A labelled bar with a readout, a sub-line and a right-hand aside.
component Metric: ColumnLayout {
id: metric
property string label: ""
property string readout: ""
property string detail: ""
property string aside: ""
property bool asideHot: false
property real value: 0
property real warn: 0.85
property bool unknown: false
Layout.fillWidth: true
spacing: 3
RowLayout {
Layout.fillWidth: true
spacing: 12
Text {
Layout.preferredWidth: 46
text: metric.label
color: "#FFD063"
font.family: "Digital-7 Mono"
font.pointSize: 15
font.letterSpacing: 1
}
VitalBar {
Layout.fillWidth: true
value: metric.value
warn: metric.warn
unknown: metric.unknown
}
Text {
Layout.preferredWidth: 54
horizontalAlignment: Text.AlignRight
text: metric.readout
color: "#EEEEEE"
font.family: "Digital-7 Mono"
font.pointSize: 16
}
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 58
spacing: 12
Text {
text: metric.detail
color: "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
Item {
Layout.fillWidth: true
}
Text {
visible: metric.aside.length > 0
text: metric.aside
color: metric.asideHot ? "#FF6B4A" : "#7A7B7D"
font.family: "Digital-7 Mono"
font.pointSize: 11
}
}
}
// Slanted divider — the launcher/sidebar motif.
component Slant: Item {
Layout.fillWidth: true
implicitHeight: 3
Shape {
id: divider
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeWidth: 0
fillColor: "#FFD063"
startX: 6
startY: 0
PathLine { x: divider.width; y: 0 }
PathLine { x: divider.width - 6; y: divider.height }
PathLine { x: 0; y: divider.height }
PathLine { x: 6; y: 0 }
}
}
}
}