mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
feat(orbit): in-app diagnostics popover with copyable event log (#524)
* feat(orbit): in-app diagnostics popover with copyable event log Multiple users on Discord report Orbit guests stopping after the first song with no errors anywhere — Settings → Debug → Export Logs is too buried for non-technical reporters, and the relevant code branches have no logging at all (silent fail). This adds a one-click "Copy log" path right inside the Orbit session bar. The new Activity-icon button next to Help opens a popover with: - Live mini-display: role, host vs. guest track id + position, drift, age of the host's last state write — all updating once a second. - Scrolling event log textarea fed by an in-memory ring (200 events). - Copy + Clear buttons. Copy formats `[ISO] [scope] body` lines and drops them on the clipboard — paste straight into a Discord report. Instrumentation lands at the previously-silent decision points: - Guest pull tick: full snapshot of host vs. guest state on every read. - Each branch of the divergence detection in `useOrbitGuest.ts` logs which path it took and why (initial / track-change-followed / track-change-diverged / play-pause-flip), making the "stuck after first song" symptom diagnosable from the buffer alone. - Host pushes log track id, isPlaying, queue length, guest count. Events are also bridged to the existing `frontend_debug_log` Tauri command when Settings → Logging is on Debug, so power users still get the same data in `psysonic-logs-*.log` for offline triage. i18n: full `orbit.diag.*` namespace in all eight locales. EN + DE are native; ES / FR / NB / NL / RU / ZH are first-pass and may want a polish from native speakers later. * docs(changelog): add orbit diagnostics popover entry
This commit is contained in:
committed by
GitHub
parent
acadc1be34
commit
a702a5dd5b
@@ -8,6 +8,7 @@ import {
|
||||
writeOrbitHeartbeat,
|
||||
} from '../utils/orbit';
|
||||
import { orbitOutboxPlaylistName, estimateLivePosition, type OrbitState } from '../api/orbit';
|
||||
import { pushOrbitEvent } from '../utils/orbitDiag';
|
||||
|
||||
/**
|
||||
* Orbit — guest-side tick hook.
|
||||
@@ -130,6 +131,7 @@ export function useOrbitGuest(): void {
|
||||
// explicit `state.ended` branch does; the store still holds the last
|
||||
// known state so the modal can render the host + session name copy.
|
||||
// Outbox cleanup runs from the modal's OK handler via leaveOrbitSession.
|
||||
pushOrbitEvent('pull', 'state read returned null — playlist gone, ending session');
|
||||
useOrbitStore.getState().setPhase('ended');
|
||||
return;
|
||||
}
|
||||
@@ -203,6 +205,12 @@ export function useOrbitGuest(): void {
|
||||
const hostPlaying = state.isPlaying;
|
||||
const last = lastAppliedRef.current;
|
||||
|
||||
pushOrbitEvent('pull', JSON.stringify({
|
||||
host: { track: hostTrackId, playing: hostPlaying, posMs: state.positionMs, posAt: state.positionAt },
|
||||
guest: { track: player.currentTrack?.id ?? null, playing: player.isPlaying, posSec: Math.round(player.currentTime ?? 0) },
|
||||
last,
|
||||
}));
|
||||
|
||||
if (!last) {
|
||||
// Initial sync: only record `last` *after* syncToHost actually
|
||||
// landed. If the first attempt loses the race (engine not ready,
|
||||
@@ -211,9 +219,12 @@ export function useOrbitGuest(): void {
|
||||
// failed sync set `last` anyway and the guest was stuck on their
|
||||
// pre-join state until they clicked Catch Up.
|
||||
if (hostTrackId) {
|
||||
pushOrbitEvent('initial-sync', `attempting initial sync to ${hostTrackId} (hostPlaying=${hostPlaying})`);
|
||||
const ok = await syncToHost(hostTrackId, state);
|
||||
pushOrbitEvent('initial-sync', `result: ${ok ? 'success' : 'failed (will retry)'}`);
|
||||
if (ok) lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
||||
} else {
|
||||
pushOrbitEvent('initial-sync', 'host has no current track yet, anchor only');
|
||||
lastAppliedRef.current = { trackId: null, isPlaying: hostPlaying };
|
||||
}
|
||||
} else if (last.trackId !== hostTrackId) {
|
||||
@@ -222,11 +233,15 @@ export function useOrbitGuest(): void {
|
||||
// Guest is running their own show (typically: paused while host
|
||||
// kept going). Do not load/start the host's new track — just
|
||||
// track the host state so the catch-up prompt stays accurate.
|
||||
pushOrbitEvent('track-change',
|
||||
`host: ${last.trackId} → ${hostTrackId} BUT guest diverged (player.isPlaying=${player.isPlaying} ≠ last.isPlaying=${last.isPlaying}) — NOT loading new track`);
|
||||
lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
||||
} else if (hostTrackId) {
|
||||
pushOrbitEvent('track-change', `host: ${last.trackId} → ${hostTrackId}, guest in sync, following`);
|
||||
void syncToHost(hostTrackId, state);
|
||||
lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
||||
} else {
|
||||
pushOrbitEvent('track-change', `host cleared current track, pausing guest`);
|
||||
if (player.isPlaying) player.pause();
|
||||
lastAppliedRef.current = { trackId: hostTrackId, isPlaying: hostPlaying };
|
||||
}
|
||||
@@ -234,7 +249,10 @@ export function useOrbitGuest(): void {
|
||||
// Only mirror when the guest hasn't diverged. We compare against the
|
||||
// *last applied* host state, not the new one — divergence means the
|
||||
// local player no longer matches what we last pushed in.
|
||||
if (player.isPlaying === last.isPlaying) {
|
||||
const localMatchesLast = player.isPlaying === last.isPlaying;
|
||||
pushOrbitEvent('play-pause-flip',
|
||||
`host: ${last.isPlaying} → ${hostPlaying}, guest matches last=${localMatchesLast} (will ${localMatchesLast ? 'mirror' : 'skip'})`);
|
||||
if (localMatchesLast) {
|
||||
if (hostPlaying) player.resume();
|
||||
else player.pause();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user