Brings in panama-doctor (a 25-check diagnostic with fixture-backed
contracts), a Health service, a System Health page replacing Startup &
Services, and a bar indicator that stays absent until something is
actually degraded. All seven of its contracts pass on the merge.
Three things needed resolving rather than accepting:
The branch predates the debranding, so its user-visible strings still
named the product -- "Panama desktop is healthy", "Restart Panama",
"Panama tools". Rewritten to say the same thing without the name, which
is what the rest of the app now does.
Its Fedora hand-off card was a single button calling openGnomePanel
("network") under a subtitle naming five subjects. Main had already
replaced that with a row per subject, each opening the panel that owns
it, so those rows are ported into HealthPage instead. Printers and
online accounts stay on Network & Devices with the rest of the network
hardware.
That broke its own assertion, which matched the literal
openGnomePanel("network") string. Rewritten rather than reverted: it now
checks the boundary card exists and that every panel named in HealthPage
is one openGnomePanel actually allows, since a name outside the
allow-list opens nothing at all. Verified it catches a plausible-looking
wrong name.
SettingsShell and SettingsSidebar conflicted because both sides added
pages; resolved as the union, keeping its System Health page and live
footer alongside main's Mouse & Touchpad, Privacy & Security, Region &
Language and Online Accounts.
Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
424 lines
16 KiB
QML
424 lines
16 KiB
QML
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")
|
|
}
|
|
}
|
|
}
|