Files
Void-Homelab/public/app.js
root 359ae21d59 feat(speedtest): full speedtest-tracker-style automation (2.9.0)
Switch worker to the Ookla CLI (jitter, packet loss, server, ISP,
shareable result URL, bytes). Migration 028 enriches speedtest_results
+ adds a generic app_settings store. New /speedtest page: KPIs,
throughput + latency charts, window stats, configurable schedule
(reschedulable cron) & low-speed alert threshold, history table.
SV card gains ping/jitter + a link through to the page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 22:55:04 +10:00

95 lines
3.7 KiB
JavaScript

// Void 2.0 SPA bootstrap. Mounts chrome (sidebar/topbar/rightrail) and
// delegates the #main panel to a view module based on the current route.
// Views are loaded dynamically so a missing view never breaks the shell.
import { api } from './api.js';
import { route, current, navigate } from './router.js';
import { renderSidebar } from './components/sidebar.js';
import { renderTopbar } from './components/topbar.js';
import { renderRightrail } from './components/rightrail.js';
import { emit, state } from './state.js';
import { el, mount } from './dom.js';
import { attachDropzone } from './components/dropzone.js';
import { initChrome } from './components/chrome.js';
const VIEWS = {
home: () => import('./views/home.js'),
space: () => import('./views/space.js'),
project: () => import('./views/project.js'),
page: () => import('./views/page.js'),
ref: () => import('./views/reference.js'),
resource: () => import('./views/resource.js'),
search: () => import('./views/search.js'),
inbox: () => import('./views/inbox.js'),
'sacred-valley': () => import('./views/sacred_valley.js'),
yerin: () => import('./views/sentinel.js'),
'little-blue': () => import('./views/little_blue.js'),
terminal: () => import('./views/terminal.js'),
timelapse: () => import('./views/timelapse.js'),
'ai-usage': () => import('./views/aiusage.js'),
obd2: () => import('./views/obd2.js'),
links: () => import('./views/links.js'),
mirror: () => import('./views/mirror.js'),
settings: () => import('./views/settings.js'),
jobs: () => import('./views/jobs.js'),
speedtest: () => import('./views/speedtest.js')
};
async function renderView(ctx) {
// Update cross-component state so the right rail knows the active Space + view.
if (ctx.name === 'space') {
state.spaceId = ctx.params.id || null;
state.view = null;
} else if (ctx.name === 'project' || ctx.name === 'page' || ctx.name === 'ref' || ctx.name === 'resource') {
// Keep the last known spaceId; update the focused entity.
state.view = { entityType: ctx.name, entityId: ctx.params.id || null };
} else {
state.view = null;
}
// Notify subscribers (right rail) of the active Space. The state bus replays
// the last value on subscribe, so this covers both the initial route() call
// and every subsequent navigation with one path.
emit('space-active', state.spaceId);
const main = document.getElementById('main');
const loader = VIEWS[ctx.name] || VIEWS.home;
try {
const mod = await loader();
await mod.render(main, ctx);
} catch (e) {
console.error('view render failed', e);
mount(main,
el('h1', { class: 'view-h1' }, 'Something went wrong'),
el('p', { class: 'view-sub' }, 'View failed to load. Check console for details.'),
el('pre', {}, String(e && e.stack || e))
);
}
}
async function pollPending() {
try {
const rows = await api.get('/api/pending-changes');
emit('pending-count', rows.length);
} catch (e) {
if (e.status === 403) emit('pending-count', 0);
}
}
async function init() {
if (!api.hasToken()) {
try { await api.get('/api/spaces'); }
catch { /* api wrapper opens the modal on 401 */ }
}
renderTopbar(document.getElementById('topbar'));
renderSidebar(document.getElementById('sidebar'));
renderRightrail(document.getElementById('rightrail'));
initChrome();
attachDropzone(document.getElementById('main'));
route(renderView);
pollPending();
setInterval(pollPending, 15000);
}
window.addEventListener('DOMContentLoaded', init);
window.voidNav = navigate; // dev convenience: window.voidNav('/space/abc')