- New /api/activity endpoint returning combined timeline of progress notes and comments across all tasks, sorted chronologically - Activity page now fetches from unified endpoint instead of extracting from task data client-side - Type filter (progress/comment) and status filter on Activity page - Comment entries show author avatars and type badges - 30s auto-refresh on activity feed
106 lines
3.1 KiB
TypeScript
106 lines
3.1 KiB
TypeScript
import { Elysia, t } from "elysia";
|
|
import { db } from "../db";
|
|
import { tasks, taskComments } from "../db/schema";
|
|
import { desc, sql } from "drizzle-orm";
|
|
import { auth } from "../lib/auth";
|
|
|
|
const BEARER_TOKEN = process.env.API_BEARER_TOKEN || "hammer-dev-token";
|
|
|
|
async function requireSessionOrBearer(request: Request, headers: Record<string, string | undefined>) {
|
|
const authHeader = headers["authorization"];
|
|
if (authHeader === `Bearer ${BEARER_TOKEN}`) return;
|
|
try {
|
|
const session = await auth.api.getSession({ headers: request.headers });
|
|
if (session?.user) return;
|
|
} catch {}
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
export interface ActivityFeedItem {
|
|
type: "progress" | "comment";
|
|
timestamp: string;
|
|
taskId: string;
|
|
taskNumber: number | null;
|
|
taskTitle: string;
|
|
taskStatus: string;
|
|
// For progress notes
|
|
note?: string;
|
|
// For comments
|
|
commentId?: string;
|
|
authorName?: string;
|
|
authorId?: string | null;
|
|
content?: string;
|
|
}
|
|
|
|
export const activityRoutes = new Elysia({ prefix: "/api/activity" })
|
|
.onError(({ error, set }) => {
|
|
const msg = (error as any)?.message || String(error);
|
|
if (msg === "Unauthorized") {
|
|
set.status = 401;
|
|
return { error: "Unauthorized" };
|
|
}
|
|
set.status = 500;
|
|
return { error: "Internal server error" };
|
|
})
|
|
|
|
// GET /api/activity — unified feed of progress notes + comments
|
|
.get("/", async ({ request, headers, query }) => {
|
|
await requireSessionOrBearer(request, headers);
|
|
|
|
const limit = Math.min(Number(query.limit) || 50, 200);
|
|
|
|
// Fetch all tasks with progress notes
|
|
const allTasks = await db.select().from(tasks);
|
|
|
|
// Collect progress note items
|
|
const items: ActivityFeedItem[] = [];
|
|
for (const task of allTasks) {
|
|
const notes = (task.progressNotes || []) as { timestamp: string; note: string }[];
|
|
for (const note of notes) {
|
|
items.push({
|
|
type: "progress",
|
|
timestamp: note.timestamp,
|
|
taskId: task.id,
|
|
taskNumber: task.taskNumber,
|
|
taskTitle: task.title,
|
|
taskStatus: task.status,
|
|
note: note.note,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fetch all comments
|
|
const allComments = await db
|
|
.select()
|
|
.from(taskComments)
|
|
.orderBy(desc(taskComments.createdAt));
|
|
|
|
// Build task lookup for comment items
|
|
const taskMap = new Map(allTasks.map(t => [t.id, t]));
|
|
|
|
for (const comment of allComments) {
|
|
const task = taskMap.get(comment.taskId);
|
|
if (!task) continue;
|
|
items.push({
|
|
type: "comment",
|
|
timestamp: comment.createdAt.toISOString(),
|
|
taskId: task.id,
|
|
taskNumber: task.taskNumber,
|
|
taskTitle: task.title,
|
|
taskStatus: task.status,
|
|
commentId: comment.id,
|
|
authorName: comment.authorName,
|
|
authorId: comment.authorId,
|
|
content: comment.content,
|
|
});
|
|
}
|
|
|
|
// Sort by timestamp descending, take limit
|
|
items.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
|
|
|
|
return {
|
|
items: items.slice(0, limit),
|
|
total: items.length,
|
|
};
|
|
});
|