🔱 GOD PANEL: Visual diagnostics dashboard
Features: - Completely standalone (no Directus, no middleware, no redirects) - Live service status for all 4 containers - SQL Console for direct database queries - Quick Actions (check sites, count articles, etc.) - Table browser with row counts - Memory & performance metrics - Auto-refresh option (5s interval) - Raw health data viewer Tech: - Pure React via ESM CDN imports - Tailwind CDN (no build dependency) - Dark theme with gold accents - Works even when everything else is broken URL: /god
This commit is contained in:
367
frontend/src/pages/god.astro
Normal file
367
frontend/src/pages/god.astro
Normal file
@@ -0,0 +1,367 @@
|
||||
---
|
||||
/**
|
||||
* 🔱 GOD PANEL - System Diagnostics Dashboard
|
||||
*
|
||||
* This page is COMPLETELY STANDALONE:
|
||||
* - No middleware
|
||||
* - No Directus dependency
|
||||
* - No redirects
|
||||
* - Works even when everything else is broken
|
||||
*/
|
||||
export const prerender = false;
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>🔱 God Panel - Spark Platform</title>
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
god: {
|
||||
gold: '#FFD700',
|
||||
dark: '#0a0a0a',
|
||||
card: '#111111',
|
||||
border: '#333333'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@keyframes pulse-gold {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.4); }
|
||||
50% { box-shadow: 0 0 20px 10px rgba(255, 215, 0, 0.1); }
|
||||
}
|
||||
.pulse-gold { animation: pulse-gold 2s infinite; }
|
||||
.status-healthy { color: #22c55e; }
|
||||
.status-unhealthy { color: #ef4444; }
|
||||
.status-warning { color: #eab308; }
|
||||
pre { white-space: pre-wrap; word-wrap: break-word; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-god-dark text-white min-h-screen">
|
||||
<div id="god-panel"></div>
|
||||
|
||||
<script type="module">
|
||||
import React from 'https://esm.sh/react@18';
|
||||
import ReactDOM from 'https://esm.sh/react-dom@18/client';
|
||||
|
||||
const { useState, useEffect, useCallback } = React;
|
||||
const h = React.createElement;
|
||||
|
||||
// API Helper
|
||||
const api = {
|
||||
async get(endpoint) {
|
||||
const token = localStorage.getItem('godToken') || '';
|
||||
const res = await fetch(`/api/god/${endpoint}`, {
|
||||
headers: { 'X-God-Token': token }
|
||||
});
|
||||
return res.json();
|
||||
},
|
||||
async post(endpoint, data) {
|
||||
const token = localStorage.getItem('godToken') || '';
|
||||
const res = await fetch(`/api/god/${endpoint}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-God-Token': token
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
return res.json();
|
||||
}
|
||||
};
|
||||
|
||||
// Status Badge Component
|
||||
function StatusBadge({ status }) {
|
||||
const isHealthy = status?.includes('✅') || status === 'healthy' || status === 'connected';
|
||||
const isWarning = status?.includes('⚠️');
|
||||
const className = isHealthy ? 'status-healthy' : isWarning ? 'status-warning' : 'status-unhealthy';
|
||||
return h('span', { className: `font-bold ${className}` }, status || 'Unknown');
|
||||
}
|
||||
|
||||
// Service Card Component
|
||||
function ServiceCard({ name, data, icon }) {
|
||||
const status = data?.status || 'Unknown';
|
||||
const isHealthy = status?.includes('✅') || status?.includes('healthy');
|
||||
|
||||
return h('div', {
|
||||
className: `bg-god-card border border-god-border rounded-xl p-4 ${isHealthy ? '' : 'border-red-500/50'}`
|
||||
}, [
|
||||
h('div', { className: 'flex items-center justify-between mb-2' }, [
|
||||
h('div', { className: 'flex items-center gap-2' }, [
|
||||
h('span', { className: 'text-2xl' }, icon),
|
||||
h('span', { className: 'font-semibold text-lg' }, name)
|
||||
]),
|
||||
h(StatusBadge, { status })
|
||||
]),
|
||||
data?.latency_ms && h('div', { className: 'text-sm text-gray-400' },
|
||||
`Latency: ${data.latency_ms}ms`
|
||||
),
|
||||
data?.error && h('div', { className: 'text-sm text-red-400 mt-1' },
|
||||
`Error: ${data.error}`
|
||||
)
|
||||
]);
|
||||
}
|
||||
|
||||
// SQL Console Component
|
||||
function SQLConsole() {
|
||||
const [query, setQuery] = useState('SELECT * FROM sites LIMIT 5;');
|
||||
const [result, setResult] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const execute = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await api.post('sql', { query });
|
||||
setResult(data);
|
||||
} catch (err) {
|
||||
setResult({ error: err.message });
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4' }, [
|
||||
h('h3', { className: 'text-lg font-semibold mb-3 flex items-center gap-2' }, [
|
||||
'🗄️ SQL Console'
|
||||
]),
|
||||
h('textarea', {
|
||||
className: 'w-full bg-black border border-god-border rounded-lg p-3 font-mono text-sm text-green-400 mb-3',
|
||||
rows: 4,
|
||||
value: query,
|
||||
onChange: e => setQuery(e.target.value),
|
||||
placeholder: 'Enter SQL query...'
|
||||
}),
|
||||
h('button', {
|
||||
className: 'bg-god-gold text-black font-bold px-4 py-2 rounded-lg hover:bg-yellow-400 disabled:opacity-50',
|
||||
onClick: execute,
|
||||
disabled: loading
|
||||
}, loading ? 'Executing...' : 'Execute SQL'),
|
||||
result && h('div', { className: 'mt-4' }, [
|
||||
result.error ?
|
||||
h('div', { className: 'text-red-400 font-mono text-sm' }, `Error: ${result.error}`) :
|
||||
h('div', {}, [
|
||||
h('div', { className: 'text-sm text-gray-400 mb-2' },
|
||||
`${result.rowCount || 0} rows returned`
|
||||
),
|
||||
h('pre', {
|
||||
className: 'bg-black rounded-lg p-3 overflow-auto max-h-64 text-xs font-mono text-gray-300'
|
||||
}, JSON.stringify(result.rows, null, 2))
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Tables List Component
|
||||
function TablesList({ tables }) {
|
||||
if (!tables?.tables) return null;
|
||||
|
||||
const customTables = tables.tables.filter(t => !t.name.startsWith('directus_'));
|
||||
const systemTables = tables.tables.filter(t => t.name.startsWith('directus_'));
|
||||
|
||||
return h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4' }, [
|
||||
h('h3', { className: 'text-lg font-semibold mb-3' },
|
||||
`📊 Database Tables (${tables.total})`
|
||||
),
|
||||
h('div', { className: 'grid grid-cols-2 gap-4' }, [
|
||||
h('div', {}, [
|
||||
h('h4', { className: 'text-sm font-semibold text-god-gold mb-2' },
|
||||
`Custom Tables (${customTables.length})`
|
||||
),
|
||||
h('div', { className: 'space-y-1 max-h-48 overflow-auto' },
|
||||
customTables.map(t =>
|
||||
h('div', {
|
||||
key: t.name,
|
||||
className: 'text-xs font-mono flex justify-between bg-black/50 px-2 py-1 rounded'
|
||||
}, [
|
||||
h('span', {}, t.name),
|
||||
h('span', { className: 'text-gray-500' }, `${t.rows} rows`)
|
||||
])
|
||||
)
|
||||
)
|
||||
]),
|
||||
h('div', {}, [
|
||||
h('h4', { className: 'text-sm font-semibold text-gray-400 mb-2' },
|
||||
`Directus System (${systemTables.length})`
|
||||
),
|
||||
h('div', { className: 'text-xs text-gray-500' },
|
||||
systemTables.length + ' system tables'
|
||||
)
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Quick Actions Component
|
||||
function QuickActions() {
|
||||
const actions = [
|
||||
{ label: 'Check Sites', query: 'SELECT id, name, url, status FROM sites LIMIT 10' },
|
||||
{ label: 'Count Articles', query: 'SELECT COUNT(*) as count FROM generated_articles' },
|
||||
{ label: 'Active Connections', query: 'SELECT count(*) FROM pg_stat_activity' },
|
||||
{ label: 'DB Size', query: "SELECT pg_size_pretty(pg_database_size(current_database())) as size" },
|
||||
];
|
||||
|
||||
const [result, setResult] = useState(null);
|
||||
|
||||
const run = async (query) => {
|
||||
const data = await api.post('sql', { query });
|
||||
setResult(data);
|
||||
};
|
||||
|
||||
return h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4' }, [
|
||||
h('h3', { className: 'text-lg font-semibold mb-3' }, '⚡ Quick Actions'),
|
||||
h('div', { className: 'flex flex-wrap gap-2' },
|
||||
actions.map(a =>
|
||||
h('button', {
|
||||
key: a.label,
|
||||
className: 'bg-god-border hover:bg-god-gold hover:text-black px-3 py-1 rounded text-sm transition-colors',
|
||||
onClick: () => run(a.query)
|
||||
}, a.label)
|
||||
)
|
||||
),
|
||||
result && h('pre', {
|
||||
className: 'mt-3 bg-black rounded-lg p-3 text-xs font-mono text-gray-300 overflow-auto max-h-32'
|
||||
}, JSON.stringify(result.rows || result, null, 2))
|
||||
]);
|
||||
}
|
||||
|
||||
// Main God Panel Component
|
||||
function GodPanel() {
|
||||
const [services, setServices] = useState(null);
|
||||
const [health, setHealth] = useState(null);
|
||||
const [tables, setTables] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||
const [lastUpdate, setLastUpdate] = useState(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
const [svc, hlth, tbl] = await Promise.all([
|
||||
api.get('services'),
|
||||
api.get('health'),
|
||||
api.get('tables')
|
||||
]);
|
||||
setServices(svc);
|
||||
setHealth(hlth);
|
||||
setTables(tbl);
|
||||
setLastUpdate(new Date().toLocaleTimeString());
|
||||
} catch (err) {
|
||||
console.error('Refresh failed:', err);
|
||||
}
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
if (autoRefresh) {
|
||||
const interval = setInterval(refresh, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [refresh, autoRefresh]);
|
||||
|
||||
return h('div', { className: 'max-w-6xl mx-auto p-6' }, [
|
||||
// Header
|
||||
h('div', { className: 'flex items-center justify-between mb-8' }, [
|
||||
h('div', {}, [
|
||||
h('h1', { className: 'text-3xl font-bold flex items-center gap-3' }, [
|
||||
h('span', { className: 'text-god-gold pulse-gold inline-block' }, '🔱'),
|
||||
'God Panel'
|
||||
]),
|
||||
h('p', { className: 'text-gray-400 mt-1' },
|
||||
'System Diagnostics & Emergency Access'
|
||||
)
|
||||
]),
|
||||
h('div', { className: 'flex items-center gap-4' }, [
|
||||
h('label', { className: 'flex items-center gap-2 text-sm' }, [
|
||||
h('input', {
|
||||
type: 'checkbox',
|
||||
checked: autoRefresh,
|
||||
onChange: e => setAutoRefresh(e.target.checked),
|
||||
className: 'rounded'
|
||||
}),
|
||||
'Auto-refresh (5s)'
|
||||
]),
|
||||
h('button', {
|
||||
className: 'bg-god-gold text-black font-bold px-4 py-2 rounded-lg hover:bg-yellow-400',
|
||||
onClick: refresh
|
||||
}, '🔄 Refresh'),
|
||||
lastUpdate && h('span', { className: 'text-xs text-gray-500' },
|
||||
`Last: ${lastUpdate}`
|
||||
)
|
||||
])
|
||||
]),
|
||||
|
||||
// Summary Banner
|
||||
services?.summary && h('div', {
|
||||
className: `rounded-xl p-4 mb-6 text-center font-bold text-lg ${
|
||||
services.summary.includes('✅') ? 'bg-green-900/30 border border-green-500/50' :
|
||||
'bg-red-900/30 border border-red-500/50'
|
||||
}`
|
||||
}, services.summary),
|
||||
|
||||
// Service Grid
|
||||
h('div', { className: 'grid grid-cols-2 md:grid-cols-4 gap-4 mb-6' }, [
|
||||
h(ServiceCard, { name: 'Frontend', data: services?.frontend, icon: '🌐' }),
|
||||
h(ServiceCard, { name: 'PostgreSQL', data: services?.postgresql, icon: '🐘' }),
|
||||
h(ServiceCard, { name: 'Redis', data: services?.redis, icon: '🔴' }),
|
||||
h(ServiceCard, { name: 'Directus', data: services?.directus, icon: '📦' }),
|
||||
]),
|
||||
|
||||
// Memory & Performance
|
||||
health && h('div', { className: 'grid grid-cols-3 gap-4 mb-6' }, [
|
||||
h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4 text-center' }, [
|
||||
h('div', { className: 'text-3xl font-bold text-god-gold' },
|
||||
health.uptime_seconds ? Math.round(health.uptime_seconds / 60) + 'm' : '-'
|
||||
),
|
||||
h('div', { className: 'text-sm text-gray-400' }, 'Uptime')
|
||||
]),
|
||||
h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4 text-center' }, [
|
||||
h('div', { className: 'text-3xl font-bold text-god-gold' },
|
||||
health.memory?.heap_used_mb ? health.memory.heap_used_mb + 'MB' : '-'
|
||||
),
|
||||
h('div', { className: 'text-sm text-gray-400' }, 'Memory Used')
|
||||
]),
|
||||
h('div', { className: 'bg-god-card border border-god-border rounded-xl p-4 text-center' }, [
|
||||
h('div', { className: 'text-3xl font-bold text-god-gold' },
|
||||
health.total_latency_ms ? health.total_latency_ms + 'ms' : '-'
|
||||
),
|
||||
h('div', { className: 'text-sm text-gray-400' }, 'Health Check')
|
||||
])
|
||||
]),
|
||||
|
||||
// Main Content Grid
|
||||
h('div', { className: 'grid md:grid-cols-2 gap-6' }, [
|
||||
h(SQLConsole, {}),
|
||||
h('div', { className: 'space-y-6' }, [
|
||||
h(QuickActions, {}),
|
||||
h(TablesList, { tables })
|
||||
])
|
||||
]),
|
||||
|
||||
// Raw Health Data
|
||||
health && h('details', { className: 'mt-6' }, [
|
||||
h('summary', { className: 'cursor-pointer text-gray-400 hover:text-white' },
|
||||
'📋 Raw Health Data'
|
||||
),
|
||||
h('pre', {
|
||||
className: 'mt-2 bg-god-card border border-god-border rounded-xl p-4 text-xs font-mono overflow-auto max-h-64'
|
||||
}, JSON.stringify(health, null, 2))
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
// Render
|
||||
const root = ReactDOM.createRoot(document.getElementById('god-panel'));
|
||||
root.render(h(GodPanel));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user