Genesis
This commit is contained in:
16
dashboard/index.html
Normal file
16
dashboard/index.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>TSE Monitor</title>
|
||||
<meta name="description" content="CloudTSE availability and response-time dashboard" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
148
dashboard/src/App.jsx
Normal file
148
dashboard/src/App.jsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
ThemeProvider, CssBaseline, Box, Container, Grid,
|
||||
Typography, ToggleButton, ToggleButtonGroup,
|
||||
Chip, CircularProgress, Alert, IconButton, Tooltip,
|
||||
} from '@mui/material';
|
||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||
import { theme } from './theme.js';
|
||||
import SummaryCards from './components/SummaryCards.jsx';
|
||||
import AvailabilityChart from './components/AvailabilityChart.jsx';
|
||||
import ResponseTimeChart from './components/ResponseTimeChart.jsx';
|
||||
import OutageTable from './components/OutageTable.jsx';
|
||||
import { fetchSummary, fetchHourly, fetchOutages, fetchLive } from './api.js';
|
||||
|
||||
const RANGE_DAYS = { '24h': 1, '7d': 7, '30d': 30 };
|
||||
const REFRESH_MS = 60_000;
|
||||
|
||||
export default function App() {
|
||||
const [summary, setSummary] = useState(null);
|
||||
const [hourly, setHourly] = useState([]);
|
||||
const [outages, setOutages] = useState([]);
|
||||
const [live, setLive] = useState(null);
|
||||
const [range, setRange] = useState('7d');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [lastRefresh, setLastRefresh] = useState(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const days = RANGE_DAYS[range] ?? 7;
|
||||
try {
|
||||
const [s, h, o, l] = await Promise.all([
|
||||
fetchSummary(),
|
||||
fetchHourly(days),
|
||||
fetchOutages(50),
|
||||
fetchLive(),
|
||||
]);
|
||||
setSummary(s); setHourly(h); setOutages(o); setLive(l);
|
||||
setError(null);
|
||||
setLastRefresh(new Date());
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [range]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
refresh();
|
||||
const id = setInterval(refresh, REFRESH_MS);
|
||||
return () => clearInterval(id);
|
||||
}, [refresh]);
|
||||
|
||||
const isOnline = live && (Date.now() - new Date(live.ts).getTime()) < 180_000;
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Box sx={{ minHeight: '100vh', bgcolor: 'background.default', pb: 8 }}>
|
||||
|
||||
{/* ── Header ── */}
|
||||
<Box sx={{
|
||||
borderBottom: '1px solid', borderColor: 'divider',
|
||||
position: 'sticky', top: 0, zIndex: 100,
|
||||
backdropFilter: 'blur(12px)',
|
||||
bgcolor: 'rgba(8,13,20,0.85)',
|
||||
}}>
|
||||
<Container maxWidth="xl">
|
||||
<Box sx={{ py: 1.5, display: 'flex', alignItems: 'center', gap: 2, flexWrap: 'wrap' }}>
|
||||
<Typography variant="h5" sx={{ flexGrow: 1, letterSpacing: '-0.5px' }}>
|
||||
⚡ TSE Monitor
|
||||
</Typography>
|
||||
|
||||
{live && (
|
||||
<>
|
||||
<Chip
|
||||
label={isOnline ? '● ONLINE' : '○ STALE'}
|
||||
color={isOnline ? 'success' : 'warning'}
|
||||
size="small"
|
||||
sx={{ fontWeight: 700 }}
|
||||
/>
|
||||
<Chip label={`sig #${live.signature_counter}`} variant="outlined" size="small" />
|
||||
<Chip label={`tx #${live.transaction_counter}`} variant="outlined" size="small" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{lastRefresh && (
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
updated {lastRefresh.toLocaleTimeString()}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Tooltip title="Refresh now">
|
||||
<IconButton size="small" onClick={refresh} disabled={loading}>
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
|
||||
<Container maxWidth="xl" sx={{ mt: 4 }}>
|
||||
{error && <Alert severity="error" sx={{ mb: 3 }}>API error: {error}</Alert>}
|
||||
|
||||
{loading && !summary ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 10 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
) : (
|
||||
<Grid container spacing={3}>
|
||||
{/* Summary cards */}
|
||||
<Grid size={12}>
|
||||
<SummaryCards summary={summary} />
|
||||
</Grid>
|
||||
|
||||
{/* Time-range toggle */}
|
||||
<Grid size={12} sx={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<ToggleButtonGroup
|
||||
value={range} exclusive size="small"
|
||||
onChange={(_, v) => v && setRange(v)}
|
||||
>
|
||||
{['24h','7d','30d'].map(v => (
|
||||
<ToggleButton key={v} value={v} sx={{ px: 2 }}>{v}</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
|
||||
{/* Availability chart */}
|
||||
<Grid size={12}>
|
||||
<AvailabilityChart data={hourly} />
|
||||
</Grid>
|
||||
|
||||
{/* Response-time chart */}
|
||||
<Grid size={12}>
|
||||
<ResponseTimeChart data={hourly} />
|
||||
</Grid>
|
||||
|
||||
{/* Outage table */}
|
||||
<Grid size={12}>
|
||||
<OutageTable outages={outages} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Container>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
13
dashboard/src/api.js
Normal file
13
dashboard/src/api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const BASE = ''; // proxied via vite → localhost:20081
|
||||
|
||||
async function get(path) {
|
||||
const r = await fetch(BASE + path);
|
||||
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
|
||||
return r.json();
|
||||
}
|
||||
|
||||
export const fetchSummary = () => get('/api/summary');
|
||||
export const fetchHourly = (days = 7) => get(`/api/hourly?days=${days}`);
|
||||
export const fetchDaily = (days = 30)=> get(`/api/daily?days=${days}`);
|
||||
export const fetchOutages = (limit=50) => get(`/api/outages?limit=${limit}`);
|
||||
export const fetchLive = () => get('/api/live');
|
||||
73
dashboard/src/components/AvailabilityChart.jsx
Normal file
73
dashboard/src/components/AvailabilityChart.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Paper, Typography, Box } from '@mui/material';
|
||||
import {
|
||||
AreaChart, Area, XAxis, YAxis, CartesianGrid,
|
||||
Tooltip, ResponsiveContainer, ReferenceLine,
|
||||
} from 'recharts';
|
||||
|
||||
function fmt(isoStr) {
|
||||
const d = new Date(isoStr);
|
||||
return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
function CustomTooltip({ active, payload, label }) {
|
||||
if (!active || !payload?.length) return null;
|
||||
const d = payload[0]?.payload;
|
||||
return (
|
||||
<Box sx={{
|
||||
bgcolor: 'background.paper', border: '1px solid rgba(255,255,255,0.12)',
|
||||
borderRadius: 2, p: 1.5, fontSize: 13,
|
||||
}}>
|
||||
<div style={{ color: '#94a3b8', marginBottom: 4 }}>{fmt(label)}</div>
|
||||
<div style={{ color: '#22c55e', fontWeight: 700 }}>
|
||||
Uptime: {d?.uptime_pct ?? '—'}%
|
||||
</div>
|
||||
<div style={{ color: '#94a3b8' }}>Polls: {d?.total_polls ?? 0}</div>
|
||||
{d?.outage_s > 0 && (
|
||||
<div style={{ color: '#f59e0b' }}>Outage: {d.outage_s}s</div>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AvailabilityChart({ data }) {
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>Availability</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
|
||||
Hourly uptime %
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<AreaChart data={data} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="uptimeGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor="#22c55e" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickFormatter={v => new Date(v).toLocaleDateString(undefined,{month:'short',day:'numeric'})}
|
||||
tick={{ fill: '#64748b', fontSize: 11 }}
|
||||
axisLine={false} tickLine={false}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
domain={[0, 100]} tickFormatter={v => `${v}%`}
|
||||
tick={{ fill: '#64748b', fontSize: 11 }}
|
||||
axisLine={false} tickLine={false} width={42}
|
||||
/>
|
||||
<ReferenceLine y={100} stroke="#22c55e" strokeDasharray="4 4" strokeOpacity={0.3} />
|
||||
<ReferenceLine y={99} stroke="#f59e0b" strokeDasharray="4 4" strokeOpacity={0.4} />
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
<Area
|
||||
type="monotone" dataKey="uptime_pct"
|
||||
stroke="#22c55e" strokeWidth={2}
|
||||
fill="url(#uptimeGrad)"
|
||||
dot={false} activeDot={{ r: 4, fill: '#22c55e' }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
63
dashboard/src/components/OutageTable.jsx
Normal file
63
dashboard/src/components/OutageTable.jsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
Paper, Typography, Table, TableBody, TableCell,
|
||||
TableContainer, TableHead, TableRow, Chip, Box,
|
||||
} from '@mui/material';
|
||||
|
||||
function duration(s) {
|
||||
if (s == null) return '?';
|
||||
if (s < 60) return `${s}s`;
|
||||
if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s`;
|
||||
return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`;
|
||||
}
|
||||
|
||||
export default function OutageTable({ outages }) {
|
||||
if (!outages?.length) {
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>Outages</Typography>
|
||||
<Typography color="text.secondary">No outages recorded.</Typography>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>Outages</Typography>
|
||||
<TableContainer>
|
||||
<Table size="small">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell sx={{ color: 'text.secondary', fontWeight: 600 }}>Started</TableCell>
|
||||
<TableCell sx={{ color: 'text.secondary', fontWeight: 600 }}>Recovered</TableCell>
|
||||
<TableCell sx={{ color: 'text.secondary', fontWeight: 600 }}>Duration</TableCell>
|
||||
<TableCell sx={{ color: 'text.secondary', fontWeight: 600 }}>Status</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{outages.map((o) => (
|
||||
<TableRow key={o.id} hover sx={{ '&:last-child td': { border: 0 } }}>
|
||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: 12 }}>
|
||||
{new Date(o.started_at).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell sx={{ fontFamily: 'monospace', fontSize: 12, color: 'text.secondary' }}>
|
||||
{o.recovered_at ? new Date(o.recovered_at).toLocaleString() : '—'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Box component="span" sx={{ fontFamily: 'monospace', fontWeight: 600 }}>
|
||||
{duration(o.duration_s)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{o.recovered_at
|
||||
? <Chip label="Resolved" color="success" size="small" />
|
||||
: <Chip label="Ongoing" color="error" size="small" />
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
76
dashboard/src/components/ResponseTimeChart.jsx
Normal file
76
dashboard/src/components/ResponseTimeChart.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Paper, Typography, Box } from '@mui/material';
|
||||
import {
|
||||
ComposedChart, Line, Area, XAxis, YAxis, CartesianGrid,
|
||||
Tooltip, ResponsiveContainer, Legend,
|
||||
} from 'recharts';
|
||||
|
||||
function CustomTooltip({ active, payload, label }) {
|
||||
if (!active || !payload?.length) return null;
|
||||
const d = payload[0]?.payload;
|
||||
return (
|
||||
<Box sx={{
|
||||
bgcolor: 'background.paper', border: '1px solid rgba(255,255,255,0.12)',
|
||||
borderRadius: 2, p: 1.5, fontSize: 13,
|
||||
}}>
|
||||
<div style={{ color: '#94a3b8', marginBottom: 4 }}>
|
||||
{new Date(label).toLocaleString(undefined, { month:'short', day:'numeric', hour:'2-digit', minute:'2-digit' })}
|
||||
</div>
|
||||
{d?.avg_ms != null && <div style={{ color: '#3b82f6', fontWeight: 700 }}>Avg: {d.avg_ms} ms</div>}
|
||||
{d?.min_ms != null && <div style={{ color: '#a78bfa' }}>Min: {d.min_ms} ms</div>}
|
||||
{d?.max_ms != null && <div style={{ color: '#f59e0b' }}>Max: {d.max_ms} ms</div>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResponseTimeChart({ data }) {
|
||||
// build min-max range for area band
|
||||
const enriched = data.map(d => ({
|
||||
...d,
|
||||
range: d.min_ms != null && d.max_ms != null ? [d.min_ms, d.max_ms] : null,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>Response Time</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
|
||||
Avg / min / max per hour (ms)
|
||||
</Typography>
|
||||
<ResponsiveContainer width="100%" height={260}>
|
||||
<ComposedChart data={enriched} margin={{ top: 8, right: 16, left: 0, bottom: 0 }}>
|
||||
<defs>
|
||||
<linearGradient id="rttGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor="#3b82f6" stopOpacity={0.2} />
|
||||
<stop offset="95%" stopColor="#3b82f6" stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.05)" />
|
||||
<XAxis
|
||||
dataKey="hour"
|
||||
tickFormatter={v => new Date(v).toLocaleDateString(undefined,{month:'short',day:'numeric'})}
|
||||
tick={{ fill: '#64748b', fontSize: 11 }}
|
||||
axisLine={false} tickLine={false}
|
||||
interval="preserveStartEnd"
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={v => `${v}ms`}
|
||||
tick={{ fill: '#64748b', fontSize: 11 }}
|
||||
axisLine={false} tickLine={false} width={52}
|
||||
/>
|
||||
<Tooltip content={<CustomTooltip />} />
|
||||
{/* min-max band */}
|
||||
<Area
|
||||
type="monotone" dataKey="range"
|
||||
fill="url(#rttGrad)" stroke="none"
|
||||
dot={false} connectNulls={false}
|
||||
/>
|
||||
{/* avg line */}
|
||||
<Line
|
||||
type="monotone" dataKey="avg_ms"
|
||||
stroke="#3b82f6" strokeWidth={2}
|
||||
dot={false} activeDot={{ r: 4 }}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
76
dashboard/src/components/SummaryCards.jsx
Normal file
76
dashboard/src/components/SummaryCards.jsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Grid, Paper, Typography, Box, Skeleton } from '@mui/material';
|
||||
import UptimeIcon from '@mui/icons-material/CheckCircleOutlined';
|
||||
import SpeedIcon from '@mui/icons-material/Speed';
|
||||
import PollIcon from '@mui/icons-material/DataUsage';
|
||||
import OutageIcon from '@mui/icons-material/Warning';
|
||||
|
||||
|
||||
function StatCard({ icon: Icon, label, value, unit, color = 'primary.main', loading }) {
|
||||
return (
|
||||
<Paper sx={{ p: 3, height: '100%', position: 'relative', overflow: 'hidden' }}>
|
||||
{/* glow accent */}
|
||||
<Box sx={{
|
||||
position: 'absolute', top: -20, right: -20,
|
||||
width: 120, height: 120, borderRadius: '50%',
|
||||
bgcolor: color, opacity: 0.06, filter: 'blur(30px)',
|
||||
}} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 1.5 }}>
|
||||
<Box sx={{
|
||||
p: 1, borderRadius: 2,
|
||||
bgcolor: `${color}22`,
|
||||
color,
|
||||
display: 'flex',
|
||||
}}>
|
||||
<Icon fontSize="small" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', letterSpacing: 1 }}>
|
||||
{label}
|
||||
</Typography>
|
||||
{loading ? (
|
||||
<Skeleton width={80} height={40} />
|
||||
) : (
|
||||
<Typography variant="h4" fontWeight={700} color={color} sx={{ lineHeight: 1.1, mt: 0.5 }}>
|
||||
{value}
|
||||
{unit && (
|
||||
<Typography component="span" variant="body2" color="text.secondary" ml={0.5}>
|
||||
{unit}
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function SummaryCards({ summary }) {
|
||||
const loading = !summary;
|
||||
|
||||
const uptime = summary?.avg_uptime_pct != null ? `${summary.avg_uptime_pct}` : '—';
|
||||
const avgMs = summary?.avg_ms != null ? summary.avg_ms : '—';
|
||||
const polls = summary?.total_polls != null
|
||||
? summary.total_polls >= 1000
|
||||
? `${(summary.total_polls / 1000).toFixed(1)}k`
|
||||
: summary.total_polls
|
||||
: '—';
|
||||
const outages = summary?.outage_count ?? '—';
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={UptimeIcon} label="Avg Uptime" value={uptime} unit="%" color="#22c55e" loading={loading} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={SpeedIcon} label="Avg RTT" value={avgMs} unit="ms" color="#3b82f6" loading={loading} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={PollIcon} label="Total Polls" value={polls} color="#a78bfa" loading={loading} />
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 6, md: 3 }}>
|
||||
<StatCard icon={OutageIcon} label="Outages" value={outages} color="#f59e0b" loading={loading} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
9
dashboard/src/main.jsx
Normal file
9
dashboard/src/main.jsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
31
dashboard/src/theme.js
Normal file
31
dashboard/src/theme.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createTheme } from '@mui/material/styles';
|
||||
|
||||
export const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
background: { default: '#080d14', paper: '#0f1724' },
|
||||
primary: { main: '#3b82f6' },
|
||||
secondary: { main: '#a78bfa' },
|
||||
success: { main: '#22c55e' },
|
||||
error: { main: '#ef4444' },
|
||||
warning: { main: '#f59e0b' },
|
||||
divider: 'rgba(255,255,255,0.07)',
|
||||
},
|
||||
typography: {
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
h5: { fontWeight: 700 },
|
||||
h6: { fontWeight: 600 },
|
||||
},
|
||||
shape: { borderRadius: 12 },
|
||||
components: {
|
||||
MuiPaper: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
backgroundImage: 'none',
|
||||
border: '1px solid rgba(255,255,255,0.07)',
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiChip: { styleOverrides: { root: { fontFamily: 'monospace' } } },
|
||||
},
|
||||
});
|
||||
10
dashboard/vite.config.js
Normal file
10
dashboard/vite.config.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
port: 20080,
|
||||
proxy: { '/api': 'http://localhost:20081' },
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user