<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
body { background: #020617; color: #f8fafc; font-family: ui-monospace, monospace; }
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-thumb { background: #1e293b; border-radius: 10px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
// ⚠️ CAMBIA ESTO POR LA URL DE TU WORKER ORQUESTADOR
const ORCHESTRATOR_URL = "https://metadatamanager.rodrimm3006.workers.dev";
const App = () => {
const [metadata, setMetadata] = useState({});
const [query, setQuery] = useState("SELECT * FROM users_data LIMIT 10");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState({ done: 0, total: 0 });
const fetchMetadata = async () => {
try {
const res = await fetch(`${ORCHESTRATOR_URL}/metadata`);
const data = await res.json();
setMetadata(data);
setProgress(prev => ({ ...prev, total: Object.keys(data).length }));
} catch (e) { console.error("Error cargando metadatos", e); }
};
const run = async () => {
if (!query.trim()) return;
setLoading(true);
setResults([]);
setProgress(prev => ({ ...prev, done: 0 }));
try {
const res = await fetch(`${ORCHESTRATOR_URL}/query`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sql: query })
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.error || "Error de red");
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const {done, value} = await reader.read();
if (done) break;
const lines = decoder.decode(value).split('\n').filter(Boolean);
lines.forEach(line => {
try {
const data = JSON.parse(line);
setResults(prev => [...prev, data]);
setProgress(p => ({ ...p, done: p.done + 1 }));
} catch (parseErr) {}
});
}
} catch (e) { alert(e.message); }
setLoading(false);
};
useEffect(() => { fetchMetadata(); }, []);
return (
<div className="flex h-screen overflow-hidden text-[13px]">
<aside className="w-72 bg-slate-950 border-r border-slate-900 p-6 flex flex-col shadow-2xl z-10">
<h1 className="text-blue-500 font-black italic tracking-tighter text-lg uppercase mb-1">Shard_Master</h1>
<p className="text-[10px] text-emerald-500 font-bold mb-8">PAGES_V2.0 // DISTRIBUTED</p>
<div className="flex-1 overflow-y-auto">
{Object.entries(metadata).map(([shard, tables]) => (
<div key={shard} className="mb-6">
<div className="text-[10px] text-slate-500 font-bold mb-2 uppercase tracking-widest flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full animate-pulse"></span>
{shard.split(':')[1]}
</div>
{tables.map(t => (
<div key={t.name} onClick={() => setQuery(\`SELECT * FROM \${t.name} LIMIT 5\`)} className="pl-3 py-1 text-slate-400 hover:text-white cursor-pointer border-l border-slate-900 hover:border-blue-500 transition-all text-[11px]">
{t.name}
</div>
))}
</div>
))}
</div>
</aside>
<main className="flex-1 flex flex-col bg-[#020617]">
<div className="p-8 border-b border-slate-900 bg-slate-900/20">
<div className="flex gap-4">
<input
value={query}
onChange={e => setQuery(e.target.value)}
className="bg-slate-950 px-4 py-3 rounded-lg flex-1 border border-slate-800 outline-none focus:border-blue-500 transition-colors text-blue-300 font-medium"
/>
<button onClick={run} disabled={loading} className="bg-blue-600 hover:bg-blue-500 text-white px-8 rounded-lg font-bold text-[11px] uppercase tracking-wider disabled:opacity-50 transition-all shadow-lg shadow-blue-900/20">
{loading ? 'Processing...' : 'Run Query'}
</button>
</div>
{/* Barra de progreso de lotes */}
<div className="mt-4 flex items-center gap-4 text-[10px] font-bold tracking-widest uppercase">
<div className="text-slate-500">Shards Processed: <span className={progress.done === progress.total ? "text-emerald-500" : "text-blue-500"}>{progress.done} / {progress.total}</span></div>
{loading && <div className="text-blue-500 animate-pulse">Streaming data batches...</div>}
</div>
</div>
<div className="flex-1 overflow-auto p-8 pt-6 grid grid-cols-1 xl:grid-cols-2 gap-6 items-start auto-rows-max">
{results.map((r, i) => (
<div key={i} className="bg-slate-950 border border-slate-800 rounded-xl overflow-hidden shadow-2xl animate-in slide-in-from-bottom-4 duration-300">
<div className="bg-slate-900/80 px-4 py-2 flex justify-between items-center border-b border-slate-800">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{r.shard}</span>
<span className="text-[10px] text-emerald-500 font-mono bg-emerald-500/10 px-2 py-0.5 rounded">{r.time}ms</span>
</div>
<div className="p-0 overflow-x-auto">
{r.error ? (
<div className="p-4 text-red-400 text-xs">{r.error}</div>
) : r.data && r.data.length > 0 ? (
<table className="w-full text-left text-[11px]">
<thead>
<tr className="bg-slate-900/30 text-slate-500 border-b border-slate-800 text-[9px] uppercase tracking-tighter">
{Object.keys(r.data[0]).map(k => <th key={k} className="px-4 py-3">{k}</th>)}
</tr>
</thead>
<tbody className="divide-y divide-slate-800/50">
{r.data.map((row, idx) => (
<tr key={idx} className="hover:bg-blue-500/10 transition-colors">
{Object.values(row).map((v, ki) => <td key={ki} className="px-4 py-2.5 text-slate-300">{String(v)}</td>)}
</tr>
))}
</tbody>
</table>
) : (
<div className="p-4 text-slate-600 text-xs italic">0 rows returned</div>
)}
</div>
</div>
))}
</div>
</main>
</div>
);
};
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>