pragma ComponentBehavior: Bound import QtQuick import "sysinfo.js" as Sys Item { id: root required property Theme theme readonly property Flickable scrollView: contentScroll required property Resources resourceService implicitHeight: contentColumn.implicitHeight height: parent ? parent.height : implicitHeight // service handle; identity facts and the live issue ARRAY come straight // off Resources.qml (no per-frame JSON parse). readonly property var rs: root.resourceService readonly property var identity: rs ? rs.identity : ({}) // touch the inputs so the band re-evaluates when the slow/rare loops // mutate them; issues() itself is a plain function call. property var issueList: [] readonly property var currentIssues: { if (!rs) return []; rs.failedUnits; rs.incidents; rs.cpuTemperatureC; rs.diskUsage; rs.updateCount; rs.memoryUsage; rs.swapGrowing; rs.loadOne; rs.healthPercentage; return rs.issues(); } onCurrentIssuesChanged: { now = Date.now(); if (JSON.stringify(currentIssues) !== JSON.stringify(issueList)) issueList = currentIssues; } property real now: Date.now() Timer { interval: 30000 running: true repeat: true onTriggered: root.now = Date.now() } // popoutOpen is a WRITE contract: it gates the slow + rare loops. Component.onCompleted: { if (rs) rs.popoutOpen = true; } Component.onDestruction: { if (rs) rs.popoutOpen = false; } Flickable { id: contentScroll anchors.fill: parent contentHeight: contentColumn.implicitHeight clip: true boundsBehavior: Flickable.StopAtBounds function revealItem(item) { const point = item.mapToItem(contentColumn, 0, 0); if (point.y < contentY) contentY = point.y; else if (point.y + item.height > contentY + height) contentY = Math.min(point.y + item.height - height, Math.max(0, contentHeight - height)); } Column { id: contentColumn width: contentScroll.width spacing: 10 // ---- two rounded cards: identity | live, split on the golden ratio // (live gets the larger ~0.618 share, that's where the live data lives) ---- Row { id: cardRow readonly property real avail: Math.max(1, width - spacing) readonly property real machineW: avail * 0.382 readonly property real liveW: avail * 0.618 width: parent.width height: 292 spacing: 10 // ===== MACHINE IDENTITY ===== PopupCard { theme: root.theme padding: 0 width: cardRow.machineW height: cardRow.height Column { anchors.fill: parent anchors.margins: 14 spacing: 6 Row { spacing: 8 Text { text: "" color: root.theme ? root.theme.ui.accent : "white" font.family: "Symbols Nerd Font Mono" font.pixelSize: 15 } NuguText { theme: root.theme variant: "title" text: "machine" } } IdentityRow { theme: root.theme glyph: "" value: root.identity.host || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.model || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.distro || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.kernel || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.wm || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.packages ? `${root.identity.packages} packages` : "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.shell || "—" } IdentityRow { theme: root.theme glyph: "" value: root.identity.uptime || "—" } IdentityRow { theme: root.theme glyph: "" // battery row hidden entirely when there is no laptop battery visible: root.rs && root.rs.isLaptopBattery value: { if (!root.rs) return "—"; const p = root.rs.batteryPercent; const st = root.rs.batteryStatus; const h = root.rs.healthPercentage; let s = (p === null || p === undefined) ? "battery n/a" : `battery ${p}%`; if (st) s += ` · ${st}`; if (h !== null && h !== undefined) s += ` · health ${Math.round(Number(h))}%`; return s; } } } } // ===== LIVE STATS ===== PopupCard { theme: root.theme padding: 0 width: cardRow.liveW height: cardRow.height Column { anchors.fill: parent anchors.margins: 14 spacing: 8 Row { spacing: 8 Text { text: "" color: root.theme ? root.theme.ui.accent : "white" font.family: "Symbols Nerd Font Mono" font.pixelSize: 15 } NuguText { theme: root.theme variant: "title" text: "live" } } MetricRow { theme: root.theme label: "cpu" value: root.rs ? `${Math.round(root.rs.cpuUsage * 100)}%` : "—" fraction: root.rs ? root.rs.cpuUsage : 0 barColor: root.theme ? root.theme.ui.accent : "#d86dd8" ring: root.rs ? root.rs.cpuRing : [] sparkScaleMax: 1 } MetricRow { theme: root.theme label: "mem" value: root.rs ? `${Math.round(root.rs.memoryUsage * 100)}% · ${Sys.formatBytes(Math.max(0, root.rs.memTotal - root.rs.memAvailable))}` : "—" fraction: root.rs ? root.rs.memoryUsage : 0 barColor: root.theme ? root.theme.ui.focus : "#9aabae" ring: root.rs ? root.rs.memRing : [] sparkScaleMax: 1 } MetricRow { theme: root.theme label: "swap" value: { if (!root.rs || root.rs.swapTotal <= 0) return "off"; return `${Math.round(root.rs.swapUsage * 100)}% · ${Sys.formatBytes(Math.max(0, root.rs.swapTotal - root.rs.swapFree))}`; } fraction: root.rs ? root.rs.swapUsage : 0 barColor: root.theme ? root.theme.warning : "#f0ee69" showSpark: false ring: [] } MetricRow { theme: root.theme label: "temp" value: { if (!root.rs) return "n/a"; const t = root.rs.cpuTemperatureC; const f = root.rs.fanRpm; const ts = (t === null || t === undefined) ? "n/a" : `${Math.round(Number(t))}°C`; const fs = (f === null || f === undefined) ? "" : ` · ${Math.round(Number(f))} rpm`; return ts + fs; } fraction: { if (!root.rs) return 0; const t = root.rs.cpuTemperatureC; if (t === null || t === undefined) return 0; return Math.max(0, Math.min(1, Number(t) / Math.max(1, root.rs.tempHighC))); } barColor: root.theme ? root.theme.error : "#ec6376" // sparkline hidden when the sensor is unavailable (null) showSpark: root.rs && root.rs.cpuTemperatureC !== null ring: root.rs ? root.rs.tempRing : [] sparkScaleMax: root.rs ? Math.max(1, root.rs.tempHighC) : 1 } MetricRow { theme: root.theme label: "net" value: root.rs ? `↓ ${Sys.formatRate(root.rs.networkRxRate)} ↑ ${Sys.formatRate(root.rs.networkTxRate)}` : "—" // net/disk have no natural ceiling: scale against the ring's own max fraction: { if (!root.rs) return 0; const ring = root.rs.netRing || []; let m = 0; for (let i = 0; i < ring.length; i++) m = Math.max(m, Number(ring[i]) || 0); const cur = root.rs.networkRxRate + root.rs.networkTxRate; return m > 0 ? Math.min(1, cur / m) : 0; } // directional spark: ↑ upload (accent) above the line, // ↓ download (info) below; fill-bar stays combined. barColor: root.theme ? root.theme.ui.accent : "#d86dd8" splitSpark: true ring: root.rs ? root.rs.netTxRing : [] ringDown: root.rs ? root.rs.netRxRing : [] barColorDown: root.theme ? root.theme.info : "#3ec5e3" } MetricRow { theme: root.theme label: "disk" value: root.rs ? `R ${Sys.formatRate(root.rs.diskReadRate)} W ${Sys.formatRate(root.rs.diskWriteRate)}` : "—" fraction: { if (!root.rs) return 0; const ring = root.rs.diskRing || []; let m = 0; for (let i = 0; i < ring.length; i++) m = Math.max(m, Number(ring[i]) || 0); const cur = root.rs.diskReadRate + root.rs.diskWriteRate; return m > 0 ? Math.min(1, cur / m) : 0; } // directional spark: R read (accent) above the line, // W write (info) below; fill-bar stays combined. barColor: root.theme ? root.theme.ui.accent : "#d86dd8" splitSpark: true ring: root.rs ? root.rs.diskReadRing : [] ringDown: root.rs ? root.rs.diskWriteRing : [] barColorDown: root.theme ? root.theme.info : "#3ec5e3" } NuguText { theme: root.theme variant: "meta" text: root.rs ? `load ${root.rs.loadOne.toFixed(2)} · ${root.rs.loadFive.toFixed(2)} · ${root.rs.loadFifteen.toFixed(2)} (${root.rs.cores} cores)` : "load —" } } } } // ---- compact top-processes row (chips) ---- Flow { width: parent.width spacing: 8 Repeater { model: root.rs ? root.rs.processes : [] delegate: PopupBadge { id: procChip required property var modelData theme: root.theme text: `${String(modelData.name || "?")} · ${Number(modelData.cpu || 0).toFixed(0)}%` role: "normal" } } } // Current conditions and historical events have different meanings. Repeater { model: [false, true] delegate: Column { id: issueGroup required property bool modelData readonly property var entries: root.issueList.filter(issue => !!issue.context === modelData) width: contentColumn.width spacing: 8 Row { width: parent.width spacing: 12 NuguText { theme: root.theme variant: "title" text: issueGroup.modelData ? "incident history" : "needs attention" } NuguText { theme: root.theme variant: "meta" color: root.theme.ui.minor text: issueGroup.modelData ? issueGroup.entries.reduce((count, issue) => count + (issue.events ? issue.events.length : 1), 0) + " recorded" : issueGroup.entries.length + (issueGroup.entries.length === 1 ? " problem" : " problems") } PopupButton { visible: issueGroup.modelData theme: root.theme icon: "" showText: false width: 24 height: 24 accessibleName: "incident coverage" tooltip: "recorded crashes and oom kills · current boot · last 24 hours · latest 100\nnot every app exit produces a record" } } NuguText { visible: issueGroup.entries.length === 0 || issueGroup.modelData && root.rs && root.rs.incidentStatus !== "ready" theme: root.theme width: parent.width wrapMode: Text.Wrap variant: "meta" text: issueGroup.modelData ? (root.rs && root.rs.incidentStatus === "ready" ? "no recent crashes or oom kills recorded" : "incident history unavailable or incomplete") : "disk · memory · battery · updates ok" } Repeater { model: issueGroup.entries delegate: SystemIssueRow { id: issueRow required property var modelData theme: root.theme width: issueGroup.width issue: modelData now: root.now pending: root.rs && root.rs.agentPending && root.rs.agentIssueId === modelData.id launchBusy: root.rs && root.rs.agentPending launchError: root.rs && root.rs.agentIssueId === modelData.id ? root.rs.agentError : "" onFocusEntered: Qt.callLater(contentScroll.revealItem, issueRow) onOpenRequested: if (root.rs) root.rs.runIssueAction(modelData.id) onAgentRequested: if (root.rs) root.rs.runIssueAgent(modelData.id) } } } } } } }