mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-29 02:24:02 +02:00
feat: enhance SyncStatusBar with popover for service logs and increase event limit
- Introduced a Popover component to display sync activity and logs. - Increased the maximum sync events from 30 to 1000. - Added a mapping for service labels to improve user feedback on active services. - Refactored state management to track active services and log events more effectively.
This commit is contained in:
parent
950c862e02
commit
2bb27e477f
1 changed files with 133 additions and 46 deletions
|
|
@ -43,6 +43,11 @@ import {
|
|||
SidebarRail,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
|
|
@ -110,9 +115,18 @@ type BackgroundTaskItem = {
|
|||
|
||||
type ServiceEventType = z.infer<typeof ServiceEvent>
|
||||
|
||||
const MAX_SYNC_EVENTS = 30
|
||||
const MAX_SYNC_EVENTS = 1000
|
||||
const RUN_STALE_MS = 2 * 60 * 60 * 1000
|
||||
|
||||
const SERVICE_LABELS: Record<string, string> = {
|
||||
gmail: "Syncing Gmail",
|
||||
calendar: "Syncing Calendar",
|
||||
fireflies: "Syncing Fireflies",
|
||||
granola: "Syncing Granola",
|
||||
graph: "Updating knowledge",
|
||||
voice_memo: "Processing voice memo",
|
||||
}
|
||||
|
||||
type TasksActions = {
|
||||
onNewChat: () => void
|
||||
onSelectRun: (runId: string) => void
|
||||
|
|
@ -146,29 +160,28 @@ function formatEventTime(ts: string): string {
|
|||
|
||||
function SyncStatusBar() {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const [events, setEvents] = useState<ServiceEventType[]>([])
|
||||
const [activeRuns, setActiveRuns] = useState<Set<string>>(new Set())
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [activeServices, setActiveServices] = useState<Map<string, string>>(new Map())
|
||||
const [popoverOpen, setPopoverOpen] = useState(false)
|
||||
const [logEvents, setLogEvents] = useState<ServiceEventType[]>([])
|
||||
const [logLoading, setLogLoading] = useState(false)
|
||||
const runTimeoutsRef = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map())
|
||||
|
||||
// Track active runs from real-time events
|
||||
useEffect(() => {
|
||||
const cleanup = window.ipc.on('services:events', (event) => {
|
||||
const nextEvent = event as ServiceEventType
|
||||
setEvents((prev) => [nextEvent, ...prev].slice(0, MAX_SYNC_EVENTS))
|
||||
if (nextEvent.type === 'run_start') {
|
||||
setActiveRuns((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.add(nextEvent.runId)
|
||||
setActiveServices((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.set(nextEvent.runId, nextEvent.service)
|
||||
return next
|
||||
})
|
||||
const existingTimeout = runTimeoutsRef.current.get(nextEvent.runId)
|
||||
if (existingTimeout) {
|
||||
clearTimeout(existingTimeout)
|
||||
}
|
||||
if (existingTimeout) clearTimeout(existingTimeout)
|
||||
const timeout = setTimeout(() => {
|
||||
setActiveRuns((prev) => {
|
||||
setActiveServices((prev) => {
|
||||
if (!prev.has(nextEvent.runId)) return prev
|
||||
const next = new Set(prev)
|
||||
const next = new Map(prev)
|
||||
next.delete(nextEvent.runId)
|
||||
return next
|
||||
})
|
||||
|
|
@ -176,8 +189,8 @@ function SyncStatusBar() {
|
|||
}, RUN_STALE_MS)
|
||||
runTimeoutsRef.current.set(nextEvent.runId, timeout)
|
||||
} else if (nextEvent.type === 'run_complete') {
|
||||
setActiveRuns((prev) => {
|
||||
const next = new Set(prev)
|
||||
setActiveServices((prev) => {
|
||||
const next = new Map(prev)
|
||||
next.delete(nextEvent.runId)
|
||||
return next
|
||||
})
|
||||
|
|
@ -198,9 +211,48 @@ function SyncStatusBar() {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const isSyncing = activeRuns.size > 0
|
||||
// Load logs from JSONL file when popover opens
|
||||
useEffect(() => {
|
||||
if (!popoverOpen) return
|
||||
let cancelled = false
|
||||
async function loadLogs() {
|
||||
setLogLoading(true)
|
||||
try {
|
||||
const result = await window.ipc.invoke('workspace:readFile', {
|
||||
path: 'logs/services.jsonl',
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (cancelled) return
|
||||
const lines = result.data.trim().split('\n').filter(Boolean)
|
||||
const parsed: ServiceEventType[] = []
|
||||
for (const line of lines) {
|
||||
try {
|
||||
parsed.push(JSON.parse(line))
|
||||
} catch {
|
||||
// skip malformed lines
|
||||
}
|
||||
}
|
||||
// Newest first, limit to 1000
|
||||
setLogEvents(parsed.reverse().slice(0, MAX_SYNC_EVENTS))
|
||||
} catch {
|
||||
if (!cancelled) setLogEvents([])
|
||||
} finally {
|
||||
if (!cancelled) setLogLoading(false)
|
||||
}
|
||||
}
|
||||
loadLogs()
|
||||
return () => { cancelled = true }
|
||||
}, [popoverOpen])
|
||||
|
||||
const isSyncing = activeServices.size > 0
|
||||
const isCollapsed = state === "collapsed"
|
||||
|
||||
// Build status label from active services
|
||||
const activeServiceNames = [...new Set(activeServices.values())]
|
||||
const statusLabel = isSyncing
|
||||
? activeServiceNames.map((s) => SERVICE_LABELS[s] || s).join(", ")
|
||||
: "All caught up"
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isMobile && isCollapsed && isSyncing && (
|
||||
|
|
@ -213,37 +265,72 @@ function SyncStatusBar() {
|
|||
</div>
|
||||
)}
|
||||
<SidebarFooter className="border-t border-sidebar-border px-2 py-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded((prev) => !prev)}
|
||||
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
{isSyncing ? (
|
||||
<LoaderIcon className="h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/60" />
|
||||
)}
|
||||
{isSyncing ? "Syncing" : "All caught up"}
|
||||
</span>
|
||||
<ChevronRight className={`h-3 w-3 transition-transform ${isExpanded ? "rotate-90" : ""}`} />
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="mt-2 max-h-40 space-y-1 overflow-auto rounded-md border border-border bg-background p-2 text-xs text-muted-foreground">
|
||||
{events.length === 0 ? (
|
||||
<div>No recent activity.</div>
|
||||
) : (
|
||||
events.map((event, idx) => (
|
||||
<div key={`${event.runId}-${event.ts}-${idx}`} className="flex items-start gap-2">
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground/70">
|
||||
{formatEventTime(event.ts)}
|
||||
</span>
|
||||
<span className="leading-4">{event.message}</span>
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center justify-between rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-sidebar-accent"
|
||||
>
|
||||
<span className="flex items-center gap-2 min-w-0">
|
||||
{isSyncing ? (
|
||||
<LoaderIcon className="h-3 w-3 shrink-0 animate-spin" />
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 shrink-0 rounded-full bg-muted-foreground/60" />
|
||||
)}
|
||||
<span className="truncate">{statusLabel}</span>
|
||||
</span>
|
||||
<ChevronRight className="h-3 w-3 shrink-0" />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="end"
|
||||
sideOffset={8}
|
||||
className="w-96 p-0"
|
||||
>
|
||||
<div className="p-3 border-b">
|
||||
<h4 className="font-semibold text-sm">Sync Activity</h4>
|
||||
<p className="text-xs text-muted-foreground mt-0.5">
|
||||
{isSyncing ? statusLabel : "All services up to date"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto p-2">
|
||||
{logLoading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<LoaderIcon className="h-4 w-4 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
) : logEvents.length === 0 ? (
|
||||
<div className="py-4 text-center text-xs text-muted-foreground">
|
||||
No recent activity.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-0.5">
|
||||
{logEvents.map((event, idx) => (
|
||||
<div
|
||||
key={`${event.runId}-${event.ts}-${idx}`}
|
||||
className="flex items-start gap-2 rounded px-2 py-1 text-xs hover:bg-accent"
|
||||
>
|
||||
<span className="shrink-0 text-[10px] leading-4 text-muted-foreground/70">
|
||||
{formatEventTime(event.ts)}
|
||||
</span>
|
||||
<span className="shrink-0">
|
||||
<span className={cn(
|
||||
"inline-block rounded px-1 py-0.5 text-[10px] font-medium leading-none",
|
||||
event.level === 'error' ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" :
|
||||
event.level === 'warn' ? "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" :
|
||||
"bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{SERVICE_LABELS[event.service]?.split(" ").slice(-1)[0] || event.service}
|
||||
</span>
|
||||
</span>
|
||||
<span className="leading-4 text-foreground/80">{event.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</SidebarFooter>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue