/** * SPA bootstrap + state machine. On load: check the session → render login or dashboard. The * dashboard polls the host list on an interval and refreshes after mutations. A 401 from any proxy * call bounces the operator back to the login screen (session expired). */ import { mount } from './dom.js' import * as api from './api.js' import { ApiError } from './api.js' import { renderLogin, renderDashboard, pairingModal, confirmDialog, toast, type DashboardHandlers } from './views.js' import type { HostView } from './api.js' const POLL_INTERVAL_MS = 15_000 function rootEl(): HTMLElement { const root = document.getElementById('app') if (root === null) throw new Error('#app root not found') return root } let pollTimer: number | undefined function stopPolling(): void { if (pollTimer !== undefined) { clearInterval(pollTimer) pollTimer = undefined } } /** True if an error is an auth failure that should bounce to login. */ function isUnauthorized(err: unknown): boolean { return err instanceof ApiError && err.status === 401 } async function showLogin(): Promise { stopPolling() const root = rootEl() const view = renderLogin(async (password) => { const errorNode = (view as HTMLElement & { errorNode?: HTMLElement }).errorNode try { await api.login(password) await showDashboard() } catch (err) { const msg = err instanceof ApiError && err.status === 429 ? 'Too many attempts. Wait and retry.' : err instanceof ApiError && err.status === 503 ? 'Panel not configured (no password set).' : 'Incorrect password.' if (errorNode) errorNode.textContent = msg } }) mount(root, view) } async function loadHosts(): Promise { return api.getHosts() } async function refreshDashboard(root: HTMLElement, handlers: DashboardHandlers): Promise { try { const hosts = await loadHosts() mount(root, renderDashboard(hosts, handlers)) } catch (err) { if (isUnauthorized(err)) { await showLogin() return } toast(root, 'Failed to load hosts.', 'error') } } async function showDashboard(): Promise { const root = rootEl() const handlers: DashboardHandlers = { onRefresh: () => void refreshDashboard(root, handlers), onLogout: async () => { stopPolling() await api.logout() await showLogin() }, onNewCode: async () => { try { const artifacts = await api.createPairingCode() document.body.append(pairingModal(artifacts)) } catch (err) { if (isUnauthorized(err)) return void showLogin() toast(root, 'Failed to create pairing code.', 'error') } }, onRevoke: async (host) => { const ok = await confirmDialog(`Revoke host "${host.subdomain}"? This removes its tunnel access.`) if (!ok) return try { await api.revokeHost(host.hostId) toast(root, `Revoked ${host.subdomain}.`, 'info') await refreshDashboard(root, handlers) } catch (err) { if (isUnauthorized(err)) return void showLogin() toast(root, 'Failed to revoke host.', 'error') } }, } await refreshDashboard(root, handlers) stopPolling() pollTimer = window.setInterval(() => void refreshDashboard(root, handlers), POLL_INTERVAL_MS) } async function boot(): Promise { try { const authed = await api.getSession() await (authed ? showDashboard() : showLogin()) } catch { await showLogin() } } void boot()