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 is architected around a strict separation of concerns that mirrors a traditional backend service pattern — but runs entirely in the browser. Data flows in one direction: a page calls a service, the service delegates to a repository, the repository calls ApiClient, and ApiClient speaks to the REST backend. Going the other way, server-pushed Socket.IO events are normalised by EventBus into an internal publish/subscribe channel that any page can subscribe to without coupling itself to the socket transport. The result is a codebase where every file has a single, clear responsibility and can be understood in isolation.

Project Structure

The full directory tree matches the separation of concerns described below:
SEAM/
├── index.html
├── server.js              # Node.js HTTP server (SPA host)
├── build-tailwind.mjs     # Tailwind CSS compilation script
├── tailwind.config.js     # Tailwind v4 config (darkMode, primary colour)
├── package.json
├── nodemon.json
├── styles.css
├── css/
│   ├── tailwind-base.css  # Tailwind entry point
│   ├── tailwind.css       # Compiled output (generated)
│   ├── components/        # Component-scoped CSS
│   ├── layout/            # Layout-scoped CSS
│   └── pages/             # Page-scoped CSS
└── js/
    ├── main.js            # App entry point — hydrate session, start router
    ├── config/            # API_BASE and other constants
    ├── core/              # Router and EventBus
    ├── layout/            # PrivateLayout and PublicLayout
    ├── pages/             # One directory per module (auth, users, roles, …)
    ├── repositories/      # Raw HTTP calls — one repository per resource
    ├── services/          # Business logic — orchestrates repositories
    ├── shared/            # Reusable UI components (Sidebar, Header, Toast…)
    └── state/             # Singleton session state

Architectural Layers

SEAM’s runtime follows a five-layer stack. Each layer depends only on the layer below it:
Pages  →  Services  →  Repositories  →  ApiClient  →  Backend REST API

Pages (js/pages/)

Each module directory contains at minimum a *.page.js file that exports an async function returning an HTML string. The router calls that function, assigns the result to app.innerHTML, and the browser renders it. Pages import services for data and call registerPageCleanup() to unsubscribe from EventBus before the next navigation.

Services (js/services/)

Service modules contain business logic: validation, data transformation, and orchestration across multiple repositories. Pages never call repositories directly — all data access is mediated through a service, keeping pages thin and testable in isolation.

Repositories (js/repositories/)

Repository modules are responsible for one thing: making HTTP requests and returning raw response data. They import API_BASE from js/config/api.js and session.token for the Authorization header. No transformation or business logic lives here.

ApiClient / config (js/config/)

js/config/api.js exports the single API_BASE constant that all repositories reference. Centralising the base URL here means switching environments (local → staging → production) requires editing exactly one line.

State (js/state/)

session.state.js exports a plain mutable object (session) that holds user, token, permissions (a Set), and sidebarItems. It is hydrated from localStorage in main.js at startup and is the single source of truth for authentication status across all layers.

Entry Point

js/main.js is the application bootstrap. It runs once when index.html loads the <script type="module"> tag:
js/main.js
import { loadRoute } from './core/Router.js';
import { session }   from './state/session.state.js';
import { EventBus }  from './core/EventBus.js';
import './shared/components/Toast.js'; // Initialise global real-time notifications

// Hydrate state from localStorage
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 if token exists (page reload case)
        if (session.token) EventBus.connect(session.token);
    } catch {
        localStorage.removeItem('currentUser');
    }
}

window.addEventListener('hashchange', loadRoute);
loadRoute();
Three responsibilities happen here in sequence: session hydration from localStorage, Socket.IO reconnection if a token is already present, and router initialisation by binding hashchange and calling loadRoute() immediately for the current URL.

SPA Routing

