// Miroir Admin UI - SPA (function() { 'use strict'; // Configuration const API_BASE = '/_miroir'; const SESSION_CHECK_INTERVAL = 60000; // 1 minute // State let currentRoute = '/'; let sessionValid = false; // Router const routes = { '/': renderOverview, '/topology': renderTopology, '/indexes': renderIndexes, '/aliases': renderAliases, '/documents': renderDocuments, '/tasks': renderTasks, '/settings': renderSettings }; // Initialize function init() { checkSession(); setupEventListeners(); setupRouter(); loadVersion(); setInterval(checkSession, SESSION_CHECK_INTERVAL); } // Session management async function checkSession() { try { const response = await fetch(`${API_BASE}/admin/session`, { credentials: 'include' }); if (response.ok) { const data = await response.json(); sessionValid = data.valid; if (!sessionValid) { window.location.href = '/_miroir/admin/login.html'; return false; } return true; } else { window.location.href = '/_miroir/admin/login.html'; return false; } } catch (error) { console.error('Session check failed:', error); window.location.href = '/_miroir/admin/login.html'; return false; } } // API helper async function apiFetch(endpoint, options = {}) { const url = endpoint.startsWith('/') ? `${API_BASE}${endpoint}` : endpoint; const response = await fetch(url, { ...options, credentials: 'include', headers: { 'Content-Type': 'application/json', ...options.headers } }); if (!response.ok) { throw new Error(`API error: ${response.status} ${response.statusText}`); } return response.json(); } // Router setup function setupRouter() { window.addEventListener('hashchange', handleRoute); handleRoute(); } async function handleRoute() { const hash = window.location.hash.slice(1) || '/'; const [path, queryString] = hash.split('?'); const route = routes[path] || routes['/']; if (route) { currentRoute = path; await route(); updateNav(); } } function updateNav() { document.querySelectorAll('.nav-link').forEach(link => { const href = link.getAttribute('href'); if (href === `#${currentRoute}`) { link.classList.add('active'); } else { link.classList.remove('active'); } }); } // Event listeners function setupEventListeners() { document.getElementById('logoutBtn').addEventListener('click', handleLogout); } async function handleLogout() { try { await apiFetch('/admin/logout', { method: 'POST' }); window.location.href = '/_miroir/admin/login.html'; } catch (error) { console.error('Logout failed:', error); } } async function loadVersion() { try { const data = await apiFetch('/metrics'); document.getElementById('version').textContent = data.version || ''; } catch (error) { console.error('Failed to load version:', error); } } // Render functions async function renderOverview() { const content = document.getElementById('content'); try { const [topology, ready, metrics] = await Promise.all([ apiFetch('/topology'), apiFetch('/ready'), apiFetch('/metrics') ]); const degradedCount = topology.shards.filter(s => !s.healthy).length; content.innerHTML = `
Nodes
${topology.nodes.length}
Shards
${topology.shards}
Degraded Shards
${degradedCount}
Status
${ready.ready ? 'Ready' : 'Not Ready'}

Cluster Health

Replica Groups: ${topology.replica_groups}

Replication Factor: ${topology.replication_factor}

Ready: ${ready.ready ? 'Yes' : 'No'}

`; } catch (error) { renderError(error); } } async function renderTopology() { const content = document.getElementById('content'); try { const topology = await apiFetch('/topology'); const rows = topology.nodes.map(node => { const statusClass = node.is_healthy() ? 'badge-success' : 'badge-error'; return ` ${node.id} ${node.address} ${node.group} ${node.status} `; }).join(''); content.innerHTML = `

Topology

${rows}
Node ID Address Group Status
`; } catch (error) { renderError(error); } } async function renderIndexes() { const content = document.getElementById('content'); try { const stats = await apiFetch('/stats'); const indexes = stats.indexes || {}; const rows = Object.entries(indexes).map(([name, info]) => ` ${name} ${info.numberOfDocuments || 0} ${info.isIndexing ? 'Yes' : 'No'} `).join(''); content.innerHTML = `

Indexes

${rows ? `
${rows}
Name Documents Indexing Actions
` : '
No indexes found
'}
`; } catch (error) { renderError(error); } } async function renderAliases() { const content = document.getElementById('content'); try { const aliases = await apiFetch('/aliases'); const rows = aliases.map(alias => ` ${alias.name} ${alias.indexUid} ${alias.kind || 'single'} ${alias.createdAt ? new Date(alias.createdAt).toLocaleString() : 'N/A'} ${alias.updatedAt ? new Date(alias.updatedAt).toLocaleString() : 'N/A'} `).join(''); content.innerHTML = `

Aliases

${rows ? `
${rows}
Name Target Index Kind Created Updated
` : '
No aliases found
'}
`; } catch (error) { renderError(error); } } async function renderDocuments() { const content = document.getElementById('content'); // Get index from query string const params = new URLSearchParams(window.location.hash.split('?')[1] || ''); const index = params.get('index'); if (!index) { content.innerHTML = `

Documents

Select an index from the Indexes page to view documents.

`; return; } content.innerHTML = `

Documents: ${index}

`; try { const data = await apiFetch(`/stats`); const indexStats = data.indexes?.[index]; content.innerHTML = `

Documents: ${index}

Document count: ${indexStats?.numberOfDocuments || 0}

Document browser is a placeholder - full implementation would query documents from the index.

`; } catch (error) { renderError(error); } } async function renderTasks() { const content = document.getElementById('content'); content.innerHTML = `

Tasks

`; try { const tasks = await apiFetch('/tasks?limit=20'); const rows = tasks.map(task => ` ${task.uid} ${task.status} ${task.type} ${task.enqueuedAt ? new Date(task.enqueuedAt).toLocaleString() : 'N/A'} `).join(''); content.innerHTML = `

Recent Tasks

${rows ? `
${rows}
UID Status Type Enqueued
` : '
No tasks found
'}
`; } catch (error) { renderError(error); } } async function renderSettings() { const content = document.getElementById('content'); content.innerHTML = `

Settings

`; try { const topology = await apiFetch('/topology'); content.innerHTML = `

Cluster Configuration

Shards: ${topology.shards}

Replica Groups: ${topology.replica_groups}

Replication Factor: ${topology.replication_factor}

Settings management is a placeholder - full implementation would allow editing cluster configuration.

`; } catch (error) { renderError(error); } } function renderError(error) { const content = document.getElementById('content'); content.innerHTML = `
Error: ${error.message}
`; } // Start the app if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();