diff --git a/api/openapi.json b/api/openapi.json
index b62c484c..d463c6cb 100644
--- a/api/openapi.json
+++ b/api/openapi.json
@@ -4045,6 +4045,51 @@
}
}
},
+ "AudioWiring": {
+ "type": "object",
+ "description": "The Windows host's audio wiring verdict — which endpoint carries each role. The names are\nthe endpoints' friendly names as the Sound settings show them (on current hosts the minted\n\"Punktfunk\" instances of Steam's streaming drivers).",
+ "required": [
+ "readiness",
+ "mic_withheld",
+ "last_resort"
+ ],
+ "properties": {
+ "last_resort": {
+ "type": "boolean",
+ "description": "The loopback is the known-degraded last resort — desktop audio may be silent until the\nendpoint set changes."
+ },
+ "loopback": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Friendly name of the desktop-audio loopback source; absent = desktop audio unavailable."
+ },
+ "mic": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Friendly name of the virtual-mic write target; absent = mic passthrough unavailable."
+ },
+ "mic_withheld": {
+ "type": "boolean",
+ "description": "The mic was WITHHELD so game audio could keep the only working sink — mic passthrough\nneeds Steam installed (the host mints its own microphone) or a virtual cable."
+ },
+ "narrowing": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Why the chosen loopback endpoint NARROWS the desktop mix (rate/channels), when it does."
+ },
+ "readiness": {
+ "type": "string",
+ "description": "`full` | `audio_only` | `mic_only` | `none` — whether desktop audio and mic passthrough\neach have an endpoint at all.",
+ "example": "full"
+ }
+ }
+ },
"AvailableCompositor": {
"type": "object",
"description": "A compositor backend the host can drive a virtual output on, and whether it's usable now.",
@@ -6805,6 +6850,17 @@
"description": "Number of live streaming sessions across BOTH planes (GameStream + native punktfunk/1). The\nnative server admits concurrent sessions, so this can exceed 1; `session`/`stream` below\ndescribe a single representative session for the detail card.",
"minimum": 0
},
+ "audio": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/AudioWiring",
+ "description": "The audio wiring verdict (Windows hosts; absent on other platforms and before the first\nwiring pass). Present even while idle — the wiring exists for the host's lifetime."
+ }
+ ]
+ },
"audio_streaming": {
"type": "boolean",
"description": "True while the audio stream thread is running."
diff --git a/web/messages/de.json b/web/messages/de.json
index 6e29a9e8..56f2cb49 100644
--- a/web/messages/de.json
+++ b/web/messages/de.json
@@ -73,6 +73,16 @@
"status_paired_count": "Gekoppelte Geräte",
"status_pin_waiting": "Wartet",
"status_pin_none": "Keine",
+ "audio_wiring_title": "Audio-Verkabelung",
+ "audio_output": "Spielaudio",
+ "audio_microphone": "Mikrofon",
+ "audio_unavailable": "Nicht verfügbar",
+ "audio_ready": "Bereit",
+ "audio_ready_no_mic": "Kein Mikrofon",
+ "audio_no_output": "Kein Spielaudio",
+ "audio_none": "Nicht verkabelt",
+ "audio_mic_withheld": "Der Mikrofon-Endpunkt überträgt gerade das Spielaudio — installiere Steam, damit der Host sein eigenes Mikrofon anlegen kann (Steam muss nie laufen).",
+ "audio_last_resort": "Spielaudio läuft über einen eingeschränkten Ersatz-Endpunkt und kann stumm bleiben, bis ein Ausgabegerät erscheint.",
"status_pin_pending": "Kopplungs-PIN ausstehend",
"stream_codec": "Codec",
"stream_resolution": "Auflösung",
diff --git a/web/messages/en.json b/web/messages/en.json
index 3e5f70f2..da6286ae 100644
--- a/web/messages/en.json
+++ b/web/messages/en.json
@@ -73,6 +73,16 @@
"status_paired_count": "Paired clients",
"status_pin_waiting": "Waiting",
"status_pin_none": "None",
+ "audio_wiring_title": "Audio wiring",
+ "audio_output": "Game audio",
+ "audio_microphone": "Microphone",
+ "audio_unavailable": "Unavailable",
+ "audio_ready": "Ready",
+ "audio_ready_no_mic": "No microphone",
+ "audio_no_output": "No game audio",
+ "audio_none": "Not wired",
+ "audio_mic_withheld": "The microphone endpoint is carrying game audio — install Steam so the host can mint its own microphone (Steam never has to run).",
+ "audio_last_resort": "Game audio is on a degraded fallback endpoint and may be silent until an output device appears.",
"status_pin_pending": "Pairing PIN pending",
"stream_codec": "Codec",
"stream_resolution": "Resolution",
diff --git a/web/src/sections/Dashboard/view.tsx b/web/src/sections/Dashboard/view.tsx
index 81d3dd9b..792b3994 100644
--- a/web/src/sections/Dashboard/view.tsx
+++ b/web/src/sections/Dashboard/view.tsx
@@ -2,6 +2,7 @@ import Section from "@unom/ui/section";
import { MonitorPlay, RefreshCw, Video, Volume2, ZapOff } from "lucide-react";
import type { FC, ReactNode } from "react";
import type { ActiveGame } from "@/api/gen/model/activeGame";
+import type { AudioWiring } from "@/api/gen/model/audioWiring";
import type { GameEntry } from "@/api/gen/model/gameEntry";
import type { RuntimeStatus } from "@/api/gen/model/runtimeStatus";
import { QueryState } from "@/components/query-state";
@@ -87,6 +88,12 @@ export const DashboardView: FC<{
+ {/* The wiring verdict (Windows hosts): WHICH endpoints carry game
+ audio and the microphone, and the degradations that used to be
+ visible only in the host log — a silent host looks identical to a
+ quiet game without this. */}
+ {s.audio && }
+
{/* Above the session card: a game the host is about to close is the most
time-sensitive thing on this page. */}
= ({ audio }) => {
+ const badge: { variant: "success" | "secondary" | "destructive"; text: string } =
+ audio.readiness === "full"
+ ? { variant: "success", text: m.audio_ready() }
+ : audio.readiness === "audio_only"
+ ? { variant: "secondary", text: m.audio_ready_no_mic() }
+ : audio.readiness === "mic_only"
+ ? { variant: "destructive", text: m.audio_no_output() }
+ : { variant: "destructive", text: m.audio_none() };
+ const notes = [
+ audio.mic_withheld ? m.audio_mic_withheld() : undefined,
+ audio.last_resort ? m.audio_last_resort() : undefined,
+ audio.narrowing,
+ ].filter((n): n is string => !!n);
+ return (
+
+
+
+
+ {m.audio_wiring_title()}
+
+ {badge.text}
+
+
+
+
+
+
+ {notes.length > 0 && (
+
+ {notes.map((n) => (
+ - {n}
+ ))}
+
+ )}
+
+
+ );
+};
+
const StatCard: FC<{ icon: ReactNode; label: string; on: boolean }> = ({
icon,
label,