consolidate dashboard into scan page; / now renders Scan
All checks were successful
Build and Push Docker Image / build (push) Successful in 45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 45s
Single landing page: stats grid up top, then scan controls + progress, then recent items log. Drops the 'click → bounce to Scan' indirection. - ScanPage pulls /api/dashboard for the stats card grid; refetches when a scan completes so totals reflect the new state - Scan page also owns the setup-complete redirect to /settings (was on Dashboard) and the empty-library 'click Start Scan' nudge - / route now renders ScanPage; /scan route deleted - DashboardPage and its feature dir gone - Nav: drop 'Dashboard', repoint 'Scan' to /
This commit is contained in:
@@ -1,112 +0,0 @@
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Alert } from "~/shared/components/ui/alert";
|
||||
import { Button } from "~/shared/components/ui/button";
|
||||
import { api } from "~/shared/lib/api";
|
||||
|
||||
interface Stats {
|
||||
totalItems: number;
|
||||
scanned: number;
|
||||
needsAction: number;
|
||||
approved: number;
|
||||
done: number;
|
||||
errors: number;
|
||||
noChange: number;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
stats: Stats;
|
||||
scanRunning: boolean;
|
||||
setupComplete: boolean;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, danger }: { label: string; value: number; danger?: boolean }) {
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg px-3.5 py-2.5">
|
||||
<div className={`text-[1.6rem] font-bold leading-[1.1] ${danger ? "text-red-600" : ""}`}>
|
||||
{value.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[0.7rem] text-gray-500 mt-0.5">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const navigate = useNavigate();
|
||||
const [data, setData] = useState<DashboardData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [starting, setStarting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<DashboardData>("/api/dashboard")
|
||||
.then((d) => {
|
||||
setData(d);
|
||||
setLoading(false);
|
||||
if (!d.setupComplete) navigate({ to: "/settings" });
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [navigate]);
|
||||
|
||||
const startScan = async () => {
|
||||
setStarting(true);
|
||||
await api.post("/api/scan/start", {}).catch(() => {});
|
||||
navigate({ to: "/scan" });
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-gray-400 py-8 text-center">Loading…</div>;
|
||||
if (!data) return <Alert variant="error">Failed to load dashboard.</Alert>;
|
||||
|
||||
const { stats, scanRunning } = data;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h1 className="text-xl font-bold m-0">Dashboard</h1>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-2.5 mb-5">
|
||||
<StatCard label="Total items" value={stats.totalItems} />
|
||||
<StatCard label="Scanned" value={stats.scanned} />
|
||||
<StatCard label="Needs action" value={stats.needsAction} />
|
||||
<StatCard label="No change needed" value={stats.noChange} />
|
||||
<StatCard label="Approved / queued" value={stats.approved} />
|
||||
<StatCard label="Done" value={stats.done} />
|
||||
{stats.errors > 0 && <StatCard label="Errors" value={stats.errors} danger />}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 mb-8">
|
||||
{scanRunning ? (
|
||||
<Link
|
||||
to="/scan"
|
||||
className="inline-flex items-center justify-center gap-1 rounded px-3 py-1.5 text-sm font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 no-underline"
|
||||
>
|
||||
⏳ Scan running…
|
||||
</Link>
|
||||
) : (
|
||||
<Button onClick={startScan} disabled={starting}>
|
||||
{starting ? "Starting…" : "▶ Start Scan"}
|
||||
</Button>
|
||||
)}
|
||||
<Link
|
||||
to="/review"
|
||||
className="inline-flex items-center justify-center gap-1 rounded px-3 py-1.5 text-sm font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 no-underline"
|
||||
>
|
||||
Review changes
|
||||
</Link>
|
||||
<Link
|
||||
to="/execute"
|
||||
className="inline-flex items-center justify-center gap-1 rounded px-3 py-1.5 text-sm font-medium bg-white text-gray-700 border border-gray-300 hover:bg-gray-50 no-underline"
|
||||
>
|
||||
Execute jobs
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{stats.scanned === 0 && (
|
||||
<Alert variant="info">
|
||||
Library not scanned yet. Click <strong>Start Scan</strong> to begin.
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { Link, useNavigate } from "@tanstack/react-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Alert } from "~/shared/components/ui/alert";
|
||||
import { Badge } from "~/shared/components/ui/badge";
|
||||
import { Button } from "~/shared/components/ui/button";
|
||||
import { api } from "~/shared/lib/api";
|
||||
@@ -10,6 +11,33 @@ interface ScanStatus {
|
||||
recentItems: { name: string; type: string; scan_status: string; file_path: string }[];
|
||||
scanLimit: number | null;
|
||||
}
|
||||
|
||||
interface DashboardStats {
|
||||
totalItems: number;
|
||||
scanned: number;
|
||||
needsAction: number;
|
||||
approved: number;
|
||||
done: number;
|
||||
errors: number;
|
||||
noChange: number;
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
stats: DashboardStats;
|
||||
scanRunning: boolean;
|
||||
setupComplete: boolean;
|
||||
}
|
||||
|
||||
function StatCard({ label, value, danger }: { label: string; value: number; danger?: boolean }) {
|
||||
return (
|
||||
<div className="border border-gray-200 rounded-lg px-3.5 py-2.5">
|
||||
<div className={`text-[1.6rem] font-bold leading-[1.1] ${danger ? "text-red-600" : ""}`}>
|
||||
{value.toLocaleString()}
|
||||
</div>
|
||||
<div className="text-[0.7rem] text-gray-500 mt-0.5">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface LogEntry {
|
||||
name: string;
|
||||
type: string;
|
||||
@@ -36,7 +64,10 @@ function freshBuf(): SseBuf {
|
||||
const FLUSH_MS = 200;
|
||||
|
||||
export function ScanPage() {
|
||||
const navigate = useNavigate();
|
||||
const [status, setStatus] = useState<ScanStatus | null>(null);
|
||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||
const [setupChecked, setSetupChecked] = useState(false);
|
||||
const [limit, setLimit] = useState("");
|
||||
const [log, setLog] = useState<LogEntry[]>([]);
|
||||
const [statusLabel, setStatusLabel] = useState("");
|
||||
@@ -49,6 +80,24 @@ export function ScanPage() {
|
||||
const bufRef = useRef<SseBuf>(freshBuf());
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// Pull dashboard stats + redirect-on-unconfigured. Re-fetch on scan completion
|
||||
// so the counts above the scan controls reflect the new totals.
|
||||
const loadStats = useCallback(() => {
|
||||
api
|
||||
.get<DashboardData>("/api/dashboard")
|
||||
.then((d) => {
|
||||
setStats(d.stats);
|
||||
if (!setupChecked) {
|
||||
setSetupChecked(true);
|
||||
if (!d.setupComplete) navigate({ to: "/settings" });
|
||||
}
|
||||
})
|
||||
.catch(() => setSetupChecked(true));
|
||||
}, [navigate, setupChecked]);
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
}, [loadStats]);
|
||||
|
||||
// Stop the periodic flush interval. Inlined into flush() to avoid a
|
||||
// circular useCallback dep (flush → stopFlushing → flush) that tripped
|
||||
// TDZ in prod builds: "can't access lexical declaration 'o' before initialization".
|
||||
@@ -83,6 +132,7 @@ export function ScanPage() {
|
||||
setScanComplete(true);
|
||||
setStatus((prev) => (prev ? { ...prev, running: false } : prev));
|
||||
clearFlushTimer();
|
||||
loadStats(); // refresh the totals above the controls
|
||||
}
|
||||
|
||||
if (b.lost) {
|
||||
@@ -208,7 +258,25 @@ export function ScanPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold mb-4">Library Scan</h1>
|
||||
<h1 className="text-xl font-bold mb-4">Scan</h1>
|
||||
|
||||
{stats && (
|
||||
<div className="grid grid-cols-[repeat(auto-fill,minmax(120px,1fr))] gap-2.5 mb-5">
|
||||
<StatCard label="Total items" value={stats.totalItems} />
|
||||
<StatCard label="Scanned" value={stats.scanned} />
|
||||
<StatCard label="Needs action" value={stats.needsAction} />
|
||||
<StatCard label="No change needed" value={stats.noChange} />
|
||||
<StatCard label="Approved / queued" value={stats.approved} />
|
||||
<StatCard label="Done" value={stats.done} />
|
||||
{stats.errors > 0 && <StatCard label="Errors" value={stats.errors} danger />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stats && stats.scanned === 0 && (
|
||||
<Alert variant="info" className="mb-5">
|
||||
Library not scanned yet. Click <strong>Start Scan</strong> below to begin.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="border border-gray-200 rounded-lg px-4 py-3 mb-6">
|
||||
<div className="flex items-center flex-wrap gap-2 mb-3">
|
||||
|
||||
@@ -65,9 +65,8 @@ function RootLayout() {
|
||||
<VersionBadge />
|
||||
<div className="flex flex-wrap items-center gap-0.5">
|
||||
<NavLink to="/" exact>
|
||||
Dashboard
|
||||
Scan
|
||||
</NavLink>
|
||||
<NavLink to="/scan">Scan</NavLink>
|
||||
<NavLink to="/pipeline">Pipeline</NavLink>
|
||||
<NavLink to="/review/subtitles">Subtitles</NavLink>
|
||||
<NavLink to="/execute">Jobs</NavLink>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { DashboardPage } from "~/features/dashboard/DashboardPage";
|
||||
import { ScanPage } from "~/features/scan/ScanPage";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: DashboardPage,
|
||||
component: ScanPage,
|
||||
});
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { ScanPage } from "~/features/scan/ScanPage";
|
||||
|
||||
export const Route = createFileRoute("/scan")({
|
||||
component: ScanPage,
|
||||
});
|
||||
Reference in New Issue
Block a user