Merge codex's System Health and recovery work

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
This commit is contained in:
Gabriel Brown
2026-08-18 11:20:06 -04:00
31 changed files with 4049 additions and 171 deletions
+23
View File
@@ -0,0 +1,23 @@
import Quickshell
import Quickshell.Io
import QtQuick
import qs.services
ShellRoot {
// The production singleton still performs its delayed startup scan. The
// harness turns it off before its 2200 ms deadline so each fixture drives
// only the state transition it is asserting.
Component.onCompleted: Health.startupScanEnabled = false
IpcHandler {
target: "health-test"
function accept(text: string, generation: int): bool { return Health.consumeSnapshot(text, generation); }
function queue(): void { Health.refresh(); Health.refresh(); }
function status(): string { return JSON.stringify(Health.diagnostics()); }
function repair(id: string): bool { return Health.repair(id, false); }
function report(): string { return JSON.stringify(Health.snapshot, null, 2); }
function copy(): bool { return Health.copyReport(); }
}
}
@@ -108,6 +108,10 @@ PanelWindow {
anchors.verticalCenter: parent.verticalCenter
}
HealthIndicator {
anchors.verticalCenter: parent.verticalCenter
}
ActivityIndicator {
anchors.verticalCenter: parent.verticalCenter
}
@@ -0,0 +1,89 @@
// A deliberately absent-until-needed health affordance. Healthy and merely
// unconfigured systems leave no ornament or layout residue in the bar.
import QtQuick
import QtQuick.Controls
import qs.config
import qs.services
Rectangle {
id: root
signal activated
readonly property int issueCount: Health.summary.warnings + Health.summary.errors
readonly property color tone: Health.status === "error"
? Theme.danger
: (Health.status === "warning" ? Theme.warn : "transparent")
readonly property string statusText: root.issueCount === 1
? "1 system health issue"
: root.issueCount + " system health issues"
readonly property string accessibleLabel: Health.status === "error"
? "System Health: " + root.issueCount + (root.issueCount === 1 ? " issue requires action" : " issues require action")
: "System Health: " + root.issueCount + (root.issueCount === 1 ? " issue needs attention" : " issues need attention")
readonly property string tooltipText: root.accessibleLabel
visible: Health.actionable
implicitWidth: visible ? content.implicitWidth + 16 : 0
implicitHeight: visible ? 24 : 0
width: implicitWidth
height: implicitHeight
radius: 9
color: visible ? Theme.alpha(root.tone, 0.09) : "transparent"
border.width: activeFocus ? 2 : 1
border.color: visible ? Theme.alpha(root.tone, activeFocus ? 0.72 : 0.20) : "transparent"
activeFocusOnTab: visible
Accessible.role: Accessible.Button
Accessible.name: root.accessibleLabel
Accessible.description: "Open System Health"
Accessible.onPressAction: root.activated()
onActivated: {
ShellState.openSettings("services");
Health.refresh();
}
Keys.onReturnPressed: root.activated()
Keys.onEnterPressed: root.activated()
Keys.onSpacePressed: root.activated()
Row {
id: content
anchors.centerIn: parent
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: "\u{F0ECD}" // md-shield-alert-outline
color: root.tone
font.family: Theme.fontMono
font.pixelSize: 14
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: String(root.issueCount)
color: root.tone
font.family: Theme.fontFamily
font.features: Theme.tabularFigures
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
}
}
MouseArea {
id: pointer
anchors.fill: parent
enabled: root.visible
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activated()
}
ToolTip.visible: pointer.containsMouse && root.visible
ToolTip.delay: 500
ToolTip.text: root.tooltipText
}
@@ -0,0 +1,137 @@
import QtQuick
import qs.config
import qs.services
Item {
id: root
required property var check
property bool issue: false
property bool divider: true
property int actionActivationCount: 0
signal actionRequested(var check)
readonly property bool repairWorking: Health.repairingId === root.check.id
readonly property bool repairFailed: root.check.status !== "ok"
&& Health.lastRepair.checkId === root.check.id
&& (Health.lastRepair.accepted === false || Health.lastRepair.exitCode !== 0)
objectName: `health-check-row:${root.issue ? "issue" : "quiet"}:${root.check.id}`
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 {
if (status === "ok") return "Healthy";
if (status === "warning") return "Needs attention";
if (status === "error") return "Action required";
return "Not set up";
}
function statusColor(status: string): color {
if (status === "ok") return Theme.ok;
if (status === "warning") return Theme.warn;
if (status === "error") return Theme.danger;
return Theme.fgMuted;
}
function displayedStatus(): string {
if (root.repairWorking)
return "Working…";
if (root.repairFailed)
return "Repair failed";
return root.statusLabel(root.check.status);
}
Rectangle {
id: issueMark
visible: root.issue
width: 7
height: 7
radius: 4
anchors.left: parent.left
anchors.leftMargin: 1
anchors.verticalCenter: parent.verticalCenter
color: root.statusColor(root.check.status)
border.width: 1
border.color: Theme.alpha(Theme.bgPanel, 0.9)
}
Column {
anchors.left: parent.left
anchors.leftMargin: root.issue ? 22 : 0
anchors.right: trailing.left
anchors.rightMargin: 16
anchors.verticalCenter: parent.verticalCenter
spacing: 3
Text {
width: parent.width
text: root.check.title
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
font.weight: Font.Medium
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.check.detail
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
}
Row {
id: trailing
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 12
Text {
objectName: `health-status-text:${root.issue ? "issue" : "quiet"}:${root.check.id}`
anchors.verticalCenter: parent.verticalCenter
text: root.displayedStatus()
color: root.statusColor(root.check.status)
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
SettingsButton {
id: actionButton
objectName: `health-row-action:${root.issue ? "issue" : "quiet"}:${root.check.id}`
visible: root.check.action !== undefined
text: root.check.action?.label ?? ""
enabled: visible && !root.repairWorking && !Health.busy
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: root.actionRequested(root.check)
Keys.onReturnPressed: if (enabled) root.actionRequested(root.check)
Keys.onSpacePressed: if (enabled) root.actionRequested(root.check)
}
}
Rectangle {
visible: root.divider
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 1
color: Theme.alpha(Theme.fg, 0.07)
}
}
@@ -0,0 +1,423 @@
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")
}
}
}
@@ -0,0 +1,169 @@
import QtQuick
import qs.config
import qs.services
SettingsCard {
id: root
objectName: "health-summary"
implicitHeight: 126
readonly property int observationCount: Health.summary.warnings + Health.summary.errors
readonly property string heroTitle: {
if (Health.diagnosticUnavailable)
return "Health check unavailable";
if (Health.checks.length === 0)
return "Checking the desktop";
if (Health.status === "error")
return "Action required";
if (Health.status === "warning")
return "Needs attention";
return "Healthy";
}
readonly property string heroDetail: {
if (Health.diagnosticUnavailable)
return Health.lastError || "The latest health check could not be completed.";
if (Health.checks.length === 0)
return "Checking the desktop services, tools, and integrations this app owns.";
if (Health.status === "error")
return root.observationCount === 1
? "One part of the desktop needs action."
: `${root.observationCount} parts of the desktop need action.`;
if (Health.status === "warning")
return root.observationCount === 1
? "Your desktop is working. One feature needs a decision."
: `Your desktop is working. ${root.observationCount} features need a decision.`;
return "Desktop services and tools are working normally.";
}
readonly property color statusColor: {
if (Health.diagnosticUnavailable || Health.status === "error")
return Theme.danger;
if (Health.status === "warning")
return Theme.warn;
if (Health.checks.length === 0)
return Theme.fgMuted;
return Theme.ok;
}
function scanTime(): string {
const generatedAt = String(Health.snapshot.generatedAt || "");
if (generatedAt === "")
return "No completed check yet";
const date = new Date(generatedAt);
if (Number.isNaN(date.getTime()))
return "Last check completed";
return `Last checked ${date.toLocaleTimeString(Qt.locale(), Locale.ShortFormat)}`;
}
Item {
width: parent.width
implicitHeight: 96
Column {
anchors.left: parent.left
anchors.right: actions.left
anchors.rightMargin: 22
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Text {
width: parent.width
text: Health.diagnosticUnavailable
? "DIAGNOSTICS"
: root.observationCount > 0
? root.observationCount + (root.observationCount === 1 ? " OBSERVATION" : " OBSERVATIONS")
: "PANAMA DESKTOP"
color: root.statusColor
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.DemiBold
font.letterSpacing: 1.15
}
Text {
width: parent.width
text: root.heroTitle
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: 24
font.weight: Font.DemiBold
elide: Text.ElideRight
}
Text {
width: parent.width
text: root.heroDetail
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
Row {
spacing: 10
Text {
text: root.scanTime()
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
objectName: "health-checking-label"
visible: Health.busy
text: "Checking…"
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
Text {
visible: Health.lastCopyResult !== ""
text: Health.lastCopyResult
color: Health.lastCopyResult === "Report copied." ? Theme.ok : Theme.warn
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
Row {
id: actions
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 8
SettingsButton {
id: copyButton
objectName: "health-copy-report-button"
visible: !Health.diagnosticUnavailable
text: "Copy report"
enabled: Health.checks.length > 0
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : 1
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: Health.copyReport()
Keys.onReturnPressed: if (enabled) Health.copyReport()
Keys.onSpacePressed: if (enabled) Health.copyReport()
}
SettingsButton {
id: refreshButton
objectName: "health-refresh-button"
text: Health.diagnosticUnavailable ? "Retry" : "Refresh"
tone: Health.diagnosticUnavailable ? "normal" : "accent"
enabled: !Health.busy
activeFocusOnTab: enabled
border.width: activeFocus ? 2 : (tone === "accent" ? 0 : 1)
border.color: activeFocus ? Theme.accent : Theme.alpha(Theme.fg, 0.08)
onClicked: Health.refresh()
Keys.onReturnPressed: if (enabled) Health.refresh()
Keys.onSpacePressed: if (enabled) Health.refresh()
}
}
}
}
@@ -1,159 +0,0 @@
import QtQuick
import qs.config
import qs.services
SettingsPage {
title: "Startup & Services"
lede: "A clear view of the background tools that make the desktop feel complete."
function status(active: bool): string {
return active ? "Running" : "Stopped";
}
Item {
width: parent.width
implicitHeight: refresh.implicitHeight
SettingsButton {
id: refresh
anchors.right: parent.right
text: SystemSettings.busy ? "Refreshing…" : "Refresh"
enabled: !SystemSettings.busy
onClicked: SystemSettings.refresh()
}
}
SettingsCard {
title: "Your services"
SettingRow {
label: "Nextcloud"
detail: "File synchronization and tray status"
controlWidth: 190
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Text {
anchors.verticalCenter: parent.verticalCenter
text: status(SystemSettings.nextcloudActive)
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
SettingsButton {
text: "Open"
onClicked: SystemSettings.openApplication("nextcloud")
}
}
}
SettingRow {
label: "RustDesk"
detail: "Remote access through the enabled system service"
controlWidth: 190
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Text {
anchors.verticalCenter: parent.verticalCenter
text: status(SystemSettings.rustdeskActive)
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
SettingsButton {
text: "Open"
onClicked: SystemSettings.openApplication("rustdesk")
}
}
}
SettingRow {
label: "KDE Connect"
detail: "Phone pairing, clipboard, files, and remote controls"
divider: false
controlWidth: 190
Row {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 10
Text {
anchors.verticalCenter: parent.verticalCenter
text: status(SystemSettings.kdeconnectActive)
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
}
SettingsButton {
text: "Open"
onClicked: SystemSettings.openApplication("kdeconnect")
}
}
}
}
SettingsCard {
title: "Desktop foundation"
TextRow {
label: "Hyprpaper"
detail: "Wallpaper service"
value: status(SystemSettings.hyprpaperActive)
}
TextRow {
label: "Hypridle"
detail: "Idle and lock policy"
value: status(SystemSettings.hypridleActive)
}
TextRow {
label: "Vicinae"
detail: "Spotlight-style launcher daemon"
value: status(SystemSettings.vicinaeActive)
divider: false
}
}
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."
// This was one row listing five subjects and opening the network panel
// regardless. Naming a panel and then not opening it is worse than not
// offering it: it looks like a broken button rather than a deliberate
// hand-off, and someone looking for printers had to know 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")
}
}
}
@@ -11,6 +11,19 @@ Rectangle {
&& pageLoader.item.objectName === "home-phone-page"
? pageLoader.item.pageDiagnostics
: ({})
readonly property var healthDiagnostics: pageLoader.status === Loader.Ready
&& pageLoader.item
&& pageLoader.item.objectName === "system-health-page"
? pageLoader.item.uiDiagnostics()
: ({})
function requestHealthAction(id: string): bool {
if (pageLoader.status !== Loader.Ready
|| !pageLoader.item
|| pageLoader.item.objectName !== "system-health-page")
return false;
return pageLoader.item.activateRenderedAction(id);
}
color: Theme.bg
radius: 18
@@ -107,7 +120,7 @@ Rectangle {
case "power": return powerPage;
case "datetime": return dateTimePage;
case "applications": return applicationsPage;
case "services": return servicesPage;
case "services": return healthPage;
case "about": return aboutPage;
default: return homePage;
}
@@ -161,7 +174,7 @@ Rectangle {
Component { id: privacyPage; PrivacyPage {} }
Component { id: regionPage; RegionPage {} }
Component { id: onlineAccountsPage; OnlineAccountsPage {} }
Component { id: servicesPage; ServicesPage {} }
Component { id: healthPage; HealthPage {} }
Component { id: aboutPage; AboutPage {} }
Shortcut {
@@ -40,7 +40,7 @@ Rectangle {
{ page: "power", label: "Power & Lock", icon: "\u{F0425}" },
{ page: "datetime", label: "Date & Time", icon: "\u{F0954}" },
{ page: "applications", label: "Applications", icon: "\u{F003B}" },
{ page: "services", label: "Startup & Services", icon: "\u{F0493}" },
{ page: "services", label: "System Health", icon: "\u{F0493}" },
{ page: "about", label: "About", icon: "\u{F02FD}" }
]
@@ -292,8 +292,34 @@ Rectangle {
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 54
color: Theme.alpha(Theme.bg, 0.35)
border.width: 0
activeFocusOnTab: true
color: footerTap.hovered || activeFocus
? Theme.alpha(Theme.fg, 0.07)
: Theme.alpha(Theme.bg, 0.35)
border.width: activeFocus ? 2 : 0
border.color: Theme.accent
function footerText(): string {
if (Health.checks.length === 0)
return Health.diagnosticUnavailable ? "Health check unavailable" : "Checking the desktop";
if (Health.status === "error")
return "Desktop needs attention";
if (Health.status === "warning") {
const count = Health.summary.warnings + Health.summary.errors;
return count + (count === 1 ? " health observation" : " health observations");
}
return "Desktop is healthy";
}
function footerColor(): color {
if (Health.checks.length === 0)
return Health.diagnosticUnavailable ? Theme.danger : Theme.fgMuted;
if (Health.status === "error")
return Theme.danger;
if (Health.status === "warning")
return Theme.warn;
return Theme.ok;
}
Rectangle {
width: 7
@@ -302,17 +328,28 @@ Rectangle {
anchors.left: parent.left
anchors.leftMargin: 19
anchors.verticalCenter: parent.verticalCenter
color: Theme.ok
color: healthFooter.footerColor()
}
Text {
anchors.left: parent.left
anchors.leftMargin: 36
anchors.right: parent.right
anchors.rightMargin: 12
anchors.verticalCenter: parent.verticalCenter
text: "Desktop is healthy"
text: healthFooter.footerText()
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
elide: Text.ElideRight
}
TapHandler {
id: footerTap
onTapped: root.pageRequested("services")
}
Keys.onReturnPressed: root.pageRequested("services")
Keys.onSpacePressed: root.pageRequested("services")
}
}
@@ -10,7 +10,9 @@ DisplaysPage 1.0 DisplaysPage.qml
HomePage 1.0 HomePage.qml
NotificationsPage 1.0 NotificationsPage.qml
ScreenIntelligencePage 1.0 ScreenIntelligencePage.qml
ServicesPage 1.0 ServicesPage.qml
HealthPage 1.0 HealthPage.qml
HealthSummary 1.0 HealthSummary.qml
HealthCheckRow 1.0 HealthCheckRow.qml
SettingRow 1.0 SettingRow.qml
SettingsCard 1.0 SettingsCard.qml
SettingsButton 1.0 SettingsButton.qml
+2 -1
View File
@@ -109,6 +109,7 @@ case "$action" in
clipboard) qs ipc call clipboard open ;;
overview) qs ipc call overview open ;;
settings) qs ipc call settings open ;;
health) qs ipc call health open ;;
dnd)
state="$(qs ipc call notifications dnd)"
@@ -148,7 +149,7 @@ case "$action" in
;;
*)
printf 'Usage: panama-action {%s}\n' \
'control-center|notifications|calendar|clipboard|overview|settings|dnd|caffeine|night-light|focus-start|focus-end|capture|intelligence|screenshot|microphone|gallery|restart-shell' >&2
'control-center|notifications|calendar|clipboard|overview|settings|health|dnd|caffeine|night-light|focus-start|focus-end|capture|intelligence|screenshot|microphone|gallery|restart-shell' >&2
exit 2
;;
esac
+656
View File
@@ -0,0 +1,656 @@
#!/usr/bin/env python3
"""Redacted diagnostics and bounded repairs for Panama-owned functionality."""
from __future__ import annotations
import argparse
import json
import os
import re
import shutil
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from types import MappingProxyType
from typing import Callable, Literal
Status = Literal["ok", "warning", "error", "unconfigured"]
Group = Literal["desktop-foundation", "input-media", "integrations", "panama-tools"]
ActionKind = Literal["repair", "open", "instructions"]
@dataclass(frozen=True)
class Action:
kind: ActionKind
label: str
confirm: bool = False
# Only authored Settings page IDs and instruction IDs are allowed here.
# Repair commands never receive a caller-controlled target.
target: str | None = None
@dataclass(frozen=True)
class Check:
id: str
group: Group
title: str
status: Status
detail: str
action: Action | None = None
@dataclass(frozen=True)
class DoctorConfig:
root: Path
home: Path
config_home: Path
state_home: Path
runtime_dir: Path
path: str
timeout: float
@property
def command_env(self) -> dict[str, str]:
environment = {
"PATH": self.path,
"HOME": str(self.home),
"XDG_CONFIG_HOME": str(self.config_home),
"XDG_STATE_HOME": str(self.state_home),
"XDG_RUNTIME_DIR": str(self.runtime_dir),
}
for name in PROBE_ENVIRONMENT_KEYS:
if value := os.environ.get(name):
environment[name] = value
return environment
@dataclass(frozen=True)
class CommandResult:
state: Literal["ok", "missing", "timeout", "failed", "unavailable"]
stdout: str = ""
@dataclass(frozen=True)
class RepairResult:
check_id: str
accepted: bool
exit_code: int
message: str
def as_json(self) -> dict[str, object]:
return {
"schemaVersion": 1,
"checkId": self.check_id,
"accepted": self.accepted,
"exitCode": self.exit_code,
"message": self.message,
}
CHECK_ORDER = (
"desktop.hyprland", "desktop.quickshell", "desktop.notifications", "desktop.portals",
"desktop.hyprpaper", "desktop.hypridle", "desktop.vicinae", "input.pipewire",
"input.clipboard", "input.wallpaper", "input.capture", "input.ocr", "input.brightness",
"integration.nextcloud", "integration.rustdesk", "integration.kdeconnect", "integration.bluebubbles",
"integration.home-assistant", "integration.calendar", "panama.runtime-links", "panama.vicinae-commands",
"panama.selected-terminal", "panama.selected-launcher", "panama.processes", "panama.caffeine",
)
SYSTEMCTL_COMMANDS = {
"hyprpaper": ("systemctl", "--user", "is-active", "--quiet", "hyprpaper.service"),
"hypridle": ("systemctl", "--user", "is-active", "--quiet", "hypridle.service"),
"vicinae": ("systemctl", "--user", "is-active", "--quiet", "vicinae.service"),
"pipewire": ("systemctl", "--user", "is-active", "--quiet", "pipewire.service"),
"nextcloud": ("systemctl", "--user", "is-active", "--quiet", "nextcloud.service"),
"rustdesk": ("systemctl", "--user", "is-active", "--quiet", "rustdesk.service"),
}
REPAIR_COMMANDS = MappingProxyType({
"desktop.hyprpaper": ("systemctl", "--user", "restart", "hyprpaper.service"),
"desktop.hypridle": ("systemctl", "--user", "restart", "hypridle.service"),
"desktop.vicinae": ("systemctl", "--user", "restart", "vicinae.service"),
"desktop.quickshell": ("panama-action", "restart-shell"),
})
RUNTIME_LINK_TARGETS = (
("hypr", Path("config/dot/hypr")),
("quickshell", Path("config/dot/quickshell")),
("uwsm", Path("config/dot/uwsm")),
("vicinae", Path("config/dot/vicinae")),
)
REPAIR_IDS = frozenset((*REPAIR_COMMANDS.keys(), "panama.runtime-links", "panama.vicinae-commands", "panama.caffeine"))
PROCESS_NAMES = ("quickshell", "vicinae", "hyprpaper", "hypridle")
VERSION_PATTERN = re.compile(r"\b\d+(?:\.\d+){0,3}(?:[-+._][A-Za-z0-9._-]+)?\b")
REVISION_PATTERN = re.compile(r"\b[0-9a-f]{7,40}\b", re.IGNORECASE)
PROBE_ENVIRONMENT_KEYS = (
"LANG",
"LC_ALL",
"LC_CTYPE",
"TZ",
"DBUS_SESSION_BUS_ADDRESS",
"WAYLAND_DISPLAY",
"DISPLAY",
"XAUTHORITY",
"PANAMA_DOCTOR_FIXTURE_STOPPED",
"PANAMA_DOCTOR_FIXTURE_PROCESSES",
"PANAMA_DOCTOR_FIXTURE_BUS",
"PANAMA_DOCTOR_FIXTURE_QS",
"PANAMA_DOCTOR_FIXTURE_QS_VERSION",
"PANAMA_DOCTOR_FIXTURE_BLUEBUBBLES",
"PANAMA_DOCTOR_FIXTURE_CALENDAR",
"PANAMA_DOCTOR_FIXTURE_BRIGHTNESS",
"PANAMA_DOCTOR_FIXTURE_CAFFEINE",
)
CHECK_TITLES = {
"desktop.hyprland": "Hyprland",
"desktop.quickshell": "Quickshell",
"desktop.notifications": "Notifications",
"desktop.portals": "Desktop portals",
"desktop.hyprpaper": "Hyprpaper",
"desktop.hypridle": "Hypridle",
"desktop.vicinae": "Vicinae",
"input.pipewire": "PipeWire",
"input.clipboard": "Clipboard",
"input.wallpaper": "Wallpaper",
"input.capture": "Capture",
"input.ocr": "OCR",
"input.brightness": "External monitor brightness",
"integration.nextcloud": "Nextcloud",
"integration.rustdesk": "RustDesk",
"integration.kdeconnect": "KDE Connect",
"integration.bluebubbles": "BlueBubbles",
"integration.home-assistant": "Home Assistant",
"integration.calendar": "Calendar",
"panama.runtime-links": "Panama runtime links",
"panama.vicinae-commands": "Panama commands",
"panama.selected-terminal": "Selected terminal",
"panama.selected-launcher": "Selected launcher",
"panama.processes": "Panama processes",
"panama.caffeine": "Caffeine inhibitor",
}
def environment_path(name: str, default: Path) -> Path:
value = os.environ.get(name)
return Path(value).expanduser() if value else default
def config_from_environment() -> DoctorConfig:
home = environment_path("PANAMA_DOCTOR_HOME", Path.home())
config_home = environment_path("PANAMA_DOCTOR_CONFIG_HOME", Path(os.environ.get("XDG_CONFIG_HOME", home / ".config")))
state_home = environment_path("PANAMA_DOCTOR_STATE_HOME", Path(os.environ.get("XDG_STATE_HOME", home / ".local/state")))
runtime_dir = environment_path("PANAMA_DOCTOR_RUNTIME_DIR", Path(os.environ.get("XDG_RUNTIME_DIR", "/run/user/0")))
root = environment_path("PANAMA_DOCTOR_ROOT", Path(__file__).resolve().parents[4])
try:
timeout = float(os.environ.get("PANAMA_DOCTOR_TIMEOUT", "3"))
except ValueError:
timeout = 3.0
return DoctorConfig(root, home, config_home, state_home, runtime_dir, os.environ.get("PANAMA_DOCTOR_PATH", os.environ.get("PATH", "")), max(0.05, min(timeout, 15.0)))
def run_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> CommandResult:
"""Run an authored read-only command without reporting its unparsed output."""
try:
completed = subprocess.run(command, capture_output=True, text=True, timeout=config.timeout, check=False, env=config.command_env, cwd=cwd)
except FileNotFoundError:
return CommandResult("missing")
except subprocess.TimeoutExpired:
return CommandResult("timeout")
except OSError:
return CommandResult("unavailable")
if completed.returncode != 0:
return CommandResult("failed")
return CommandResult("ok", completed.stdout)
def run_repair_command(command: tuple[str, ...], config: DoctorConfig, cwd: Path | None = None) -> tuple[int, str]:
"""Execute one authored repair argv and retain output only for strict parsing."""
try:
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=config.timeout,
check=False,
env=config.command_env,
cwd=cwd,
)
except FileNotFoundError:
return 127, ""
except subprocess.TimeoutExpired:
return 124, ""
except OSError:
return 126, ""
exit_code = completed.returncode if 0 <= completed.returncode <= 255 else 1
return exit_code, completed.stdout
def executable_exists(name: str, config: DoctorConfig) -> bool:
return shutil.which(name, path=config.path) is not None
def action_json(action: Action) -> dict[str, object]:
result: dict[str, object] = {"kind": action.kind, "label": action.label, "confirm": action.confirm}
if action.target is not None:
result["target"] = action.target
return result
def check_json(check: Check) -> dict[str, object]:
result: dict[str, object] = {"id": check.id, "group": check.group, "title": check.title, "status": check.status, "detail": check.detail}
if check.action is not None:
result["action"] = action_json(check.action)
return result
def group_for(check_id: str) -> Group:
if check_id.startswith("desktop."):
return "desktop-foundation"
if check_id.startswith("input."):
return "input-media"
if check_id.startswith("integration."):
return "integrations"
return "panama-tools"
def service_check(check_id: str, title: str, service: str, config: DoctorConfig, action: Action | None = None) -> Check:
result = run_command(SYSTEMCTL_COMMANDS[service], config)
if result.state == "ok":
return Check(check_id, group_for(check_id), title, "ok", "Service is active.")
if result.state in {"missing", "unavailable"}:
return Check(check_id, group_for(check_id), title, "error", "Required system service probe is unavailable.")
return Check(check_id, group_for(check_id), title, "warning", "Service is not active.", action)
def ipc_target(config: DoctorConfig, target: str) -> CommandResult:
result = run_command(("qs", "ipc", "show"), config)
if result.state != "ok":
return result
return CommandResult("ok") if f"target {target}" in result.stdout.splitlines() else CommandResult("failed")
def simple_ipc_check(check_id: str, title: str, target: str, config: DoctorConfig) -> Check:
result = ipc_target(config, target)
if result.state == "ok":
return Check(check_id, "input-media", title, "ok", "Panama IPC target is available.")
if result.state == "missing":
return Check(check_id, "input-media", title, "error", "Required Quickshell executable is unavailable.")
if result.state == "timeout":
return Check(check_id, "input-media", title, "warning", "Panama IPC probe timed out.")
return Check(check_id, "input-media", title, "warning", "Panama IPC target is unavailable.")
def check_hyprland(config: DoctorConfig) -> Check:
if "hyprland" in os.environ.get("XDG_CURRENT_DESKTOP", "").casefold():
return Check("desktop.hyprland", "desktop-foundation", "Hyprland", "ok", "Hyprland session detected.")
return Check("desktop.hyprland", "desktop-foundation", "Hyprland", "error", "Hyprland session is not active.")
def check_quickshell(config: DoctorConfig) -> Check:
result = run_command(("qs", "--version"), config)
repair = Action("repair", "Restart Panama", True)
if result.state == "ok" and VERSION_PATTERN.search(result.stdout):
return Check("desktop.quickshell", "desktop-foundation", "Quickshell", "ok", "Quickshell executable is available.")
if result.state == "missing":
return Check("desktop.quickshell", "desktop-foundation", "Quickshell", "error", "Required Quickshell executable is unavailable.", repair)
return Check("desktop.quickshell", "desktop-foundation", "Quickshell", "warning", "Quickshell probe returned an invalid result.", repair)
def check_notifications(config: DoctorConfig) -> Check:
result = ipc_target(config, "notifications")
return Check("desktop.notifications", "desktop-foundation", "Notifications", "ok", "Notification service is available.") if result.state == "ok" else Check("desktop.notifications", "desktop-foundation", "Notifications", "warning", "Notification service is unavailable.")
def check_portals(config: DoctorConfig) -> Check:
result = run_command(("busctl", "--user", "--no-pager", "list"), config)
if result.state == "ok" and any(line.startswith("org.freedesktop.portal.Desktop ") for line in result.stdout.splitlines()):
return Check("desktop.portals", "desktop-foundation", "Desktop portals", "ok", "Desktop portal service is available.")
detail = "Desktop portal probe timed out." if result.state == "timeout" else "Desktop portal probe is unavailable." if result.state == "missing" else "Desktop portal service is unavailable."
return Check("desktop.portals", "desktop-foundation", "Desktop portals", "warning", detail)
def check_brightness(config: DoctorConfig) -> Check:
result = run_command(("panama-brightness", "list"), config)
instructions = Action("instructions", "View setup instructions", target="ddc-permissions")
if result.state == "timeout":
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe timed out.", instructions)
if result.state != "ok":
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI support is unavailable.", instructions)
try:
listing = json.loads(result.stdout)
displays, error = listing["displays"], listing["error"]
if not isinstance(displays, list) or not isinstance(error, str):
raise ValueError
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "DDC/CI probe returned an invalid result.", instructions)
if error:
return Check("input.brightness", "input-media", "External monitor brightness", "warning", "No accessible DDC/CI bus.", instructions)
if not displays:
return Check("input.brightness", "input-media", "External monitor brightness", "unconfigured", "No DDC/CI display is configured.")
return Check("input.brightness", "input-media", "External monitor brightness", "ok", f"{len(displays)} DDC/CI display{'s' if len(displays) != 1 else ''} available.")
def check_nextcloud(config: DoctorConfig) -> Check:
if not (config.config_home / "autostart" / "nextcloud.desktop").is_file():
return Check("integration.nextcloud", "integrations", "Nextcloud", "unconfigured", "Nextcloud autostart is not configured.")
return service_check("integration.nextcloud", "Nextcloud", "nextcloud", config, Action("open", "Open Nextcloud"))
def check_rustdesk(config: DoctorConfig) -> Check:
if not executable_exists("rustdesk", config):
return Check("integration.rustdesk", "integrations", "RustDesk", "unconfigured", "RustDesk is not installed.")
return service_check("integration.rustdesk", "RustDesk", "rustdesk", config, Action("open", "Open RustDesk"))
def check_kdeconnect(config: DoctorConfig) -> Check:
if not executable_exists("kdeconnect-cli", config):
return Check("integration.kdeconnect", "integrations", "KDE Connect", "unconfigured", "KDE Connect is not installed.")
result = run_command(("busctl", "--user", "--no-pager", "list"), config)
if result.state == "ok" and any(line.startswith("org.kde.kdeconnect ") for line in result.stdout.splitlines()):
return Check("integration.kdeconnect", "integrations", "KDE Connect", "ok", "KDE Connect service is available.")
return Check("integration.kdeconnect", "integrations", "KDE Connect", "warning", "KDE Connect service is unavailable.", Action("open", "Open KDE Connect"))
def check_bluebubbles(config: DoctorConfig) -> Check:
result = run_command(("flatpak", "info", "app.bluebubbles.BlueBubbles"), config)
if result.state == "ok":
return Check("integration.bluebubbles", "integrations", "BlueBubbles", "ok", "BlueBubbles is installed.")
if result.state in {"missing", "failed"}:
return Check("integration.bluebubbles", "integrations", "BlueBubbles", "unconfigured", "BlueBubbles is not installed.")
return Check("integration.bluebubbles", "integrations", "BlueBubbles", "warning", "BlueBubbles installation probe timed out.", Action("open", "Open BlueBubbles"))
def check_home_assistant(config: DoctorConfig) -> Check:
configured = all(name in os.environ for name in ("PANAMA_HOME_ASSISTANT_URL", "PANAMA_HOME_ASSISTANT_TOKEN"))
helper = config.config_home / "quickshell" / "scripts" / "panama-home-assistant"
if not configured:
return Check("integration.home-assistant", "integrations", "Home Assistant", "unconfigured", "Home Assistant is not configured.")
if not helper.is_file():
return Check("integration.home-assistant", "integrations", "Home Assistant", "warning", "Home Assistant bridge is unavailable.", Action("open", "Open Home settings", target="home-phone"))
return Check("integration.home-assistant", "integrations", "Home Assistant", "ok", "Home Assistant credentials are configured.")
def check_calendar(config: DoctorConfig) -> Check:
result = run_command(("calendar-agenda", "probe"), config)
action = Action("open", "Open Date & Time", target="datetime")
if result.state == "missing":
return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "Calendar integration is not installed.")
if result.state == "timeout":
return Check("integration.calendar", "integrations", "Calendar", "warning", "Calendar probe timed out.", action)
if result.state != "ok":
return Check("integration.calendar", "integrations", "Calendar", "warning", "Calendar probe failed.", action)
try:
enabled_sources = json.loads(result.stdout)["enabledSources"]
if not isinstance(enabled_sources, int) or isinstance(enabled_sources, bool):
raise ValueError
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
return Check("integration.calendar", "integrations", "Calendar", "warning", "Calendar probe returned an invalid result.", action)
if enabled_sources <= 0:
return Check("integration.calendar", "integrations", "Calendar", "unconfigured", "No enabled calendar source is configured.")
return Check("integration.calendar", "integrations", "Calendar", "ok", f"{enabled_sources} enabled calendar source{'s' if enabled_sources != 1 else ''} configured.")
def check_runtime_links(config: DoctorConfig) -> Check:
def valid_link(name: str, relative_source: Path) -> bool:
destination = config.config_home / name
source = config.root / relative_source
try:
return source.is_dir() and destination.is_symlink() \
and destination.resolve(strict=False) == source.resolve(strict=True)
except OSError:
return False
if any(not valid_link(name, relative_source) for name, relative_source in RUNTIME_LINK_TARGETS):
return Check("panama.runtime-links", "panama-tools", "Panama runtime links", "warning", "One or more Panama runtime links are unavailable.", Action("repair", "Repair runtime links"))
return Check("panama.runtime-links", "panama-tools", "Panama runtime links", "ok", "Panama runtime links are available.")
def check_vicinae_commands(config: DoctorConfig) -> Check:
source = config.root / "config/local/share/vicinae/scripts"
installed = config.home / ".local/share/vicinae/scripts"
if source.is_dir() and installed.is_symlink() and installed.exists():
return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "ok", "Panama Vicinae commands are linked.")
return Check("panama.vicinae-commands", "panama-tools", "Panama commands", "warning", "Panama Vicinae commands are not linked.", Action("repair", "Repair command link"))
def executable_check(check_id: str, title: str, executable: str, config: DoctorConfig) -> Check:
if executable_exists(executable, config):
return Check(check_id, group_for(check_id), title, "ok", f"{title} executable is available.")
return Check(check_id, group_for(check_id), title, "warning", f"{title} executable is unavailable.")
def check_processes(config: DoctorConfig) -> Check:
counts: list[int] = []
for name in PROCESS_NAMES:
result = run_command(("pgrep", "-u", str(os.getuid()), "-x", name), config)
if result.state == "ok":
pids = result.stdout.splitlines()
if not pids or any(not pid.isdecimal() for pid in pids):
return Check("panama.processes", "panama-tools", "Panama processes", "warning", "Process probe returned an invalid result.")
counts.append(len(pids))
elif result.state == "failed":
counts.append(0)
else:
return Check("panama.processes", "panama-tools", "Panama processes", "warning", "Process probe is unavailable.")
if any(count > 1 for count in counts):
return Check("panama.processes", "panama-tools", "Panama processes", "warning", "Duplicate Panama desktop processes detected.")
if counts[0] == 0:
return Check("panama.processes", "panama-tools", "Panama processes", "error", "Quickshell process is not running.")
return Check("panama.processes", "panama-tools", "Panama processes", "ok", "Panama desktop process counts are normal.")
def check_caffeine(config: DoctorConfig) -> Check:
result = run_command(("systemd-inhibit", "--list", "--no-pager", "--no-legend"), config)
if result.state != "ok":
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe is unavailable.")
uid = str(os.getuid())
inhibitors = 0
malformed = False
for line in result.stdout.splitlines():
parts = line.split()
relevant = len(parts) >= 2 and parts[0] == "Panama" and parts[1] == uid and "Caffeine" in parts
if not relevant:
continue
if len(parts) >= 8 and parts[3].isdecimal() and parts[-2:] == ["Caffeine", "block"]:
inhibitors += 1
else:
malformed = True
if malformed:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Caffeine inhibitor probe returned an invalid result.")
if inhibitors > 1:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "warning", "Duplicate Panama Caffeine inhibitors detected.", Action("repair", "Release duplicate inhibitors"))
if inhibitors == 1:
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "ok", "One Panama Caffeine inhibitor is active.")
return Check("panama.caffeine", "panama-tools", "Caffeine inhibitor", "ok", "No Panama Caffeine inhibitor is active.")
def parse_version(result: CommandResult, pattern: re.Pattern[str] = VERSION_PATTERN) -> str:
match = pattern.search(result.stdout) if result.state == "ok" else None
return match.group(0) if match else "unavailable"
def context_versions(config: DoctorConfig) -> list[dict[str, str]]:
hyprland = run_command(("hyprctl", "version"), config)
quickshell = run_command(("qs", "--version"), config)
revision = run_command(("git", "rev-parse", "--short", "HEAD"), config, config.root)
fedora = "unavailable"
try:
match = re.search(r"^VERSION_ID=\"?([^\n\"]+)", Path("/etc/os-release").read_text(encoding="utf-8"), re.MULTILINE)
if match and re.fullmatch(r"[0-9.]+", match.group(1)):
fedora = match.group(1)
except OSError:
pass
return [{"id": "hyprland", "version": parse_version(hyprland)}, {"id": "quickshell", "version": parse_version(quickshell)}, {"id": "fedora", "version": fedora}, {"id": "panama", "version": parse_version(revision, REVISION_PATTERN)}]
def unavailable_check(check_id: str) -> Check:
return Check(check_id, group_for(check_id), CHECK_TITLES[check_id], "warning", "Diagnostic probe could not be completed.")
def unavailable_versions() -> list[dict[str, str]]:
return [{"id": name, "version": "unavailable"} for name in ("hyprland", "quickshell", "fedora", "panama")]
def collect_checks(config: DoctorConfig) -> list[Check]:
probes: dict[str, Callable[[], Check]] = {
"desktop.hyprland": lambda: check_hyprland(config), "desktop.quickshell": lambda: check_quickshell(config), "desktop.notifications": lambda: check_notifications(config), "desktop.portals": lambda: check_portals(config),
"desktop.hyprpaper": lambda: service_check("desktop.hyprpaper", "Hyprpaper", "hyprpaper", config, Action("repair", "Restart Hyprpaper")), "desktop.hypridle": lambda: service_check("desktop.hypridle", "Hypridle", "hypridle", config, Action("repair", "Restart Hypridle")), "desktop.vicinae": lambda: service_check("desktop.vicinae", "Vicinae", "vicinae", config, Action("repair", "Restart Vicinae")), "input.pipewire": lambda: service_check("input.pipewire", "PipeWire", "pipewire", config),
"input.clipboard": lambda: simple_ipc_check("input.clipboard", "Clipboard", "clipboard", config), "input.wallpaper": lambda: simple_ipc_check("input.wallpaper", "Wallpaper", "wallpaper", config), "input.capture": lambda: simple_ipc_check("input.capture", "Capture", "capture", config), "input.ocr": lambda: executable_check("input.ocr", "OCR", "tesseract", config), "input.brightness": lambda: check_brightness(config),
"integration.nextcloud": lambda: check_nextcloud(config), "integration.rustdesk": lambda: check_rustdesk(config), "integration.kdeconnect": lambda: check_kdeconnect(config), "integration.bluebubbles": lambda: check_bluebubbles(config), "integration.home-assistant": lambda: check_home_assistant(config), "integration.calendar": lambda: check_calendar(config),
"panama.runtime-links": lambda: check_runtime_links(config), "panama.vicinae-commands": lambda: check_vicinae_commands(config), "panama.selected-terminal": lambda: executable_check("panama.selected-terminal", "Selected terminal", "kitty", config), "panama.selected-launcher": lambda: executable_check("panama.selected-launcher", "Selected launcher", "vicinae", config), "panama.processes": lambda: check_processes(config), "panama.caffeine": lambda: check_caffeine(config),
}
with ThreadPoolExecutor(max_workers=8) as executor:
futures = {check_id: executor.submit(probes[check_id]) for check_id in CHECK_ORDER}
checks: list[Check] = []
for check_id in CHECK_ORDER:
try:
checks.append(futures[check_id].result())
except Exception:
checks.append(unavailable_check(check_id))
return checks
def snapshot(config: DoctorConfig) -> dict[str, object]:
try:
checks = collect_checks(config)
except Exception:
checks = [unavailable_check(check_id) for check_id in CHECK_ORDER]
counts = {status: sum(check.status == status for check in checks) for status in ("ok", "warning", "error", "unconfigured")}
overall: Literal["healthy", "warning", "error"] = "error" if counts["error"] else "warning" if counts["warning"] else "healthy"
session = "hyprland" if "hyprland" in os.environ.get("XDG_CURRENT_DESKTOP", "").casefold() else "other"
try:
versions = context_versions(config)
except Exception:
versions = unavailable_versions()
return {"schemaVersion": 1, "generatedAt": datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), "summary": {"status": overall, "healthy": counts["ok"], "warnings": counts["warning"], "errors": counts["error"], "unconfigured": counts["unconfigured"]}, "context": {"session": session, "versions": versions}, "checks": [check_json(check) for check in checks]}
def repair_authored_command(check_id: str, config: DoctorConfig) -> RepairResult:
command = REPAIR_COMMANDS[check_id]
exit_code, _ = run_repair_command(command, config)
message = "Repair completed. A fresh health check will verify recovery." if exit_code == 0 \
else "The authored repair command could not be completed."
return RepairResult(check_id, True, exit_code, message)
def repair_runtime_links(config: DoctorConfig) -> RepairResult:
sources = [(name, config.root / relative_source) for name, relative_source in RUNTIME_LINK_TARGETS]
if any(not source.is_dir() for _, source in sources):
return RepairResult("panama.runtime-links", True, 1, "Tracked Panama link destinations are unavailable.")
try:
config.config_home.mkdir(parents=True, exist_ok=True)
except OSError:
return RepairResult("panama.runtime-links", True, 1, "Panama runtime links could not be accessed.")
blocked = False
failed = False
for name, source in sources:
destination = config.config_home / name
try:
if destination.is_symlink():
if destination.resolve(strict=False) == source.resolve(strict=True):
continue
destination.unlink()
destination.symlink_to(source, target_is_directory=True)
elif destination.exists():
# A regular file or directory is user-owned unless proven
# otherwise. Report it, but never replace it.
blocked = True
else:
destination.symlink_to(source, target_is_directory=True)
except OSError:
failed = True
if failed:
return RepairResult("panama.runtime-links", True, 1, "One or more Panama runtime links could not be recreated.")
if blocked:
return RepairResult("panama.runtime-links", True, 1, "A user-owned file or directory is blocking a Panama runtime link.")
return RepairResult("panama.runtime-links", True, 0, "Panama runtime links were recreated. A fresh health check will verify them.")
def repair_vicinae_commands(config: DoctorConfig) -> RepairResult:
helper = config.root / "setup/scripts/link-vicinae-scripts"
if not helper.is_file():
return RepairResult("panama.vicinae-commands", True, 127, "The authored Vicinae link helper is unavailable.")
exit_code, _ = run_repair_command((str(helper),), config, config.root)
message = "Panama commands were relinked. A fresh health check will verify them." if exit_code == 0 \
else "Panama commands could not be relinked."
return RepairResult("panama.vicinae-commands", True, exit_code, message)
def repair_caffeine(config: DoctorConfig) -> RepairResult:
list_command = ("systemd-inhibit", "--list", "--no-pager", "--no-legend")
list_exit, output = run_repair_command(list_command, config)
if list_exit != 0:
return RepairResult("panama.caffeine", True, list_exit, "Caffeine inhibitors could not be inspected.")
uid = str(os.getuid())
inhibitor_pids: list[str] = []
for line in output.splitlines():
parts = line.split()
if len(parts) < 2 or parts[0] != "Panama" or parts[1] != uid:
continue
if len(parts) != 8:
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
if parts[6] != "Caffeine" or parts[7] != "block":
continue
if not parts[3].isdecimal():
return RepairResult("panama.caffeine", True, 1, "Caffeine inhibitor metadata was invalid; nothing was released.")
inhibitor_pids.append(parts[3])
if len(inhibitor_pids) <= 1:
return RepairResult("panama.caffeine", True, 0, "No duplicate Panama Caffeine inhibitors needed release.")
for pid in inhibitor_pids[1:]:
exit_code, _ = run_repair_command(("kill", "--", pid), config)
if exit_code != 0:
return RepairResult("panama.caffeine", True, exit_code, "A duplicate Panama Caffeine inhibitor could not be released.")
return RepairResult("panama.caffeine", True, 0, "Duplicate Panama Caffeine inhibitors were released. A fresh health check will verify recovery.")
def repair(check_id: str, config: DoctorConfig) -> RepairResult:
if check_id in REPAIR_COMMANDS:
return repair_authored_command(check_id, config)
if check_id == "panama.runtime-links":
return repair_runtime_links(config)
if check_id == "panama.vicinae-commands":
return repair_vicinae_commands(config)
if check_id == "panama.caffeine":
return repair_caffeine(config)
return RepairResult(check_id, False, 2, "This health check has no authored repair.")
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(description="Panama system diagnostics and bounded repairs")
output = parser.add_mutually_exclusive_group()
output.add_argument("--json", action="store_true")
output.add_argument("--summary", action="store_true")
parser.add_argument("--repair", metavar="CHECK_ID")
args = parser.parse_args(argv)
if args.repair is not None:
if args.repair not in REPAIR_IDS or args.summary:
result = RepairResult(args.repair, False, 2, "This health check has no authored repair.")
else:
try:
result = repair(args.repair, config_from_environment())
except Exception:
result = RepairResult(args.repair, True, 1, "The authored repair could not be completed.")
print(json.dumps(result.as_json(), separators=(",", ":"), sort_keys=False))
return result.exit_code
result = snapshot(config_from_environment())
if args.summary:
summary = result["summary"]
assert isinstance(summary, dict)
print(f"Panama system health: {summary['status']} ({summary['healthy']} ok, {summary['warnings']} warnings, {summary['errors']} errors, {summary['unconfigured']} unconfigured)")
else:
print(json.dumps(result, separators=(",", ":"), sort_keys=False))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+440
View File
@@ -0,0 +1,440 @@
pragma Singleton
// The health helper is deliberately not a state owner. This singleton accepts
// complete, typed snapshots and keeps the last valid one available while a
// later scan or repair fails.
import Quickshell
import Quickshell.Io
import QtQuick
Singleton {
id: root
property var snapshot: ({})
property var checks: []
property var summary: ({ status: "healthy", healthy: 0, warnings: 0, errors: 0, unconfigured: 0 })
property string status: "healthy"
property bool diagnosticUnavailable: false
property bool queuedRefresh: false
property int generation: 0
property int acceptedGeneration: 0
property string lastError: ""
property string repairingId: ""
property var lastRepair: ({})
property string lastCopyResult: ""
property bool startupScanEnabled: true
property bool postRepairScanPending: false
readonly property bool actionable: root.status === "warning" || root.status === "error"
readonly property bool busy: scanProcess.running || repairProcess.running || root.postRepairScanPending
readonly property string helperPath: Quickshell.env("PANAMA_HEALTH_HELPER")
|| Quickshell.shellDir + "/scripts/panama-doctor"
readonly property var statuses: ["ok", "warning", "error", "unconfigured"]
readonly property var groups: ["desktop-foundation", "input-media", "integrations", "panama-tools"]
readonly property var overallStatuses: ["healthy", "warning", "error"]
readonly property var settingsTargets: ["home-phone", "datetime"]
readonly property var instructionTargets: ["ddc-permissions"]
Process {
id: scanProcess
property int scanGeneration: 0
property string outputText: ""
property int exitCode: -1
property bool exited: false
property bool streamFinished: false
property bool settled: false
stdout: StdioCollector {
id: scanOutput
property int generation: 0
onStreamFinished: {
scanProcess.outputText = this.text;
scanProcess.scanGeneration = generation;
scanProcess.streamFinished = true;
root.settleScan();
}
}
onExited: (exitCode, exitStatus) => {
scanProcess.exitCode = exitCode;
scanProcess.exited = true;
root.settleScan();
}
}
Process {
id: repairProcess
property string checkId: ""
property bool external: false
property string outputText: ""
property int exitCode: -1
property bool exited: false
property bool streamFinished: false
property bool settled: false
stdout: StdioCollector {
onStreamFinished: {
repairProcess.outputText = this.text;
repairProcess.streamFinished = true;
root.settleRepair();
}
}
onExited: (exitCode, exitStatus) => {
repairProcess.exitCode = exitCode;
repairProcess.exited = true;
root.settleRepair();
}
}
Process {
id: copyProcess
property string payload: ""
onStarted: copyProcess.write(copyProcess.payload)
onExited: (exitCode, exitStatus) => {
root.lastCopyResult = exitCode === 0
? "Report copied."
: "Could not copy the health report."
copyProcess.payload = ""
}
}
Process {
id: failureNotification
}
Timer {
id: startupScan
interval: 2200
repeat: false
running: root.startupScanEnabled
onTriggered: root.refresh()
}
function refresh(): bool {
if (scanProcess.running || repairProcess.running) {
root.queuedRefresh = true;
return false;
}
root.generation += 1;
scanProcess.scanGeneration = root.generation;
scanProcess.outputText = "";
scanProcess.exitCode = -1;
scanProcess.exited = false;
scanProcess.streamFinished = false;
scanProcess.settled = false;
scanOutput.generation = root.generation;
scanProcess.exec([root.helperPath, "--json"]);
return true;
}
function settleScan(): void {
if (scanProcess.settled || !scanProcess.exited || !scanProcess.streamFinished)
return;
scanProcess.settled = true;
root.finishScan(scanProcess.exitCode, scanProcess.scanGeneration, scanProcess.outputText);
}
function finishScan(exitCode: int, scanGeneration: int, text: string): void {
if (exitCode === 0)
root.consumeSnapshot(text, scanGeneration);
else
root.rejectSnapshot("Panama diagnostics could not be read. Try refreshing.");
if (!root.queuedRefresh)
return;
root.queuedRefresh = false;
root.refresh();
}
function consumeSnapshot(text: string, scanGeneration: int): bool {
if (scanGeneration < root.acceptedGeneration)
return false;
let candidate;
try {
candidate = JSON.parse(text.trim());
} catch (error) {
root.rejectSnapshot("Panama diagnostics returned an unreadable response.");
return false;
}
if (!root.validSnapshot(candidate)) {
root.rejectSnapshot("Panama diagnostics returned an invalid response.");
return false;
}
const accepted = root.safeSnapshot(candidate);
root.snapshot = accepted;
root.checks = accepted.checks;
root.summary = accepted.summary;
root.status = accepted.summary.status;
root.acceptedGeneration = scanGeneration;
root.diagnosticUnavailable = false;
root.lastError = "";
return true;
}
function rejectSnapshot(message: string): void {
root.diagnosticUnavailable = true;
root.lastError = message;
}
function safeSnapshot(candidate: var): var {
return {
schemaVersion: 1,
generatedAt: candidate.generatedAt,
summary: {
status: candidate.summary.status,
healthy: candidate.summary.healthy,
warnings: candidate.summary.warnings,
errors: candidate.summary.errors,
unconfigured: candidate.summary.unconfigured
},
context: {
session: candidate.context.session,
versions: candidate.context.versions.map(version => ({
id: version.id,
version: version.version
}))
},
checks: candidate.checks.map(check => root.safeCheck(check))
};
}
function safeCheck(candidate: var): var {
const check = {
id: candidate.id,
group: candidate.group,
title: candidate.title,
status: candidate.status,
detail: candidate.detail
};
if (candidate.action !== undefined)
check.action = root.safeAction(candidate.action);
return check;
}
function safeAction(candidate: var): var {
const action = {
kind: candidate.kind,
label: candidate.label,
confirm: candidate.confirm
};
if (Object.prototype.hasOwnProperty.call(candidate, "target"))
action.target = candidate.target;
return action;
}
function repair(id: string, external: bool): bool {
if (root.busy)
return false;
const check = root.checks.find(candidate => candidate.id === id);
if (!check || !check.action || check.action.kind !== "repair")
return false;
root.repairingId = id;
root.lastError = "";
repairProcess.checkId = id;
repairProcess.external = external;
repairProcess.outputText = "";
repairProcess.exitCode = -1;
repairProcess.exited = false;
repairProcess.streamFinished = false;
repairProcess.settled = false;
repairProcess.exec([root.helperPath, "--repair", id, "--json"]);
return true;
}
function settleRepair(): void {
if (repairProcess.settled || !repairProcess.exited || !repairProcess.streamFinished)
return;
repairProcess.settled = true;
root.finishRepair(
repairProcess.exitCode,
repairProcess.checkId,
repairProcess.external,
repairProcess.outputText
);
}
function finishRepair(exitCode: int, id: string, external: bool, text: string): void {
let candidate;
try {
candidate = JSON.parse(text.trim());
} catch (error) {
candidate = null;
}
const result = root.validRepairResult(candidate, id, exitCode)
? {
schemaVersion: 1,
checkId: candidate.checkId,
accepted: candidate.accepted,
exitCode: candidate.exitCode,
message: candidate.message
}
: {
schemaVersion: 1,
checkId: id,
accepted: false,
exitCode: exitCode,
message: "Panama returned an invalid repair response."
};
const failed = !result.accepted || result.exitCode !== 0;
root.repairingId = "";
root.lastRepair = result;
root.lastError = failed ? result.message : "";
if (failed && external && !failureNotification.running) {
failureNotification.exec([
"notify-send", "-a", "Panama", "-i", "dialog-error-symbolic",
"Panama action failed", "The requested health repair could not be completed."
]);
}
// Any refresh requested while the repair was running is satisfied by
// this one observed post-repair scan. Process exit alone never changes
// the accepted check rows.
root.queuedRefresh = false;
root.postRepairScanPending = true;
Qt.callLater(root.startPostRepairScan);
}
function startPostRepairScan(): void {
root.postRepairScanPending = false;
root.queuedRefresh = false;
root.refresh();
}
function validRepairResult(candidate: var, id: string, processExitCode: int): bool {
if (!root.plainObject(candidate))
return false;
const keys = Object.keys(candidate).sort();
const expectedKeys = ["accepted", "checkId", "exitCode", "message", "schemaVersion"];
if (keys.length !== expectedKeys.length
|| !keys.every((key, index) => key === expectedKeys[index]))
return false;
return candidate.schemaVersion === 1
&& candidate.checkId === id
&& typeof candidate.accepted === "boolean"
&& Number.isInteger(candidate.exitCode)
&& candidate.exitCode === processExitCode
&& typeof candidate.message === "string"
&& candidate.message.length > 0;
}
function copyReport(): bool {
if (copyProcess.running)
return false;
copyProcess.payload = JSON.stringify(root.snapshot, null, 2);
root.lastCopyResult = "";
copyProcess.exec(["wl-copy"]);
return true;
}
function diagnostics(): var {
return {
status: root.status,
summary: root.summary,
busy: root.busy,
diagnosticUnavailable: root.diagnosticUnavailable,
queuedRefresh: root.queuedRefresh,
generation: root.generation,
acceptedGeneration: root.acceptedGeneration,
repairingId: root.repairingId,
lastRepair: root.lastRepair,
lastError: root.lastError,
checks: root.checks.map(check => check.id),
checkStates: root.checks.map(check => ({ id: check.id, status: check.status }))
};
}
function validSnapshot(candidate: var): bool {
if (!root.plainObject(candidate)
|| candidate.schemaVersion !== 1
|| typeof candidate.generatedAt !== "string" || candidate.generatedAt.length === 0
|| !root.validSummary(candidate.summary)
|| !root.validContext(candidate.context)
|| !Array.isArray(candidate.checks) || candidate.checks.length === 0)
return false;
const ids = {};
const counts = { ok: 0, warning: 0, error: 0, unconfigured: 0 };
for (const check of candidate.checks) {
if (!root.validCheck(check) || ids[check.id])
return false;
ids[check.id] = true;
counts[check.status] += 1;
}
const computedStatus = counts.error > 0 ? "error" : counts.warning > 0 ? "warning" : "healthy";
return candidate.summary.healthy === counts.ok
&& candidate.summary.warnings === counts.warning
&& candidate.summary.errors === counts.error
&& candidate.summary.unconfigured === counts.unconfigured
&& candidate.summary.status === computedStatus;
}
function validSummary(candidate: var): bool {
if (!root.plainObject(candidate) || root.overallStatuses.indexOf(candidate.status) < 0)
return false;
for (const key of ["healthy", "warnings", "errors", "unconfigured"]) {
if (!Number.isInteger(candidate[key]) || candidate[key] < 0)
return false;
}
return true;
}
function validContext(candidate: var): bool {
if (!root.plainObject(candidate)
|| ["hyprland", "other"].indexOf(candidate.session) < 0
|| !Array.isArray(candidate.versions))
return false;
return candidate.versions.every(version => root.plainObject(version)
&& typeof version.id === "string" && version.id.length > 0
&& typeof version.version === "string" && version.version.length > 0);
}
function validCheck(candidate: var): bool {
if (!root.plainObject(candidate)
|| !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)+$/.test(candidate.id)
|| root.groups.indexOf(candidate.group) < 0
|| root.statuses.indexOf(candidate.status) < 0
|| typeof candidate.title !== "string" || candidate.title.length === 0
|| typeof candidate.detail !== "string" || candidate.detail.length === 0)
return false;
return candidate.action === undefined || root.validAction(candidate.action);
}
function validAction(candidate: var): bool {
if (!root.plainObject(candidate)
|| ["repair", "open", "instructions"].indexOf(candidate.kind) < 0
|| typeof candidate.label !== "string" || candidate.label.length === 0
|| typeof candidate.confirm !== "boolean")
return false;
const allowedKeys = ["kind", "label", "confirm", "target"];
if (!Object.keys(candidate).every(key => allowedKeys.indexOf(key) >= 0))
return false;
const hasTarget = Object.prototype.hasOwnProperty.call(candidate, "target");
if (!hasTarget)
return true;
if (typeof candidate.target !== "string" || candidate.target.length === 0 || candidate.kind === "repair")
return false;
return candidate.kind === "open"
? root.settingsTargets.indexOf(candidate.target) >= 0
: root.instructionTargets.indexOf(candidate.target) >= 0;
}
function plainObject(value: var): bool {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
}
@@ -52,7 +52,9 @@ Singleton {
{ label: "Printers", detail: "Managed by GNOME Settings", page: "connectivity" },
{ label: "Default applications", detail: "Browser, mail, files", page: "applications" },
{ label: "Restore defaults", detail: "Return every Panama setting to its shipped value", page: "desktop" },
{ label: "Keyboard shortcuts", detail: "Every shortcut the compositor has bound", page: "shortcuts" }
{ label: "Keyboard shortcuts", detail: "Every shortcut the compositor has bound", page: "shortcuts" },
{ label: "System Health", detail: "Check Panama services, integrations, tools, and recovery actions", page: "services" },
{ label: "Copy health report", detail: "Copy a redacted Panama doctor report", page: "services" }
]
function pageFor(group: string): string {
+20
View File
@@ -145,6 +145,26 @@ ShellRoot {
}
}
IpcHandler {
target: "health"
function refresh(): bool { return Health.refresh(); }
function status(): string {
return JSON.stringify({
summary: Health.summary,
busy: Health.busy,
generation: Health.generation,
acceptedGeneration: Health.acceptedGeneration,
checks: Health.checks.map(check => ({ id: check.id, status: check.status }))
});
}
function open(): void {
ShellState.openSettings("services");
Health.refresh();
}
function repair(id: string): bool { return Health.repair(id, true); }
}
// A small diagnostics surface doubles as a deterministic contract harness.
// Real producers call StatusEvents.publish() directly; fixtures never run
// unless explicitly requested over IPC by the test suite.