import QtQuick import qs.config import qs.services SettingsPage { id: root objectName: "system-health-page" title: "System Health" lede: "Checks the parts of the desktop this app owns, and explains what needs attention." property var pendingConfirmation: null property string instructionTarget: "" readonly property var issueChecks: Health.checks.filter(check => check.status === "warning" || check.status === "error") readonly property var groups: [ { group: "desktop-foundation", title: "Desktop foundation", subtitle: "Compositor, shell, portals, wallpaper, idle policy, and launcher." }, { group: "input-media", title: "Input & media", subtitle: "Sound, clipboard, capture, OCR, and display controls." }, { group: "integrations", title: "Integrations", subtitle: "Only configured integrations affect health." }, { group: "panama-tools", title: "Desktop tools", subtitle: "Tracked links, launcher commands, apps, and inhibitors." } ] readonly property var applicationTargets: ({ "integration.nextcloud": "nextcloud", "integration.rustdesk": "rustdesk", "integration.kdeconnect": "kdeconnect", "integration.bluebubbles": "bluebubbles" }) function statusLabel(status: string): string { if (status === "ok") return "Healthy"; if (status === "warning") return "Needs attention"; if (status === "error") return "Action required"; return "Not set up"; } function checksForGroup(group: string): var { return Health.checks.filter(check => check.group === group && (check.status === "ok" || check.status === "unconfigured")); } function handleAction(check: var): void { if (!check || !check.action) return; if (check.action.kind === "open") { if (check.action.target) { ShellState.openSettings(check.action.target); return; } const application = root.applicationTargets[check.id]; if (application) SystemSettings.openApplication(application); return; } if (check.action.kind === "instructions") { root.instructionTarget = check.action.target || ""; return; } if (check.action.kind !== "repair") return; if (check.action.confirm) { root.pendingConfirmation = check; return; } Health.repair(check.id, false); } function confirmRepair(): void { const check = root.pendingConfirmation; root.pendingConfirmation = null; if (check) Health.repair(check.id, false); } function descendants(item: var, prefix: string): var { let matches = []; if (!item) return matches; if (String(item.objectName || "").indexOf(prefix) === 0) matches.push(item); for (const child of item.children || []) matches = matches.concat(root.descendants(child, prefix)); return matches; } function activateRenderedAction(id: string): bool { const suffix = `:${id}`; const rows = root.descendants(root, "health-check-row:").filter(row => row.visible && String(row.objectName).endsWith(suffix)); return rows.length === 1 && rows[0].activateAction(); } function renderedFocusChain(): var { // Repeater delegates enter Qt's tab chain lazily after their enabled // binding changes at the end of a scan. Touch each rendered action's // real next-focus link before traversing from the first hero control. const renderedActions = root.descendants(root, "health-row-action:").filter(item => item.visible && item.enabled && item.activeFocusOnTab); for (const action of renderedActions) action.nextItemInFocusChain(true); const starts = root.descendants(root, "health-copy-report-button").filter(item => item.visible && item.enabled); if (starts.length !== 1) return []; const names = []; const start = starts[0]; let current = start; for (let index = 0; index < 128; index++) { const name = String(current.objectName || ""); if (name !== "" && names.indexOf(name) < 0) names.push(name); current = current.nextItemInFocusChain(true); if (!current || current === start) break; } return names; } // Deterministic, read-only fixture seam used by the offscreen contract. function uiDiagnostics(): var { const summaries = root.descendants(root, "health-summary"); const rows = root.descendants(root, "health-check-row:").filter(row => row.visible); const checkingLabels = root.descendants(root, "health-checking-label").filter(label => label.visible); const confirmationSheets = root.descendants(root, "health-confirmation-sheet:").filter(sheet => sheet.visible); const emptyGroups = root.descendants(root, "health-empty-group:").filter(label => label.visible); return { renderedRows: rows.map(row => { const objectName = String(row.objectName); const parts = objectName.split(":"); const statusTexts = root.descendants(row, "health-status-text:").filter(text => text.visible); return { objectName: objectName, id: parts.slice(2).join(":"), section: parts[1], statusText: statusTexts.length === 1 ? statusTexts[0].text : "" }; }), summaryHeight: summaries.length > 0 ? summaries[0].height : 0, rowHeights: rows.map(row => row.height), checking: Health.busy, checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "", focusChain: root.renderedFocusChain(), activatedRows: rows.filter(row => row.actionActivationCount > 0).map(row => String(row.objectName)), emptyQuietGroups: emptyGroups.map(label => String(label.objectName).slice("health-empty-group:".length)), confirmationVisible: confirmationSheets.length === 1, confirmationId: confirmationSheets.length === 1 ? String(confirmationSheets[0].objectName).slice("health-confirmation-sheet:".length) : "" }; } Component.onCompleted: Health.refresh() header: Component { Rectangle { objectName: `health-confirmation-sheet:${root.pendingConfirmation ? root.pendingConfirmation.id : ""}` visible: root.pendingConfirmation !== null implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0 radius: Theme.cardRadius color: Theme.mix(Theme.bgPanel, Theme.warn, 0.1) border.width: 1 border.color: Theme.alpha(Theme.warn, 0.34) Row { id: confirmRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: 16 spacing: 14 Column { width: parent.width - cancelButton.width - repairButton.width - 28 anchors.verticalCenter: parent.verticalCenter spacing: 3 Text { width: parent.width text: root.pendingConfirmation ? `${root.pendingConfirmation.action.label}?` : "Restart the shell?" color: Theme.fg font.family: Theme.fontFamily font.pixelSize: Theme.fontSize font.weight: Font.DemiBold } Text { width: parent.width text: "The desktop chrome will disappear briefly and return when Quickshell restarts." color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } } SettingsButton { id: cancelButton text: "Cancel" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: root.pendingConfirmation = null Keys.onReturnPressed: root.pendingConfirmation = null Keys.onSpacePressed: root.pendingConfirmation = null } SettingsButton { id: repairButton text: root.pendingConfirmation ? root.pendingConfirmation.action.label : "Restart the shell" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: root.confirmRepair() Keys.onReturnPressed: root.confirmRepair() Keys.onSpacePressed: root.confirmRepair() } } } } HealthSummary {} SettingsCard { visible: root.issueChecks.length > 0 title: Health.status === "error" ? "Action required" : "Needs attention" subtitle: Health.status === "error" ? "Resolve these items first. Healthy systems remain listed below." : "Nothing here prevents you from using the desktop." Item { width: parent.width implicitHeight: issueRows.implicitHeight Rectangle { anchors.left: parent.left anchors.leftMargin: 4 anchors.top: parent.top anchors.topMargin: 25 anchors.bottom: parent.bottom anchors.bottomMargin: 25 width: 1 visible: root.issueChecks.length > 1 color: Theme.alpha(Health.status === "error" ? Theme.danger : Theme.warn, 0.44) } Column { id: issueRows width: parent.width Repeater { id: issueRepeater model: root.issueChecks HealthCheckRow { required property var modelData required property int index width: issueRows.width check: modelData issue: true divider: index < issueRepeater.count - 1 onActionRequested: check => root.handleAction(check) } } } } } SettingsCard { visible: root.instructionTarget === "ddc-permissions" title: "External monitor brightness" subtitle: "The monitor reports DDC/CI support, but this session cannot reach the monitor bus." Item { width: parent.width implicitHeight: Math.max(instructionCopy.implicitHeight, doneButton.implicitHeight) + 8 Text { id: instructionCopy anchors.left: parent.left anchors.right: doneButton.left anchors.rightMargin: 18 anchors.verticalCenter: parent.verticalCenter text: "Reload the installed udev rules and trigger the i2c-dev and DRM devices. Sign out and back in if monitor access is still unavailable." color: Theme.fgDim font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall wrapMode: Text.WordWrap } SettingsButton { id: doneButton anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter text: "Done" activeFocusOnTab: true border.width: activeFocus ? 2 : 1 border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08) onClicked: root.instructionTarget = "" Keys.onReturnPressed: root.instructionTarget = "" Keys.onSpacePressed: root.instructionTarget = "" } } } Grid { id: groupGrid width: parent.width columns: width >= 700 ? 2 : 1 columnSpacing: 16 rowSpacing: 16 Repeater { model: root.groups SettingsCard { required property var modelData readonly property var quietChecks: root.checksForGroup(modelData.group) width: groupGrid.columns === 2 ? (groupGrid.width - groupGrid.columnSpacing) / 2 : groupGrid.width title: modelData.title subtitle: modelData.subtitle Column { id: groupRows width: parent.width Text { objectName: `health-empty-group:${modelData.group}` width: parent.width visible: quietChecks.length === 0 text: "Items needing attention are listed above." color: Theme.fgMuted font.family: Theme.fontFamily font.pixelSize: Theme.fontSizeSmall topPadding: 8 bottomPadding: 8 wrapMode: Text.WordWrap } Repeater { id: groupRepeater model: quietChecks HealthCheckRow { required property var modelData required property int index width: groupRows.width check: modelData divider: index < groupRepeater.count - 1 onActionRequested: check => root.handleAction(check) } } } } } } SettingsCard { title: "Fedora system settings" subtitle: "Panels this app does not own, because they configure system services rather than the desktop. Each row opens the panel that actually owns it. Printers and online accounts live with the rest of the network hardware, on Network & Devices." // One row per subject rather than a single "Open GNOME Settings" // button. Naming five things and then opening the network panel // regardless reads as a broken button rather than a deliberate // hand-off, and left someone looking for printers to navigate once // GNOME Settings appeared on the wrong page. ActionRow { label: "Users" detail: "Accounts, passwords, and automatic login" action: "Open users" onTriggered: SystemSettings.openGnomePanel("system", "users") } ActionRow { label: "Sharing" detail: "Remote desktop, media sharing, and remote login" action: "Open sharing" onTriggered: SystemSettings.openGnomePanel("sharing") } ActionRow { label: "Colour profiles" detail: "ICC profiles for displays, printers, and scanners" action: "Open colour" onTriggered: SystemSettings.openGnomePanel("color") } ActionRow { label: "Digital wellbeing" detail: "Screen time and break reminders" action: "Open wellbeing" divider: false onTriggered: SystemSettings.openGnomePanel("wellbeing") } } }