import { api } from './api.js';
class App {
constructor() {
this.currentView = 'logs';
this.connectionCheckInterval = null;
this.renderedViews = new Set();
this.views = {};
this.popoverOpen = false;
this.lastHealthData = null;
this.init();
}
init() {
this.views = {
logs: new window.LogsView(api),
traces: new window.TracesView(api),
metrics: new window.MetricsView(api),
usage: new window.UsageView(api),
};
this.setupNavigation();
this.setupConnectionMonitoring();
this.loadInitialView();
}
setupNavigation() {
const navButtons = document.querySelectorAll('.nav-btn');
navButtons.forEach(btn => {
btn.addEventListener('click', () => {
const view = btn.dataset.view;
this.switchView(view);
});
});
}
switchView(viewName) {
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.view === viewName);
});
document.querySelectorAll('.view').forEach(view => {
view.classList.toggle('active', view.id === `${viewName}-view`);
});
this.currentView = viewName;
if (this.views[viewName] && !this.renderedViews.has(viewName)) {
this.renderedViews.add(viewName);
this.views[viewName].render();
}
this.dispatchViewChange(viewName);
}
dispatchViewChange(viewName) {
const event = new CustomEvent('viewchange', { detail: { view: viewName } });
window.dispatchEvent(event);
}
setupConnectionMonitoring() {
this.checkConnection();
this.connectionCheckInterval = setInterval(() => {
this.checkConnection();
}, 5000);
const statusWrapper = document.getElementById('status-wrapper');
if (statusWrapper) {
statusWrapper.addEventListener('click', (e) => {
e.stopPropagation();
this.togglePopover();
});
}
document.addEventListener('click', () => {
if (this.popoverOpen) {
this.closePopover();
}
});
}
async checkConnection() {
const indicator = document.getElementById('status-indicator');
const text = document.getElementById('status-text');
try {
const health = await api.getHealth();
this.lastHealthData = health;
indicator.classList.remove('disconnected');
text.textContent = 'Connected';
if (this.popoverOpen) {
this.refreshPopover();
}
} catch (error) {
this.lastHealthData = null;
indicator.classList.add('disconnected');
text.textContent = 'Disconnected';
console.error('Connection check failed:', error);
if (this.popoverOpen) {
this.closePopover();
}
}
}
formatUptime(seconds) {
if (seconds < 60) return `${seconds}s`;
const mins = Math.floor(seconds / 60) % 60;
const hours = Math.floor(seconds / 3600) % 24;
const days = Math.floor(seconds / 86400);
const parts = [];
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (mins > 0) parts.push(`${mins}m`);
return parts.join(' ') || '0m';
}
async togglePopover() {
if (this.popoverOpen) {
this.closePopover();
} else {
await this.openPopover();
}
}
async openPopover() {
this.popoverOpen = true;
const popover = document.getElementById('status-popover');
if (!popover) return;
popover.classList.add('visible');
await this.refreshPopover();
}
async refreshPopover() {
const popover = document.getElementById('status-popover');
if (!popover) return;
const health = this.lastHealthData;
if (!health) {
popover.innerHTML = '<div class="popover-row popover-error">Server unreachable</div>';
return;
}
let statsHtml = '<div class="popover-row">Loading counts…</div>';
try {
const stats = await api.getStats();
statsHtml = `
<div class="popover-row"><span class="popover-label">Logs</span><span class="popover-value">${stats.log_count.toLocaleString()}</span></div>
<div class="popover-row"><span class="popover-label">Traces</span><span class="popover-value">${stats.span_count.toLocaleString()}</span></div>
<div class="popover-row"><span class="popover-label">Metric points</span><span class="popover-value">${stats.metric_count.toLocaleString()}</span></div>`;
} catch (_) {
statsHtml = '<div class="popover-row popover-error">Could not load counts</div>';
}
popover.innerHTML = `
<div class="popover-row"><span class="popover-label">Version</span><span class="popover-value">${health.version}</span></div>
<div class="popover-row"><span class="popover-label">Uptime</span><span class="popover-value">${this.formatUptime(health.uptime_seconds)}</span></div>
<div class="popover-divider"></div>
${statsHtml}
<div class="popover-divider"></div>
<div class="popover-row"><span class="popover-label">gRPC</span><span class="popover-value">:4317</span></div>
<div class="popover-row"><span class="popover-label">HTTP</span><span class="popover-value">:4318</span></div>
<div class="popover-divider"></div>
<div class="popover-row popover-action-row"><button class="popover-danger-btn" onclick="app.clearAllData()">Clear all data</button></div>`;
}
closePopover() {
this.popoverOpen = false;
const popover = document.getElementById('status-popover');
if (popover) {
popover.classList.remove('visible');
}
}
async clearAllData() {
if (!confirm('Delete all telemetry data? This cannot be undone.')) {
return;
}
try {
const response = await fetch('/api/admin/purge', { method: 'POST' });
if (!response.ok) {
const popover = document.getElementById('status-popover');
if (popover) {
const errRow = document.createElement('div');
errRow.className = 'popover-row popover-error';
errRow.textContent = `Clear failed: HTTP ${response.status}`;
popover.prepend(errRow);
}
return;
}
const popover = document.getElementById('status-popover');
if (popover) {
const okRow = document.createElement('div');
okRow.className = 'popover-row';
okRow.style.color = '#4ade80';
okRow.textContent = 'All data cleared.';
popover.prepend(okRow);
setTimeout(() => okRow.remove(), 3000);
}
await this.refreshPopover();
} catch (err) {
const popover = document.getElementById('status-popover');
if (popover) {
const errRow = document.createElement('div');
errRow.className = 'popover-row popover-error';
errRow.textContent = `Clear failed: ${err.message}`;
popover.prepend(errRow);
}
}
}
loadInitialView() {
this.switchView(this.currentView);
}
showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
hideLoading() {
document.getElementById('loading-overlay').classList.add('hidden');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
window.app = new App();
});
} else {
window.app = new App();
}
export { App };