Fix System Health ledger rendering

This commit is contained in:
Gabriel Brown
2026-08-18 10:08:12 -04:00
parent 3238623934
commit 8a0a09dbb4
4 changed files with 116 additions and 34 deletions
@@ -8,11 +8,22 @@ Item {
required property var check required property var check
property bool issue: false property bool issue: false
property bool divider: true property bool divider: true
property int actionActivationCount: 0
signal actionRequested(var check) signal actionRequested(var check)
objectName: `health-check-row:${root.check.id}:${root.issue ? "issue" : "ledger"}` objectName: `health-check-row:${root.issue ? "issue" : "quiet"}:${root.check.id}`
implicitHeight: 62 implicitHeight: 62
// The isolated contract uses the same signal path as a pointer or keyboard
// activation instead of calling HealthPage's action handler directly.
function activateAction(): bool {
if (!actionButton.visible || !actionButton.enabled)
return false;
root.actionActivationCount += 1;
actionButton.clicked();
return true;
}
function statusLabel(status: string): string { function statusLabel(status: string): string {
if (status === "ok") return "Healthy"; if (status === "ok") return "Healthy";
if (status === "warning") return "Needs attention"; if (status === "warning") return "Needs attention";
@@ -78,7 +89,7 @@ Item {
spacing: 12 spacing: 12
Text { Text {
objectName: `health-status-text:${root.check.id}:${root.issue ? "issue" : "ledger"}` objectName: `health-status-text:${root.issue ? "issue" : "quiet"}:${root.check.id}`
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: root.statusLabel(root.check.status) text: root.statusLabel(root.check.status)
color: root.statusColor(root.check.status) color: root.statusColor(root.check.status)
@@ -89,7 +100,7 @@ Item {
SettingsButton { SettingsButton {
id: actionButton id: actionButton
objectName: `health-row-action:${root.check.id}:${root.issue ? "issue" : "ledger"}` objectName: `health-row-action:${root.issue ? "issue" : "quiet"}:${root.check.id}`
visible: root.check.action !== undefined visible: root.check.action !== undefined
text: Health.repairingId === root.check.id ? "Working…" : (root.check.action?.label ?? "") text: Health.repairingId === root.check.id ? "Working…" : (root.check.action?.label ?? "")
enabled: visible && Health.repairingId !== root.check.id && !Health.busy enabled: visible && Health.repairingId !== root.check.id && !Health.busy
@@ -51,7 +51,8 @@ SettingsPage {
} }
function checksForGroup(group: string): var { function checksForGroup(group: string): var {
return Health.checks.filter(check => check.group === group); return Health.checks.filter(check => check.group === group
&& (check.status === "ok" || check.status === "unconfigured"));
} }
function handleAction(check: var): void { function handleAction(check: var): void {
@@ -101,28 +102,70 @@ SettingsPage {
return matches; 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. // Deterministic, read-only fixture seam used by the offscreen contract.
function uiDiagnostics(): var { function uiDiagnostics(): var {
const summaries = root.descendants(root, "health-summary"); const summaries = root.descendants(root, "health-summary");
const rows = root.descendants(root, "health-check-row:").filter(row => row.visible); 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); 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 { return {
issueIds: root.issueChecks.map(check => check.id), renderedRows: rows.map(row => {
desktopIds: root.checksForGroup("desktop-foundation").map(check => check.id), const objectName = String(row.objectName);
integrationIds: root.checksForGroup("integrations").map(check => check.id), const parts = objectName.split(":");
statusLabels: Health.checks.map(check => root.statusLabel(check.status)), 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, summaryHeight: summaries.length > 0 ? summaries[0].height : 0,
rowHeights: rows.map(row => row.height), rowHeights: rows.map(row => row.height),
checking: Health.busy, checking: Health.busy,
checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "", checkingText: checkingLabels.length > 0 ? checkingLabels[0].text : "",
copyFocusable: copyButtons.length > 0 && copyButtons[0].activeFocusOnTab, focusChain: root.renderedFocusChain(),
refreshFocusable: refreshButtons.length > 0 && refreshButtons[0].activeFocusOnTab, activatedRows: rows.filter(row => row.actionActivationCount > 0).map(row => String(row.objectName)),
rowActionFocusable: rowActions.length > 0 && rowActions[0].activeFocusOnTab, emptyQuietGroups: emptyGroups.map(label => String(label.objectName).slice("health-empty-group:".length)),
confirmationVisible: root.pendingConfirmation !== null, confirmationVisible: confirmationSheets.length === 1,
confirmationId: root.pendingConfirmation ? root.pendingConfirmation.id : "" confirmationId: confirmationSheets.length === 1
? String(confirmationSheets[0].objectName).slice("health-confirmation-sheet:".length)
: ""
}; };
} }
@@ -130,7 +173,7 @@ SettingsPage {
header: Component { header: Component {
Rectangle { Rectangle {
objectName: "health-confirmation-sheet" objectName: `health-confirmation-sheet:${root.pendingConfirmation ? root.pendingConfirmation.id : ""}`
visible: root.pendingConfirmation !== null visible: root.pendingConfirmation !== null
implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0 implicitHeight: visible ? confirmRow.implicitHeight + 28 : 0
radius: Theme.cardRadius radius: Theme.cardRadius
@@ -295,6 +338,7 @@ SettingsPage {
SettingsCard { SettingsCard {
required property var modelData required property var modelData
readonly property var quietChecks: root.checksForGroup(modelData.group)
width: groupGrid.columns === 2 width: groupGrid.columns === 2
? (groupGrid.width - groupGrid.columnSpacing) / 2 ? (groupGrid.width - groupGrid.columnSpacing) / 2
@@ -306,9 +350,22 @@ SettingsPage {
id: groupRows id: groupRows
width: parent.width 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 { Repeater {
id: groupRepeater id: groupRepeater
model: root.checksForGroup(modelData.group) model: quietChecks
HealthCheckRow { HealthCheckRow {
required property var modelData required property var modelData
@@ -22,11 +22,7 @@ Rectangle {
|| !pageLoader.item || !pageLoader.item
|| pageLoader.item.objectName !== "system-health-page") || pageLoader.item.objectName !== "system-health-page")
return false; return false;
const check = Health.checks.find(candidate => candidate.id === id); return pageLoader.item.activateRenderedAction(id);
if (!check)
return false;
pageLoader.item.handleAction(check);
return true;
} }
color: Theme.bg color: Theme.bg
+29 -11
View File
@@ -120,9 +120,11 @@ ShellRoot {
Health.consumeSnapshot(Quickshell.env("PANAMA_HEALTH_FIXTURE"), 100); Health.consumeSnapshot(Quickshell.env("PANAMA_HEALTH_FIXTURE"), 100);
} }
Item { FloatingWindow {
width: 980 title: "Panama Health UI Contract"
height: 820 visible: true
implicitWidth: 980
implicitHeight: 820
SettingsShell { SettingsShell {
id: settingsShell id: settingsShell
@@ -185,12 +187,19 @@ run ipc show 2>/dev/null | rg -q '^target health-ui-test$' \
checking_state="$(run ipc call health-ui-test state)" checking_state="$(run ipc call health-ui-test state)"
jq -e ' jq -e '
.issueIds == ["desktop.vicinae", "desktop.quickshell", "integration.calendar"] .renderedRows == [
and .desktopIds == ["desktop.vicinae", "desktop.quickshell"] {objectName:"health-check-row:issue:desktop.vicinae", id:"desktop.vicinae", section:"issue", statusText:"Needs attention"},
and .integrationIds == ["integration.bluebubbles", "integration.calendar"] {objectName:"health-check-row:issue:desktop.quickshell", id:"desktop.quickshell", section:"issue", statusText:"Action required"},
and .statusLabels == ["Needs attention", "Action required", "Healthy", "Not set up", "Needs attention", "Healthy"] {objectName:"health-check-row:issue:integration.calendar", id:"integration.calendar", section:"issue", statusText:"Needs attention"},
{objectName:"health-check-row:quiet:input.pipewire", id:"input.pipewire", section:"quiet", statusText:"Healthy"},
{objectName:"health-check-row:quiet:integration.bluebubbles", id:"integration.bluebubbles", section:"quiet", statusText:"Not set up"},
{objectName:"health-check-row:quiet:panama.caffeine", id:"panama.caffeine", section:"quiet", statusText:"Healthy"}
]
and (.renderedRows | map(.id) | length) == 6
and (.renderedRows | map(.id) | unique | length) == 6
and .emptyQuietGroups == ["desktop-foundation"]
and .summaryHeight == 126 and .summaryHeight == 126
and (.rowHeights | length) == 9 and (.rowHeights | length) == 6
and (.rowHeights | all(. >= 62)) and (.rowHeights | all(. >= 62))
and .checking == true and .checking == true
and .checkingText == "Checking…" and .checkingText == "Checking…"
@@ -202,15 +211,24 @@ for _ in $(seq 1 40); do
[[ "$(jq -r .checking <<<"$settled_state")" == "false" ]] && break [[ "$(jq -r .checking <<<"$settled_state")" == "false" ]] && break
sleep 0.1 sleep 0.1
done done
sleep 0.1
settled_state="$(run ipc call health-ui-test state)"
[[ "$(jq -c .rowHeights <<<"$settled_state")" == "$checking_heights" ]] \ [[ "$(jq -c .rowHeights <<<"$settled_state")" == "$checking_heights" ]] \
|| fail 'row geometry changed after refresh settled' || fail 'row geometry changed after refresh settled'
jq -e '.copyFocusable == true and .refreshFocusable == true and .rowActionFocusable == true' \ jq -e '
>/dev/null <<<"$settled_state" || fail 'keyboard focus does not reach hero and row actions' (.focusChain | index("health-copy-report-button")) != null
and (.focusChain | index("health-refresh-button")) != null
and (.focusChain | any(startswith("health-row-action:")))
' >/dev/null <<<"$settled_state" || fail "actual focus-chain traversal does not reach hero and row actions: $settled_state"
[[ "$(run ipc call health-ui-test request desktop.quickshell)" == "true" ]] \ [[ "$(run ipc call health-ui-test request desktop.quickshell)" == "true" ]] \
|| fail 'restart confirmation fixture could not be requested' || fail 'restart confirmation fixture could not be requested'
confirmation_state="$(run ipc call health-ui-test state)" confirmation_state="$(run ipc call health-ui-test state)"
jq -e '.confirmationVisible == true and .confirmationId == "desktop.quickshell"' \ jq -e '
.confirmationVisible == true
and .confirmationId == "desktop.quickshell"
and .activatedRows == ["health-check-row:issue:desktop.quickshell"]
' \
>/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet' >/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet'
if rg -i 'QQml|ReferenceError|TypeError|binding loop|failed to load component' "$shell_log"; then if rg -i 'QQml|ReferenceError|TypeError|binding loop|failed to load component' "$shell_log"; then