feat(tray): system-tray status icon for the host (Windows + Linux)
New crates/punktfunk-tray — a small per-user companion showing the host service state at a glance (running / stopped / starting / degraded / failed + the live session in the tooltip) with one-click actions: open web console, approve a pending pairing request, start/stop/restart, open logs. No more digging through logs to learn whether the service came back after a reboot or an update. Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can never fake Running), then the new loopback-only unauthenticated GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth). Windows: windows_subsystem binary (a console exe in the Run key would flash a terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single instance, TaskbarCreated re-add, --quit for the uninstaller; service actions elevate per click via ShellExecuteW "runas" onto the new `punktfunk-host service restart` (stop → wait Stopped → start). Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit), /etc/xdg/autostart entry whose --autostart self-gates to actual host users. Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons. Live-validated: Linux on the headless KDE session (SNI registration, state transitions, menu-driven start, dbusmenu layout); Windows on the RTX box (session-1 launch with no NIM_ADD failure, single instance, --quit, restart round-trip, summary loopback-200/LAN-401). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -578,6 +578,41 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/local/summary": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"host"
|
||||
],
|
||||
"summary": "Local status summary for the tray icon",
|
||||
"description": "Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device\nnames). Unauthenticated, but served to loopback peers only.",
|
||||
"operationId": "getLocalSummary",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Non-sensitive local host status (loopback peers only)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/LocalSummary"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Non-loopback peer",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/logs": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -2083,6 +2118,66 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"LocalSummary": {
|
||||
"type": "object",
|
||||
"description": "Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,\nno fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see\n`require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on Windows, so the\nper-user tray process cannot authenticate — this narrow read-only route is its status source.",
|
||||
"required": [
|
||||
"version",
|
||||
"video_streaming",
|
||||
"audio_streaming",
|
||||
"paired_clients",
|
||||
"native_paired_clients",
|
||||
"pin_pending",
|
||||
"pending_approvals"
|
||||
],
|
||||
"properties": {
|
||||
"audio_streaming": {
|
||||
"type": "boolean",
|
||||
"description": "True while the audio stream thread is running."
|
||||
},
|
||||
"native_paired_clients": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "Number of paired native (punktfunk/1) devices.",
|
||||
"minimum": 0
|
||||
},
|
||||
"paired_clients": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "Number of pinned (paired) GameStream client certificates.",
|
||||
"minimum": 0
|
||||
},
|
||||
"pending_approvals": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "Native pairing knocks awaiting the operator's approval (count only).",
|
||||
"minimum": 0
|
||||
},
|
||||
"pin_pending": {
|
||||
"type": "boolean",
|
||||
"description": "True while a GameStream pairing handshake is parked waiting for the user's PIN."
|
||||
},
|
||||
"session": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "null"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/SessionInfo",
|
||||
"description": "The active launch session (set by Moonlight's `/launch`, cleared on cancel/stop)."
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "Host version (mirrors `/health`)."
|
||||
},
|
||||
"video_streaming": {
|
||||
"type": "boolean",
|
||||
"description": "True while the video stream thread is running."
|
||||
}
|
||||
}
|
||||
},
|
||||
"LogEntry": {
|
||||
"type": "object",
|
||||
"description": "One captured log event.",
|
||||
|
||||
Reference in New Issue
Block a user