feat: Dashboard stats from OCM API, fix module warnings (v0.10.1)

- Rename postcss.config.js/tailwind.config.js to .cjs to fix ESM warning
- Replace Stats Summary placeholder with real OCM /api/stats data
- Handle loading, error, and offline states gracefully
- Bump version to 0.10.1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-20 08:07:10 +10:00
co-authored by Claude Opus 4.6
parent cdcf28ff4e
commit 7c107471cf
5 changed files with 105 additions and 12 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "odo",
"version": "0.10.0",
"version": "0.10.1",
"description": "ODO — OpenClaw Dashboard Orchestrator",
"main": "main.js",
"scripts": {
+1 -1
View File
@@ -1,4 +1,4 @@
export default {
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
+1 -1
View File
@@ -4,7 +4,7 @@ const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('odo', {
platform: process.platform,
version: '0.10.0',
version: '0.10.1',
isElectron: true,
gateway: {
+97 -4
View File
@@ -2,11 +2,27 @@ import { useState, useEffect, useCallback } from 'react';
type GatewayStatus = 'unknown' | 'checking' | 'running' | 'stopped' | 'error';
interface StatsData {
totalTokens?: number;
totalCost?: number;
activeAgents?: number;
totalRequests?: number;
[key: string]: unknown;
}
type StatsState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'loaded'; data: StatsData }
| { status: 'error'; message: string };
function Dashboard() {
const [gwStatus, setGwStatus] = useState<GatewayStatus>('unknown');
const [gwOutput, setGwOutput] = useState('');
const [actionInProgress, setActionInProgress] = useState(false);
const [stats, setStats] = useState<StatsState>({ status: 'idle' });
const isElectron = typeof window !== 'undefined' && window.odo?.isElectron;
const checkStatus = useCallback(async () => {
@@ -38,6 +54,27 @@ function Dashboard() {
return () => clearInterval(interval);
}, [checkStatus]);
const fetchStats = useCallback(async () => {
if (!isElectron) {
setStats({ status: 'error', message: 'Not running in Electron' });
return;
}
setStats({ status: 'loading' });
try {
const data = (await window.odo.ocm.api('GET', '/api/stats')) as StatsData;
setStats({ status: 'loaded', data });
} catch (err) {
setStats({ status: 'error', message: String(err) });
}
}, [isElectron]);
useEffect(() => {
fetchStats();
// Refresh stats every 30 seconds
const interval = setInterval(fetchStats, 30000);
return () => clearInterval(interval);
}, [fetchStats]);
const handleAction = async (action: 'start' | 'stop' | 'restart') => {
if (!isElectron) return;
setActionInProgress(true);
@@ -136,20 +173,76 @@ function Dashboard() {
</p>
</section>
{/* Stats Summary (placeholder) */}
{/* Stats Summary */}
<section className="bg-odo-surface border border-odo-border rounded-lg p-6 lg:col-span-2">
<h2 className="text-lg font-semibold text-white mb-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-white">
Stats Summary
</h2>
<button
onClick={fetchStats}
disabled={stats.status === 'loading'}
className="px-3 py-1 bg-odo-border hover:bg-gray-600 disabled:opacity-50 text-white rounded text-xs font-medium transition-colors"
>
{stats.status === 'loading' ? 'Loading...' : 'Refresh'}
</button>
</div>
{stats.status === 'idle' && (
<p className="text-odo-text-dim text-sm">Initializing...</p>
)}
{stats.status === 'loading' && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="bg-odo-bg border border-odo-border rounded-lg p-4 animate-pulse">
<div className="h-4 bg-odo-border rounded w-24 mb-2" />
<div className="h-6 bg-odo-border rounded w-16" />
</div>
))}
</div>
)}
{stats.status === 'error' && (
<p className="text-odo-text-dim text-sm">
System statistics and metrics will be available in a future update.
Unable to load stats OCM server may not be running.
</p>
)}
{stats.status === 'loaded' && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="bg-odo-bg border border-odo-border rounded-lg p-4">
<p className="text-odo-text-dim text-xs uppercase tracking-wide mb-1">Total Tokens</p>
<p className="text-2xl font-bold text-white">
{typeof stats.data.totalTokens === 'number'
? stats.data.totalTokens.toLocaleString()
: '—'}
</p>
</div>
<div className="bg-odo-bg border border-odo-border rounded-lg p-4">
<p className="text-odo-text-dim text-xs uppercase tracking-wide mb-1">Total Cost</p>
<p className="text-2xl font-bold text-white">
{typeof stats.data.totalCost === 'number'
? `$${stats.data.totalCost.toFixed(4)}`
: '—'}
</p>
</div>
<div className="bg-odo-bg border border-odo-border rounded-lg p-4">
<p className="text-odo-text-dim text-xs uppercase tracking-wide mb-1">Active Agents</p>
<p className="text-2xl font-bold text-white">
{typeof stats.data.activeAgents === 'number'
? stats.data.activeAgents
: '—'}
</p>
</div>
</div>
)}
</section>
</div>
{/* Footer */}
<footer className="mt-8 text-center text-odo-text-dim text-xs">
ODO v{window.odo?.version ?? '0.10.0'} &middot; OpenClaw Dashboard
ODO v{window.odo?.version ?? '0.10.1'} &middot; OpenClaw Dashboard
Orchestrator
</footer>
</div>
+1 -1
View File
@@ -1,5 +1,5 @@
/** @type {import('tailwindcss').Config} */
export default {
module.exports = {
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
theme: {
extend: {