Documentation Index
Fetch the complete documentation index at: https://mintlify.com/TheSerchCp/SEAM/llms.txt
Use this file to discover all available pages before exploring further.
SEAM uses a lightweight hash-based router instead of a third-party routing library. Every navigation event is triggered by a change to location.hash (e.g. #/home, #/usuarios), keeping the SPA fully static and requiring no server-side route handling.
Route Map
All application routes live in a single routes object inside Router.js. Eagerly-loaded pages are imported at the top of the module; the rest use dynamic import() so their JavaScript bundles are fetched on demand — only when a user actually visits that route.
import { HomePage } from '../pages/home/home.page.js';
import { UsersPage } from '../pages/users/users.page.js';
import { LoginPage } from '../pages/auth/auth.page.js';
import { session } from '../state/session.state.js';
import { Error404Page } from '../pages/error/error.404.page.js';
const routes = {
"/": LoginPage,
"/home": HomePage,
"/usuarios": UsersPage,
// Lazy-loaded pages — bundle is fetched only on first visit
"/reportes": () => import('../pages/reportes/reportes.page.js').then(m => m.ReportesPage()),
"/permisos": () => import('../pages/permissions/permissions.page.js').then(m => m.PermisosPage()),
"/calificaciones": () => import('../pages/calificaciones/calificaciones.page.js').then(m => m.CalificacionesPage()),
"/tareas": () => import('../pages/tareas/tareas.page.js').then(m => m.TareasPage()),
"/proyectos": () => import('../pages/proyectos/proyectos.page.js').then(m => m.ProyectosPage()),
"/roles": () => import('../pages/roles/roles.page.js').then(m => m.RolesPage()),
"/login": LoginPage,
};
Lazy Loading
Pages that are not part of the initial load are defined as arrow functions that call import(). When loadRoute() resolves the matched entry it simply calls await page(), which triggers the dynamic import and immediately invokes the page factory:
// The entry in `routes`
"/tareas": () => import('../pages/tareas/tareas.page.js').then(m => m.TareasPage()),
// Inside loadRoute() — works for both eager and lazy entries
const pageContent = typeof page === 'function' ? await page() : await page;
app.innerHTML = pageContent;
The browser caches the module after the first fetch, so subsequent visits to the same route are instant.
How Routing Works on Hash Change
hashchange fires
main.js registers a single global listener: window.addEventListener('hashchange', loadRoute). Every anchor click or programmatic location.hash assignment triggers loadRoute.
Cleanup previous page
loadRoute drains the _cleanupFns array, calling every function registered by the page that is being left. This removes stale EventBus subscriptions, clears timers, and releases any other resources the old page held.
Resolve the path
location.hash.slice(1) strips the leading #, giving a plain path like /usuarios/5. The router then splits on / and matches on the base segment (/usuarios) so nested paths like /usuarios/5 still resolve correctly.
Auth guard
For every route except /login and /, the router checks session.user. If it is null it falls back to localStorage. If neither source has a session, the user is redirected to #/login. Conversely, an already-authenticated user visiting /login is bounced to #/home.
Render
The resolved page function is awaited. The returned HTML string replaces app.innerHTML, painting the new page.
loadRoute()
loadRoute is an async function exported from Router.js and called both on startup and on every hashchange event.
export async function loadRoute() {
// 1. Run and empty all cleanup functions from the previous page
while (_cleanupFns.length) {
try { _cleanupFns.pop()(); } catch { /* don't block navigation */ }
}
// 2. Resolve base path from the hash fragment
const path = location.hash.slice(1) || "/";
const pathSegments = path.split('/');
const basePath = '/' + pathSegments[1];
const page = routes[basePath] || routes[path];
const app = document.getElementById('app');
// 3. 404 fallback
if (!page) {
app.innerHTML = await Error404Page();
return;
}
// 4. Auth guard — try localStorage when session is cold
if (path !== '/login' && path !== '/' && !session.user) {
const stored = localStorage.getItem('currentUser');
if (!stored) { location.hash = '/login'; return; }
const currentSession = JSON.parse(stored);
session.user = currentSession.user ?? currentSession;
session.token = currentSession.token ?? null;
session.permissions = currentSession.permissions
? new Set(currentSession.permissions) : null;
session.sidebarItems = currentSession.sidebarItems ?? null;
}
// 5. Redirect authenticated users away from login
if (session.user && (path === '/login' || path === '/')) {
location.hash = '/home';
return;
}
// 6. Invoke the page factory and paint the result
const pageContent = typeof page === 'function' ? await page() : await page;
app.innerHTML = pageContent;
}
Auth Guard
The guard runs inside loadRoute before the page factory is called. It has two branches:
| Condition | Action |
|---|
Protected route + no session.user + no localStorage entry | Redirect to #/login |
Any login route + session.user is set | Redirect to #/home |
The guard also performs a lazy hydration of session from localStorage when the user lands directly on a deep link (e.g. refreshing the page at #/usuarios). This mirrors the eager hydration that happens in main.js on initial load.
registerPageCleanup(fn)
Every page that subscribes to EventBus events, sets intervals, or attaches DOM listeners must register a teardown function so those resources are released when the user navigates away.
// Lista de funciones de limpieza registradas por la página activa.
// Se ejecutan al navegar a otra ruta para evitar handlers sobre DOM desmontado.
const _cleanupFns = [];
/**
* Registra una función de limpieza que se ejecutará al abandonar la página actual.
* @param {() => void} fn
*/
export function registerPageCleanup(fn) {
_cleanupFns.push(fn);
}
Without cleanup, an EventBus subscriber added by page A continues to fire after the user navigates to page B. If that subscriber touches document.getElementById(...) it finds null (the element was replaced), silently fails, or — worse — mutates data that belongs to the new page.
Failing to call registerPageCleanup for every EventBus.on(...) subscription is a common source of ghost listeners. Each navigation stacks another subscriber on the same event until the tab is refreshed.
Usage Example — Cleaning Up a Page
// inside tareas.page.js (simplified)
import { EventBus } from '../../core/EventBus.js';
import { registerPageCleanup } from '../../core/Router.js';
export async function TareasPage() {
// Subscribe to real-time updates
const unsubDataChanged = EventBus.on('data:changed', ({ operation }) => {
if (operation === 'tareas:update') renderTareas();
});
const unsubProgress = EventBus.on('operation:progress', ({ status, message }) => {
updateProgressUI(status, message);
});
// Register teardown — called automatically when the user leaves this route
registerPageCleanup(() => {
unsubDataChanged();
unsubProgress();
});
return buildPageHTML();
}
EventBus.on() returns an unsubscribe function, so you can pass it directly to registerPageCleanup when you only have a single subscription: registerPageCleanup(EventBus.on('data:changed', handler)).