pragma ComponentBehavior: Bound import QtQuick import "../services" as S import NovaStats as NS // One section of the systemd applet: a header with title, aggregate counts, // state chip; an auto-expanded list of failed units (hidden when empty); a // lazy-loaded, collapsed-by-default list of running units. Column { id: root property color accentColor // SSH target string (for remote hosts/their containers); "" for local-side entries. property string hostTarget: "" property string machineName: "" property string title: "" property string marker: "" property string systemState: "unknown" property int runningCount: 0 property int totalCount: 0 property var failedUnits: [] property var runningUnits: [] property string errorKind: "" property string errorReason: "" property int lastSeen: 0 property var containers: [] property int depth: 0 signal contentResized onHeightChanged: root.contentResized() width: parent?.width ?? 0 property bool _runningExpanded: false readonly property int _failedCount: (failedUnits ?? []).length // Header Item { width: root.width height: 32 Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: _hdrHover.hovered ? NS.ThemeService.base02 : "transparent" radius: NS.ThemeService.radius z: -1 } HoverHandler { id: _hdrHover } Text { anchors.left: parent.left anchors.leftMargin: 12 anchors.right: _stateChip.left anchors.rightMargin: 6 anchors.verticalCenter: parent.verticalCenter text: root.title + (root.marker !== "" ? " " + root.marker : "") color: NS.ThemeService.base05 font.pixelSize: NS.ThemeService.fontSize font.family: NS.ThemeService.fontFamily elide: Text.ElideRight } // Aggregate counts: "n running, m/total failed" or "n running" if no failures. Text { id: _counts anchors.right: _stateChip.left anchors.rightMargin: 8 anchors.verticalCenter: parent.verticalCenter text: { if (root.totalCount === 0) return ""; const r = root.runningCount + " running"; if (root._failedCount > 0) return r + ", " + root._failedCount + "/" + root.totalCount + " failed"; return r; } color: NS.ThemeService.base04 font.pixelSize: NS.ThemeService.fontSize - 3 font.family: NS.ThemeService.fontFamily } Rectangle { id: _stateChip anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter visible: root.systemState !== "unknown" color: { const st = root.systemState; if (root.errorKind === "permanent") return NS.ThemeService.base08; if (root.errorKind === "transient") return NS.ThemeService.base04; if (st === "running") return NS.ThemeService.base0B; if (st === "degraded") return NS.ThemeService.base0A; if (st === "pending") return NS.ThemeService.base04; if (st === "unreachable") return NS.ThemeService.base04; return NS.ThemeService.base08; } opacity: 0.85 radius: 3 width: _stateLbl.width + 8 height: 14 Text { id: _stateLbl anchors.centerIn: parent text: root.systemState color: NS.ThemeService.base00 font.pixelSize: NS.ThemeService.fontSize - 3 font.family: NS.ThemeService.fontFamily } } } // Error / last-seen line for remote machines. Hidden when there's no // error and the machine is reachable (lastSeen > 0 with no errorKind). Item { visible: root.errorKind !== "" || (root.lastSeen > 0 && root.systemState === "unreachable") width: root.width height: visible ? 22 : 0 Text { anchors.left: parent.left anchors.leftMargin: 24 anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: { const ago = root.lastSeen > 0 ? " (last seen " + _agoString(root.lastSeen) + ")" : ""; if (root.errorReason !== "") return root.errorReason + ago; return ago.replace(/^\s+/, ""); } color: root.errorKind === "permanent" ? NS.ThemeService.base08 : NS.ThemeService.base03 font.pixelSize: NS.ThemeService.fontSize - 3 font.family: NS.ThemeService.fontFamily elide: Text.ElideRight } } function _agoString(unix) { const now = Math.floor(Date.now() / 1000); const d = Math.max(0, now - unix); if (d < 60) return d + "s ago"; if (d < 3600) return Math.floor(d / 60) + "m ago"; if (d < 86400) return Math.floor(d / 3600) + "h ago"; return Math.floor(d / 86400) + "d ago"; } // Failed units (auto-expanded; entire block hidden when there are none). Repeater { model: root._failedCount > 0 ? root.failedUnits : [] delegate: SystemdUnitRow { required property var modelData unitName: modelData.name description: modelData.description ?? "" subState: modelData.subState ?? "" scope: modelData.scope ?? "system" hostTarget: root.hostTarget machineName: root.machineName accentColor: root.accentColor } } // Running units toggle row (only meaningful when there are running units to show). Item { visible: (root.runningUnits ?? []).length > 0 width: root.width height: 26 Rectangle { anchors.fill: parent anchors.leftMargin: 4 anchors.rightMargin: 4 color: _runHdrHover.hovered ? NS.ThemeService.base02 : "transparent" radius: NS.ThemeService.radius z: -1 } HoverHandler { id: _runHdrHover } Text { anchors.left: parent.left anchors.leftMargin: 24 anchors.verticalCenter: parent.verticalCenter text: "running units (" + (root.runningUnits ?? []).length + ")" color: NS.ThemeService.base04 font.pixelSize: NS.ThemeService.fontSize - 2 font.family: NS.ThemeService.fontFamily font.letterSpacing: 1 } Text { anchors.right: parent.right anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter text: root._runningExpanded ? "" : "" color: NS.ThemeService.base04 font.pixelSize: NS.ThemeService.fontSize - 2 font.family: NS.ThemeService.iconFontFamily } TapHandler { onTapped: root._runningExpanded = !root._runningExpanded } } // Lazy-loaded running units list. Repeater materializes rows only when the // model is non-empty, so feeding `[]` while collapsed avoids per-row cost. Repeater { model: root._runningExpanded ? root.runningUnits : [] delegate: SystemdUnitRow { required property var modelData unitName: modelData.name description: modelData.description ?? "" subState: modelData.subState ?? "" scope: modelData.scope ?? "system" hostTarget: root.hostTarget machineName: root.machineName accentColor: root.accentColor } } // Nested containers running on this machine. Indented to convey hierarchy. // QML disallows direct recursive component use, so we load the same .qml // by source path through a Loader to break the static graph. Repeater { model: root.containers ?? [] delegate: Item { id: _childWrap required property var modelData width: root.width height: _childLoader.height + 4 Loader { id: _childLoader anchors.left: parent.left anchors.leftMargin: 16 width: parent.width - 16 source: "SystemdMachineSection.qml" onLoaded: { item.accentColor = root.accentColor; item.hostTarget = root.hostTarget; item.machineName = _childWrap.modelData.name; item.title = _childWrap.modelData.name; item.marker = _childWrap.modelData.marker ?? ""; item.systemState = _childWrap.modelData.systemState ?? "unknown"; item.runningCount = _childWrap.modelData.runningCount ?? 0; item.totalCount = _childWrap.modelData.totalCount ?? 0; item.failedUnits = _childWrap.modelData.failedUnits ?? []; item.runningUnits = _childWrap.modelData.runningUnits ?? []; item.errorKind = _childWrap.modelData.errorKind ?? ""; item.errorReason = _childWrap.modelData.errorReason ?? ""; item.lastSeen = _childWrap.modelData.lastSeen ?? 0; item.containers = _childWrap.modelData.containers ?? []; item.depth = root.depth + 1; item.contentResized.connect(root.contentResized); } } } } }