Add quiet System Health entry points
This commit is contained in:
@@ -108,6 +108,10 @@ PanelWindow {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HealthIndicator {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
ActivityIndicator {
|
ActivityIndicator {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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
|
||||||
|
}
|
||||||
@@ -109,6 +109,7 @@ case "$action" in
|
|||||||
clipboard) qs ipc call clipboard open ;;
|
clipboard) qs ipc call clipboard open ;;
|
||||||
overview) qs ipc call overview open ;;
|
overview) qs ipc call overview open ;;
|
||||||
settings) qs ipc call settings open ;;
|
settings) qs ipc call settings open ;;
|
||||||
|
health) qs ipc call health open ;;
|
||||||
|
|
||||||
dnd)
|
dnd)
|
||||||
state="$(qs ipc call notifications dnd)"
|
state="$(qs ipc call notifications dnd)"
|
||||||
@@ -148,7 +149,7 @@ case "$action" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
printf 'Usage: panama-action {%s}\n' \
|
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
|
exit 2
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# @vicinae.schemaVersion 1
|
||||||
|
# @vicinae.title Panama: Check System Health
|
||||||
|
# @vicinae.mode silent
|
||||||
|
# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg
|
||||||
|
# @vicinae.description Review Panama services, integrations, and recovery actions.
|
||||||
|
# @vicinae.keywords ["health", "doctor", "repair", "services"]
|
||||||
|
|
||||||
|
exec "$HOME/.config/quickshell/scripts/panama-action" health
|
||||||
@@ -7,6 +7,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
settings_dir="$repo_dir/config/dot/quickshell/modules/settings"
|
settings_dir="$repo_dir/config/dot/quickshell/modules/settings"
|
||||||
|
bar_dir="$repo_dir/config/dot/quickshell/modules/bar"
|
||||||
|
|
||||||
fail() {
|
fail() {
|
||||||
printf 'health UI contract: %s\n' "$1" >&2
|
printf 'health UI contract: %s\n' "$1" >&2
|
||||||
@@ -16,6 +17,21 @@ fail() {
|
|||||||
for file in HealthPage.qml HealthSummary.qml HealthCheckRow.qml; do
|
for file in HealthPage.qml HealthSummary.qml HealthCheckRow.qml; do
|
||||||
[[ -f "$settings_dir/$file" ]] || fail "$file is missing"
|
[[ -f "$settings_dir/$file" ]] || fail "$file is missing"
|
||||||
done
|
done
|
||||||
|
[[ -f "$bar_dir/HealthIndicator.qml" ]] || fail 'HealthIndicator.qml is missing'
|
||||||
|
|
||||||
|
python3 - "$bar_dir/Bar.qml" <<'PY' || fail 'health indicator is not immediately before activity in the bar'
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
source = open(sys.argv[1], encoding="utf-8").read()
|
||||||
|
match = re.search(r'HealthIndicator\s*\{.*?\}\s*ActivityIndicator\s*\{', source, re.S)
|
||||||
|
assert match is not None
|
||||||
|
PY
|
||||||
|
|
||||||
|
! rg -n '#[0-9a-fA-F]{3,8}' "$bar_dir/HealthIndicator.qml" >/dev/null \
|
||||||
|
|| fail 'health indicator introduced colors outside Theme'
|
||||||
|
! rg -n 'Behavior|Animation|Transition|pulse|shimmer' "$bar_dir/HealthIndicator.qml" >/dev/null \
|
||||||
|
|| fail 'health indicator introduced motion'
|
||||||
[[ ! -e "$settings_dir/ServicesPage.qml" ]] || fail 'ServicesPage.qml still exists'
|
[[ ! -e "$settings_dir/ServicesPage.qml" ]] || fail 'ServicesPage.qml still exists'
|
||||||
rg -Fq 'HealthPage 1.0 HealthPage.qml' "$settings_dir/qmldir" \
|
rg -Fq 'HealthPage 1.0 HealthPage.qml' "$settings_dir/qmldir" \
|
||||||
|| fail 'HealthPage is not registered in the Settings QML module'
|
|| fail 'HealthPage is not registered in the Settings QML module'
|
||||||
@@ -108,6 +124,8 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
|
||||||
|
import "modules/bar" as BarModule
|
||||||
|
import qs.config
|
||||||
import qs.modules.settings
|
import qs.modules.settings
|
||||||
import qs.services
|
import qs.services
|
||||||
|
|
||||||
@@ -130,6 +148,11 @@ ShellRoot {
|
|||||||
id: settingsShell
|
id: settingsShell
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BarModule.HealthIndicator {
|
||||||
|
id: healthIndicator
|
||||||
|
objectName: "health-indicator"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
@@ -142,6 +165,58 @@ ShellRoot {
|
|||||||
function request(id: string): bool {
|
function request(id: string): bool {
|
||||||
return settingsShell.requestHealthAction(id);
|
return settingsShell.requestHealthAction(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function indicatorMode(mode: string): bool {
|
||||||
|
const checkStatus = mode === "healthy" ? "ok" : (mode === "unconfigured" ? "unconfigured" : mode);
|
||||||
|
const overallStatus = mode === "warning" || mode === "error" ? mode : "healthy";
|
||||||
|
const snapshot = {
|
||||||
|
schemaVersion: 1,
|
||||||
|
generatedAt: "2026-08-18T12:00:00Z",
|
||||||
|
summary: {
|
||||||
|
status: overallStatus,
|
||||||
|
healthy: mode === "healthy" ? 1 : 0,
|
||||||
|
warnings: mode === "warning" ? 1 : 0,
|
||||||
|
errors: mode === "error" ? 1 : 0,
|
||||||
|
unconfigured: mode === "unconfigured" ? 1 : 0
|
||||||
|
},
|
||||||
|
context: { session: "hyprland", versions: [] },
|
||||||
|
checks: [{
|
||||||
|
id: "desktop.vicinae",
|
||||||
|
group: "desktop-foundation",
|
||||||
|
title: "Vicinae",
|
||||||
|
status: checkStatus,
|
||||||
|
detail: "Fixture observation."
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
return Health.consumeSnapshot(JSON.stringify(snapshot), Health.acceptedGeneration + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function indicatorState(): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
visible: healthIndicator.visible,
|
||||||
|
width: healthIndicator.width,
|
||||||
|
implicitWidth: healthIndicator.implicitWidth,
|
||||||
|
issueCount: healthIndicator.issueCount,
|
||||||
|
statusText: healthIndicator.statusText,
|
||||||
|
accessibleLabel: healthIndicator.accessibleLabel,
|
||||||
|
tooltipText: healthIndicator.tooltipText,
|
||||||
|
warningTone: healthIndicator.tone === Theme.warn,
|
||||||
|
errorTone: healthIndicator.tone === Theme.danger,
|
||||||
|
activeFocusOnTab: healthIndicator.activeFocusOnTab
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function activateIndicator(): string {
|
||||||
|
ShellState.settingsPage = "home";
|
||||||
|
ShellState.settingsOpen = false;
|
||||||
|
const generation = Health.generation;
|
||||||
|
healthIndicator.activated();
|
||||||
|
return JSON.stringify({
|
||||||
|
page: ShellState.settingsPage,
|
||||||
|
settingsOpen: ShellState.settingsOpen,
|
||||||
|
refreshRequested: Health.generation > generation || Health.queuedRefresh
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QML
|
QML
|
||||||
@@ -231,6 +306,54 @@ jq -e '
|
|||||||
' \
|
' \
|
||||||
>/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet'
|
>/dev/null <<<"$confirmation_state" || fail 'Quickshell restart did not open confirmation sheet'
|
||||||
|
|
||||||
|
for hidden_mode in healthy unconfigured; do
|
||||||
|
[[ "$(run ipc call health-ui-test indicatorMode "$hidden_mode")" == "true" ]] \
|
||||||
|
|| fail "$hidden_mode indicator fixture was rejected"
|
||||||
|
hidden_state="$(run ipc call health-ui-test indicatorState)"
|
||||||
|
jq -e '
|
||||||
|
.visible == false
|
||||||
|
and .width == 0
|
||||||
|
and .implicitWidth == 0
|
||||||
|
and .issueCount == 0
|
||||||
|
' >/dev/null <<<"$hidden_state" \
|
||||||
|
|| fail "$hidden_mode state reserved bar space: $hidden_state"
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ "$(run ipc call health-ui-test indicatorMode warning)" == "true" ]] \
|
||||||
|
|| fail 'warning indicator fixture was rejected'
|
||||||
|
warning_state="$(run ipc call health-ui-test indicatorState)"
|
||||||
|
jq -e '
|
||||||
|
.visible == true
|
||||||
|
and .width > 0
|
||||||
|
and .implicitWidth > 0
|
||||||
|
and .issueCount == 1
|
||||||
|
and .statusText == "1 system health issue"
|
||||||
|
and .accessibleLabel == "System Health: 1 issue needs attention"
|
||||||
|
and .tooltipText == "System Health: 1 issue needs attention"
|
||||||
|
and .warningTone == true
|
||||||
|
and .errorTone == false
|
||||||
|
and .activeFocusOnTab == true
|
||||||
|
' >/dev/null <<<"$warning_state" || fail "warning indicator is not the approved amber accessible capsule: $warning_state"
|
||||||
|
|
||||||
|
[[ "$(run ipc call health-ui-test indicatorMode error)" == "true" ]] \
|
||||||
|
|| fail 'error indicator fixture was rejected'
|
||||||
|
error_state="$(run ipc call health-ui-test indicatorState)"
|
||||||
|
jq -e '
|
||||||
|
.visible == true
|
||||||
|
and .issueCount == 1
|
||||||
|
and .accessibleLabel == "System Health: 1 issue requires action"
|
||||||
|
and .tooltipText == "System Health: 1 issue requires action"
|
||||||
|
and .warningTone == false
|
||||||
|
and .errorTone == true
|
||||||
|
' >/dev/null <<<"$error_state" || fail "error indicator is not the approved red accessible capsule: $error_state"
|
||||||
|
|
||||||
|
activation_state="$(run ipc call health-ui-test activateIndicator)"
|
||||||
|
jq -e '
|
||||||
|
.page == "services"
|
||||||
|
and .settingsOpen == true
|
||||||
|
and .refreshRequested == true
|
||||||
|
' >/dev/null <<<"$activation_state" || fail "indicator activation did not open and refresh System Health: $activation_state"
|
||||||
|
|
||||||
if rg -i 'QQml|ReferenceError|TypeError|binding loop|failed to load component' "$shell_log"; then
|
if rg -i 'QQml|ReferenceError|TypeError|binding loop|failed to load component' "$shell_log"; then
|
||||||
fail 'isolated fixture emitted QML errors or warnings'
|
fail 'isolated fixture emitted QML errors or warnings'
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ assert_commands 'qs <ipc> <call> <overview> <open>'
|
|||||||
run_action settings
|
run_action settings
|
||||||
assert_commands 'qs <ipc> <call> <settings> <open>'
|
assert_commands 'qs <ipc> <call> <settings> <open>'
|
||||||
|
|
||||||
|
run_action health
|
||||||
|
assert_commands 'qs <ipc> <call> <health> <open>'
|
||||||
|
|
||||||
run_action dnd
|
run_action dnd
|
||||||
assert_commands $'qs <ipc> <call> <notifications> <dnd>\npanama-osd <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
assert_commands $'qs <ipc> <call> <notifications> <dnd>\npanama-osd <message> <notifications-disabled-symbolic> <Do Not Disturb On>'
|
||||||
|
|
||||||
@@ -191,11 +194,13 @@ assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-o
|
|||||||
PANAMA_ACTION_TEST_FAIL_KILL=true run_action restart-shell
|
PANAMA_ACTION_TEST_FAIL_KILL=true run_action restart-shell
|
||||||
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
|
assert_commands $'qs <kill>\nquickshell <--daemonize>\nqs <ipc> <show>\npanama-osd <message> <view-refresh-symbolic> <Panama Restarted>'
|
||||||
|
|
||||||
|
usage_output="$work/usage.txt"
|
||||||
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_HELPER_DIR="$fake_bin" \
|
if HOME="$work/home" PATH="$fake_bin:$PATH" PANAMA_ACTION_HELPER_DIR="$fake_bin" \
|
||||||
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
PANAMA_ACTION_CONFIRM_ATTEMPTS=1 PANAMA_ACTION_CONFIRM_DELAY=0 PANAMA_ACTION_TEST_LOG="$command_log" \
|
||||||
"$dispatcher" definitely-not-an-action >/dev/null 2>&1; then
|
"$dispatcher" definitely-not-an-action > /dev/null 2>"$usage_output"; then
|
||||||
fail 'unknown action was accepted'
|
fail 'unknown action was accepted'
|
||||||
fi
|
fi
|
||||||
|
rg -q '\bhealth\b' "$usage_output" || fail 'health is missing from dispatcher usage'
|
||||||
|
|
||||||
: >"$command_log"
|
: >"$command_log"
|
||||||
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
|
if PANAMA_ACTION_TEST_FAIL_QS=true HOME="$work/home" PATH="$fake_bin:$PATH" \
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ declare -A expected=(
|
|||||||
[open-clipboard.sh]=clipboard
|
[open-clipboard.sh]=clipboard
|
||||||
[open-mission-control.sh]=overview
|
[open-mission-control.sh]=overview
|
||||||
[open-settings.sh]=settings
|
[open-settings.sh]=settings
|
||||||
|
[check-system-health.sh]=health
|
||||||
[toggle-dnd.sh]=dnd
|
[toggle-dnd.sh]=dnd
|
||||||
[toggle-caffeine.sh]=caffeine
|
[toggle-caffeine.sh]=caffeine
|
||||||
[toggle-night-light.sh]=night-light
|
[toggle-night-light.sh]=night-light
|
||||||
@@ -72,6 +73,17 @@ for script_name in "${!expected[@]}"; do
|
|||||||
grep -Fxq '# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg' "$script" \
|
grep -Fxq '# @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg' "$script" \
|
||||||
|| fail "$script_name does not use the Panama application identity"
|
|| fail "$script_name does not use the Panama application identity"
|
||||||
|
|
||||||
|
if [[ $script_name == check-system-health.sh ]]; then
|
||||||
|
[[ $title == 'Panama: Check System Health' ]] \
|
||||||
|
|| fail "health command has the wrong title: $title"
|
||||||
|
grep -Fxq '# @vicinae.schemaVersion 1' "$script" \
|
||||||
|
|| fail 'health command does not use schema version 1'
|
||||||
|
grep -Fxq '# @vicinae.keywords ["health", "doctor", "repair", "services"]' "$script" \
|
||||||
|
|| fail 'health command has the wrong search vocabulary'
|
||||||
|
grep -Fxq 'exec "$HOME/.config/quickshell/scripts/panama-action" health' "$script" \
|
||||||
|
|| fail 'health command bypasses the stable dispatcher path'
|
||||||
|
fi
|
||||||
|
|
||||||
: >"$dispatch_log"
|
: >"$dispatch_log"
|
||||||
HOME="$work/home" PANAMA_COMMAND_TEST_LOG="$dispatch_log" "$script"
|
HOME="$work/home" PANAMA_COMMAND_TEST_LOG="$dispatch_log" "$script"
|
||||||
dispatched="$(cat "$dispatch_log")"
|
dispatched="$(cat "$dispatch_log")"
|
||||||
|
|||||||
Reference in New Issue
Block a user