From 4331bcb7e1b5944357f3337281ee6c4eda5f6b07 Mon Sep 17 00:00:00 2001 From: zvx-echo6 Date: Tue, 12 May 2026 10:28:12 -0600 Subject: [PATCH] feat(dashboard): React frontend scaffold with overview page - Vite + React 18 + TypeScript + Tailwind CSS - Dashboard overview with health gauge, pillar bars, alerts - WebSocket hook for real-time updates - Layout with sidebar navigation and live indicator - Placeholder pages for Mesh, Environment, Config, Alerts - Dark theme ops center aesthetic with JetBrains Mono Co-Authored-By: Claude Opus 4.5 --- dashboard-frontend/index.html | 16 + dashboard-frontend/package.json | 28 ++ dashboard-frontend/postcss.config.js | 6 + dashboard-frontend/src/App.tsx | 23 ++ dashboard-frontend/src/components/Layout.tsx | 162 ++++++++ dashboard-frontend/src/hooks/useWebSocket.ts | 102 +++++ dashboard-frontend/src/index.css | 49 +++ dashboard-frontend/src/lib/api.ts | 157 ++++++++ dashboard-frontend/src/main.tsx | 13 + dashboard-frontend/src/pages/Alerts.tsx | 15 + dashboard-frontend/src/pages/Config.tsx | 15 + dashboard-frontend/src/pages/Dashboard.tsx | 381 +++++++++++++++++++ dashboard-frontend/src/pages/Environment.tsx | 15 + dashboard-frontend/src/pages/Mesh.tsx | 15 + dashboard-frontend/src/vite-env.d.ts | 1 + dashboard-frontend/tailwind.config.ts | 26 ++ dashboard-frontend/tsconfig.json | 25 ++ dashboard-frontend/tsconfig.node.json | 11 + dashboard-frontend/vite.config.ts | 28 ++ 19 files changed, 1088 insertions(+) create mode 100644 dashboard-frontend/index.html create mode 100644 dashboard-frontend/package.json create mode 100644 dashboard-frontend/postcss.config.js create mode 100644 dashboard-frontend/src/App.tsx create mode 100644 dashboard-frontend/src/components/Layout.tsx create mode 100644 dashboard-frontend/src/hooks/useWebSocket.ts create mode 100644 dashboard-frontend/src/index.css create mode 100644 dashboard-frontend/src/lib/api.ts create mode 100644 dashboard-frontend/src/main.tsx create mode 100644 dashboard-frontend/src/pages/Alerts.tsx create mode 100644 dashboard-frontend/src/pages/Config.tsx create mode 100644 dashboard-frontend/src/pages/Dashboard.tsx create mode 100644 dashboard-frontend/src/pages/Environment.tsx create mode 100644 dashboard-frontend/src/pages/Mesh.tsx create mode 100644 dashboard-frontend/src/vite-env.d.ts create mode 100644 dashboard-frontend/tailwind.config.ts create mode 100644 dashboard-frontend/tsconfig.json create mode 100644 dashboard-frontend/tsconfig.node.json create mode 100644 dashboard-frontend/vite.config.ts diff --git a/dashboard-frontend/index.html b/dashboard-frontend/index.html new file mode 100644 index 0000000..8066e01 --- /dev/null +++ b/dashboard-frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + MeshAI Dashboard + + + + + +
+ + + diff --git a/dashboard-frontend/package.json b/dashboard-frontend/package.json new file mode 100644 index 0000000..f155a9f --- /dev/null +++ b/dashboard-frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "meshai-dashboard", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-router-dom": "^6.23.0", + "recharts": "^2.12.0", + "lucide-react": "^0.383.0" + }, + "devDependencies": { + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.0", + "typescript": "^5.4.0", + "vite": "^5.4.0", + "tailwindcss": "^3.4.0", + "postcss": "^8.4.0", + "autoprefixer": "^10.4.0" + } +} diff --git a/dashboard-frontend/postcss.config.js b/dashboard-frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/dashboard-frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/dashboard-frontend/src/App.tsx b/dashboard-frontend/src/App.tsx new file mode 100644 index 0000000..b313ef7 --- /dev/null +++ b/dashboard-frontend/src/App.tsx @@ -0,0 +1,23 @@ +import { Routes, Route } from 'react-router-dom' +import Layout from './components/Layout' +import Dashboard from './pages/Dashboard' +import Mesh from './pages/Mesh' +import Environment from './pages/Environment' +import Config from './pages/Config' +import Alerts from './pages/Alerts' + +function App() { + return ( + + + } /> + } /> + } /> + } /> + } /> + + + ) +} + +export default App diff --git a/dashboard-frontend/src/components/Layout.tsx b/dashboard-frontend/src/components/Layout.tsx new file mode 100644 index 0000000..778d77c --- /dev/null +++ b/dashboard-frontend/src/components/Layout.tsx @@ -0,0 +1,162 @@ +import { ReactNode, useEffect, useState } from 'react' +import { Link, useLocation } from 'react-router-dom' +import { + LayoutDashboard, + Radio, + Cloud, + Settings, + Bell, +} from 'lucide-react' +import { fetchStatus, type SystemStatus } from '@/lib/api' +import { useWebSocket } from '@/hooks/useWebSocket' + +interface LayoutProps { + children: ReactNode +} + +const navItems = [ + { path: '/', label: 'Dashboard', icon: LayoutDashboard }, + { path: '/mesh', label: 'Mesh', icon: Radio }, + { path: '/environment', label: 'Environment', icon: Cloud }, + { path: '/config', label: 'Config', icon: Settings }, + { path: '/alerts', label: 'Alerts', icon: Bell }, +] + +function formatUptime(seconds: number): string { + const days = Math.floor(seconds / 86400) + const hours = Math.floor((seconds % 86400) / 3600) + const mins = Math.floor((seconds % 3600) / 60) + + if (days > 0) return `${days}d ${hours}h` + if (hours > 0) return `${hours}h ${mins}m` + return `${mins}m` +} + +function getPageTitle(pathname: string): string { + const item = navItems.find((i) => i.path === pathname) + return item?.label || 'Dashboard' +} + +export default function Layout({ children }: LayoutProps) { + const location = useLocation() + const { connected } = useWebSocket() + const [status, setStatus] = useState(null) + const [currentTime, setCurrentTime] = useState(new Date()) + + useEffect(() => { + fetchStatus().then(setStatus).catch(console.error) + const interval = setInterval(() => { + fetchStatus().then(setStatus).catch(console.error) + }, 30000) + return () => clearInterval(interval) + }, []) + + useEffect(() => { + const interval = setInterval(() => setCurrentTime(new Date()), 1000) + return () => clearInterval(interval) + }, []) + + const timeStr = currentTime.toLocaleTimeString('en-US', { + hour12: false, + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }) + + return ( +
+ {/* Sidebar */} + + + {/* Main content */} +
+ {/* Header */} +
+

+ {getPageTitle(location.pathname)} +

+
+ {/* Live indicator */} +
+
+ + {connected ? 'Live' : 'Offline'} + +
+ {/* Clock */} +
+ {timeStr} MT +
+
+
+ + {/* Page content */} +
{children}
+
+
+ ) +} diff --git a/dashboard-frontend/src/hooks/useWebSocket.ts b/dashboard-frontend/src/hooks/useWebSocket.ts new file mode 100644 index 0000000..7df00f6 --- /dev/null +++ b/dashboard-frontend/src/hooks/useWebSocket.ts @@ -0,0 +1,102 @@ +import { useEffect, useRef, useState, useCallback } from 'react' +import type { MeshHealth, Alert } from '@/lib/api' + +interface WebSocketMessage { + type: string + data: unknown +} + +interface UseWebSocketReturn { + connected: boolean + lastHealth: MeshHealth | null + lastAlert: Alert | null +} + +export function useWebSocket(): UseWebSocketReturn { + const [connected, setConnected] = useState(false) + const [lastHealth, setLastHealth] = useState(null) + const [lastAlert, setLastAlert] = useState(null) + const wsRef = useRef(null) + const reconnectTimeoutRef = useRef(null) + const reconnectDelayRef = useRef(1000) + + const connect = useCallback(() => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + return + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' + const wsUrl = `${protocol}//${window.location.host}/ws/live` + + try { + const ws = new WebSocket(wsUrl) + wsRef.current = ws + + ws.onopen = () => { + setConnected(true) + reconnectDelayRef.current = 1000 // Reset backoff on successful connection + } + + ws.onmessage = (event) => { + try { + const message: WebSocketMessage = JSON.parse(event.data) + + switch (message.type) { + case 'health_update': + setLastHealth(message.data as MeshHealth) + break + case 'alert_fired': + setLastAlert(message.data as Alert) + break + } + } catch (e) { + console.error('Failed to parse WebSocket message:', e) + } + } + + ws.onclose = () => { + setConnected(false) + wsRef.current = null + + // Schedule reconnect with exponential backoff + const delay = Math.min(reconnectDelayRef.current, 30000) + reconnectTimeoutRef.current = window.setTimeout(() => { + reconnectDelayRef.current = Math.min(delay * 2, 30000) + connect() + }, delay) + } + + ws.onerror = () => { + ws.close() + } + + // Keepalive ping every 30 seconds + const pingInterval = setInterval(() => { + if (ws.readyState === WebSocket.OPEN) { + ws.send('ping') + } + }, 30000) + + ws.addEventListener('close', () => { + clearInterval(pingInterval) + }) + } catch (e) { + console.error('Failed to create WebSocket:', e) + } + }, []) + + useEffect(() => { + connect() + + return () => { + if (reconnectTimeoutRef.current) { + clearTimeout(reconnectTimeoutRef.current) + } + if (wsRef.current) { + wsRef.current.close() + } + } + }, [connect]) + + return { connected, lastHealth, lastAlert } +} diff --git a/dashboard-frontend/src/index.css b/dashboard-frontend/src/index.css new file mode 100644 index 0000000..8e9f519 --- /dev/null +++ b/dashboard-frontend/src/index.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + background: #0a0e17; + margin: 0; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #0a0e17; +} + +::-webkit-scrollbar-thumb { + background: #2d3a4d; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #3b4a5d; +} + +/* Data values use JetBrains Mono */ +.font-mono { + font-family: 'JetBrains Mono', monospace; +} + +/* Pulsing animation for live indicator */ +@keyframes pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.animate-pulse-slow { + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} diff --git a/dashboard-frontend/src/lib/api.ts b/dashboard-frontend/src/lib/api.ts new file mode 100644 index 0000000..20eaa2c --- /dev/null +++ b/dashboard-frontend/src/lib/api.ts @@ -0,0 +1,157 @@ +// API types matching actual backend responses + +export interface SystemStatus { + version: string + uptime_seconds: number + bot_name: string + connection_type: string + connection_target: string + connected: boolean + node_count: number + source_count: number + env_feeds_enabled: boolean + dashboard_port: number +} + +export interface MeshHealth { + score: number + tier: string + pillars: { + infrastructure: number + utilization: number + behavior: number + power: number + } + infra_online: number + infra_total: number + util_percent: number + flagged_nodes: number + battery_warnings: number + total_nodes: number + total_regions: number + unlocated_count: number + last_computed: string + recommendations: string[] +} + +export interface NodeInfo { + node_num: number + node_id_hex: string + short_name: string + long_name: string + role: string + latitude: number | null + longitude: number | null + last_heard: string | null + battery_level: number | null + voltage: number | null + snr: number | null + firmware: string + hardware: string + uptime: number | null + sources: string[] +} + +export interface EdgeInfo { + from_node: number + to_node: number + snr: number + quality: string +} + +export interface SourceHealth { + name: string + type: string + url: string + is_loaded: boolean + last_error: string | null + consecutive_errors: number + response_time_ms: number | null + tick_count: number + node_count: number +} + +export interface Alert { + type: string + severity: string + message: string + timestamp: string + scope_type?: string + scope_value?: string +} + +export interface EnvStatus { + enabled: boolean + feeds: unknown[] +} + +export interface EnvEvent { + type: string + [key: string]: unknown +} + +// API fetch helpers + +async function fetchJson(url: string): Promise { + const response = await fetch(url) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +export async function fetchStatus(): Promise { + return fetchJson('/api/status') +} + +export async function fetchHealth(): Promise { + return fetchJson('/api/health') +} + +export async function fetchNodes(): Promise { + return fetchJson('/api/nodes') +} + +export async function fetchEdges(): Promise { + return fetchJson('/api/edges') +} + +export async function fetchSources(): Promise { + return fetchJson('/api/sources') +} + +export async function fetchConfig(section?: string): Promise { + const url = section ? `/api/config/${section}` : '/api/config' + return fetchJson(url) +} + +export async function updateConfig( + section: string, + data: unknown +): Promise<{ saved: boolean; restart_required: boolean }> { + const response = await fetch(`/api/config/${section}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + throw new Error(`API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +export async function fetchAlerts(): Promise { + return fetchJson('/api/alerts/active') +} + +export async function fetchEnvStatus(): Promise { + return fetchJson('/api/env/status') +} + +export async function fetchEnvActive(): Promise { + return fetchJson('/api/env/active') +} + +export async function fetchRegions(): Promise { + return fetchJson('/api/regions') +} diff --git a/dashboard-frontend/src/main.tsx b/dashboard-frontend/src/main.tsx new file mode 100644 index 0000000..fa94fac --- /dev/null +++ b/dashboard-frontend/src/main.tsx @@ -0,0 +1,13 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import App from './App' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/dashboard-frontend/src/pages/Alerts.tsx b/dashboard-frontend/src/pages/Alerts.tsx new file mode 100644 index 0000000..c096225 --- /dev/null +++ b/dashboard-frontend/src/pages/Alerts.tsx @@ -0,0 +1,15 @@ +import { Bell } from 'lucide-react' + +export default function Alerts() { + return ( +
+
+ +
+

Alerts

+

+ Alert history and subscriptions coming in Phase 11 +

+
+ ) +} diff --git a/dashboard-frontend/src/pages/Config.tsx b/dashboard-frontend/src/pages/Config.tsx new file mode 100644 index 0000000..21a08c5 --- /dev/null +++ b/dashboard-frontend/src/pages/Config.tsx @@ -0,0 +1,15 @@ +import { Settings } from 'lucide-react' + +export default function Config() { + return ( +
+
+ +
+

Configuration

+

+ Configuration management coming in Phase 5 +

+
+ ) +} diff --git a/dashboard-frontend/src/pages/Dashboard.tsx b/dashboard-frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..d046efb --- /dev/null +++ b/dashboard-frontend/src/pages/Dashboard.tsx @@ -0,0 +1,381 @@ +import { useEffect, useState } from 'react' +import { + fetchHealth, + fetchSources, + fetchAlerts, + fetchEnvStatus, + type MeshHealth, + type SourceHealth, + type Alert, + type EnvStatus, +} from '@/lib/api' +import { useWebSocket } from '@/hooks/useWebSocket' +import { + AlertTriangle, + AlertCircle, + Info, + CheckCircle, + Radio, + Cpu, + Activity, + MapPin, +} from 'lucide-react' + +function HealthGauge({ health }: { health: MeshHealth }) { + const score = health.score + const tier = health.tier + + // Color based on score + const getColor = (s: number) => { + if (s >= 80) return '#22c55e' + if (s >= 60) return '#f59e0b' + return '#ef4444' + } + + const color = getColor(score) + const circumference = 2 * Math.PI * 45 + const progress = (score / 100) * circumference + + return ( +
+ + {/* Background circle */} + + {/* Progress arc */} + + {/* Score text */} + + {score.toFixed(1)} + + + {tier} + + +
+ ) +} + +function PillarBar({ + label, + value, +}: { + label: string + value: number +}) { + const getColor = (v: number) => { + if (v >= 80) return 'bg-green-500' + if (v >= 60) return 'bg-amber-500' + return 'bg-red-500' + } + + return ( +
+
{label}
+
+
+
+
+ {value.toFixed(1)} +
+
+ ) +} + +function AlertItem({ alert }: { alert: Alert }) { + const getSeverityStyles = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + case 'emergency': + return { + bg: 'bg-red-500/10', + border: 'border-red-500', + icon: AlertCircle, + iconColor: 'text-red-500', + } + case 'warning': + return { + bg: 'bg-amber-500/10', + border: 'border-amber-500', + icon: AlertTriangle, + iconColor: 'text-amber-500', + } + default: + return { + bg: 'bg-green-500/10', + border: 'border-green-500', + icon: Info, + iconColor: 'text-green-500', + } + } + } + + const styles = getSeverityStyles(alert.severity) + const Icon = styles.icon + + return ( +
+ +
+
{alert.message}
+
+ {alert.timestamp || 'Just now'} +
+
+
+ ) +} + +function SourceCard({ source }: { source: SourceHealth }) { + const getStatusColor = () => { + if (!source.is_loaded) return 'bg-red-500' + if (source.last_error) return 'bg-amber-500' + return 'bg-green-500' + } + + return ( +
+
+
+
{source.name}
+
+ {source.node_count} nodes • {source.type} +
+
+
+ ) +} + +function StatCard({ + icon: Icon, + label, + value, + subvalue, +}: { + icon: typeof Radio + label: string + value: string | number + subvalue?: string +}) { + return ( +
+
+ + {label} +
+
{value}
+ {subvalue && ( +
{subvalue}
+ )} +
+ ) +} + +export default function Dashboard() { + const [health, setHealth] = useState(null) + const [sources, setSources] = useState([]) + const [alerts, setAlerts] = useState([]) + const [envStatus, setEnvStatus] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + + const { lastHealth } = useWebSocket() + + useEffect(() => { + Promise.all([ + fetchHealth(), + fetchSources(), + fetchAlerts(), + fetchEnvStatus(), + ]) + .then(([h, src, a, e]) => { + setHealth(h) + setSources(src) + setAlerts(a) + setEnvStatus(e) + setLoading(false) + }) + .catch((err) => { + setError(err.message) + setLoading(false) + }) + }, []) + + // Update health from WebSocket + useEffect(() => { + if (lastHealth) { + setHealth(lastHealth) + } + }, [lastHealth]) + + if (loading) { + return ( +
+
Loading...
+
+ ) + } + + if (error) { + return ( +
+
Error: {error}
+
+ ) + } + + return ( +
+ {/* Mesh Health */} +
+

Mesh Health

+ {health && ( + <> + +
+ + + + +
+ + )} +
+ + {/* Alerts + Stats */} +
+ {/* Active Alerts */} +
+

+ Active Alerts +

+ {alerts.length > 0 ? ( +
+ {alerts.map((alert, i) => ( + + ))} +
+ ) : ( +
+ + No active alerts +
+ )} +
+ + {/* Quick Stats */} +
+ + + + +
+
+ + {/* Mesh Sources */} +
+

+ Mesh Sources ({sources.length}) +

+ {sources.length > 0 ? ( +
+ {sources.map((source, i) => ( + + ))} +
+ ) : ( +
No sources configured
+ )} +
+ + {/* Environmental Feeds */} +
+

+ Environmental Feeds +

+ {envStatus?.enabled ? ( +
+ {envStatus.feeds.length} feeds active +
+ ) : ( +
+

Environmental feeds not enabled.

+

+ Enable in Config → Mesh Intelligence +

+
+ )} +
+ + {/* HF Propagation placeholder */} +
+

+ HF Propagation +

+
+

Space weather data not enabled.

+

Coming in Phase 1

+
+
+
+ ) +} diff --git a/dashboard-frontend/src/pages/Environment.tsx b/dashboard-frontend/src/pages/Environment.tsx new file mode 100644 index 0000000..d32f08d --- /dev/null +++ b/dashboard-frontend/src/pages/Environment.tsx @@ -0,0 +1,15 @@ +import { Cloud } from 'lucide-react' + +export default function Environment() { + return ( +
+
+ +
+

Environment

+

+ Environmental feeds and space weather detail coming soon +

+
+ ) +} diff --git a/dashboard-frontend/src/pages/Mesh.tsx b/dashboard-frontend/src/pages/Mesh.tsx new file mode 100644 index 0000000..aeae8b2 --- /dev/null +++ b/dashboard-frontend/src/pages/Mesh.tsx @@ -0,0 +1,15 @@ +import { Radio } from 'lucide-react' + +export default function Mesh() { + return ( +
+
+ +
+

Mesh

+

+ Topology graph and geographic map coming in Phase 6 +

+
+ ) +} diff --git a/dashboard-frontend/src/vite-env.d.ts b/dashboard-frontend/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/dashboard-frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dashboard-frontend/tailwind.config.ts b/dashboard-frontend/tailwind.config.ts new file mode 100644 index 0000000..cacd7c8 --- /dev/null +++ b/dashboard-frontend/tailwind.config.ts @@ -0,0 +1,26 @@ +import type { Config } from 'tailwindcss' + +export default { + content: ['./index.html', './src/**/*.{ts,tsx}'], + darkMode: 'class', + theme: { + extend: { + colors: { + bg: { + DEFAULT: '#0a0e17', + card: '#111827', + hover: '#1a2332', + }, + border: { + DEFAULT: '#1e2a3a', + light: '#2d3a4d', + }, + accent: '#3b82f6', + }, + fontFamily: { + mono: ['JetBrains Mono', 'monospace'], + }, + }, + }, + plugins: [], +} satisfies Config diff --git a/dashboard-frontend/tsconfig.json b/dashboard-frontend/tsconfig.json new file mode 100644 index 0000000..5413626 --- /dev/null +++ b/dashboard-frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/dashboard-frontend/tsconfig.node.json b/dashboard-frontend/tsconfig.node.json new file mode 100644 index 0000000..97ede7e --- /dev/null +++ b/dashboard-frontend/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/dashboard-frontend/vite.config.ts b/dashboard-frontend/vite.config.ts new file mode 100644 index 0000000..275917b --- /dev/null +++ b/dashboard-frontend/vite.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import path from 'path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + build: { + outDir: '../meshai/dashboard/static', + emptyOutDir: true, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:8080', + changeOrigin: true, + }, + '/ws': { + target: 'ws://localhost:8080', + ws: true, + }, + }, + }, +})