consolidate dashboard into scan page; / now renders Scan
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:
2026-04-13 12:21:03 +02:00
parent 962b5efc6f
commit e8f33c6224
5 changed files with 73 additions and 124 deletions

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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,
});

View File

@@ -1,6 +0,0 @@
import { createFileRoute } from "@tanstack/react-router";
import { ScanPage } from "~/features/scan/ScanPage";
export const Route = createFileRoute("/scan")({
component: ScanPage,
});