zeroclaw/web/src/pages/Config.tsx
Zeki Kocabıyık 79337c76e8 feat(gateway): add embedded web dashboard with React frontend
Add a complete web management panel for ZeroClaw, served directly from
the binary via rust-embed. The dashboard provides real-time monitoring,
agent chat, configuration editing, and system diagnostics — all
accessible at http://localhost:5555/ after pairing.

Backend (Rust):
- Add 15+ REST API endpoints under /api/* with bearer token auth
- Add WebSocket agent chat at /ws/chat with query param auth
- Add SSE event stream at /api/events via BroadcastObserver
- Add rust-embed static file serving at /_app/* with SPA fallback
- Extend AppState with tools_registry, cost_tracker, event_tx
- Extract doctor::diagnose() for structured diagnostic results
- Add Serialize derives to IntegrationStatus, CliCategory, DiscoveredCli

Frontend (React + Vite + Tailwind CSS):
- 10 dashboard pages: Dashboard, AgentChat, Tools, Cron, Integrations,
  Memory, Config, Cost, Logs, Doctor
- WebSocket client with auto-reconnect for agent chat
- SSE client (fetch-based, supports auth headers) for live events
- Full EN/TR internationalization (~190 translation keys)
- Dark theme with responsive layouts
- Auth flow via 6-digit pairing code, token stored in localStorage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 16:14:01 +08:00

126 lines
4.3 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
Settings,
Save,
CheckCircle,
AlertTriangle,
ShieldAlert,
} from 'lucide-react';
import { getConfig, putConfig } from '@/lib/api';
export default function Config() {
const [config, setConfig] = useState('');
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
useEffect(() => {
getConfig()
.then((data) => {
// The API may return either a raw string or a JSON string
setConfig(typeof data === 'string' ? data : JSON.stringify(data, null, 2));
})
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
const handleSave = async () => {
setSaving(true);
setError(null);
setSuccess(null);
try {
await putConfig(config);
setSuccess('Configuration saved successfully.');
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Failed to save configuration');
} finally {
setSaving(false);
}
};
// Auto-dismiss success after 4 seconds
useEffect(() => {
if (!success) return;
const timer = setTimeout(() => setSuccess(null), 4000);
return () => clearTimeout(timer);
}, [success]);
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-blue-500 border-t-transparent" />
</div>
);
}
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Settings className="h-5 w-5 text-blue-400" />
<h2 className="text-base font-semibold text-white">Configuration</h2>
</div>
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium px-4 py-2 rounded-lg transition-colors disabled:opacity-50"
>
<Save className="h-4 w-4" />
{saving ? 'Saving...' : 'Save'}
</button>
</div>
{/* Sensitive fields note */}
<div className="flex items-start gap-3 bg-yellow-900/20 border border-yellow-700/40 rounded-lg p-4">
<ShieldAlert className="h-5 w-5 text-yellow-400 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm text-yellow-300 font-medium">
Sensitive fields are masked
</p>
<p className="text-sm text-yellow-400/70 mt-0.5">
API keys, tokens, and passwords are hidden for security. To update a
masked field, replace the entire masked value with your new value.
</p>
</div>
</div>
{/* Success message */}
{success && (
<div className="flex items-center gap-2 bg-green-900/30 border border-green-700 rounded-lg p-3">
<CheckCircle className="h-4 w-4 text-green-400 flex-shrink-0" />
<span className="text-sm text-green-300">{success}</span>
</div>
)}
{/* Error message */}
{error && (
<div className="flex items-center gap-2 bg-red-900/30 border border-red-700 rounded-lg p-3">
<AlertTriangle className="h-4 w-4 text-red-400 flex-shrink-0" />
<span className="text-sm text-red-300">{error}</span>
</div>
)}
{/* Config Editor */}
<div className="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
<div className="flex items-center justify-between px-4 py-2 border-b border-gray-800 bg-gray-800/50">
<span className="text-xs text-gray-400 font-medium uppercase tracking-wider">
TOML Configuration
</span>
<span className="text-xs text-gray-500">
{config.split('\n').length} lines
</span>
</div>
<textarea
value={config}
onChange={(e) => setConfig(e.target.value)}
spellCheck={false}
className="w-full min-h-[500px] bg-gray-950 text-gray-200 font-mono text-sm p-4 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-inset"
style={{ tabSize: 4 }}
/>
</div>
</div>
);
}