diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5e67423..fa8fcce 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { DashboardLayout } from "./components/DashboardLayout"; +import { DashboardPage } from "./pages/DashboardPage"; import { QueuePage } from "./pages/QueuePage"; import { ChatPage } from "./pages/ChatPage"; import { ProjectsPage } from "./pages/ProjectsPage"; @@ -12,11 +13,12 @@ function AuthenticatedApp() { }> + } /> } /> } /> } /> } /> - } /> + } /> diff --git a/frontend/src/components/DashboardLayout.tsx b/frontend/src/components/DashboardLayout.tsx index a4baf7f..a325052 100644 --- a/frontend/src/components/DashboardLayout.tsx +++ b/frontend/src/components/DashboardLayout.tsx @@ -4,6 +4,7 @@ import { useCurrentUser } from "../hooks/useCurrentUser"; import { signOut } from "../lib/auth-client"; const navItems = [ + { to: "/", label: "Dashboard", icon: "🔨" }, { to: "/queue", label: "Queue", icon: "📋" }, { to: "/projects", label: "Projects", icon: "📁" }, { to: "/chat", label: "Chat", icon: "💬" }, @@ -89,6 +90,7 @@ export function DashboardLayout() { `flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition ${ diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx new file mode 100644 index 0000000..0040b9b --- /dev/null +++ b/frontend/src/pages/DashboardPage.tsx @@ -0,0 +1,217 @@ +import { useMemo } from "react"; +import { Link } from "react-router-dom"; +import { useTasks } from "../hooks/useTasks"; +import type { Task, ProgressNote } from "../lib/types"; + +function StatCard({ label, value, icon, color }: { label: string; value: number; icon: string; color: string }) { + return ( +
+
+ {icon} + {value} +
+

{label}

+
+ ); +} + +function timeAgo(dateStr: string): string { + const seconds = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000); + if (seconds < 60) return "just now"; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + const days = Math.floor(hours / 24); + return `${days}d ago`; +} + +function RecentActivity({ tasks }: { tasks: Task[] }) { + // Gather all progress notes with task context, sorted by timestamp desc + const recentNotes = useMemo(() => { + const notes: { task: Task; note: ProgressNote }[] = []; + for (const task of tasks) { + if (task.progressNotes) { + for (const note of task.progressNotes) { + notes.push({ task, note }); + } + } + } + notes.sort((a, b) => new Date(b.note.timestamp).getTime() - new Date(a.note.timestamp).getTime()); + return notes.slice(0, 8); + }, [tasks]); + + if (recentNotes.length === 0) { + return ( +
+ No recent activity +
+ ); + } + + return ( +
+ {recentNotes.map((item, i) => ( +
+
+ 🔨 +
+
+
+ + HQ-{item.task.taskNumber} + + {timeAgo(item.note.timestamp)} +
+

{item.note.note}

+

{item.task.title}

+
+
+ ))} +
+ ); +} + +export function DashboardPage() { + const { tasks, loading } = useTasks(10000); + + const stats = useMemo(() => { + const active = tasks.filter((t) => t.status === "active").length; + const queued = tasks.filter((t) => t.status === "queued").length; + const blocked = tasks.filter((t) => t.status === "blocked").length; + const completed = tasks.filter((t) => t.status === "completed").length; + return { active, queued, blocked, completed, total: tasks.length }; + }, [tasks]); + + const activeTasks = useMemo(() => tasks.filter((t) => t.status === "active"), [tasks]); + const upNext = useMemo(() => tasks.filter((t) => t.status === "queued").slice(0, 3), [tasks]); + const recentlyCompleted = useMemo( + () => + tasks + .filter((t) => t.status === "completed" && t.completedAt) + .sort((a, b) => new Date(b.completedAt!).getTime() - new Date(a.completedAt!).getTime()) + .slice(0, 3), + [tasks] + ); + + if (loading && tasks.length === 0) { + return ( +
+ Loading dashboard... +
+ ); + } + + return ( +
+
+
+

🔨 Dashboard

+

Overview of Hammer's work

+
+
+ +
+ {/* Stats Grid */} +
+ + + + +
+ +
+ {/* Currently Working On */} +
+
+

⚡ Currently Working On

+ + View Queue → + +
+
+ {activeTasks.length === 0 ? ( +
+ Hammer is idle — no active tasks +
+ ) : ( +
+ {activeTasks.map((task) => ( + +
+
+ + + + + HQ-{task.taskNumber} + {task.priority} +
+

{task.title}

+ {task.progressNotes?.length > 0 && ( +

+ Latest: {task.progressNotes[task.progressNotes.length - 1].note} +

+ )} +
+ + ))} +
+ )} + + {/* Up Next */} + {upNext.length > 0 && ( +
+

Up Next

+
+ {upNext.map((task, i) => ( +
+ {i + 1}. + HQ-{task.taskNumber} + {task.title} +
+ ))} +
+
+ )} +
+
+ + {/* Recent Activity */} +
+
+

📝 Recent Activity

+
+
+ +
+
+
+ + {/* Recently Completed */} + {recentlyCompleted.length > 0 && ( +
+
+

✅ Recently Completed

+
+
+
+ {recentlyCompleted.map((task) => ( +
+
+ HQ-{task.taskNumber} + {task.completedAt && ( + {timeAgo(task.completedAt)} + )} +
+

{task.title}

+
+ ))} +
+
+
+ )} +
+
+ ); +}