Add Panama health state service

This commit is contained in:
Gabriel Brown
2026-08-18 09:20:16 -04:00
parent 0ca83f74c7
commit e2e03252e4
4 changed files with 455 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
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); }
}
}
+307
View File
@@ -0,0 +1,307 @@
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
readonly property bool actionable: root.status === "warning" || root.status === "error"
readonly property bool busy: scanProcess.running || repairProcess.running
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
onExited: (exitCode, exitStatus) => root.finishRepair(exitCode, checkId, external)
}
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;
}
root.snapshot = candidate;
root.checks = candidate.checks;
root.summary = candidate.summary;
root.status = candidate.summary.status;
root.acceptedGeneration = scanGeneration;
root.diagnosticUnavailable = false;
root.lastError = "";
return true;
}
function rejectSnapshot(message: string): void {
root.diagnosticUnavailable = true;
root.lastError = message;
}
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.exec([root.helperPath, "--repair", id, "--json"]);
return true;
}
function finishRepair(exitCode: int, id: string, external: bool): void {
const succeeded = exitCode === 0;
root.repairingId = "";
root.lastRepair = ({ id: id, succeeded: succeeded });
if (!succeeded) {
root.lastError = "Panama could not repair this item. Try refreshing or use the recommended setup steps.";
if (external) {
failureNotification.exec([
"notify-send", "-a", "Panama", "-i", "dialog-error-symbolic",
"Panama action failed", "The requested health repair could not be completed."
]);
}
}
root.refresh();
}
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,
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);
}
}
+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. // A small diagnostics surface doubles as a deterministic contract harness.
// Real producers call StatusEvents.publish() directly; fixtures never run // Real producers call StatusEvents.publish() directly; fixtures never run
// unless explicitly requested over IPC by the test suite. // unless explicitly requested over IPC by the test suite.
+107
View File
@@ -0,0 +1,107 @@
#!/usr/bin/env bash
# Health owns the accepted diagnostic snapshot. A newer unreadable response
# must degrade diagnostics without discarding the last report that Settings
# and future health surfaces will render.
set -euo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
harness="$repo_dir/config/dot/quickshell/health-harness.qml"
service="$repo_dir/config/dot/quickshell/services/Health.qml"
warning_snapshot='{"schemaVersion":1,"generatedAt":"2026-08-18T00:00:00Z","summary":{"status":"warning","healthy":0,"warnings":2,"errors":0,"unconfigured":0},"context":{"session":"hyprland","versions":[{"id":"quickshell","version":"0.3.0"}]},"checks":[{"id":"integration.calendar","group":"integrations","title":"Calendar","status":"warning","detail":"Calendar probe timed out.","action":{"kind":"open","label":"Open Date & Time","confirm":false,"target":"datetime"}},{"id":"panama.caffeine","group":"panama-tools","title":"Caffeine","status":"warning","detail":"Duplicate inhibitors are active.","action":{"kind":"repair","label":"Release duplicate inhibitors","confirm":false}}]}'
fail() {
printf 'health service contract: %s\n' "$1" >&2
exit 1
}
[[ -f "$service" ]] || fail 'Health.qml is missing'
[[ -f "$harness" ]] || fail 'health harness is missing'
fixture_dir="$(mktemp -d /tmp/panama-health.XXXXXX)"
helper="$fixture_dir/panama-doctor"
printf '%s\n' \
'#!/usr/bin/env bash' \
'if [[ "$1" == "--json" ]]; then' \
' sleep 0.2' \
" printf '%s\\n' '$warning_snapshot'" \
' exit 0' \
'fi' \
'if [[ "$1" == "--repair" && "$2" == "panama.caffeine" && "$3" == "--json" ]]; then' \
' exit 0' \
'fi' \
'exit 2' >"$helper"
chmod +x "$helper"
run() { PANAMA_HEALTH_HELPER="$helper" qs -p "$harness" "$@"; }
harness_pid=""
cleanup() {
[[ -n "$harness_pid" ]] && kill "$harness_pid" >/dev/null 2>&1 || true
rm -rf "$fixture_dir"
}
trap cleanup EXIT
PANAMA_HEALTH_HELPER="$helper" qs -p "$harness" --daemonize >/dev/null
for _ in $(seq 1 40); do
run ipc show 2>/dev/null | rg -q '^target health-test$' && break
sleep 0.1
done
run ipc show 2>/dev/null | rg -q '^target health-test$' || fail 'test IPC target did not start'
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
[[ "$(run ipc call health-test accept "$warning_snapshot" 0)" == "true" ]] \
|| fail 'valid warning snapshot was rejected'
state="$(run ipc call health-test status)"
jq -e '.status == "warning" and .acceptedGeneration == 0 and .checks == ["integration.calendar", "panama.caffeine"] and .diagnosticUnavailable == false' \
>/dev/null <<<"$state" || fail "valid warning snapshot was not accepted intact: $state"
[[ "$(run ipc call health-test accept "$warning_snapshot" -1)" == "false" ]] \
|| fail 'older generation replaced the current snapshot'
state="$(run ipc call health-test status)"
jq -e '.acceptedGeneration == 0 and .checks == ["integration.calendar", "panama.caffeine"]' \
>/dev/null <<<"$state" || fail "older generation altered accepted state: $state"
[[ "$(run ipc call health-test accept '{not json' 1)" == "false" ]] \
|| fail 'malformed snapshot was accepted'
state="$(run ipc call health-test status)"
jq -e '.diagnosticUnavailable == true and .checks == ["integration.calendar", "panama.caffeine"]' \
>/dev/null <<<"$state" || fail "malformed snapshot discarded the last valid checks: $state"
before_generation="$(jq -r .generation <<<"$state")"
run ipc call health-test queue >/dev/null
state="$(run ipc call health-test status)"
jq -e '.queuedRefresh == true and .generation == ($before + 1)' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "two refreshes did not retain exactly one follow-up: $state"
for _ in $(seq 1 120); do
state="$(run ipc call health-test status)"
jq -e '.busy == false and .generation == ($before + 2) and .queuedRefresh == false' --argjson before "$before_generation" \
>/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.busy == false and .generation == ($before + 2) and .queuedRefresh == false' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "queued refresh did not run exactly once: $state"
[[ "$(run ipc call health-test repair panama.caffeine)" == "true" ]] \
|| fail 'repairable check was refused'
for _ in $(seq 1 120); do
state="$(run ipc call health-test status)"
jq -e '.busy == false and .generation == ($before + 3)' --argjson before "$before_generation" \
>/dev/null <<<"$state" && break
sleep 0.1
done
jq -e '.busy == false and .generation == ($before + 3)' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "accepted repair did not trigger one rescan: $state"
[[ "$(run ipc call health-test repair unknown.check)" == "false" ]] \
|| fail 'unknown check started a repair'
[[ "$(run ipc call health-test repair integration.calendar)" == "false" ]] \
|| fail 'non-repairable check started a repair'
state="$(run ipc call health-test status)"
jq -e '.repairingId == "" and .generation == ($before + 3)' --argjson before "$before_generation" \
>/dev/null <<<"$state" || fail "rejected repair altered process state: $state"
trap - EXIT
cleanup
printf 'health service contract: PASS\n'