Download react-dashboard/src/App.jsx from engineofperplexity/openclaw-ops-dashboard: direct link, hf CLI and curl.
- Browser
- Download file 12.1 kB
-
https://huggingface.co/engineofperplexity/openclaw-ops-dashboard/resolve/main/react-dashboard/src/App.jsx
- Command line
-
hf download hf://engineofperplexity/openclaw-ops-dashboard/react-dashboard/src/App.jsx
-
curl -L -o App.jsx https://huggingface.co/engineofperplexity/openclaw-ops-dashboard/resolve/main/react-dashboard/src/App.jsx
12.1 kB
| import { useEffect, useMemo, useState } from 'react'; | |
| import { | |
| Activity, | |
| AlertTriangle, | |
| Bot, | |
| CheckCircle2, | |
| CirclePause, | |
| CirclePlay, | |
| ClipboardCheck, | |
| ExternalLink, | |
| RefreshCw, | |
| ShieldCheck, | |
| Sparkles, | |
| TerminalSquare, | |
| } from 'lucide-react'; | |
| const API_BASE = import.meta.env.VITE_SESSION_AMPLIFIER_BASE_URL || '/session-amplifier'; | |
| const POLL_MS = 15000; | |
| async function fetchJson(path) { | |
| const response = await fetch(API_BASE + path); | |
| if (!response.ok) { | |
| throw new Error(response.status + ' ' + response.statusText); | |
| } | |
| return response.json(); | |
| } | |
| function formatAge(value) { | |
| if (!value) return 'unknown'; | |
| const at = new Date(value); | |
| if (Number.isNaN(at.getTime())) return 'unknown'; | |
| const seconds = Math.max(0, Math.floor((Date.now() - at.getTime()) / 1000)); | |
| if (seconds < 60) return seconds + 's'; | |
| const minutes = Math.floor(seconds / 60); | |
| if (minutes < 60) return minutes + 'm'; | |
| const hours = Math.floor(minutes / 60); | |
| if (hours < 36) return hours + 'h'; | |
| return Math.floor(hours / 24) + 'd'; | |
| } | |
| function compactText(value, limit = 110) { | |
| const text = String(value || '').replace(/\s+/g, ' ').trim(); | |
| if (!text) return ''; | |
| return text.length > limit ? text.slice(0, limit - 1).trimEnd() + '…' : text; | |
| } | |
| function looksLikeOpaqueId(value) { | |
| return /^[0-9a-f]{8}-[0-9a-f-]{27,}$/i.test(String(value || '').trim()); | |
| } | |
| function firstMeaningfulEvent(events = []) { | |
| return events.find((event) => { | |
| const text = event?.details || event?.summary || event?.clean_text || event?.preview || ''; | |
| return text && text !== 'assistant' && (event.role === 'user' || event.event_type === 'user_message'); | |
| }) || events.find((event) => { | |
| const text = event?.details || event?.summary || event?.clean_text || event?.preview || ''; | |
| return text && text !== 'assistant'; | |
| }); | |
| } | |
| function sessionTextLabel(event) { | |
| if (!event) return ''; | |
| const raw = event.details || event.summary || event.clean_text || event.preview || ''; | |
| const firstLine = String(raw).split('\n').find((line) => line.trim()) || ''; | |
| const cronMatch = firstLine.match(/^\[cron:[^\s\]]+\s+([^\]]+)\]\s*(.*)$/); | |
| if (cronMatch) { | |
| return compactText(cronMatch[1] + ': ' + cronMatch[2], 96); | |
| } | |
| return compactText(firstLine.replace(/^#+\s*/, ''), 96); | |
| } | |
| function quotedHint(event) { | |
| if (!event) return ''; | |
| const raw = event.details || event.summary || event.clean_text || event.preview || ''; | |
| const quote = String(raw).match(/["“']([^"”']{8,130})["”']/); | |
| if (quote?.[1]) return compactText('"' + quote[1] + '"', 120); | |
| const label = sessionTextLabel(event); | |
| return label ? compactText('"' + label + '"', 120) : ''; | |
| } | |
| function shortSessionId(session) { | |
| return String(session.session_id || '').slice(0, 8) || 'unknown'; | |
| } | |
| function pickTitle(session, events = []) { | |
| const configuredTitle = ( | |
| session.display_title || | |
| session.displayTitle || | |
| session.display_name || | |
| session.displayName || | |
| session.origin_label | |
| ); | |
| if (configuredTitle && !looksLikeOpaqueId(configuredTitle)) { | |
| return { title: configuredTitle, hint: quotedHint(firstMeaningfulEvent(events)) }; | |
| } | |
| const firstEvent = firstMeaningfulEvent(events); | |
| const derivedTitle = sessionTextLabel(firstEvent); | |
| return { | |
| title: derivedTitle || 'Session ' + shortSessionId(session), | |
| hint: quotedHint(firstEvent), | |
| }; | |
| } | |
| function deriveState(session, events = []) { | |
| if (session.derived_state) return session.derived_state; | |
| if (session.health === 'error') return 'error'; | |
| const latest = [...events].reverse().find(Boolean); | |
| if (!latest) return 'idle'; | |
| if (latest.is_error) return 'error'; | |
| if (latest.tool_name) return 'active'; | |
| if ((latest.preview || latest.clean_text || '').toLowerCase().includes('permission')) return 'waiting'; | |
| return latest.role === 'assistant' || latest.role === 'user' ? 'active' : 'idle'; | |
| } | |
| function stateMeta(state, needsPermission) { | |
| if (needsPermission || state === 'waiting') { | |
| return { label: 'Waiting', tone: 'waiting', Icon: CirclePause }; | |
| } | |
| if (state === 'active') return { label: 'Active', tone: 'active', Icon: Activity }; | |
| if (state === 'error') return { label: 'Error', tone: 'error', Icon: AlertTriangle }; | |
| return { label: 'Idle', tone: 'idle', Icon: CheckCircle2 }; | |
| } | |
| function MetricCard({ icon: Icon, label, value, sublabel, tone = 'neutral' }) { | |
| return ( | |
| <section className={'metric metric--' + tone}> | |
| <div className="metric__icon"><Icon size={18} /></div> | |
| <div> | |
| <div className="metric__value">{value}</div> | |
| <div className="metric__label">{label}</div> | |
| {sublabel ? <div className="metric__sublabel">{sublabel}</div> : null} | |
| </div> | |
| </section> | |
| ); | |
| } | |
| function SessionCard({ session, events }) { | |
| const state = deriveState(session, events); | |
| const meta = stateMeta(state, session.needs_permission); | |
| const latest = [...events].reverse().find(Boolean); | |
| const currentTool = session.current_tool_name || latest?.tool_name || 'none'; | |
| const { title, hint } = pickTitle(session, events); | |
| return ( | |
| <article className={'session-card session-card--' + meta.tone}> | |
| <header className="session-card__head"> | |
| <div className="session-card__identity"> | |
| <span className={'status-pill status-pill--' + meta.tone}> | |
| <meta.Icon size={14} /> | |
| {meta.label} | |
| </span> | |
| <h2 title={title}>{title}</h2> | |
| {hint ? <p className="session-card__quote">{hint}</p> : null} | |
| </div> | |
| <span className="session-card__age">{formatAge(session.last_activity_at || session.updated_at || latest?.timestamp)}</span> | |
| </header> | |
| <div className="session-card__meta"> | |
| <span><Bot size={14} />{session.agent_id || 'unknown'}</span> | |
| <span><TerminalSquare size={14} />{currentTool}</span> | |
| <span><Activity size={14} />{shortSessionId(session)} · {events.length} events</span> | |
| </div> | |
| <p className="session-card__preview"> | |
| {latest?.preview || latest?.clean_text || session.active_reason || 'No recent transcript preview'} | |
| </p> | |
| <footer className="session-card__actions"> | |
| <button type="button" title="Open transcript when a route is wired" disabled> | |
| <ExternalLink size={15} /> | |
| Open | |
| </button> | |
| <button type="button" title="Run control requires an action endpoint" disabled> | |
| <CirclePlay size={15} /> | |
| Run | |
| </button> | |
| <button type="button" title="Pause/resume requires an action endpoint" disabled> | |
| <CirclePause size={15} /> | |
| Pause | |
| </button> | |
| </footer> | |
| </article> | |
| ); | |
| } | |
| function App() { | |
| const [health, setHealth] = useState(null); | |
| const [bulk, setBulk] = useState({ sessions: [], activity: {} }); | |
| const [skills, setSkills] = useState(null); | |
| const [error, setError] = useState(''); | |
| const [loading, setLoading] = useState(true); | |
| async function refresh() { | |
| setError(''); | |
| try { | |
| const results = await Promise.allSettled([ | |
| fetchJson('/health'), | |
| fetchJson('/sessions/active-bulk?limit=30&activity_limit=80'), | |
| fetchJson('/review/skills'), | |
| ]); | |
| const [healthResult, bulkResult, skillsResult] = results; | |
| if (healthResult.status === 'fulfilled') setHealth(healthResult.value); | |
| if (bulkResult.status === 'fulfilled') setBulk(bulkResult.value); | |
| if (skillsResult.status === 'fulfilled') setSkills(skillsResult.value); | |
| const rejected = [healthResult, bulkResult].find((result) => result.status === 'rejected'); | |
| if (rejected) throw rejected.reason; | |
| } catch (err) { | |
| setError(err.message || String(err)); | |
| } finally { | |
| setLoading(false); | |
| } | |
| } | |
| useEffect(() => { | |
| refresh(); | |
| const timer = window.setInterval(refresh, POLL_MS); | |
| return () => window.clearInterval(timer); | |
| }, []); | |
| const sessions = bulk.sessions || []; | |
| const activity = bulk.activity || {}; | |
| const metrics = useMemo(() => { | |
| const states = sessions.map((session) => deriveState(session, activity[session.session_id] || [])); | |
| const active = states.filter((state) => state === 'active').length; | |
| const waiting = sessions.filter((session, index) => session.needs_permission || states[index] === 'waiting').length; | |
| const errors = states.filter((state) => state === 'error').length; | |
| const toolHeavy = sessions.filter((session) => (activity[session.session_id] || []).filter((row) => row.tool_name).length >= 5).length; | |
| const candidateCount = Array.isArray(skills?.candidates) ? skills.candidates.length : Array.isArray(skills?.recommendations) ? skills.recommendations.length : 0; | |
| return { active, waiting, errors, toolHeavy, candidateCount }; | |
| }, [sessions, activity, skills]); | |
| return ( | |
| <main className="app-shell"> | |
| <header className="topbar"> | |
| <div> | |
| <p className="eyebrow">OpenClaw Ops</p> | |
| <h1>Action Dashboard</h1> | |
| </div> | |
| <button className="refresh-button" type="button" onClick={refresh}> | |
| <RefreshCw size={16} className={loading ? 'spin' : ''} /> | |
| Refresh | |
| </button> | |
| </header> | |
| {error ? ( | |
| <section className="alert"> | |
| <AlertTriangle size={16} /> | |
| <span>{error}</span> | |
| </section> | |
| ) : null} | |
| <section className="metrics-grid"> | |
| <MetricCard icon={ShieldCheck} label="Amplifier" value={health?.status || 'unknown'} sublabel={(health?.sessions ?? 0) + ' indexed sessions'} tone={health?.status === 'ok' ? 'good' : 'warn'} /> | |
| <MetricCard icon={Activity} label="Active" value={metrics.active} sublabel={sessions.length + ' recent sessions'} tone="info" /> | |
| <MetricCard icon={CirclePause} label="Waiting" value={metrics.waiting} sublabel="permission or stalled state" tone={metrics.waiting ? 'warn' : 'neutral'} /> | |
| <MetricCard icon={AlertTriangle} label="Errors" value={metrics.errors} sublabel="recent session state" tone={metrics.errors ? 'bad' : 'neutral'} /> | |
| <MetricCard icon={TerminalSquare} label="Tool Heavy" value={metrics.toolHeavy} sublabel="5+ tool events" tone="neutral" /> | |
| <MetricCard icon={ClipboardCheck} label="Skill Signals" value={metrics.candidateCount || 'review'} sublabel="from skill review API" tone="neutral" /> | |
| </section> | |
| <section className="workspace-grid"> | |
| <section className="panel panel--wide"> | |
| <div className="panel__head"> | |
| <h2>Live Sessions</h2> | |
| <span>{bulk.generated_at ? 'Updated ' + formatAge(bulk.generated_at) + ' ago' : 'Not loaded'}</span> | |
| </div> | |
| <div className="sessions-grid"> | |
| {sessions.length ? ( | |
| sessions.map((session) => ( | |
| <SessionCard | |
| key={session.session_id} | |
| session={session} | |
| events={activity[session.session_id] || []} | |
| /> | |
| )) | |
| ) : ( | |
| <div className="empty-state"> | |
| <Sparkles size={18} /> | |
| <span>No recent sessions returned.</span> | |
| </div> | |
| )} | |
| </div> | |
| </section> | |
| <aside className="panel"> | |
| <div className="panel__head"> | |
| <h2>Promotion Lane</h2> | |
| <span>advisory</span> | |
| </div> | |
| <div className="lane-list"> | |
| <div className="lane-row"> | |
| <ShieldCheck size={16} /> | |
| <span>Installed skills may be used automatically when the match is safe.</span> | |
| </div> | |
| <div className="lane-row"> | |
| <ClipboardCheck size={16} /> | |
| <span>New installs, agent allowlists, and config changes stay approval-gated.</span> | |
| </div> | |
| <div className="lane-row"> | |
| <ExternalLink size={16} /> | |
| <span>Approval packets are summarized by scripts/openclaw_skill_approval_status.py.</span> | |
| </div> | |
| </div> | |
| </aside> | |
| </section> | |
| </main> | |
| ); | |
| } | |
| export default App; | |