feat: now-playing liveness dot + admin-gated radio management (#1086)

* feat(now-playing): liveness indicator dot in the listening popover

Replace the raw "Nm ago" line in the "Who is listening?" popover with a
derived presence dot (green playing / amber paused / dim idle). The presence
is computed in one tested helper that unifies the playbackReport transport
state with the legacy getNowPlaying recency, instead of formatting a raw
timestamp inline. The dot carries the localized status as an aria-label and
tooltip so it is not conveyed by colour alone.

* feat(radio): gate station create/edit/delete behind Navidrome admin role

Navidrome >= 0.62 restricts internet-radio management to admins
(GHSA-jw24-qqrj-633c); non-admin requests fail. Hide Add Station, Search
Directory, the per-card edit chip and delete button for confirmed standard
Navidrome users via a canManageNavidromeRadio() helper on the existing
useNavidromeAdminRole framework. Admins, non-Navidrome servers and transient
states stay unrestricted; playback and favourites remain available to all.

* docs(changelog): now-playing status dot + admin-gated radio (#1086)
This commit is contained in:
Psychotoxical
2026-06-14 01:30:25 +02:00
committed by GitHub
parent 0b7d9eae2d
commit c1403f8bd6
19 changed files with 231 additions and 44 deletions
+13 -1
View File
@@ -8,7 +8,7 @@ vi.mock('@/api/navidromeAdmin', () => ({
}));
import { ndLogin } from '@/api/navidromeAdmin';
import { useNavidromeAdminRole } from './useNavidromeAdminRole';
import { useNavidromeAdminRole, canManageNavidromeRadio } from './useNavidromeAdminRole';
beforeEach(() => {
resetAuthStore();
@@ -112,3 +112,15 @@ describe('useNavidromeAdminRole', () => {
await waitFor(() => expect(result.current).toBe('error'));
});
});
describe('canManageNavidromeRadio', () => {
it('blocks only a confirmed standard Navidrome user', () => {
expect(canManageNavidromeRadio('user')).toBe(false);
});
it('allows admins, non-Navidrome servers, and transient/unknown states', () => {
for (const role of ['admin', 'na', 'idle', 'checking', 'error'] as const) {
expect(canManageNavidromeRadio(role)).toBe(true);
}
});
});