feat(HAI-002): complete React conversion with build fixes and project setup
- Fix Vite alias to resolve @hai/core to types-only module (avoids Node.js deps in browser bundle) - Add tsconfig.app.json for client-side typecheck - Fix server.ts return type annotation - Update imports to use @hai/core (resolved via Vite alias) - Add base project files needed for workspace
This commit is contained in:
1
packages/dashboard/src/index.ts
Normal file
1
packages/dashboard/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createServer } from "./server.js";
|
||||
92
packages/dashboard/src/routes.ts
Normal file
92
packages/dashboard/src/routes.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { Router } from "express";
|
||||
import type { TaskStore, Column } from "@hai/core";
|
||||
import { COLUMNS } from "@hai/core";
|
||||
|
||||
export function createApiRoutes(store: TaskStore): Router {
|
||||
const router = Router();
|
||||
|
||||
// List all tasks
|
||||
router.get("/tasks", async (_req, res) => {
|
||||
try {
|
||||
const tasks = await store.listTasks();
|
||||
res.json(tasks);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get single task with prompt content
|
||||
router.get("/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const task = await store.getTask(req.params.id);
|
||||
res.json(task);
|
||||
} catch (err: any) {
|
||||
res.status(404).json({ error: `Task ${req.params.id} not found` });
|
||||
}
|
||||
});
|
||||
|
||||
// Create task
|
||||
router.post("/tasks", async (req, res) => {
|
||||
try {
|
||||
const { title, description, column, dependencies } = req.body;
|
||||
if (!title || typeof title !== "string") {
|
||||
res.status(400).json({ error: "title is required" });
|
||||
return;
|
||||
}
|
||||
const task = await store.createTask({
|
||||
title,
|
||||
description,
|
||||
column,
|
||||
dependencies,
|
||||
});
|
||||
res.status(201).json(task);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Move task to column
|
||||
router.post("/tasks/:id/move", async (req, res) => {
|
||||
try {
|
||||
const { column } = req.body;
|
||||
if (!column || !COLUMNS.includes(column as Column)) {
|
||||
res.status(400).json({
|
||||
error: `Invalid column. Must be one of: ${COLUMNS.join(", ")}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const task = await store.moveTask(req.params.id, column as Column);
|
||||
res.json(task);
|
||||
} catch (err: any) {
|
||||
const status = err.message.includes("Invalid transition") ? 400 : 500;
|
||||
res.status(status).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Update task
|
||||
router.patch("/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const { title, description, prompt } = req.body;
|
||||
const task = await store.updateTask(req.params.id, {
|
||||
title,
|
||||
description,
|
||||
prompt,
|
||||
});
|
||||
res.json(task);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete task
|
||||
router.delete("/tasks/:id", async (req, res) => {
|
||||
try {
|
||||
const task = await store.deleteTask(req.params.id);
|
||||
res.json(task);
|
||||
} catch (err: any) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { createSSE } from "./sse.js";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export function createServer(store: TaskStore) {
|
||||
export function createServer(store: TaskStore): ReturnType<typeof express> {
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
46
packages/dashboard/src/sse.ts
Normal file
46
packages/dashboard/src/sse.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { TaskStore } from "@hai/core";
|
||||
|
||||
export function createSSE(store: TaskStore) {
|
||||
return (_req: Request, res: Response) => {
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
res.flushHeaders();
|
||||
|
||||
// Send initial heartbeat
|
||||
res.write(": connected\n\n");
|
||||
|
||||
const onCreated = (task: any) => {
|
||||
res.write(`event: task:created\ndata: ${JSON.stringify(task)}\n\n`);
|
||||
};
|
||||
const onMoved = (data: any) => {
|
||||
res.write(`event: task:moved\ndata: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
const onUpdated = (task: any) => {
|
||||
res.write(`event: task:updated\ndata: ${JSON.stringify(task)}\n\n`);
|
||||
};
|
||||
const onDeleted = (task: any) => {
|
||||
res.write(`event: task:deleted\ndata: ${JSON.stringify(task)}\n\n`);
|
||||
};
|
||||
|
||||
store.on("task:created", onCreated);
|
||||
store.on("task:moved", onMoved);
|
||||
store.on("task:updated", onUpdated);
|
||||
store.on("task:deleted", onDeleted);
|
||||
|
||||
// Heartbeat every 30s to keep connection alive
|
||||
const heartbeat = setInterval(() => {
|
||||
res.write(": heartbeat\n\n");
|
||||
}, 30_000);
|
||||
|
||||
_req.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
store.off("task:created", onCreated);
|
||||
store.off("task:moved", onMoved);
|
||||
store.off("task:updated", onUpdated);
|
||||
store.off("task:deleted", onDeleted);
|
||||
});
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user