feat(web): the Dashboard shows the audio wiring verdict
An Audio wiring card (Windows hosts) below the status tiles: a readiness badge (Ready / No microphone / No game audio / Not wired), the friendly names carrying each role, and the degradation notes that were previously visible only in the host log — mic withheld for game audio, the known- degraded last resort, a narrowing endpoint. api/openapi.json regenerated from the host build (AudioWiring + RuntimeStatus.audio); en+de messages.
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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<{
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 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 && <AudioWiringCard audio={s.audio} />}
|
||||
|
||||
{/* Above the session card: a game the host is about to close is the most
|
||||
time-sensitive thing on this page. */}
|
||||
<RunningGames
|
||||
@@ -193,6 +200,57 @@ export const DashboardView: FC<{
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* One line per role plus a readiness badge; the degradation notes are spelled out because the
|
||||
* failure they describe (silent audio, a mic that quietly vanished) is invisible everywhere
|
||||
* else except the host log.
|
||||
*/
|
||||
const AudioWiringCard: FC<{ audio: AudioWiring }> = ({ 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 (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0">
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Volume2 className="size-4" />
|
||||
{m.audio_wiring_title()}
|
||||
</CardTitle>
|
||||
<Badge variant={badge.variant}>{badge.text}</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<dl className="grid gap-4 sm:grid-cols-2">
|
||||
<Field
|
||||
label={m.audio_output()}
|
||||
value={audio.loopback ?? m.audio_unavailable()}
|
||||
/>
|
||||
<Field
|
||||
label={m.audio_microphone()}
|
||||
value={audio.mic ?? m.audio_unavailable()}
|
||||
/>
|
||||
</dl>
|
||||
{notes.length > 0 && (
|
||||
<ul className="flex flex-col gap-1 text-sm text-muted-foreground">
|
||||
{notes.map((n) => (
|
||||
<li key={n}>{n}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const StatCard: FC<{ icon: ReactNode; label: string; on: boolean }> = ({
|
||||
icon,
|
||||
label,
|
||||
|
||||
Reference in New Issue
Block a user