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 { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { apiClient } from '../client';
|
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 ──
|
// ── Query Keys ──
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@ export const systemKeys = {
|
||||||
health: ['system', 'health'] as const,
|
health: ['system', 'health'] as const,
|
||||||
apiKey: ['system', 'apikey'] as const,
|
apiKey: ['system', 'apikey'] as const,
|
||||||
appSettings: ['system', 'appSettings'] as const,
|
appSettings: ['system', 'appSettings'] as const,
|
||||||
|
ytdlpStatus: ['system', 'ytdlpStatus'] as const,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Queries ──
|
// ── 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 { RefreshCw, Server, Activity, Cpu, HardDrive, Download, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
|
||||||
import { useSystemStatus, useHealth } from '../api/hooks/useSystem';
|
import { useSystemStatus, useHealth, useYtDlpStatus, useUpdateYtDlp } from '../api/hooks/useSystem';
|
||||||
import { HealthStatus } from '../components/HealthStatus';
|
import { HealthStatus } from '../components/HealthStatus';
|
||||||
import { SkeletonSystem } from '../components/Skeleton';
|
import { SkeletonSystem } from '../components/Skeleton';
|
||||||
import { formatBytes } from '../utils/format';
|
import { formatBytes } from '../utils/format';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
// ── Helpers ──
|
// ── Helpers ──
|
||||||
|
|
||||||
function formatUptime(seconds: number): string {
|
function formatUptime(seconds: number): string {
|
||||||
|
|
@ -23,6 +25,9 @@ function formatUptime(seconds: number): string {
|
||||||
export function SystemPage() {
|
export function SystemPage() {
|
||||||
const { data: health, isLoading: healthLoading, error: healthError, refetch: refetchHealth } = useHealth();
|
const { data: health, isLoading: healthLoading, error: healthError, refetch: refetchHealth } = useHealth();
|
||||||
const { data: status, isLoading: statusLoading, error: statusError, refetch: refetchStatus } = useSystemStatus();
|
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;
|
const isLoading = healthLoading || statusLoading;
|
||||||
|
|
||||||
|
|
@ -84,6 +89,105 @@ export function SystemPage() {
|
||||||
) : null}
|
) : null}
|
||||||
</section>
|
</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 ── */}
|
{/* ── System Status section ── */}
|
||||||
<section>
|
<section>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-4)' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 'var(--space-4)' }}>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue