feat(web): subscribe Dashboard to SSE for real-time session updates
Wire the Dashboard component to the existing /api/events SSE endpoint so session status, activity, and lastActivityAt update in real-time without requiring a full page refresh. SSE snapshots are lightweight patches merged into the full server-rendered session objects. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4cda43795b
commit
0929a51ee7
|
|
@ -101,6 +101,6 @@ export default async function Home() {
|
|||
}
|
||||
|
||||
return (
|
||||
<Dashboard sessions={sessions} stats={computeStats(sessions)} orchestratorId={orchestratorId} projectName={projectName} />
|
||||
<Dashboard initialSessions={sessions} stats={computeStats(sessions)} orchestratorId={orchestratorId} projectName={projectName} />
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ import { CI_STATUS } from "@composio/ao-core/types";
|
|||
import { AttentionZone } from "./AttentionZone";
|
||||
import { PRTableRow } from "./PRStatus";
|
||||
import { DynamicFavicon } from "./DynamicFavicon";
|
||||
import { useSessionEvents } from "@/hooks/useSessionEvents";
|
||||
|
||||
interface DashboardProps {
|
||||
sessions: DashboardSession[];
|
||||
initialSessions: DashboardSession[];
|
||||
stats: DashboardStats;
|
||||
orchestratorId?: string | null;
|
||||
projectName?: string;
|
||||
|
|
@ -23,7 +24,8 @@ interface DashboardProps {
|
|||
|
||||
const KANBAN_LEVELS = ["working", "pending", "review", "respond", "merge"] as const;
|
||||
|
||||
export function Dashboard({ sessions, stats, orchestratorId, projectName }: DashboardProps) {
|
||||
export function Dashboard({ initialSessions, stats, orchestratorId, projectName }: DashboardProps) {
|
||||
const sessions = useSessionEvents(initialSessions);
|
||||
const [rateLimitDismissed, setRateLimitDismissed] = useState(false);
|
||||
const grouped = useMemo(() => {
|
||||
const zones: Record<AttentionLevel, DashboardSession[]> = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useReducer } from "react";
|
||||
import type { DashboardSession, SSESnapshotEvent } from "@/lib/types";
|
||||
|
||||
type Action =
|
||||
| { type: "reset"; sessions: DashboardSession[] }
|
||||
| { type: "snapshot"; patches: SSESnapshotEvent["sessions"] };
|
||||
|
||||
function reducer(state: DashboardSession[], action: Action): DashboardSession[] {
|
||||
switch (action.type) {
|
||||
case "reset":
|
||||
return action.sessions;
|
||||
case "snapshot": {
|
||||
const patchMap = new Map(action.patches.map((p) => [p.id, p]));
|
||||
let changed = false;
|
||||
const next = state.map((s) => {
|
||||
const patch = patchMap.get(s.id);
|
||||
if (!patch) return s;
|
||||
if (
|
||||
s.status === patch.status &&
|
||||
s.activity === patch.activity &&
|
||||
s.lastActivityAt === patch.lastActivityAt
|
||||
) {
|
||||
return s;
|
||||
}
|
||||
changed = true;
|
||||
return { ...s, status: patch.status, activity: patch.activity, lastActivityAt: patch.lastActivityAt };
|
||||
});
|
||||
return changed ? next : state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useSessionEvents(initialSessions: DashboardSession[]): DashboardSession[] {
|
||||
const [sessions, dispatch] = useReducer(reducer, initialSessions);
|
||||
|
||||
// Reset state when server-rendered props change (e.g. full page refresh)
|
||||
useEffect(() => {
|
||||
dispatch({ type: "reset", sessions: initialSessions });
|
||||
}, [initialSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource("/api/events");
|
||||
|
||||
es.onmessage = (event: MessageEvent) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data as string) as { type: string };
|
||||
if (data.type === "snapshot") {
|
||||
const snapshot = data as SSESnapshotEvent;
|
||||
dispatch({ type: "snapshot", patches: snapshot.sessions });
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed messages
|
||||
}
|
||||
};
|
||||
|
||||
es.onerror = () => {
|
||||
// EventSource auto-reconnects; nothing to do here
|
||||
};
|
||||
|
||||
return () => {
|
||||
es.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return sessions;
|
||||
}
|
||||
Loading…
Reference in New Issue