repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
The Living OS cockpit
stars
latest
clone command
git clone gitlawb://did:key:z6Mku78K...XywC/living-os-cockp...git clone gitlawb://did:key:z6Mku78K.../living-os-cockp...59751530feat: surface worker supervisor health in live work5h ago| #1 | 'use client'; |
| #2 | import { useEffect, useState } from 'react'; |
| #3 | import useSWR from 'swr'; |
| #4 | import { swrFetcher } from '@/lib/client-api'; |
| #5 | import { Activity, Clock, CheckCircle2 } from 'lucide-react'; |
| #6 | |
| #7 | const fetcher = swrFetcher; |
| #8 | |
| #9 | function priorityColor(p: string): string { |
| #10 | switch (p.toUpperCase()) { |
| #11 | case 'CRITICAL': return 'text-red-400 border-red-900/40'; |
| #12 | case 'HIGH': return 'text-amber-400 border-amber-900/40'; |
| #13 | case 'MEDIUM': return 'text-blue-400 border-blue-900/40'; |
| #14 | case 'LOW': return 'text-zinc-500 border-zinc-700'; |
| #15 | default: return 'text-zinc-400 border-zinc-700'; |
| #16 | } |
| #17 | } |
| #18 | |
| #19 | export default function AethonTasks({ section = 'all' }: { section?: 'all' | 'active' | 'completed' }) { |
| #20 | const { data } = useSWR('/api/aethon-tasks', fetcher, { refreshInterval: 8000 }); |
| #21 | const [streamMessage, setStreamMessage] = useState(''); |
| #22 | |
| #23 | useEffect(() => { |
| #24 | const source = new EventSource('/api/v1/tasks/stream'); |
| #25 | source.addEventListener('task-progress', (event) => { |
| #26 | try { |
| #27 | const payload = JSON.parse((event as MessageEvent).data); |
| #28 | setStreamMessage(payload.message || ''); |
| #29 | } catch { |
| #30 | setStreamMessage(''); |
| #31 | } |
| #32 | }); |
| #33 | source.onerror = () => { |
| #34 | source.close(); |
| #35 | }; |
| #36 | return () => source.close(); |
| #37 | }, []); |
| #38 | |
| #39 | if (!data) return <div className="text-[color:var(--text-3)] text-sm">Loading tasks...</div>; |
| #40 | |
| #41 | return ( |
| #42 | <div className="space-y-3"> |
| #43 | {(streamMessage || data.liveStatus?.message) && ( |
| #44 | <div className="rounded border border-[color:var(--gold)]/35 bg-[color:var(--gold)]/10 p-2 text-xs text-[color:var(--text-1)]"> |
| #45 | <div className="mb-1 font-semibold text-[color:var(--gold)]">Live progress</div> |
| #46 | <div>{streamMessage || data.liveStatus.message}</div> |
| #47 | </div> |
| #48 | )} |
| #49 | |
| #50 | {section !== 'completed' && data.inProgress.length > 0 && ( |
| #51 | <div> |
| #52 | <div className="flex items-center gap-1.5 text-xs mb-1.5 text-[color:var(--amber)] live-pulse"> |
| #53 | <Activity size={12} /> In Progress ({data.inProgress.length}) |
| #54 | </div> |
| #55 | <div className="space-y-1"> |
| #56 | {data.inProgress.map((t: any) => ( |
| #57 | <TaskRow key={t.id} task={t} /> |
| #58 | ))} |
| #59 | </div> |
| #60 | </div> |
| #61 | )} |
| #62 | |
| #63 | {section !== 'completed' && data.queued.length > 0 && ( |
| #64 | <div> |
| #65 | <div className="flex items-center gap-1.5 text-xs mb-1.5 text-[color:var(--text-3)]"> |
| #66 | <Clock size={12} /> Queued ({data.queued.length}) |
| #67 | </div> |
| #68 | <div className="space-y-1"> |
| #69 | {data.queued.map((t: any) => ( |
| #70 | <TaskRow key={t.id} task={t} /> |
| #71 | ))} |
| #72 | </div> |
| #73 | </div> |
| #74 | )} |
| #75 | |
| #76 | {section !== 'active' && data.completed.length > 0 && ( |
| #77 | <details className="text-xs" open={section === 'completed'}> |
| #78 | <summary className={`cursor-pointer text-[color:var(--text-3)] hover:text-[color:var(--text-1)] flex items-center gap-1.5 ${section === 'completed' ? 'hidden' : ''}`}> |
| #79 | <CheckCircle2 size={12} /> Completed ({data.totals.completed}) |
| #80 | </summary> |
| #81 | <div className="space-y-1 mt-2"> |
| #82 | {data.completed.map((t: any) => ( |
| #83 | <TaskRow key={t.id} task={t} faded /> |
| #84 | ))} |
| #85 | </div> |
| #86 | </details> |
| #87 | )} |
| #88 | |
| #89 | {section !== 'completed' && data.queued.length === 0 && data.inProgress.length === 0 && ( |
| #90 | <div className="text-sm text-[color:var(--text-3)]">No active tasks. Queue one in Aethon.</div> |
| #91 | )} |
| #92 | {section === 'completed' && data.completed.length === 0 && ( |
| #93 | <div className="text-sm text-[color:var(--text-3)]">No completed work recorded yet.</div> |
| #94 | )} |
| #95 | </div> |
| #96 | ); |
| #97 | } |
| #98 | |
| #99 | function TaskRow({ task, faded }: { task: any; faded?: boolean }) { |
| #100 | return ( |
| #101 | <div className={`rounded p-2 border ${priorityColor(task.priority)} ${faded ? 'opacity-50' : ''} bg-[color:var(--bg-3)]/40`}> |
| #102 | <div className="flex items-start justify-between gap-2"> |
| #103 | <div className="flex-1 min-w-0"> |
| #104 | <div className="text-sm text-[color:var(--text-1)] truncate">{task.title}</div> |
| #105 | <div className="text-[10px] text-[color:var(--text-3)] mt-0.5 flex gap-2"> |
| #106 | <span>{task.priority}</span> |
| #107 | <span>·</span> |
| #108 | <span>{task.type}</span> |
| #109 | <span>·</span> |
| #110 | <span>Tier 1: {task.tier1}</span> |
| #111 | </div> |
| #112 | {task.liveStatus && ( |
| #113 | <div className="mt-1 text-[11px] leading-4 text-[color:var(--gold)]">{task.liveStatus}</div> |
| #114 | )} |
| #115 | </div> |
| #116 | </div> |
| #117 | </div> |
| #118 | ); |
| #119 | } |
| #120 |