diff --git a/backend/src/index.ts b/backend/src/index.ts index 4949b53..0fa4803 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -52,7 +52,7 @@ ensureAdmin().catch(console.error); const app = new Elysia() .use( cors({ - origin: ["https://queue.donovankelly.xyz", "http://localhost:5173"], + origin: ["https://dash.donovankelly.xyz", "https://queue.donovankelly.xyz", "http://localhost:5173"], credentials: true, allowedHeaders: ["Content-Type", "Authorization", "Cookie"], methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], diff --git a/backend/src/lib/auth.ts b/backend/src/lib/auth.ts index a8ae69d..1b30da7 100644 --- a/backend/src/lib/auth.ts +++ b/backend/src/lib/auth.ts @@ -25,6 +25,7 @@ export const auth = betterAuth({ }, }, trustedOrigins: [ + "https://dash.donovankelly.xyz", "https://queue.donovankelly.xyz", "http://localhost:5173", ], diff --git a/docker-compose.dokploy.yml b/docker-compose.dokploy.yml index 3a67be2..bcfa672 100644 --- a/docker-compose.dokploy.yml +++ b/docker-compose.dokploy.yml @@ -21,7 +21,7 @@ services: DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} API_BEARER_TOKEN: ${API_BEARER_TOKEN} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} - BETTER_AUTH_URL: https://queue.donovankelly.xyz + BETTER_AUTH_URL: https://dash.donovankelly.xyz COOKIE_DOMAIN: .donovankelly.xyz CLAWDBOT_HOOK_URL: ${CLAWDBOT_HOOK_URL:-https://hooks.hammer.donovankelly.xyz/hooks/agent} CLAWDBOT_HOOK_TOKEN: ${CLAWDBOT_HOOK_TOKEN} diff --git a/frontend/bun.lock b/frontend/bun.lock index 02e1dbd..78c2118 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -9,6 +9,7 @@ "better-auth": "^1.4.17", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", "tailwindcss": "^4.1.18", }, "devDependencies": { @@ -326,6 +327,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -508,6 +511,10 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "react-router": ["react-router@7.13.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw=="], + + "react-router-dom": ["react-router-dom@7.13.0", "", { "dependencies": { "react-router": "7.13.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], diff --git a/frontend/index.html b/frontend/index.html index 361f29f..1839882 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Hammer Queue + Hammer Dashboard
diff --git a/frontend/package.json b/frontend/package.json index 10072f4..b866ffa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "better-auth": "^1.4.17", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router-dom": "^7.13.0", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b6aa7c3..2a0f43f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,254 +1,23 @@ -import { useState, useMemo } from "react"; -import { useTasks } from "./hooks/useTasks"; -import { useCurrentUser } from "./hooks/useCurrentUser"; -import { TaskCard } from "./components/TaskCard"; -import { TaskDetailPanel } from "./components/TaskDetailPanel"; -import { CreateTaskModal } from "./components/CreateTaskModal"; +import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; +import { DashboardLayout } from "./components/DashboardLayout"; +import { QueuePage } from "./pages/QueuePage"; +import { ChatPage } from "./pages/ChatPage"; import { AdminPage } from "./components/AdminPage"; import { LoginPage } from "./components/LoginPage"; -import { useSession, signOut } from "./lib/auth-client"; -import { updateTask, reorderTasks, createTask } from "./lib/api"; -import type { TaskStatus } from "./lib/types"; - -function Dashboard() { - const { tasks, loading, error, refresh } = useTasks(5000); - const { user, isAdmin, isAuthenticated } = useCurrentUser(); - const [showCreate, setShowCreate] = useState(false); - const [showCompleted, setShowCompleted] = useState(false); - const [selectedTask, setSelectedTask] = useState(null); - const [showAdmin, setShowAdmin] = useState(false); - - const selectedTaskData = useMemo(() => { - if (!selectedTask) return null; - return tasks.find((t) => t.id === selectedTask) || null; - }, [tasks, selectedTask]); - - const activeTasks = useMemo(() => tasks.filter((t) => t.status === "active"), [tasks]); - const queuedTasks = useMemo(() => tasks.filter((t) => t.status === "queued"), [tasks]); - const blockedTasks = useMemo(() => tasks.filter((t) => t.status === "blocked"), [tasks]); - const completedTasks = useMemo( - () => tasks.filter((t) => t.status === "completed" || t.status === "cancelled"), - [tasks] - ); - - const handleStatusChange = async (id: string, status: TaskStatus) => { - try { - await updateTask(id, { status }); - refresh(); - } catch (e) { - alert("Failed to update task."); - } - }; - - const handleMoveUp = async (index: number) => { - if (index === 0) return; - const ids = queuedTasks.map((t) => t.id); - [ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]; - await reorderTasks(ids); - refresh(); - }; - - const handleMoveDown = async (index: number) => { - if (index >= queuedTasks.length - 1) return; - const ids = queuedTasks.map((t) => t.id); - [ids[index], ids[index + 1]] = [ids[index + 1], ids[index]]; - await reorderTasks(ids); - refresh(); - }; - - const handleCreate = async (task: { - title: string; - description?: string; - source?: string; - priority?: string; - }) => { - await createTask(task); - refresh(); - }; - - const handleLogout = async () => { - await signOut(); - window.location.reload(); - }; - - if (showAdmin) { - return setShowAdmin(false)} />; - } +import { useSession } from "./lib/auth-client"; +function AuthenticatedApp() { return ( -
- {/* Header */} -
-
-
- ๐Ÿ”จ -

Hammer Queue

- Task Dashboard -
-
- - {isAdmin && ( - - )} -
- {user?.email} - {isAdmin && ( - - admin - - )} - -
-
-
-
- - setShowCreate(false)} - onCreate={handleCreate} - /> - -
- {loading && ( -
Loading tasks...
- )} - {error && ( -
- {error} -
- )} - - {/* Active Task */} -
-

- โšก Currently Working On -

- {activeTasks.length === 0 ? ( -
- No active task โ€” Hammer is idle -
- ) : ( -
- {activeTasks.map((task) => ( - setSelectedTask(task.id)} - /> - ))} -
- )} -
- - {/* Blocked */} - {blockedTasks.length > 0 && ( -
-

- ๐Ÿšซ Blocked ({blockedTasks.length}) -

-
- {blockedTasks.map((task) => ( - setSelectedTask(task.id)} - /> - ))} -
-
- )} - - {/* Queue */} -
-

- ๐Ÿ“‹ Queue ({queuedTasks.length}) -

- {queuedTasks.length === 0 ? ( -
- Queue is empty -
- ) : ( -
- {queuedTasks.map((task, i) => ( - handleMoveUp(i)} - onMoveDown={() => handleMoveDown(i)} - isFirst={i === 0} - isLast={i === queuedTasks.length - 1} - onClick={() => setSelectedTask(task.id)} - /> - ))} -
- )} -
- - {/* Completed */} -
- - {showCompleted && ( -
- {completedTasks.map((task) => ( - setSelectedTask(task.id)} - /> - ))} -
- )} -
-
- - {/* Task Detail Panel */} - {selectedTaskData && ( - setSelectedTask(null)} - onStatusChange={(id, status) => { - handleStatusChange(id, status); - setSelectedTask(null); - }} - onTaskUpdated={refresh} - hasToken={isAuthenticated} - token="" - /> - )} - - {/* Footer */} -
- Hammer Queue v0.2 ยท Auto-refreshes every 5s -
-
+ + + }> + } /> + } /> + } /> + } /> + + + ); } @@ -267,7 +36,7 @@ function App() { return window.location.reload()} />; } - return ; + return ; } export default App; diff --git a/frontend/src/components/AdminPage.tsx b/frontend/src/components/AdminPage.tsx index 78ed05b..6aded57 100644 --- a/frontend/src/components/AdminPage.tsx +++ b/frontend/src/components/AdminPage.tsx @@ -1,4 +1,5 @@ import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; import { fetchUsers, updateUserRole, deleteUser } from "../lib/api"; interface User { @@ -9,7 +10,8 @@ interface User { createdAt: string; } -export function AdminPage({ onBack }: { onBack: () => void }) { +export function AdminPage() { + const navigate = useNavigate(); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -56,7 +58,7 @@ export function AdminPage({ onBack }: { onBack: () => void }) {

Manage users and roles

+ + + + {/* Main Content */} +
+ +
+ + ); +} diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx new file mode 100644 index 0000000..0f5c2d5 --- /dev/null +++ b/frontend/src/pages/ChatPage.tsx @@ -0,0 +1,23 @@ +export function ChatPage() { + return ( +
+ {/* Page Header */} +
+
+

Chat

+

Talk to Hammer directly

+
+
+ +
+
+ ๐Ÿ’ฌ +

Chat coming soon

+

+ You'll be able to chat with Hammer right here in the dashboard. +

+
+
+
+ ); +} diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx new file mode 100644 index 0000000..41c791f --- /dev/null +++ b/frontend/src/pages/QueuePage.tsx @@ -0,0 +1,208 @@ +import { useState, useMemo } from "react"; +import { useTasks } from "../hooks/useTasks"; +import { useCurrentUser } from "../hooks/useCurrentUser"; +import { TaskCard } from "../components/TaskCard"; +import { TaskDetailPanel } from "../components/TaskDetailPanel"; +import { CreateTaskModal } from "../components/CreateTaskModal"; +import { updateTask, reorderTasks, createTask } from "../lib/api"; +import type { TaskStatus } from "../lib/types"; + +export function QueuePage() { + const { tasks, loading, error, refresh } = useTasks(5000); + const { isAuthenticated } = useCurrentUser(); + const [showCreate, setShowCreate] = useState(false); + const [showCompleted, setShowCompleted] = useState(false); + const [selectedTask, setSelectedTask] = useState(null); + + const selectedTaskData = useMemo(() => { + if (!selectedTask) return null; + return tasks.find((t) => t.id === selectedTask) || null; + }, [tasks, selectedTask]); + + const activeTasks = useMemo(() => tasks.filter((t) => t.status === "active"), [tasks]); + const queuedTasks = useMemo(() => tasks.filter((t) => t.status === "queued"), [tasks]); + const blockedTasks = useMemo(() => tasks.filter((t) => t.status === "blocked"), [tasks]); + const completedTasks = useMemo( + () => tasks.filter((t) => t.status === "completed" || t.status === "cancelled"), + [tasks] + ); + + const handleStatusChange = async (id: string, status: TaskStatus) => { + try { + await updateTask(id, { status }); + refresh(); + } catch (e) { + alert("Failed to update task."); + } + }; + + const handleMoveUp = async (index: number) => { + if (index === 0) return; + const ids = queuedTasks.map((t) => t.id); + [ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]; + await reorderTasks(ids); + refresh(); + }; + + const handleMoveDown = async (index: number) => { + if (index >= queuedTasks.length - 1) return; + const ids = queuedTasks.map((t) => t.id); + [ids[index], ids[index + 1]] = [ids[index + 1], ids[index]]; + await reorderTasks(ids); + refresh(); + }; + + const handleCreate = async (task: { + title: string; + description?: string; + source?: string; + priority?: string; + }) => { + await createTask(task); + refresh(); + }; + + return ( +
+ {/* Page Header */} +
+
+
+

Task Queue

+

Manage what Hammer is working on

+
+ +
+
+ + setShowCreate(false)} + onCreate={handleCreate} + /> + +
+ {loading && ( +
Loading tasks...
+ )} + {error && ( +
+ {error} +
+ )} + + {/* Active Task */} +
+

+ โšก Currently Working On +

+ {activeTasks.length === 0 ? ( +
+ No active task โ€” Hammer is idle +
+ ) : ( +
+ {activeTasks.map((task) => ( + setSelectedTask(task.id)} + /> + ))} +
+ )} +
+ + {/* Blocked */} + {blockedTasks.length > 0 && ( +
+

+ ๐Ÿšซ Blocked ({blockedTasks.length}) +

+
+ {blockedTasks.map((task) => ( + setSelectedTask(task.id)} + /> + ))} +
+
+ )} + + {/* Queue */} +
+

+ ๐Ÿ“‹ Queue ({queuedTasks.length}) +

+ {queuedTasks.length === 0 ? ( +
+ Queue is empty +
+ ) : ( +
+ {queuedTasks.map((task, i) => ( + handleMoveUp(i)} + onMoveDown={() => handleMoveDown(i)} + isFirst={i === 0} + isLast={i === queuedTasks.length - 1} + onClick={() => setSelectedTask(task.id)} + /> + ))} +
+ )} +
+ + {/* Completed */} +
+ + {showCompleted && ( +
+ {completedTasks.map((task) => ( + setSelectedTask(task.id)} + /> + ))} +
+ )} +
+
+ + {/* Task Detail Panel */} + {selectedTaskData && ( + setSelectedTask(null)} + onStatusChange={(id, status) => { + handleStatusChange(id, status); + setSelectedTask(null); + }} + onTaskUpdated={refresh} + hasToken={isAuthenticated} + token="" + /> + )} +
+ ); +}