Build the System Health settings page
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
import QtQuick
|
||||
import qs.config
|
||||
import qs.services
|
||||
|
||||
SettingsPage {
|
||||
id: root
|
||||
|
||||
objectName: "system-health-page"
|
||||
|
||||
title: "System Health"
|
||||
lede: "Panama checks the parts of your desktop it 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: "Panama 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 copyButtons = root.descendants(root, "health-copy-report-button");
|
||||
const refreshButtons = root.descendants(root, "health-refresh-button");
|
||||
const rowActions = root.descendants(root, "health-row-action:").filter(button => button.visible);
|
||||
const checkingLabels = root.descendants(root, "health-checking-label").filter(label => label.visible);
|
||||
return {
|
||||
issueIds: root.issueChecks.map(check => check.id),
|
||||
desktopIds: root.checksForGroup("desktop-foundation").map(check => check.id),
|
||||
integrationIds: root.checksForGroup("integrations").map(check => check.id),
|
||||
statusLabels: Health.checks.map(check => root.statusLabel(check.status)),
|
||||
summaryHeight: summaries.length > 0 ? summaries[0].height : 0,
|
||||
rowHeights: rows.map(row => row.height),
|
||||
checking: Health.busy,
|
||||
checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "",
|
||||
copyFocusable: copyButtons.length > 0 && copyButtons[0].activeFocusOnTab,
|
||||
refreshFocusable: refreshButtons.length > 0 && refreshButtons[0].activeFocusOnTab,
|
||||
rowActionFocusable: rowActions.length > 0 && rowActions[0].activeFocusOnTab,
|
||||
confirmationVisible: root.pendingConfirmation !== null,
|
||||
confirmationId: root.pendingConfirmation ? root.pendingConfirmation.id : ""
|
||||
};
|
||||
}
|
||||
|
||||
Component.onCompleted: Health.refresh()
|
||||
|
||||
header: Component {
|
||||
Rectangle {
|
||||
objectName: "health-confirmation-sheet"
|
||||
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 Panama?"
|
||||
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 Panama"
|
||||
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: "Panama can see DDC/CI support, but this session cannot access 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
|
||||
|
||||
width: groupGrid.columns === 2
|
||||
? (groupGrid.width - groupGrid.columnSpacing) / 2
|
||||
: groupGrid.width
|
||||
title: modelData.title
|
||||
subtitle: modelData.subtitle
|
||||
|
||||
Column {
|
||||
id: groupRows
|
||||
width: parent.width
|
||||
|
||||
Repeater {
|
||||
id: groupRepeater
|
||||
model: root.checksForGroup(modelData.group)
|
||||
|
||||
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: "Network accounts, printers, users, and Fedora updates remain owned by system tools."
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: 42
|
||||
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.right: gnomeSettingsButton.left
|
||||
anchors.rightMargin: 18
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Use GNOME Settings for the parts of the system Panama does not manage."
|
||||
color: Theme.fgDim
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
SettingsButton {
|
||||
id: gnomeSettingsButton
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Open GNOME Settings"
|
||||
activeFocusOnTab: true
|
||||
border.width: activeFocus ? 2 : 1
|
||||
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
|
||||
onClicked: SystemSettings.openGnomePanel("network")
|
||||
Keys.onReturnPressed: SystemSettings.openGnomePanel("network")
|
||||
Keys.onSpacePressed: SystemSettings.openGnomePanel("network")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user