feat: Added useYtDlpStatus/useUpdateYtDlp hooks and yt-dlp card to Syst…
- "src/frontend/src/api/hooks/useSystem.ts" - "src/frontend/src/pages/System.tsx" GSD-Task: S06/T02
This commit is contained in:
parent
c5820fe957
commit
9f35d06e88
2 changed files with 128 additions and 3 deletions
|
|
@ -1,6 +1,6 @@
|
|||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { apiClient } from '../client';
|
||||
import type { HealthResponse, SystemStatusResponse, ApiKeyResponse, AppSettingsResponse } from '@shared/types/api';
|
||||
import type { HealthResponse, SystemStatusResponse, ApiKeyResponse, AppSettingsResponse, YtDlpStatusResponse, YtDlpUpdateResponse } from '@shared/types/api';
|
||||
|
||||
// ── Query Keys ──
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ export const systemKeys = {
|
|||
health: ['system', 'health'] as const,
|
||||
apiKey: ['system', 'apikey'] as const,
|
||||
appSettings: ['system', 'appSettings'] as const,
|
||||
ytdlpStatus: ['system', 'ytdlpStatus'] as const,
|
||||
};
|
||||
|
||||
// ── Queries ──
|
||||
|
|
@ -70,3 +71,23 @@ export function useUpdateAppSettings() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch yt-dlp version and last-updated timestamp. Auto-refreshes every 60s. */
|
||||
export function useYtDlpStatus() {
|
||||
return useQuery({
|
||||
queryKey: systemKeys.ytdlpStatus,
|
||||
queryFn: () => apiClient.get<YtDlpStatusResponse>('/api/v1/system/ytdlp/status'),
|
||||
refetchInterval: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
/** Trigger a yt-dlp update check. Invalidates the status query on success. */
|
||||
export function useUpdateYtDlp() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => apiClient.post<YtDlpUpdateResponse>('/api/v1/system/ytdlp/update'),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: systemKeys.ytdlpStatus });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
import { RefreshCw, Server, Activity, Cpu, HardDrive } from 'lucide-react';
|
||||
import { useSystemStatus, useHealth } from '../api/hooks/useSystem';
|
||||
import { RefreshCw, Server, Activity, Cpu, HardDrive, Download, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
|
||||
import { useSystemStatus, useHealth, useYtDlpStatus, useUpdateYtDlp } from '../api/hooks/useSystem';
|
||||
import { HealthStatus } from '../components/HealthStatus';
|
||||
import { SkeletonSystem } from '../components/Skeleton';
|
||||
import { formatBytes } from '../utils/format';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
function formatUptime(seconds: number): string {
|
||||
|
|
@ -23,6 +25,9 @@ function formatUptime(seconds: number): string {
|
|||
export function SystemPage() {
|
||||
const { data: health, isLoading: healthLoading, error: healthError, refetch: refetchHealth } = useHealth();
|
||||
const { data: status, isLoading: statusLoading, error: statusError, refetch: refetchStatus } = useSystemStatus();
|
||||
const { data: ytdlpStatus, isLoading: ytdlpLoading, error: ytdlpError } = useYtDlpStatus();
|
||||
const updateYtDlp = useUpdateYtDlp();
|
||||
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
const isLoading = healthLoading || statusLoading;
|
||||
|
||||
|
|
@ -84,6 +89,105 @@ export function SystemPage() {
|
|||
) : null}
|
||||
</section>
|
||||
|
||||
{/* ── yt-dlp section ── */}
|
||||
<section style={{ marginBottom: 'var(--space-8)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-4)' }}>
|
||||
<h2 style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', fontSize: 'var(--font-size-lg)', fontWeight: 600, color: 'var(--text-primary)' }}>
|
||||
<Download size={18} style={{ color: 'var(--accent)' }} />
|
||||
yt-dlp
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: 'var(--bg-card)',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
border: '1px solid var(--border)',
|
||||
padding: 'var(--space-4)',
|
||||
}}
|
||||
>
|
||||
{ytdlpLoading ? (
|
||||
<p style={{ color: 'var(--text-muted)' }}>Loading yt-dlp status…</p>
|
||||
) : ytdlpError ? (
|
||||
<p style={{ color: 'var(--danger)' }}>
|
||||
Failed to load yt-dlp status: {ytdlpError instanceof Error ? ytdlpError.message : 'Unknown error'}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 'var(--space-6)', marginBottom: 'var(--space-4)' }}>
|
||||
<div>
|
||||
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Version</span>
|
||||
<p style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--text-primary)', marginTop: 'var(--space-1)' }}>
|
||||
{ytdlpStatus?.version ?? 'Unknown'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>Last Updated</span>
|
||||
<p style={{ fontFamily: 'var(--font-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--text-primary)', marginTop: 'var(--space-1)' }}>
|
||||
{ytdlpStatus?.lastUpdated ? new Date(ytdlpStatus.lastUpdated).toLocaleString() : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
|
||||
<button
|
||||
className="btn btn-secondary"
|
||||
disabled={updateYtDlp.isPending}
|
||||
onClick={() => {
|
||||
setUpdateMessage(null);
|
||||
updateYtDlp.mutate(undefined, {
|
||||
onSuccess: (data) => {
|
||||
setUpdateMessage({
|
||||
type: 'success',
|
||||
text: data.updated
|
||||
? `Updated to ${data.version} (was ${data.previousVersion})`
|
||||
: `Already up to date (${data.version})`,
|
||||
});
|
||||
},
|
||||
onError: (err) => {
|
||||
setUpdateMessage({
|
||||
type: 'error',
|
||||
text: err instanceof Error ? err.message : 'Update failed',
|
||||
});
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{updateYtDlp.isPending ? (
|
||||
<>
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
Updating…
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RefreshCw size={14} />
|
||||
Check for Updates
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{updateMessage && (
|
||||
<span style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 'var(--space-1)',
|
||||
fontSize: 'var(--font-size-sm)',
|
||||
color: updateMessage.type === 'success' ? 'var(--success)' : 'var(--danger)',
|
||||
}}>
|
||||
{updateMessage.type === 'success' ? <CheckCircle size={14} /> : <AlertCircle size={14} />}
|
||||
{updateMessage.text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p style={{ color: 'var(--text-muted)', fontSize: 'var(--font-size-xs)', marginTop: 'var(--space-2)' }}>
|
||||
Auto-refreshes every 60 seconds.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* ── System Status section ── */}
|
||||
<section>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-4)' }}>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue