<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Magic Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #0f172a; color: #f8fafc; }
.glass-card {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body class="min-h-screen p-8">
<div class="max-w-5xl mx-auto">
<header class="flex justify-between items-center mb-8">
<div>
<h1 class="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Magic Dashboard
</h1>
<p class="text-slate-400 text-sm mt-1">Live Application State</p>
</div>
<div class="flex items-center space-x-2">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span>
<span class="text-sm font-medium text-emerald-400" id="status-text">Connected</span>
</div>
</header>
<div id="tabs-container" class="flex space-x-2 mb-6 overflow-x-auto pb-2 border-slate-800">
</div>
<div id="dashboard-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
</div>
</div>
<script>
const grid = document.getElementById('dashboard-grid');
const statusText = document.getElementById('status-text');
const tabsContainer = document.getElementById('tabs-container');
let stateData = {};
let metadata = {};
let historyData = {}; let currentTab = "All";
let isEditing = {};
function formatValue(val) {
if (typeof val === 'boolean') {
return val
? `<span class="px-2 py-1 rounded text-xs font-semibold bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">TRUE</span>`
: `<span class="px-2 py-1 rounded text-xs font-semibold bg-rose-500/20 text-rose-400 border border-rose-500/30">FALSE</span>`;
}
if (typeof val === 'number') {
return `<span class="text-blue-400 font-mono text-2xl">${val}</span>`;
}
if (typeof val === 'object' && val !== null) {
return `<pre class="text-xs text-slate-300 bg-slate-800/50 p-3 rounded-lg mt-2 overflow-x-auto border border-slate-700">${JSON.stringify(val, null, 2)}</pre>`;
}
return `<span class="text-slate-200 font-medium">${val}</span>`;
}
function drawSparkline(values) {
if (values.length < 2) return '';
const max = Math.max(...values);
const min = Math.min(...values);
const range = max - min || 1;
const width = 100;
const height = 20;
const points = values.map((val, i) => {
const x = (i / (values.length - 1)) * width;
const y = height - ((val - min) / range) * height;
return `${x},${y}`;
}).join(' ');
return `
<div class="mt-4 border-t border-slate-700/50 pt-3">
<svg viewBox="-2 -2 104 24" class="w-full h-8 overflow-visible">
<polyline points="${points}" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="opacity-80 drop-shadow-[0_0_2px_rgba(59,130,246,0.5)]"></polyline>
</svg>
</div>`;
}
async function fetchMetadata() {
try {
const response = await fetch('/api/metadata');
if (response.ok) {
metadata = await response.json();
renderTabs();
}
} catch (e) {
console.warn('Could not fetch metadata', e);
}
}
function renderTabs() {
const categories = new Set(["All"]);
for (const field in metadata) {
if (metadata[field].category) {
categories.add(metadata[field].category);
}
}
if (categories.size <= 2) {
tabsContainer.innerHTML = '';
return;
}
tabsContainer.innerHTML = '';
categories.forEach(cat => {
const btn = document.createElement('button');
const isActive = cat === currentTab;
btn.className = `px-4 py-2 rounded-lg text-sm font-medium transition-colors ${isActive ? 'bg-blue-600 text-white' : 'bg-slate-800 text-slate-400 hover:bg-slate-700'}`;
btn.textContent = cat;
btn.onclick = () => {
currentTab = cat;
renderTabs();
renderGrid();
};
tabsContainer.appendChild(btn);
});
}
async function updateField(field, value) {
try {
const res = await fetch('/api/state', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ field, value })
});
if (!res.ok) {
const text = await res.text();
alert("Update failed: " + text);
} else {
isEditing[field] = false;
fetchState();
}
} catch (e) {
alert("Error: " + e.message);
}
}
window.toggleEdit = function(field) {
isEditing[field] = !isEditing[field];
renderGrid();
}
window.saveEdit = function(field, typeStr) {
let inputEl = document.getElementById(`input-${field}`);
let val = inputEl.value;
if (typeStr === 'number') {
val = Number(val);
} else if (typeStr === 'boolean') {
val = inputEl.checked;
} else if (typeStr === 'object') {
try {
val = JSON.parse(val);
} catch(e) {
alert("Invalid JSON format");
return;
}
}
updateField(field, val);
}
function renderGrid() {
grid.innerHTML = '';
for (const [key, value] of Object.entries(stateData)) {
let cat = metadata[key]?.category || "General";
if (currentTab !== "All" && cat !== currentTab) {
continue;
}
if (typeof value === 'number') {
if (!historyData[key]) historyData[key] = [];
if (!isEditing[key]) {
historyData[key].push(value);
if (historyData[key].length > 30) historyData[key].shift();
}
}
const card = document.createElement('div');
card.className = 'glass-card rounded-xl p-6 hover:border-blue-500/50 transition-colors duration-300 shadow-lg relative group flex flex-col justify-between';
const title = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
let typeStr = typeof value;
if (isEditing[key]) {
let inputHtml = '';
if (typeStr === 'boolean') {
inputHtml = `<label class="flex items-center space-x-2 mt-2 cursor-pointer"><input type="checkbox" id="input-${key}" ${value ? 'checked' : ''} class="w-5 h-5 rounded bg-slate-700 border-slate-600 text-blue-500 focus:ring-blue-500"><span class="text-sm text-slate-300">Enabled</span></label>`;
} else if (typeStr === 'number') {
inputHtml = `<input type="number" id="input-${key}" value="${value}" step="any" class="w-full bg-slate-800 border border-slate-600 focus:border-blue-500 outline-none rounded px-3 py-2 mt-2 text-slate-200">`;
} else if (typeStr === 'object' && value !== null) {
inputHtml = `<textarea id="input-${key}" class="w-full bg-slate-800 border border-slate-600 focus:border-blue-500 outline-none rounded px-3 py-2 mt-2 text-slate-200 font-mono text-xs" rows="5">${JSON.stringify(value, null, 2)}</textarea>`;
} else {
inputHtml = `<input type="text" id="input-${key}" value="${value}" class="w-full bg-slate-800 border border-slate-600 focus:border-blue-500 outline-none rounded px-3 py-2 mt-2 text-slate-200">`;
}
card.innerHTML = `
<div>
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<svg class="w-4 h-4 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
Edit ${title}
</h3>
${inputHtml}
</div>
<div class="flex gap-2 mt-4 pt-4 border-t border-slate-700/50">
<button onclick="saveEdit('${key}', '${typeStr}')" class="flex-1 py-1.5 bg-emerald-600/80 hover:bg-emerald-500 text-white text-xs font-semibold rounded transition-colors">Save</button>
<button onclick="toggleEdit('${key}')" class="flex-1 py-1.5 bg-slate-700 hover:bg-slate-600 text-slate-300 text-xs font-semibold rounded transition-colors">Cancel</button>
</div>
`;
} else {
let sparklineHtml = '';
if (typeStr === 'number' && historyData[key]) {
sparklineHtml = drawSparkline(historyData[key]);
}
card.innerHTML = `
<div>
<div class="flex justify-between items-start mb-3">
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider flex items-center gap-2">
<svg class="w-4 h-4 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
${title}
</h3>
<button onclick="toggleEdit('${key}')" class="opacity-0 group-hover:opacity-100 transition-opacity text-slate-400 hover:text-blue-400 p-1 -mt-1 -mr-1" title="Edit value">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
</button>
</div>
<div class="mt-1">${formatValue(value)}</div>
</div>
${sparklineHtml}
`;
}
grid.appendChild(card);
}
}
async function fetchState() {
try {
const response = await fetch('/api/state');
if (!response.ok) throw new Error('Network response was not ok');
stateData = await response.json();
statusText.textContent = "Connected";
statusText.className = "text-sm font-medium text-emerald-400";
renderGrid();
} catch (error) {
console.error('Failed to fetch state:', error);
statusText.textContent = "Disconnected";
statusText.className = "text-sm font-medium text-rose-400";
}
}
fetchMetadata().then(() => {
fetchState();
setInterval(() => {
if (Object.values(isEditing).every(v => !v)) {
fetchState();
}
}, 500);
});
</script>
</body>
</html>