Files
vacuum-wall/webui/static/hoover/router.js
T
mteehan b673e87c9b refactor: introduce model layer for centralized data synchronization
Add hoover model.js as a central reactive store per subsystem, replacing
per-component data fetching with a single source of truth.

- Add hoover/model.js with modelRegister, modelFetch, and WS invalidation
- Refactor websocket.js to route messages to model refresh (drop per-component
  subscribe/unsubscribe)
- Simplify component.js by removing WS subscription management
- Add refresh option to apiSubmit, deprecate refactorLoad and checkAbort
- Rewrite all pages to use getModel() instead of inline data fetching
- Bootstrap model registrations in app.js
- Add GET /api/firewall/state endpoint
- Fix restart-services.sh restart order and add service health verification
- Update hoover.md docs with model layer architecture
2026-06-22 22:54:29 +00:00

62 lines
1.9 KiB
JavaScript

/**
* Hoover — router.js
*
* Hash-based SPA router with reactive state (triggers re-render on
* navigation). Link component for client-side navigation.
*/
import { reactive } from './reactivity.js?v=7';
import { h } from './vdom.js?v=7';
/**
* Hash-based router.
*
* const router = createRouter({
* '/dashboard': () => h('#comp', { component: DashboardPage, key: '/dashboard' }, []),
* '/interfaces': () => h('#comp', { component: InterfacesPage, key: '/interfaces' }, []),
* '*': () => h('#comp', { component: NotFoundPage, key: '*' }, []),
* });
*
* Reactive `router.state.path` updates trigger re-renders automatically.
*/
export function createRouter(routes) {
const initialPath = location.hash.slice(1) || '/dashboard';
if (!location.hash) location.hash = initialPath;
const state = reactive({ path: initialPath });
window.addEventListener('hashchange', () => {
state.path = location.hash.slice(1) || '/dashboard';
});
const component = () => {
const handler = routes[state.path] || routes['*'];
if (!handler) {
return h('div', { class: 'card' },
h('div', { class: 'text-muted' }, `404 — Not found: ${state.path}`));
}
try {
return handler();
} catch (e) {
return h('div', { class: 'card' },
h('div', { class: 'text-muted' }, `Error: ${e.message || String(e)}`));
}
};
return { state, navigate: (p) => { location.hash = p; }, component };
}
/**
* Client-side navigation link component.
* Sets `location.hash` without full page navigation.
*/
export function Link(props) {
const { path, class: cls, children, ...rest } = props || {};
return h('a', {
href: '#' + path,
class: cls || '',
'on:click': (e) => { e.preventDefault(); location.hash = path; },
...rest,
}, children || []);
}