From 84aa98dd14daf2777a8f91403dbc890558db9980 Mon Sep 17 00:00:00 2001 From: Manohar Date: Fri, 5 Jun 2026 19:49:23 +0000 Subject: [PATCH] refactor(dashboard): rewrite activity page, simplify activity API --- dashboard/src/app/activity/page.tsx | 212 +++++++++------------ dashboard/src/app/api/activity/route.ts | 240 +++--------------------- 2 files changed, 120 insertions(+), 332 deletions(-) diff --git a/dashboard/src/app/activity/page.tsx b/dashboard/src/app/activity/page.tsx index b65d831..60df479 100644 --- a/dashboard/src/app/activity/page.tsx +++ b/dashboard/src/app/activity/page.tsx @@ -1,142 +1,112 @@ "use client" -import * as React from "react" -import useSWR from "swr" +import { useEffect, useState } from "react" import { ScrollText } from "lucide-react" -const fetcher = (url: string) => fetch(url).then((res) => res.json()) - interface ActivityEntry { id: string - type: "heartbeat" | "chat" | "config" | "memory" | "system" | "cron" + type: string timestamp: string description: string - source?: string -} - -const typeColors: Record = { - heartbeat: "bg-yellow-400", - chat: "bg-blue-400", - config: "bg-orange-400", - memory: "bg-green-400", - system: "bg-purple-400", - cron: "bg-cyan-400", -} - -function groupByDate(entries: ActivityEntry[]): Record { - const groups: Record = {} - for (const entry of entries) { - const date = new Date(entry.timestamp) - const key = date.toLocaleDateString("en-US", { - weekday: "long", - year: "numeric", - month: "long", - day: "numeric", - }).toUpperCase() - if (!groups[key]) groups[key] = [] - groups[key].push(entry) - } - return groups -} - -function formatTime(timestamp: string): string { - return new Date(timestamp).toLocaleTimeString("en-US", { - hour: "numeric", - minute: "2-digit", - hour12: true, - }) + source: string } export default function ActivityPage() { - const [limit, setLimit] = React.useState(200) - const { data, error } = useSWR(`/api/activity?limit=${limit}`, fetcher, { refreshInterval: 10000 }) + const [entries, setEntries] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) - const entries = (data?.entries || []) as ActivityEntry[] - const total = data?.total || 0 - const grouped = groupByDate(entries) + useEffect(() => { + fetch("/api/activity?limit=20") + .then(r => r.json()) + .then(data => { + if (data?.entries) { + setEntries(data.entries) + } + setLoading(false) + }) + .catch(e => { + console.error("Failed to load:", e) + setError(e.message) + setLoading(false) + }) + }, []) + + const formatDate = (ts: string) => { + if (!ts) return "" + return new Date(ts).toLocaleString() + } + + const getTypeColor = (type: string) => { + switch (type) { + case "heartbeat": return "text-green-500" + case "chat": return "text-blue-500" + case "config": return "text-yellow-500" + case "memory": return "text-purple-500" + case "system": return "text-orange-500" + case "cron": return "text-cyan-500" + default: return "text-muted-foreground" + } + } + + const getSourceLabel = (source: string) => { + switch (source) { + case "main": return "🐅 Tiger" + case "coder": return "📦 Cody" + case "researcher": return "🔬 Ethan" + case "pm": return "📋 Elon" + default: return source || "🤖" + } + } + + if (loading) { + return ( +
+
+ +

Activity

+
+
Loading activity log...
+
+ ) + } + + if (error) { + return ( +
+
+ +

Activity

+
+
Failed to load activity log
+
{error}
+
+ ) + } return ( -
- {/* Header */} -
-
- -
-

Activity Log

-

A chronological record of agent actions and events

-
-
-
- - {total} entries - -
+
+
+ +

Activity

+ ({entries.length} entries)
- {/* Timeline */} -
- {error ? ( -
Failed to load activity log
- ) : !data ? ( -
Loading activity...
- ) : entries.length === 0 ? ( -
No activity recorded yet
- ) : ( -
- {Object.entries(grouped).map(([dateLabel, dateEntries]) => ( -
- {/* Date Header */} -
-
- {dateLabel} -
-
- - {/* Timeline entries */} -
- {/* Vertical line */} -
- -
- {dateEntries.map((entry) => ( -
- {/* Dot */} -
-
-
- - {/* Card */} -
-
- - {formatTime(entry.timestamp)} - -

- {entry.description} -

-
-
-
- ))} -
-
+
+ {entries.map((entry, i) => ( +
+
{getSourceLabel(entry.source)}
+
+
{entry.description}
+
+ {entry.type} + + {formatDate(entry.timestamp)}
- ))} - - {/* Load more */} - {entries.length < total && ( -
- -
- )} +
- )} + ))}
) -} +} \ No newline at end of file diff --git a/dashboard/src/app/api/activity/route.ts b/dashboard/src/app/api/activity/route.ts index ee7a36f..716f201 100644 --- a/dashboard/src/app/api/activity/route.ts +++ b/dashboard/src/app/api/activity/route.ts @@ -1,226 +1,44 @@ import { NextResponse } from "next/server" -import fs from "fs" -import path from "path" -import os from "os" +import { bridgeGet } from "@/lib/bridge" -interface ActivityEntry { - id: string - type: "heartbeat" | "chat" | "config" | "memory" | "system" | "cron" - timestamp: string - description: string - source?: string -} - -function parseCommandsLog(logPath: string): ActivityEntry[] { - const entries: ActivityEntry[] = [] - try { - const content = fs.readFileSync(logPath, "utf-8").trim() - if (!content) return entries - for (const line of content.split("\n")) { - try { - const data = JSON.parse(line) as { - timestamp: string - action: string - sessionKey: string - senderId: string - source: string - } - const actionLabels: Record = { - new: "New chat session", - reset: "Session reset", - delete: "Session deleted", - } - const sourceLabels: Record = { - webchat: "Web Chat", - telegram: "Telegram", - whatsapp: "WhatsApp", - cli: "CLI", - } - const actionLabel = actionLabels[data.action] || data.action - const sourceLabel = sourceLabels[data.source] || data.source - entries.push({ - id: `cmd-${data.timestamp}-${data.action}`, - type: "chat", - timestamp: data.timestamp, - description: `${actionLabel} via ${sourceLabel}`, - source: data.source, - }) - } catch { - // skip malformed lines - } - } - } catch { - // file not found - } - return entries -} - -function parseGatewayLog(logPath: string): ActivityEntry[] { - const entries: ActivityEntry[] = [] - try { - const content = fs.readFileSync(logPath, "utf-8") - const lines = content.split("\n") - - // Track seen heartbeat dates to deduplicate (only first per gateway restart) - let lastHeartbeatDay = "" - - for (const line of lines) { - if (!line.trim()) continue - - // Parse timestamp from start of line: "2026-01-27T02:35:09.747Z [tag] message" - const match = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z)\s+(.*)$/) - if (!match) continue - - const timestamp = match[1] - const rest = match[2] - - // Heartbeat events - only include first per gateway restart (not every "started") - if (rest.startsWith("[heartbeat]")) { - const msg = rest.replace("[heartbeat]", "").trim() - // Skip repetitive "started" - only keep once per gateway boot - if (msg === "started") { - const day = timestamp.slice(0, 10) - if (day === lastHeartbeatDay) continue - lastHeartbeatDay = day - entries.push({ - id: `gw-hb-${timestamp}`, - type: "heartbeat", - timestamp, - description: "Heartbeat service started", - }) - } else { - // Non-"started" heartbeat messages are meaningful - entries.push({ - id: `gw-hb-${timestamp}`, - type: "heartbeat", - timestamp, - description: `Heartbeat: ${msg}`, - }) - } - continue - } - - // Config reload events - if (rest.startsWith("[reload]")) { - const msg = rest.replace("[reload]", "").trim() - const changedMatch = msg.match(/evaluating reload \((.+)\)/) - const changed = changedMatch ? changedMatch[1] : msg - entries.push({ - id: `gw-reload-${timestamp}`, - type: "config", - timestamp, - description: `Config reload: ${changed}`, - }) - continue - } - - // Gateway startup/lifecycle - only truly significant events - if (rest.startsWith("[gateway]")) { - const msg = rest.replace("[gateway]", "").trim() - if (msg.startsWith("listening on")) { - // Reset heartbeat dedup on new gateway start - lastHeartbeatDay = "" - entries.push({ - id: `gw-sys-${timestamp}`, - type: "system", - timestamp, - description: `Gateway started: ${msg}`, - }) - } else if (msg.startsWith("agent model:")) { - entries.push({ - id: `gw-sys-${timestamp}`, - type: "system", - timestamp, - description: `Active model: ${msg.replace("agent model:", "").trim()}`, - }) - } else if (msg.includes("signal")) { - entries.push({ - id: `gw-sys-${timestamp}`, - type: "system", - timestamp, - description: msg, - }) - } - continue - } - - // Telegram provider start (only once per restart) - if (rest.startsWith("[telegram]") && rest.includes("starting provider")) { - const botMatch = rest.match(/\(@[^)]+\)/) - entries.push({ - id: `gw-tg-${timestamp}`, - type: "system", - timestamp, - description: `Telegram provider started ${botMatch?.[0] || ""}`.trim(), - }) - continue - } - - // Cron execution events - if (rest.includes("cron.run") && rest.includes("[ws]") && rest.includes("res ✓")) { - entries.push({ - id: `gw-cron-${timestamp}`, - type: "cron", - timestamp, - description: "Cron job executed", - }) - continue - } - } - } catch { - // file not found - } - return entries -} - -function parseMemoryFiles(memoryDir: string): ActivityEntry[] { - const entries: ActivityEntry[] = [] - try { - const files = fs.readdirSync(memoryDir).filter((f) => f.endsWith(".md")) - for (const file of files) { - const filePath = path.join(memoryDir, file) - const stat = fs.statSync(filePath) - const slug = file.replace(/\.md$/, "").replace(/^\d{4}-\d{2}-\d{2}-/, "") - const title = slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) - entries.push({ - id: `mem-${file}`, - type: "memory", - timestamp: stat.mtime.toISOString(), - description: `Memory saved: ${title}`, - }) - } - } catch { - // directory not found - } - return entries -} +export const dynamic = "force-dynamic" export async function GET(request: Request) { try { const url = new URL(request.url) - const limit = parseInt(url.searchParams.get("limit") || "200", 10) + const limit = parseInt(url.searchParams.get("limit") || "50", 10) - const clawdbotLogsDir = path.join(os.homedir(), ".clawdbot", "logs") - const workspace = "/Users/manohar_air/clawd" - const memoryDir = path.join(workspace, "memory") + // Get activity from bridge endpoint that already works + const bridgeData = await bridgeGet("/tiger/agents/activity") as { + ok: boolean + events: Array<{ + agentId: string + agentName: string + agentEmoji: string + path: string + action: string + ts: number + }> + } - // Aggregate from all sources - const commandEntries = parseCommandsLog(path.join(clawdbotLogsDir, "commands.log")) - const gatewayEntries = parseGatewayLog(path.join(clawdbotLogsDir, "gateway.log")) - const memoryEntries = parseMemoryFiles(memoryDir) + if (!bridgeData?.ok || !bridgeData.events) { + return NextResponse.json({ entries: [], total: 0 }) + } - // Merge and sort by timestamp descending - const allEntries = [...commandEntries, ...gatewayEntries, ...memoryEntries] - allEntries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) - - // Apply limit - const entries = allEntries.slice(0, limit) + // Transform bridge format to activity format + const entries = bridgeData.events.slice(0, limit).map((e) => ({ + id: `${e.agentId}-${e.ts}`, + type: "system", + timestamp: new Date(e.ts).toISOString(), + description: `${e.agentName} modified ${e.path}`, + source: e.agentId, + })) return NextResponse.json({ entries, - total: allEntries.length, + total: bridgeData.events.length, }) - } catch { + } catch (err) { return NextResponse.json({ error: "Failed to fetch activity" }, { status: 500 }) } -} +} \ No newline at end of file