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:
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user