Skip to main content

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 stores all runtime state about the authenticated user in a single plain JavaScript object exported as session from js/state/session.state.js. There is no framework reactivity — pages read the object directly and ApiClient sources the JWT from it on every request. Because the object is a module-level singleton, any part of the application that imports it always sees the same values.

Session Object

// js/state/session.state.js
export const session = {
    user: null,
    token: null,
    permissions: null,   // Set<string> of permission nameUri strings
    sidebarItems: null,  // Array returned by /auth/login for this role
};
user
object | null
The authenticated user record returned by the server (contains at minimum id, name, email, role). null when no session exists.
token
string | null
The JWT returned by /auth/login. Attached as Authorization: Bearer {token} by ApiClient on every request. null when logged out.
permissions
Set<string> | null
A Set of permission nameUri strings for the active user (e.g. 'users:read', 'roles:delete'). Pages check membership with session.permissions.has('resource:action'). null until populated after login.
sidebarItems
Array | null
Navigation items returned by the server for the user’s role. The sidebar component renders this array directly. null until populated after login.
The session.state.js file also contains local seed data arrays (users, roles, grupos, calificaciones, tareas, reportes) that were used during early development. These are plain arrays on the same object and are not related to the live session fields above.

Hydration from localStorage

When a user refreshes the page or opens the app in a new tab, the in-memory session object starts empty. main.js immediately reads localStorage.getItem('currentUser') and populates the session before the first loadRoute() call. If a token is found, the Socket.IO connection is also re-established.
// js/main.js — runs synchronously before the first route render
const stored = localStorage.getItem('currentUser');
if (stored) {
    try {
        const s = JSON.parse(stored);

        session.user         = s.user         ?? s;
        session.token        = s.token        ?? null;
        session.permissions  = s.permissions  ? new Set(s.permissions) : null;
        session.sidebarItems = s.sidebarItems ?? null;

        // Reconnect socket on page reload if a token exists
        if (session.token) EventBus.connect(session.token);
    } catch {
        // Corrupted entry — wipe it and force re-login
        localStorage.removeItem('currentUser');
    }
}

window.addEventListener('hashchange', loadRoute);
loadRoute();
permissions is stored in localStorage as a plain array (JSON does not serialize Set) and converted back to new Set(s.permissions) during hydration. When persisting after login, remember to spread the set: JSON.stringify({ ...session, permissions: [...session.permissions] }).
The Router.js auth guard performs the same hydration lazily as a fallback for cold navigations to deep links — but the canonical location is main.js, which runs once per page load before any route resolution.

Operation Listeners

SEAM’s real-time system routes data:changed events from the server to the correct pages using a static map defined in js/core/OperationListeners.js.

OPERATION_LISTENERS

// js/core/OperationListeners.js
export const OPERATION_LISTENERS = {
    users: [
        'auth:register', 'auth:login',
        'users:create', 'users:update', 'users:delete'
    ],
    permissions: [
        'permissions:create', 'permissions:update', 'permissions:delete',
        'permissions:assign', 'permissions:unassign',
        'roles:create', 'roles:update', 'roles:delete'
    ],
    roles: [
        'roles:create', 'roles:update', 'roles:delete'
    ],
    sidebar: [
        'sidebar:create', 'sidebar:update', 'sidebar:delete'
    ],
};
Each key is a page type string. The value is the list of operation strings from data:changed payloads that should cause that page to refresh.

shouldUpdatePage(pageType, operation)

export const shouldUpdatePage = (pageType, operation) => {
    return OPERATION_LISTENERS[pageType]?.includes(operation);
};
Returns true if operation is in the interest list for pageType, false otherwise.
pageType
string
required
One of the keys in OPERATION_LISTENERS: 'users', 'permissions', 'roles', or 'sidebar'.
operation
string
required
The operation field from the data:changed payload (e.g. 'roles:delete').

How Pages Use shouldUpdatePage

Pages subscribe to data:changed in their factory function and call shouldUpdatePage to gate their re-render logic. This prevents every page from re-rendering every time any data changes anywhere in the system.
// Simplified pattern used in pages like users.page.js
import { EventBus }            from '../../core/EventBus.js';
import { registerPageCleanup } from '../../core/Router.js';
import { shouldUpdatePage }    from '../../core/OperationListeners.js';

export async function UsersPage() {
    async function renderUsers() {
        const users = await UsersRepository.getAll();
        document.getElementById('users-list').innerHTML = buildUsersHTML(users);
    }

    // Only re-render when an operation that affects users data fires
    const unsub = EventBus.on('data:changed', ({ operation }) => {
        if (shouldUpdatePage('users', operation)) {
            renderUsers();
        }
    });

    registerPageCleanup(unsub);

    await renderUsers();
    return pageShell(); // returns the static HTML shell for app.innerHTML
}

Targeted re-renders

shouldUpdatePage means the Users page ignores 'roles:delete' events and the Roles page ignores 'users:create' events — even though both arrive on the same data:changed channel.

No polling required

Because the server broadcasts data:changed to all connected clients, pages stay consistent across tabs and users without any polling interval.

Session Lifecycle Summary

1

App load

main.js reads localStorage, populates session, and reconnects the socket with the stored JWT.
2

Login

The auth page calls POST /auth/login, receives { user, token, permissions, sidebarItems }, writes the full object to localStorage, assigns every field to session, and calls EventBus.connect(token).
3

Authenticated requests

ApiClient.request() reads session.token on each call. No manual header management is needed in repositories or pages.
4

Permission checks

Pages call session.permissions.has('resource:action') to show or hide UI elements based on what the current user is allowed to do.
5

Logout

The logout handler calls EventBus.disconnect(), sets all session fields back to null, removes currentUser from localStorage, and redirects to #/login.

Build docs developers (and LLMs) love