Complete execution flow from server start through authenticated navigation. Current as of June 2026.
The application server is a Node.js / Express process started from server.js.
src/config/tenants.json defines one entry per tenant. Each entry holds database credentials and UI branding (title, logo URL, splash background color). The smeco tenant's DB credentials are overridden by environment variables from .env so secrets never live in committed JSON.
One mysql2 pool is created per tenant at startup. Pools are configured with:
| Setting | Value | Purpose |
|---|---|---|
| connectionLimit | 10 (default) | Max simultaneous connections |
| enableKeepAlive | true | Prevents idle connection drops |
| keepAliveInitialDelay | 10 000 ms | First keepalive ping after 10 s |
| idleTimeout | 300 000 ms | Retire idle connections after 5 min (clean COM_QUIT) |
The server registers SIGTERM and SIGINT handlers. On shutdown signal it stops accepting new connections, drains in-flight requests, then calls pool.end() on every tenant pool — sending a clean COM_QUIT to MariaDB instead of dropping connections abruptly.
The browser fetches index.html from the Express static middleware. The page contains two top-level sections:
display:flex)class="hidden")Five CSS files are loaded in <head>: main.css, logins.css, nav.css, reports.css, screens.css.
A single <script type="module"> at the bottom imports src/modules/app/app.js and wires up the load event:
initTenant() runs first and performs three tasks before delegating to initApp():
?tenant=xxx from the URL query string and persists it to sessionStorage as tenantId. If absent, defaults to "smeco".submit event to #login-form, calling performLogin().Before showing the login form, initApp() checks sessionStorage for an existing currentUser object. If a valid user with an id is found, the login screen is skipped entirely and the app proceeds directly to Session Initialization (§4).
If no valid session exists, showLogin() is called — which hides #main-layout and shows #login-overlay.
When the user submits the login form:
Route: POST /api/auth/login
Body: { username, password }
Headers: X-Tenant-ID is automatically included by safeFetch.
400 if missingusers table → 401 if not found401 if wrongusers.enabled = 1 must be true → 403 if disabledOn 200 OK, the server returns a user object:
The client stores this object as a JSON string in sessionStorage under the key currentUser, then immediately calls initApp() again (no page reload — the console log is preserved).
On any error response, the error message from res.data.message is displayed inline in #login-error (red text below the password field). No alert box is used.
Once a valid currentUser is confirmed in sessionStorage, initApp():
#login-overlay#main-layout (removes the hidden class)setupSidebarFooter(), setupPasswordModal(), and attachStaticListeners()Two API calls are made in parallel:
| Call | Route | Returns |
|---|---|---|
| Context | GET /api/auth/context/:userId | User's available departments and roles |
| Packages | GET /api/packages/active | List of active work packages |
The sidebar dropdowns are then populated:
#nav-dept-select) — one option per department the user belongs to#nav-role-select) — one option per role assigned to the user#nav-work-select) — active packages, plus an inactive entry if the session already has an inactive package selectedIf the user's dept_id or role_id is null, the first available option is auto-selected. The selected values are written back to the currentUser object in sessionStorage.
fetchAndStorePermissions() is called immediately after context is confirmed.
Route: GET /api/auth/permissions
The server resolves all permissions for the current user's role via the role_permissions → permissions tables and returns a flat array of permission name strings. This array is stored in sessionStorage as userPermissions.
The client-side hasPermission(permName) function reads this array synchronously — no API call on every permission check.
Route: GET /api/auth/nav?department_id=X&role_id=Y
The server queries the screens table filtered by department_screens assignments for the given department. Each screen row includes route, name, module_code, module_name, and module_sort.
department_id only. The role_id parameter is accepted but not currently used for filtering — all screens assigned to the department are returned regardless of role.
Screens are grouped by their module. Each module becomes a collapsible accordion section. Screens with no module assignment render flat (no accordion wrapper).
navigate(route).screenNames map is saved to sessionStorage for use by navigate() to update the active screen title.After the nav list is built, the first screen in the returned array is automatically navigated to with a 100 ms delay:
If no screens are returned (no assignments for this department/role), a "No screens found" welcome message is shown in the workspace instead.
The sidebar contains three distinct zones:
| Zone | Element | Contents |
|---|---|---|
| Header | .sidebar-header | "Maintenance" title |
| Context | .user-context | Department, Role, and Active Package dropdowns |
| Nav Menu | #nav-list | Accordion module groups + screen links |
| Footer | #nav-footer | Logged-in user's full name + Sign Out link |
Changing any of the three sidebar dropdowns (Department, Role, Active Package) triggers handleNavContextChange(). Listeners are attached once via a guard flag (data-listener-attached) to prevent duplicate bindings across re-initializations.
Route: POST /api/auth/set-context
Body: { userId, dept_id, role_id, work_id }
On success, the server persists the new context to the users table. The client:
dept_id, role_id, work_id, and workorder_name on the currentUser session objectfetchAndStorePermissions() to refresh the permission set for the new roleloadNavigation(deptId, roleId) to rebuild the sidebar nav for the new departmentThe Active Package dropdown lists packages from GET /api/packages/active. Selecting a package sets work_id and workorder_name on the session. Routes that require a work order call requireWorkorder() — which checks user.work_id and shows a toast error if no package is selected.
Permissions are enforced at two levels:
| Level | Mechanism | Where |
|---|---|---|
| Server | checkPermission("perm.name") middleware | Every API route handler |
| Client | hasPermission("perm.name") function | Screen JS files — show/hide buttons |
Common permissions that gate both UI elements and API access:
Every API call made by the frontend goes through safeFetch(), which automatically injects three headers:
The contextLoader middleware on the server reads X-User-ID and X-Tenant-ID on every request to populate req.user and req.dbPool before route handlers execute.
Clicking a sidebar nav item calls navigate(routeName). The function:
#workspace-content to a "Loading…" placeholderapp.js)import(path)"building-maintenance" → renderBuildingMaintenancemodule[funcName](workspace), passing the workspace DOM elementrouteRegistry object inside navigate() in src/modules/app/app.js. Unregistered routes render a 404 panel in the workspace.
As of May 2026 the following screens are registered, grouped by module:
The Sign Out link in the sidebar footer calls logout():
tenantId from sessionStorage (preserves tenant across logout)sessionStorage.clear() — removes currentUser, userPermissions, screenNamestenantId so the branding is restored on the next loginwindow.location.reload() — triggers initTenant() → initApp() → showLogin()The application supports multiple tenants via a single server instance. Each tenant has an isolated database pool and its own UI branding.
Append ?tenant=xxx to any URL. The tenant key is stored in sessionStorage and sent as the X-Tenant-ID header on every API request.
Each tenant entry in src/config/tenants.json defines:
| Key | Description |
|---|---|
| db.host / user / password / database | MySQL/MariaDB connection credentials |
| ui.title | Page title shown on login screen |
| ui.logoUrl | URL of logo image (leave blank for none) |
| ui.splashBackground | Login page background color |
| ui.buttonColor | CSS primary color override |
| licensed_modules | Array of module codes licensed for this tenant |
tenants.json pointing to an isolated database. Access via ?tenant=training in the URL.
To add a new screen to the application, four steps are required:
Add a file to the appropriate module directory, e.g. src/modules/electrical/screens/my-screen.js. Export a function named after the route in PascalCase:
Add an entry to the routeRegistry object inside navigate() in src/modules/app/app.js:
Navigate to Admin → Department Screen Maintenance and assign the new screen to the appropriate department. The screen will then appear in the sidebar for users in that department.
route (not path), name (not label), and sortseq (not sort_order). There is no icon column and no permission column.