Login
diff --git a/src/UI/App/src/pages/RadioAdmin.tsx b/src/UI/App/src/pages/RadioAdmin.tsx
index f72f6ba..260225b 100644
--- a/src/UI/App/src/pages/RadioAdmin.tsx
+++ b/src/UI/App/src/pages/RadioAdmin.tsx
@@ -1,6 +1,8 @@
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
+import { useNavigate } from '@tanstack/react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
+import { useAuth } from '../hooks/useAuth';
import type { RadioSource } from '../interfaces/common.interfaces';
import {
loadRadioSources,
@@ -17,6 +19,8 @@ export function RadioAdmin() {
null,
);
const queryClient = useQueryClient();
+ const navigate = useNavigate();
+ const { logout } = useAuth();
const {
data: radioStations,
@@ -27,6 +31,16 @@ export function RadioAdmin() {
queryFn: loadRadioSources,
});
+ // The route guard only checks that a token exists; an expired one still
+ // reaches this page and fails with 401, so send the user back to login.
+ const unauthorized = error !== null && String(error).includes('401');
+ useEffect(() => {
+ if (unauthorized) {
+ logout();
+ navigate({ to: '/login' });
+ }
+ }, [unauthorized, logout, navigate]);
+
const deleteMutation = useMutation({
mutationFn: deleteRadioSource,
onSuccess: () => {
@@ -74,6 +88,7 @@ export function RadioAdmin() {
});
if (isLoading) return
;
+ if (unauthorized) return null;
if (error) return
;
if (!radioStations) return null;
diff --git a/src/UI/App/src/pages/UserStats.tsx b/src/UI/App/src/pages/UserStats.tsx
index 2fbf924..1bd8e33 100644
--- a/src/UI/App/src/pages/UserStats.tsx
+++ b/src/UI/App/src/pages/UserStats.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { Fragment, useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import {
PieChart,
@@ -9,6 +9,7 @@ import {
Tooltip,
} from 'recharts';
import { loadUsers } from '../services/user-service';
+import { cleanTitle } from '../services/song-stats-service';
import { LoadingSpinner } from '../components/LoadingSpinner';
import { AppError } from '../components/AppError';
@@ -25,6 +26,7 @@ const COLORS = [
export function UserStats() {
const [viewMode, setViewMode] = useState<'table' | 'chart'>('table');
+ const [expandedUser, setExpandedUser] = useState
(null);
const {
data: userStats,
@@ -109,32 +111,82 @@ export function UserStats() {
{userStats.slice(0, 10).map((user, index) => (
-
- | {index + 1} |
-
-
-
- {user.username.slice(0, 2)}
+
+
+ setExpandedUser(
+ expandedUser === user.username ? null : user.username,
+ )
+ }
+ style={{ cursor: 'pointer' }}
+ title="Click to see recently played songs"
+ >
+ | {index + 1} |
+
+
+
+ {user.username.slice(0, 2)}
+
+
+ {user.username}
+
+ {user.displayName}
+
+
-
- {user.username}
- #{user.displayName}
-
-
- |
-
- {user.totalPlays}
- |
-
- {user.uniqueSongs}
- |
- {String(user.memberSince).split('T')[0]} |
-
- {user.lastPlayed
- ? String(user.lastPlayed).split('T')[0]
- : ''}
- |
-
+ |
+
+ {user.totalPlays}
+ |
+
+ {user.uniqueSongs}
+ |
+ {String(user.memberSince).split('T')[0]} |
+
+ {user.lastPlayed
+ ? String(user.lastPlayed).split('T')[0]
+ : ''}
+ |
+
+ {expandedUser === user.username && (
+
+
+ {user.recentSongs.length === 0 ? (
+
+ No songs played yet.
+
+ ) : (
+
+
+
+ | Recently Played |
+ Plays |
+ Last Played |
+
+
+
+ {user.recentSongs.map((song, songIndex) => (
+
+ |
+
+ {cleanTitle(song.title)}
+
+ |
+
+
+ {song.totalPlays}
+
+ |
+ {song.playedAt.split('T')[0]} |
+
+ ))}
+
+
+ )}
+ |
+
+ )}
+
))}
diff --git a/src/UI/App/src/services/radio-source-service.ts b/src/UI/App/src/services/radio-source-service.ts
index abaa57e..d1c3131 100644
--- a/src/UI/App/src/services/radio-source-service.ts
+++ b/src/UI/App/src/services/radio-source-service.ts
@@ -66,6 +66,12 @@ export async function addRadioSource(
if (response.ok) {
return await response.json();
}
- const errorText = await response.json();
- throw new Error(JSON.stringify(errorText));
+ let message = `HTTP error! status: ${response.status}`;
+ try {
+ const body = await response.json();
+ message = body?.error ?? body?.detail ?? message;
+ } catch {
+ // Non-JSON error body (e.g. empty 401 response); keep the status message.
+ }
+ throw new Error(message);
}
diff --git a/src/UI/App/src/services/song-stats-service.ts b/src/UI/App/src/services/song-stats-service.ts
index 6af1825..364798a 100644
--- a/src/UI/App/src/services/song-stats-service.ts
+++ b/src/UI/App/src/services/song-stats-service.ts
@@ -14,7 +14,8 @@ export async function loadSongStats(): Promise {
return await response.json();
}
-export function cleanTitle(title: string): string {
+export function cleanTitle(title?: string | null): string {
+ if (!title) return '';
const normalized = removeFancyUnicode(title);
return normalized
.replace(/[\s|]*[([|]?\s*(official|lirik)[^)\]|]*[)\]|]?[\s|]*/gi, '')