forked from sagnik/Velocity-OS
Initial commit: Velocity-OS migration
This commit is contained in:
297
webos/src/control-room/ControlRoom.tsx
Normal file
297
webos/src/control-room/ControlRoom.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styles from './ControlRoom.module.css';
|
||||
|
||||
/**
|
||||
* ControlRoom
|
||||
* Admin-only surface. Accessible at /control-room.
|
||||
* Protected by AdminGuard at router level AND FastAPI RBAC middleware.
|
||||
*
|
||||
* Panels (left sidebar → content area):
|
||||
* System Health — service status, GPU utilization, queue depths
|
||||
* Oracle Admin — schema catalog, canvas management, MCP tools
|
||||
* Comms Config — WAHA/Evolution provider, webhook setup
|
||||
* Users & Roles — broker roster, role assignment, audit log
|
||||
* Model Hydration — s5cmd sync status, model inventory
|
||||
* Meta Integration — OAuth, ad account, Lookalike sync
|
||||
*
|
||||
* Design: deliberately more "operator" than "broker" — still glassmorphic
|
||||
* but denser information, monospace for technical values.
|
||||
*/
|
||||
|
||||
type Panel =
|
||||
| 'system'
|
||||
| 'oracle-admin'
|
||||
| 'comms-config'
|
||||
| 'users'
|
||||
| 'models'
|
||||
| 'meta';
|
||||
|
||||
const PANELS: { id: Panel; label: string; icon: string }[] = [
|
||||
{ id: 'system', label: 'System Health', icon: '⬡' },
|
||||
{ id: 'oracle-admin', label: 'Oracle Admin', icon: '◈' },
|
||||
{ id: 'comms-config', label: 'Comms Config', icon: '⟐' },
|
||||
{ id: 'users', label: 'Users & Roles', icon: '⊛' },
|
||||
{ id: 'models', label: 'Model Hydration', icon: '⊗' },
|
||||
{ id: 'meta', label: 'Meta Integration', icon: '⊕' },
|
||||
];
|
||||
|
||||
export default function ControlRoom() {
|
||||
const navigate = useNavigate();
|
||||
const [active, setActive] = useState<Panel>('system');
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
{/* Header */}
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerLeft}>
|
||||
<span className={styles.headerIcon}>⚙</span>
|
||||
<div>
|
||||
<h1 className={styles.title}>Control Room</h1>
|
||||
<p className={styles.subtitle}>System Administration · Admin Access Only</p>
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn-ghost" onClick={() => navigate('/command')}>
|
||||
← Back to Command
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.body}>
|
||||
{/* Left sidebar */}
|
||||
<nav className={styles.sidebar}>
|
||||
{PANELS.map(({ id, label, icon }) => (
|
||||
<button
|
||||
key={id}
|
||||
className={`${styles.sideItem} ${active === id ? styles.sideActive : ''}`}
|
||||
onClick={() => setActive(id)}
|
||||
aria-current={active === id ? 'page' : undefined}
|
||||
>
|
||||
<span className={styles.sideIcon}>{icon}</span>
|
||||
<span>{label}</span>
|
||||
{active === id && (
|
||||
<motion.div
|
||||
layoutId="cr-indicator"
|
||||
className={styles.indicator}
|
||||
transition={{ type: 'spring', stiffness: 400, damping: 35 }}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Panel content */}
|
||||
<main className={styles.content}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active}
|
||||
initial={{ opacity: 0, x: 12 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -8 }}
|
||||
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className={styles.panelWrap}
|
||||
>
|
||||
{active === 'system' && <SystemHealthPanel />}
|
||||
{active === 'oracle-admin' && <OracleAdminPanel />}
|
||||
{active === 'comms-config' && <CommsConfigPanel />}
|
||||
{active === 'users' && <UsersPanel />}
|
||||
{active === 'models' && <ModelHydrationPanel />}
|
||||
{active === 'meta' && <MetaIntegrationPanel />}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── System Health ─────────────────────────────────────────────
|
||||
function SystemHealthPanel() {
|
||||
const services = [
|
||||
{ name: 'core-api', status: 'healthy', latency: '42ms', replicas: '2/2' },
|
||||
{ name: 'webos', status: 'healthy', latency: '—', replicas: '2/2' },
|
||||
{ name: 'media-engine', status: 'healthy', latency: '—', replicas: '1/1' },
|
||||
{ name: 'postgres', status: 'healthy', latency: '3ms', replicas: '1/1' },
|
||||
{ name: 'redis', status: 'healthy', latency: '0.4ms', replicas: '1/1' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>System Health</h2>
|
||||
<div className={styles.serviceGrid}>
|
||||
{services.map(svc => (
|
||||
<div key={svc.name} className={`${styles.svcCard} glass-card`}>
|
||||
<div className={styles.svcTop}>
|
||||
<span
|
||||
className={styles.statusDot}
|
||||
style={{ background: svc.status === 'healthy' ? 'var(--color-green)' : 'var(--color-red)' }}
|
||||
/>
|
||||
<code className={styles.svcName}>{svc.name}</code>
|
||||
</div>
|
||||
<div className={styles.svcMeta}>
|
||||
<span>Replicas: <code>{svc.replicas}</code></span>
|
||||
{svc.latency !== '—' && <span>Latency: <code>{svc.latency}</code></span>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* GPU MIG status */}
|
||||
<div className={`${styles.gpuCard} glass-card`}>
|
||||
<h3 className={styles.subTitle}>GPU · RTX 6000 Blackwell · MIG Active</h3>
|
||||
<div className={styles.migSlices}>
|
||||
<div className={styles.migSlice}>
|
||||
<span className={styles.migLabel}>Slice 0 · 48GB</span>
|
||||
<code className={styles.migService}>SGLang → Qwen3.6 35B</code>
|
||||
<span className={styles.migStatus} style={{ color: 'var(--color-green)' }}>● Loaded</span>
|
||||
</div>
|
||||
<div className={styles.migSlice}>
|
||||
<span className={styles.migLabel}>Slice 1 · 48GB</span>
|
||||
<code className={styles.migService}>ComfyUI → Wan 2.2 + Qwen-Image</code>
|
||||
<span className={styles.migStatus} style={{ color: 'var(--color-green)' }}>● Loaded</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Oracle Admin ──────────────────────────────────────────────
|
||||
function OracleAdminPanel() {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>Oracle Administration</h2>
|
||||
<p className={styles.panelSubtitle}>
|
||||
Canvas management, schema catalog, and MCP tools.
|
||||
These controls are not visible to broker-role users.
|
||||
</p>
|
||||
<div className={styles.adminSection}>
|
||||
<h3 className={styles.subTitle}>Active Canvas Sessions</h3>
|
||||
<p className={styles.muted}>Query canvas history, fork/merge, and revision management available here.</p>
|
||||
<button className="btn-ghost">Open Canvas Manager</button>
|
||||
</div>
|
||||
<div className={styles.adminSection}>
|
||||
<h3 className={styles.subTitle}>Schema Catalog</h3>
|
||||
<button className="btn-ghost">Browse Schema →</button>
|
||||
<button className="btn-ghost">Run Data Health Check →</button>
|
||||
</div>
|
||||
<div className={styles.adminSection}>
|
||||
<h3 className={styles.subTitle}>MCP Tools</h3>
|
||||
<button className="btn-ghost">View Registered Tools →</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Comms Config ──────────────────────────────────────────────
|
||||
function CommsConfigPanel() {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>Comms Configuration</h2>
|
||||
<p className={styles.panelSubtitle}>
|
||||
WhatsApp provider setup. Never visible to sales brokers.
|
||||
</p>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>Provider</label>
|
||||
<select className={styles.formSelect}>
|
||||
<option value="waha">WAHA</option>
|
||||
<option value="evolution">Evolution API</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>API Base URL</label>
|
||||
<input type="text" className={styles.formInput} placeholder="https://waha.internal:3000" />
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label className={styles.formLabel}>API Key</label>
|
||||
<input type="password" className={styles.formInput} placeholder="••••••••••••••••" />
|
||||
</div>
|
||||
<button className="btn-primary">Save Configuration</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Users & Roles ─────────────────────────────────────────────
|
||||
function UsersPanel() {
|
||||
const roles = ['ADMIN', 'SALES_DIRECTOR', 'SALES_BROKER'];
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>Users & Roles</h2>
|
||||
<div className={styles.userToolbar}>
|
||||
<button className="btn-primary">+ Invite User</button>
|
||||
</div>
|
||||
<table className={styles.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th><th>Email</th><th>Role</th><th>Last Active</th><th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Sagnik</td>
|
||||
<td>sagnik@desineuron.in</td>
|
||||
<td><code>ADMIN</code></td>
|
||||
<td>Now</td>
|
||||
<td><button className={styles.tableAction}>Edit</button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Model Hydration ───────────────────────────────────────────
|
||||
function ModelHydrationPanel() {
|
||||
const models = [
|
||||
{ name: 'Wan 2.2', size: '15 GB', status: 'synced', path: '/opt/models/comfy/wan2.2' },
|
||||
{ name: 'Qwen-Image 2512', size: '20 GB', status: 'synced', path: '/opt/models/comfy/qwen-image-2512' },
|
||||
{ name: 'Qwen3.6 35B A3B', size: '70 GB', status: 'synced', path: '/opt/models/llm/qwen3.6-35b-a3b' },
|
||||
];
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>Model Hydration</h2>
|
||||
<p className={styles.panelSubtitle}>NVMe-backed model store. Re-sync from S3 via s5cmd.</p>
|
||||
{models.map(m => (
|
||||
<div key={m.name} className={`${styles.modelRow} glass-card`}>
|
||||
<div>
|
||||
<span className={styles.modelName}>{m.name}</span>
|
||||
<code className={styles.modelPath}>{m.path}</code>
|
||||
</div>
|
||||
<div className={styles.modelRight}>
|
||||
<span className={styles.modelSize}>{m.size}</span>
|
||||
<span style={{ color: 'var(--color-green)', fontSize: 'var(--text-xs)' }}>
|
||||
● {m.status}
|
||||
</span>
|
||||
<button className="btn-ghost">Re-sync</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<button className="btn-ghost" style={{ marginTop: 'var(--space-4)' }}>
|
||||
Run Full Hydration
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Meta Integration ──────────────────────────────────────────
|
||||
function MetaIntegrationPanel() {
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<h2 className={styles.panelTitle}>Meta Business Integration</h2>
|
||||
<p className={styles.panelSubtitle}>
|
||||
OAuth, Ad Account, and Lookalike Audience configuration.
|
||||
Never visible to broker-role users.
|
||||
</p>
|
||||
<div className={styles.metaStatus}>
|
||||
<span className={styles.statusDot} style={{ background: 'var(--color-amber)' }} />
|
||||
<span>OAuth not connected</span>
|
||||
<button className="btn-primary">Connect Meta Business</button>
|
||||
</div>
|
||||
<div className={styles.adminSection}>
|
||||
<h3 className={styles.subTitle}>Ad Account</h3>
|
||||
<input className={styles.formInput} placeholder="Meta Ad Account ID" />
|
||||
</div>
|
||||
<button className="btn-ghost">Save</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user