Skip to content
This repository was archived by the owner on Apr 20, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 18 additions & 25 deletions src/app/api/docs/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { pool } from "@/lib/db/client";
import { NextRequest, NextResponse } from "next/server";
import { SEED_DOCUMENTS } from "@/lib/mock-docs";
import type { Document, LinkedItem } from "@/lib/mock-docs";
import type { Document, LinkedItem } from "@/lib/docs/model";
import fs from "fs";
import path from "path";
import os from "os";
Expand Down Expand Up @@ -64,7 +63,7 @@ function isRepoDoc(filename: string): boolean {
return REPO_DOC_PATTERNS.some((pattern) => pattern.test(filename));
}

function guessAgent(filename: string, content: string): string {
function guessAgent(filename: string): string {
const fn = filename.toLowerCase();
if (fn.includes("cpars") || fn.includes("seas") || fn.includes("skyward")) return "skylar";
if (fn.includes("mbe") || fn.includes("cert") || fn.includes("wosb") || fn.includes("lsbrp")) return "veronica";
Expand Down Expand Up @@ -138,7 +137,7 @@ function readWorkspaceDocs(): Document[] {
stat = fs.statSync(filePath);
} catch { /* ignore */ }

const agentId = guessAgent(f, content);
const agentId = guessAgent(f);
const meta = AGENT_META[agentId] || AGENT_META.bob;

return {
Expand All @@ -165,11 +164,9 @@ function readWorkspaceDocs(): Document[] {
}
}

/** Get documents from best available source: workspace → seed data */
/** Get documents from best available source for local/dev runtime */
function getFallbackDocs(): Document[] {
const workspaceDocs = readWorkspaceDocs();
if (workspaceDocs.length > 0) return workspaceDocs;
return SEED_DOCUMENTS;
return readWorkspaceDocs();
}

async function ensureSchema() {
Expand Down Expand Up @@ -197,23 +194,24 @@ async function ensureSchema() {
}

export async function GET(request: NextRequest) {
if (!pool) return NextResponse.json(getFallbackDocs());
if (!pool) {
return NextResponse.json(getFallbackDocs());
}

try {
await ensureSchema();
} catch {
return NextResponse.json(getFallbackDocs());
return NextResponse.json({ error: "Unable to initialize docs schema" }, { status: 500 });
}

const { searchParams } = new URL(request.url);
const docType = searchParams.get("type");
const search = searchParams.get("search");

try {
// Use minimal columns to avoid schema drift (linked_to, etc. may not exist)
const baseQuery =
"SELECT d.id, d.title, d.filename, d.doc_type, d.content, d.author_agent_id, " +
"d.status, d.file_path, d.created_at, d.updated_at " +
"d.status, d.file_path, d.linked_to, d.version_history, d.priority, d.review_status, d.category, d.notes, d.assignments, d.created_at, d.updated_at " +
"FROM docs d";
let query = baseQuery;
const conditions: string[] = [];
Expand All @@ -227,7 +225,6 @@ export async function GET(request: NextRequest) {
params.push("%" + search + "%");
conditions.push("(d.title ILIKE $" + params.length + " OR d.content ILIKE $" + params.length + ")");
}
// Skip linkedType, reviewStatus, category, priority filters - those columns may not exist in deployed schema

if (conditions.length > 0) {
query += " WHERE " + conditions.join(" AND ");
Expand All @@ -251,24 +248,20 @@ export async function GET(request: NextRequest) {
updatedAt: row.updated_at,
agent: meta.name,
agentEmoji: meta.emoji,
linkedTo: [] as LinkedItem[],
versionHistory: [{ timestamp: row.updated_at as string, summary: "Synced" }],
priority: "medium",
reviewStatus: "pending_review",
category: "uncategorized",
notes: [],
assignments: [],
linkedTo: (row.linked_to as LinkedItem[]) || [],
versionHistory: (row.version_history as { timestamp: string; summary: string }[]) || [],
priority: (row.priority as string) || "medium",
reviewStatus: (row.review_status as string) || "pending_review",
category: (row.category as string) || "uncategorized",
notes: (row.notes as unknown[]) || [],
assignments: (row.assignments as unknown[]) || [],
};
});

if (docs.length === 0 && !docType && !search) {
return NextResponse.json(getFallbackDocs());
}

return NextResponse.json(docs);
} catch (error) {
console.error("[Docs API] Error:", error);
return NextResponse.json(getFallbackDocs());
return NextResponse.json({ error: "Failed to fetch documents" }, { status: 500 });
}
}

Expand Down
205 changes: 161 additions & 44 deletions src/app/calendar/page.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,142 @@
"use client";

import { useState, useEffect } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import WeekView from "@/components/calendar/WeekView";
import ConflictPanel from "@/components/calendar/ConflictPanel";
import {
detectConflicts,
isEventDomain,
type CalendarEvent,
type EventDomain,
} from "@/lib/mock-calendar";
} from "@/lib/calendar/types";

export default function CalendarPage() {
const getCurrentWeekStart = () => {
const now = new Date();
const dayOfWeek = now.getDay();
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - dayOfWeek);
startOfWeek.setHours(0, 0, 0, 0);
return startOfWeek;
interface ApiCalendarEvent {
id?: unknown;
title?: unknown;
domain?: unknown;
startTime?: unknown;
endTime?: unknown;
start_time?: unknown;
end_time?: unknown;
protected?: unknown;
description?: unknown;
}

const getCurrentWeekStart = () => {
const now = new Date();
const dayOfWeek = now.getDay();
const startOfWeek = new Date(now);
startOfWeek.setDate(now.getDate() - dayOfWeek);
startOfWeek.setHours(0, 0, 0, 0);
return startOfWeek;
};

const formatLastUpdated = (value: Date | null) => {
if (!value) return "Never";
return value.toLocaleTimeString("en-US", {
hour: "numeric",
minute: "2-digit",
second: "2-digit",
hour12: true,
});
};

const parseEventDate = (value: unknown): Date | null => {
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : value;
}

if (typeof value !== "string") return null;
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? null : parsed;
};

const toCalendarEvent = (event: ApiCalendarEvent): CalendarEvent | null => {
const startTime = parseEventDate(event.startTime ?? event.start_time);
const endTime = parseEventDate(event.endTime ?? event.end_time);

if (!startTime || !endTime) return null;

const rawDomain = event.domain;
const domain: EventDomain = isEventDomain(rawDomain) ? rawDomain : "vorentoe";

return {
id: String(event.id ?? ""),
title: String(event.title ?? ""),
domain,
startTime,
endTime,
protected: Boolean(event.protected),
description: typeof event.description === "string" ? event.description : undefined,
};
};

export default function CalendarPage() {
const [weekStart, setWeekStart] = useState(getCurrentWeekStart());
const [events, setEvents] = useState<CalendarEvent[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [error, setError] = useState<string | null>(null);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const [refreshNonce, setRefreshNonce] = useState(0);

const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
weekEnd.setHours(23, 59, 59, 999);
const weekEnd = useMemo(() => {
const value = new Date(weekStart);
value.setDate(weekStart.getDate() + 6);
value.setHours(23, 59, 59, 999);
return value;
}, [weekStart]);

const loadEvents = useCallback(
async (signal?: AbortSignal) => {
const firstLoad = lastUpdated === null;
setError(null);
setLoading(firstLoad);
setRefreshing(!firstLoad);

try {
const start = weekStart.toISOString();
const end = weekEnd.toISOString();
const res = await fetch(`/api/calendar/events?start=${start}&end=${end}`, { signal, cache: "no-store" });

if (!res.ok) {
let message = `Failed to load events (${res.status})`;
try {
const payload = (await res.json()) as { error?: string };
if (payload?.error) message = payload.error;
} catch {
// ignore parse failures
}
throw new Error(message);
}

const data = (await res.json()) as unknown;
const list = Array.isArray(data) ? (data as ApiCalendarEvent[]) : [];
const parsed = list.map(toCalendarEvent).filter((e): e is CalendarEvent => e !== null);

useEffect(() => {
setLoading(true);
const start = weekStart.toISOString();
const end = weekEnd.toISOString();
fetch(`/api/calendar/events?start=${start}&end=${end}`)
.then((res) => res.json())
.then((data) => {
const list = Array.isArray(data) ? data : [];
const parsed: CalendarEvent[] = list.map((e: Record<string, unknown>) => ({
id: String(e.id ?? ""),
title: String(e.title ?? ""),
domain: (e.domain as EventDomain) ?? "vorentoe",
startTime: e.startTime instanceof Date ? e.startTime : new Date((e.startTime as string) ?? (e.start_time as string) ?? ""),
endTime: e.endTime instanceof Date ? e.endTime : new Date((e.endTime as string) ?? (e.end_time as string) ?? ""),
protected: Boolean(e.protected),
description: e.description as string | undefined,
}));
setEvents(parsed);
})
.catch((err) => {
setLastUpdated(new Date());
} catch (err) {
if (signal?.aborted) return;
const message = err instanceof Error ? err.message : "Failed to load calendar events";
console.error("Failed to load calendar events:", err);
setEvents([]);
})
.finally(() => setLoading(false));
}, [weekStart.toISOString().slice(0, 10)]);
setError(message);
} finally {
if (!signal?.aborted) {
setLoading(false);
setRefreshing(false);
}
}
},
[lastUpdated, weekEnd, weekStart]
);

useEffect(() => {
const controller = new AbortController();
loadEvents(controller.signal);
return () => controller.abort();
}, [loadEvents, refreshNonce]);

const conflicts = detectConflicts(events);

Expand All @@ -71,6 +156,10 @@ export default function CalendarPage() {
setWeekStart(getCurrentWeekStart());
};

const refresh = () => {
setRefreshNonce((value) => value + 1);
};

const formatWeekRange = () => {
const end = new Date(weekStart);
end.setDate(weekStart.getDate() + 6);
Expand All @@ -90,17 +179,24 @@ export default function CalendarPage() {

return (
<div className="h-[calc(100vh-4rem)] flex flex-col">
{/* Header with navigation */}
<div className="bg-gray-900 border-b border-gray-800 p-4">
<div className="flex items-center justify-between">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-100">📅 Calendar</h1>
<p className="text-sm text-gray-400 mt-1">
{formatWeekRange()}
{loading && " (loading...)"}
<p className="text-sm text-gray-400 mt-1">{formatWeekRange()}</p>
<p className="text-xs text-gray-500 mt-1">
Last updated: {formatLastUpdated(lastUpdated)}
{refreshing ? " • refreshing..." : ""}
</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={refresh}
disabled={loading || refreshing}
className="px-4 py-2 bg-emerald-700 text-white rounded-lg hover:bg-emerald-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
↻ Refresh
</button>
<button
onClick={goToPreviousWeek}
className="px-4 py-2 bg-gray-800 text-gray-100 rounded-lg hover:bg-gray-700 transition-colors"
Expand All @@ -123,10 +219,31 @@ export default function CalendarPage() {
</div>
</div>

{/* Calendar view with conflict panel */}
<div className="flex-1 flex overflow-hidden">
<WeekView weekStart={weekStart} events={events} conflicts={conflicts} />
<ConflictPanel conflicts={conflicts} />
{error && (
<div className="mx-4 mt-3 rounded-lg border border-red-700 bg-red-950/60 px-4 py-3 text-sm text-red-200 flex items-center justify-between gap-3">
<span>Could not load calendar events: {error}</span>
<button onClick={refresh} className="underline hover:text-white transition-colors">
Try again
</button>
</div>
)}

<div className="flex-1 flex overflow-hidden mt-3">
{loading ? (
<div className="flex-1 grid place-items-center bg-gray-950 text-gray-400">Loading calendar events…</div>
) : (
<>
<div className="flex-1 flex flex-col min-w-0">
{events.length === 0 && !error && (
<div className="mx-4 mb-3 rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-sm text-gray-300">
No events found for this week.
</div>
)}
<WeekView weekStart={weekStart} events={events} conflicts={conflicts} />
</div>
<ConflictPanel conflicts={conflicts} />
</>
)}
</div>
</div>
);
Expand Down
Loading