Loading...
Loading...
A lightweight web dashboard for monitoring OpenClaw agents, models, sessions, and health status in real-time without a database
npx skill4agent add aradotso/hermes-skills openclaw-bot-review-dashboardSkill by ara.so — Hermes Skills collection.
~/.openclaw/openclaw.json# Clone the repository
git clone https://github.com/xmanrui/OpenClaw-bot-review.git
cd OpenClaw-bot-review
# Install dependencies
npm install
# Start development server
npm run starthttp://localhost:3000# Build for production
npm run build
# Start production server
npm run start# Build Docker image
docker build -t openclaw-dashboard .
# Run container with default OpenClaw path (~/.openclaw)
docker run -d --name openclaw-dashboard \
-p 3000:3000 \
-v $HOME/.openclaw:/root/.openclaw:ro \
openclaw-dashboard
# Run with custom OpenClaw config path
docker run -d --name openclaw-dashboard \
-p 3000:3000 \
-e OPENCLAW_HOME=/opt/openclaw \
-v /path/to/openclaw:/opt/openclaw:ro \
openclaw-dashboard~/.openclaw/openclaw.json# Custom OpenClaw config path
export OPENCLAW_HOME=/opt/openclaw
# Port configuration (default: 3000)
export PORT=3000
# Start with custom config
OPENCLAW_HOME=/opt/openclaw npm run start~/.openclaw/
├── openclaw.json # Main configuration file
├── sessions/ # Session data per agent
│ ├── agent1/
│ │ ├── session1.json
│ │ └── session2.json
│ └── agent2/
└── skills/ # Installed skillsopenclaw.json{
"agents": [
{
"name": "my-agent",
"emoji": "🤖",
"model": "gpt-4",
"platforms": {
"feishu": {
"app_id": "${FEISHU_APP_ID}",
"app_secret": "${FEISHU_APP_SECRET}"
}
}
}
],
"models": {
"gpt-4": {
"provider": "openai",
"api_key": "${OPENAI_API_KEY}",
"context_window": 8192,
"max_output": 4096
}
},
"gateway": {
"enabled": true,
"port": 8080
}
}// The dashboard automatically reads from ~/.openclaw/openclaw.json
// Each bot card shows:
// - Name and emoji
// - Bound model
// - Platform status (Feishu, Discord, etc.)
// - Session count
// - Gateway health indicator// Models page displays:
// - Provider (OpenAI, Anthropic, etc.)
// - Model name and version
// - Context window size
// - Max output tokens
// - Reasoning support
// - Per-model connectivity test// Sessions are automatically detected from:
// ~/.openclaw/sessions/{agent-name}/*.json
// Each session shows:
// - Type (DM, group, cron)
// - Platform
// - Token usage
// - Last activity
// - Connectivity test button// Available refresh intervals:
// - Manual (no auto-refresh)
// - 10 seconds
// - 30 seconds
// - 1 minute
// - 5 minutes
// - 10 minutes
// Select from dropdown in top-right corner// Theme toggle: Light / Dark mode
// Available in sidebar
// Language toggle: English / 中文
// Available in top navigation// pages/api/bots.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { readOpenClawConfig } from '@/lib/openclaw';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const config = await readOpenClawConfig();
const bots = config.agents.map(agent => ({
name: agent.name,
emoji: agent.emoji,
model: agent.model,
platforms: Object.keys(agent.platforms || {}),
sessionCount: getSessionCount(agent.name),
}));
res.status(200).json({ bots });
} catch (error) {
res.status(500).json({ error: 'Failed to read config' });
}
}// pages/api/models.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const config = await readOpenClawConfig();
const models = Object.entries(config.models).map(([name, model]) => ({
name,
provider: model.provider,
contextWindow: model.context_window,
maxOutput: model.max_output,
reasoning: model.reasoning_support || false,
}));
res.status(200).json({ models });
}// pages/api/test/platform.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { platform, agentName } = req.query;
try {
const result = await testPlatformConnection(
platform as string,
agentName as string
);
res.status(200).json({ success: result.success, message: result.message });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
}// pages/api/gateway/health.ts
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const config = await readOpenClawConfig();
if (!config.gateway?.enabled) {
return res.status(200).json({ healthy: false, reason: 'disabled' });
}
try {
const port = config.gateway.port || 8080;
const response = await fetch(`http://localhost:${port}/health`);
const healthy = response.ok;
res.status(200).json({ healthy, port });
} catch (error) {
res.status(200).json({ healthy: false, reason: 'unreachable' });
}
}// lib/openclaw.ts
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
export interface OpenClawConfig {
agents: Agent[];
models: Record<string, Model>;
gateway?: Gateway;
}
export async function readOpenClawConfig(): Promise<OpenClawConfig> {
const openclawHome = process.env.OPENCLAW_HOME ||
path.join(os.homedir(), '.openclaw');
const configPath = path.join(openclawHome, 'openclaw.json');
try {
const content = await fs.readFile(configPath, 'utf-8');
return JSON.parse(content);
} catch (error) {
throw new Error(`Failed to read OpenClaw config: ${error.message}`);
}
}// lib/sessions.ts
export async function getAgentSessions(agentName: string) {
const openclawHome = process.env.OPENCLAW_HOME ||
path.join(os.homedir(), '.openclaw');
const sessionsDir = path.join(openclawHome, 'sessions', agentName);
try {
const files = await fs.readdir(sessionsDir);
const sessions = await Promise.all(
files
.filter(f => f.endsWith('.json'))
.map(async file => {
const content = await fs.readFile(
path.join(sessionsDir, file),
'utf-8'
);
return JSON.parse(content);
})
);
return sessions;
} catch (error) {
return [];
}
}// lib/test.ts
export async function testPlatformConnection(
platform: string,
agentName: string
): Promise<{ success: boolean; message: string }> {
const config = await readOpenClawConfig();
const agent = config.agents.find(a => a.name === agentName);
if (!agent) {
return { success: false, message: 'Agent not found' };
}
const platformConfig = agent.platforms?.[platform];
if (!platformConfig) {
return { success: false, message: `${platform} not configured` };
}
if (platform === 'feishu') {
// Test Feishu API
try {
const response = await fetch('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
app_id: process.env[platformConfig.app_id] || platformConfig.app_id,
app_secret: process.env[platformConfig.app_secret] || platformConfig.app_secret,
}),
});
const data = await response.json();
return {
success: data.code === 0,
message: data.code === 0 ? 'Connected' : data.msg,
};
} catch (error) {
return { success: false, message: error.message };
}
}
return { success: false, message: 'Platform test not implemented' };
}// components/BotCard.tsx
import { useState } from 'react';
interface BotCardProps {
bot: {
name: string;
emoji: string;
model: string;
platforms: string[];
sessionCount: number;
};
}
export default function BotCard({ bot }: BotCardProps) {
const [isTestingPlatform, setIsTestingPlatform] = useState(false);
const testPlatform = async (platform: string) => {
setIsTestingPlatform(true);
try {
const res = await fetch(
`/api/test/platform?platform=${platform}&agentName=${bot.name}`
);
const data = await res.json();
alert(data.message);
} finally {
setIsTestingPlatform(false);
}
};
return (
<div className="border rounded-lg p-4 shadow-sm hover:shadow-md transition">
<div className="flex items-center gap-2 mb-2">
<span className="text-3xl">{bot.emoji}</span>
<h3 className="text-lg font-semibold">{bot.name}</h3>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
<p>Model: {bot.model}</p>
<p>Sessions: {bot.sessionCount}</p>
</div>
<div className="mt-3 flex gap-2">
{bot.platforms.map(platform => (
<button
key={platform}
onClick={() => testPlatform(platform)}
disabled={isTestingPlatform}
className="px-2 py-1 text-xs bg-blue-500 text-white rounded hover:bg-blue-600 disabled:opacity-50"
>
Test {platform}
</button>
))}
</div>
</div>
);
}# Verify OpenClaw config exists
ls -la ~/.openclaw/openclaw.json
# Check permissions
chmod 644 ~/.openclaw/openclaw.json
# If using custom path, set env var
export OPENCLAW_HOME=/path/to/openclaw
npm run start# Check if gateway is enabled in config
cat ~/.openclaw/openclaw.json | grep -A 3 gateway
# Verify gateway is running
netstat -an | grep 8080
# Check OpenClaw gateway logs
tail -f ~/.openclaw/logs/gateway.log# Verify environment variables are set
echo $FEISHU_APP_ID
echo $FEISHU_APP_SECRET
# Check if credentials are in openclaw.json
cat ~/.openclaw/openclaw.json | grep -A 5 feishu
# Ensure proper format (${VAR_NAME} or direct value)
# Dashboard resolves process.env for ${} references# Check sessions directory exists
ls -la ~/.openclaw/sessions/
# Verify agent has session files
ls ~/.openclaw/sessions/my-agent/
# Check JSON file format
cat ~/.openclaw/sessions/my-agent/session1.json | jq .# Mount OpenClaw directory correctly
docker run -d --name openclaw-dashboard \
-p 3000:3000 \
-v $HOME/.openclaw:/root/.openclaw:ro \
openclaw-dashboard
# For custom path, use OPENCLAW_HOME
docker run -d --name openclaw-dashboard \
-p 3000:3000 \
-e OPENCLAW_HOME=/data/openclaw \
-v /opt/openclaw:/data/openclaw:ro \
openclaw-dashboard
# Check logs
docker logs openclaw-dashboard// Check if refresh interval is set
// In browser dev tools:
localStorage.getItem('dashboardRefreshInterval')
// Should return: '10s', '30s', '1m', '5m', or '10m'
// Clear and reset
localStorage.removeItem('dashboardRefreshInterval')
// Then select interval from dropdown againnpm run build# Clear cache and reinstall
rm -rf node_modules package-lock.json
npm install
# Use Node 18+
node --version # Should be v18.0.0 or higher
# Check for TypeScript errors
npm run type-check
# Build with verbose output
npm run build -- --debug