The router in js/core/Router.js maps URL hash fragments to page module functions. The route table is defined as a plain object literal:
js/core/Router.js
const routes = {
    "/":        LoginPage,
    "/home":    HomePage,
    "/usuarios": UsersPage,
    "/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,
};
Navigation works as follows:
  1. The browser fires a hashchange event when the user clicks a link or calls location.hash = '#/route'.
  2. loadRoute() runs any cleanup functions registered by the previous page via registerPageCleanup().
  3. The hash fragment is sliced (location.hash.slice(1)) and the base segment (e.g. /proyectos) is looked up in routes.
  4. Unauthenticated access to any route other than /login or / redirects to #/login.
  5. An already-authenticated user hitting / or /login is redirected to #/home.
  6. The matching page function is called (awaited if async), and its HTML string result is written to document.getElementById('app').innerHTML.
LoginPage, HomePage, and UsersPage are eagerly imported at module load time. All other pages use dynamic import() expressions so their JavaScript is only fetched from the server the first time a user navigates to that route.

Dual Layout System

SEAM has two layout wrappers that wrap the page content string before it is written to #app. PublicLayout is used for unauthenticated pages (currently the login screen). It is a transparent pass-through — it returns the content string unchanged, making it trivial to add a public header or splash background in one place later:
js/layout/PublicLayout.js
export function PublicLayout(content) {
    return `
        ${content}
        `;
}
PrivateLayout wraps every authenticated page in a full application shell consisting of a sticky Header, a fixed-left Sidebar (visible on md and above), a scrollable main content area, and a sticky Footer:
js/layout/PrivateLayout.js
export async function PrivateLayout(content) {
    return `
        <div class="flex min-h-screen flex-col bg-gray-950 text-gray-100">
            <header class="sticky top-0 z-50 h-16 border-b border-gray-800 bg-gray-900/95 shadow-lg shadow-black/20 backdrop-blur-sm">
                ${await Header()}
            </header>
            <div class="flex flex-1">
                ${await Sidebar()}
                <div class="min-w-0 flex-1 md:ml-72">
                    <main class="min-w-0 px-4 py-5 sm:px-6 lg:px-8 pb-40 md:pb-6">
                        <div class="mx-auto w-full max-w-7xl">
                            ${content}
                        </div>
                    </main>
                    <footer class="border-t border-gray-800 bg-gray-900/95 shadow-lg shadow-black/20 backdrop-blur-sm md:sticky md:bottom-0">
                        ${await Footer()}
                    </footer>
                </div>
            </div>
        </div>
    `;
}
Sidebar reads session.sidebarItems — the role-scoped navigation list returned by the backend on login — and renders only the items the current user is permitted to see.

Real-Time Layer: Socket.IO + EventBus

SEAM’s real-time communication is decoupled into two layers so that page components never need to import or reference Socket.IO directly. EventBus (js/core/EventBus.js) serves a dual purpose:
  1. Internal pub/sub — any module can call EventBus.on('eventName', handler) and EventBus.emit('eventName', data) to communicate without direct imports.
  2. Socket bridgeEventBus.connect(token) opens a Socket.IO connection, authenticates it with the JWT, and re-emits incoming socket events on the internal bus.
At startup, main.js calls EventBus.connect(session.token) if a token is already present (page reload). On fresh login the authentication page calls EventBus.connect(token) after receiving the JWT from the backend. The Toast.js shared component imported in main.js subscribes to notification events on the bus, meaning any part of the application can push a toast message simply by emitting the right event — the wiring to the DOM and the Socket.IO transport is completely hidden from the caller.

Static File Server

server.js is a zero-dependency Node.js HTTP server written with the built-in http, fs, and path modules. Its key behaviours are:
  • Extension resolution — URLs without an extension are tried with .js, .html, and .json in that order before falling back to index.html.
  • SPA fallback — extensionless paths that match no file return index.html, allowing the client-side router to handle the route.
  • Security — every resolved path is checked to confirm it starts with __dirname, preventing directory traversal attacks.
  • No-cache headers — all responses include Cache-Control: no-cache so the browser always fetches the latest source files during development.
  • Port — listens on 3000, separate from the backend API on 3001.

Build docs developers (and LLMs) love