refactor: fold yt-dlp update controls into existing Health card, remove standalone section

This commit is contained in:
jlightner 2026-04-03 07:30:25 +00:00
parent 9f35d06e88
commit f26db65121
2 changed files with 130 additions and 134 deletions

View file

@ -1,6 +1,9 @@
import { AlertTriangle, CheckCircle2, HardDrive, Play, Square, Terminal } from 'lucide-react';
import { useState } from 'react';
import { AlertTriangle, CheckCircle2, HardDrive, Loader2, Play, RefreshCw, Square, Terminal, CheckCircle, AlertCircle } from 'lucide-react';
import type { ComponentHealth } from '@shared/types/api';
import { formatBytes } from '../utils/format';
import type { YtDlpStatusResponse, YtDlpUpdateResponse } from '@shared/types/api';
import type { UseMutationResult } from '@tanstack/react-query';
// ── Status → color mapping ──
@ -27,9 +30,12 @@ const COMPONENT_LABELS: Record<string, string> = {
interface HealthStatusProps {
components: ComponentHealth[];
overallStatus: 'healthy' | 'degraded' | 'unhealthy';
ytdlpStatus?: YtDlpStatusResponse | null;
ytdlpLoading?: boolean;
updateYtDlp?: UseMutationResult<YtDlpUpdateResponse, Error, void>;
}
export function HealthStatus({ components, overallStatus }: HealthStatusProps) {
export function HealthStatus({ components, overallStatus, ytdlpStatus, ytdlpLoading, updateYtDlp }: HealthStatusProps) {
const overallColors = STATUS_COLORS[overallStatus] ?? DEFAULT_COLORS;
const overallLabel = overallStatus.charAt(0).toUpperCase() + overallStatus.slice(1);
@ -74,7 +80,13 @@ export function HealthStatus({ components, overallStatus }: HealthStatusProps) {
}}
>
{components.map((comp) => (
<ComponentCard key={comp.name} component={comp} />
<ComponentCard
key={comp.name}
component={comp}
ytdlpStatus={comp.name === 'ytDlp' ? ytdlpStatus : undefined}
ytdlpLoading={comp.name === 'ytDlp' ? ytdlpLoading : undefined}
updateYtDlp={comp.name === 'ytDlp' ? updateYtDlp : undefined}
/>
))}
</div>
@ -98,7 +110,12 @@ export function HealthStatus({ components, overallStatus }: HealthStatusProps) {
// ── Component Card ──
function ComponentCard({ component }: { component: ComponentHealth }) {
function ComponentCard({ component, ytdlpStatus, ytdlpLoading, updateYtDlp }: {
component: ComponentHealth;
ytdlpStatus?: YtDlpStatusResponse | null;
ytdlpLoading?: boolean;
updateYtDlp?: UseMutationResult<YtDlpUpdateResponse, Error, void>;
}) {
const colors = STATUS_COLORS[component.status] ?? DEFAULT_COLORS;
const label = COMPONENT_LABELS[component.name] ?? component.name;
@ -142,20 +159,24 @@ function ComponentCard({ component }: { component: ComponentHealth }) {
</span>
</div>
{/* Custom detail rendering per component type */}
<ComponentDetail component={component} />
<ComponentDetail component={component} ytdlpStatus={ytdlpStatus} ytdlpLoading={ytdlpLoading} updateYtDlp={updateYtDlp} />
</div>
);
}
// ── Detail Renderers ──
function ComponentDetail({ component }: { component: ComponentHealth }) {
function ComponentDetail({ component, ytdlpStatus, ytdlpLoading, updateYtDlp }: {
component: ComponentHealth;
ytdlpStatus?: YtDlpStatusResponse | null;
ytdlpLoading?: boolean;
updateYtDlp?: UseMutationResult<YtDlpUpdateResponse, Error, void>;
}) {
switch (component.name) {
case 'diskSpace':
return <DiskSpaceDetail component={component} />;
case 'ytDlp':
return <YtDlpDetail component={component} />;
return <YtDlpDetail component={component} ytdlpStatus={ytdlpStatus} ytdlpLoading={ytdlpLoading} updateYtDlp={updateYtDlp} />;
case 'scheduler':
return <SchedulerDetail component={component} />;
case 'recentErrors':
@ -233,30 +254,101 @@ function DiskSpaceDetail({ component }: { component: ComponentHealth }) {
// ── yt-dlp ──
function YtDlpDetail({ component }: { component: ComponentHealth }) {
function YtDlpDetail({ component, ytdlpStatus, ytdlpLoading, updateYtDlp }: {
component: ComponentHealth;
ytdlpStatus?: YtDlpStatusResponse | null;
ytdlpLoading?: boolean;
updateYtDlp?: UseMutationResult<YtDlpUpdateResponse, Error, void>;
}) {
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const details = component.details as { version?: string } | undefined;
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
<Terminal size={14} style={{ color: 'var(--text-muted)' }} aria-hidden="true" />
{details?.version ? (
<span
style={{
fontSize: 'var(--font-size-sm)',
fontFamily: 'var(--font-mono)',
color: 'var(--text-secondary)',
padding: '1px var(--space-2)',
borderRadius: 'var(--radius-sm)',
backgroundColor: 'var(--bg-hover)',
}}
>
v{details.version}
</span>
) : (
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--danger)', display: 'flex', alignItems: 'center', gap: 'var(--space-1)' }}>
<AlertTriangle size={12} aria-hidden="true" />
Not installed
</span>
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
{/* Version */}
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)' }}>
<Terminal size={14} style={{ color: 'var(--text-muted)' }} aria-hidden="true" />
{details?.version ? (
<span
style={{
fontSize: 'var(--font-size-sm)',
fontFamily: 'var(--font-mono)',
color: 'var(--text-secondary)',
padding: '1px var(--space-2)',
borderRadius: 'var(--radius-sm)',
backgroundColor: 'var(--bg-hover)',
}}
>
v{details.version}
</span>
) : (
<span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--danger)', display: 'flex', alignItems: 'center', gap: 'var(--space-1)' }}>
<AlertTriangle size={12} aria-hidden="true" />
Not installed
</span>
)}
</div>
{/* Last Updated */}
{!ytdlpLoading && ytdlpStatus && (
<div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--text-muted)' }}>
Last checked: {ytdlpStatus.lastUpdated ? new Date(ytdlpStatus.lastUpdated).toLocaleString() : 'Never'}
</div>
)}
{/* Update button */}
{updateYtDlp && (
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-2)', flexWrap: 'wrap' }}>
<button
className="btn btn-ghost"
style={{ fontSize: 'var(--font-size-xs)', padding: '2px var(--space-2)' }}
disabled={updateYtDlp.isPending}
onClick={() => {
setUpdateMessage(null);
updateYtDlp.mutate(undefined, {
onSuccess: (data) => {
setUpdateMessage({
type: 'success',
text: data.updated
? `Updated to ${data.version}`
: `Up to date`,
});
},
onError: (err) => {
setUpdateMessage({
type: 'error',
text: err instanceof Error ? err.message : 'Update failed',
});
},
});
}}
>
{updateYtDlp.isPending ? (
<>
<Loader2 size={12} className="animate-spin" />
Checking
</>
) : (
<>
<RefreshCw size={12} />
Check for Updates
</>
)}
</button>
{updateMessage && (
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
fontSize: 'var(--font-size-xs)',
color: updateMessage.type === 'success' ? 'var(--success)' : 'var(--danger)',
}}>
{updateMessage.type === 'success' ? <CheckCircle size={12} /> : <AlertCircle size={12} />}
{updateMessage.text}
</span>
)}
</div>
)}
</div>
);

View file

@ -1,11 +1,9 @@
import { RefreshCw, Server, Activity, Cpu, HardDrive, Download, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
import { RefreshCw, Server, Activity, Cpu, HardDrive } 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 {
@ -25,9 +23,8 @@ 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 { data: ytdlpStatus, isLoading: ytdlpLoading } = useYtDlpStatus();
const updateYtDlp = useUpdateYtDlp();
const [updateMessage, setUpdateMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const isLoading = healthLoading || statusLoading;
@ -85,109 +82,16 @@ export function SystemPage() {
</button>
</div>
) : health ? (
<HealthStatus components={health.components} overallStatus={health.status} />
<HealthStatus
components={health.components}
overallStatus={health.status}
ytdlpStatus={ytdlpStatus ?? null}
ytdlpLoading={ytdlpLoading}
updateYtDlp={updateYtDlp}
/>
) : 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)' }}>