feat(diagnostics): 'playback' kind, newest-first sort, fix active-route subtitle
test-go / test (push) Successful in 32s
test-web / test (push) Successful in 41s
android / Build + lint + test (push) Successful in 3m40s
test-go / integration (push) Successful in 4m30s

Relabel (#1204): route + player_state events fire for every output route,
not just UPnP — split them into a new 'playback' kind; 'upnp_sync' now
means genuinely UPnP/Sonos signal (drops, resync). Migration 0037 adds
'playback' to the kind CHECK; server whitelist, Android reporter labels,
and the web kind filter updated.

Web sort: the diagnostics list gains a Newest/Oldest-first sort (default
newest at top); export follows the displayed order.

Fix (#1205): OutputRoute.isConnected was derived from RouteInfo.connectionState,
which stays DISCONNECTED for local SYSTEM routes even when active — so a
connected Bluetooth device showed "Available" and reported connected:false.
The picker subtitle now uses isSelected (route == selected route); the dead
isConnected field is removed and the misleading `connected` field dropped
from the diagnostics route event (it only ever logs the active route).

Refs Scribe M9 (#119), tasks #1204 #1205.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K55iTxn95BtshocgdE1shW
This commit is contained in:
2026-06-30 16:33:01 -04:00
parent 96b15b75e6
commit 79f2d79a2e
7 changed files with 82 additions and 24 deletions
+31 -5
View File
@@ -22,11 +22,20 @@
const client = useQueryClient();
const KINDS = ['connectivity', 'upnp_sync', 'power', 'lifecycle', 'heartbeat', 'http'] as const;
const KINDS = [
'connectivity',
'playback',
'upnp_sync',
'power',
'lifecycle',
'heartbeat',
'http'
] as const;
function kindLabel(k: string): string {
switch (k) {
case 'connectivity': return 'Connectivity';
case 'playback': return 'Playback';
case 'upnp_sync': return 'UPnP sync';
case 'power': return 'Power';
case 'lifecycle': return 'Lifecycle';
@@ -82,10 +91,17 @@
const devicesQuery = $derived($devicesStore);
const devices = $derived(devicesQuery.data ?? []);
// Display sort. The API returns newest-first; default the view to that
// (most recent at the top), with an Oldest-first option for reading a
// timeline top-to-bottom. Export follows whatever's displayed.
let sortOrder = $state<'newest' | 'oldest'>('newest');
const diagStore = $derived(createAdminDiagnosticsQuery(filter));
const diagQuery = $derived($diagStore);
// API returns newest-first; reverse to chronological for a readable timeline.
const rows = $derived([...((diagQuery.data ?? []) as AdminDiagnostic[])].reverse());
const rows = $derived.by(() => {
const data = [...((diagQuery.data ?? []) as AdminDiagnostic[])]; // newest-first
return sortOrder === 'oldest' ? data.reverse() : data;
});
function fmtTime(iso: string): string {
const d = new Date(iso);
@@ -232,6 +248,16 @@
{/each}
</select>
</label>
<label class="block text-xs text-text-secondary">
Sort
<select
bind:value={sortOrder}
class="mt-1 block w-36 rounded border border-border bg-surface px-2 py-1.5 text-sm text-text-primary"
>
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
</select>
</label>
<div class="ml-auto flex gap-2">
<button
@@ -311,9 +337,9 @@
<div class="flex items-center justify-between">
<span class="text-xs text-text-muted">
{#if hasWindow}
{rows.length} events in window (oldest first)
{rows.length} events in window ({sortOrder === 'newest' ? 'newest' : 'oldest'} first)
{:else}
Most recent {rows.length} events (oldest first) · set a time window under Advanced
Most recent {rows.length} events ({sortOrder === 'newest' ? 'newest' : 'oldest'} first) · set a time window under Advanced
{/if}
</span>
</div>