← Documentation Index

OpenCable — Startup & Navigation Guide

Complete execution flow from server start through authenticated navigation. Current as of June 2026.

Contents

  1. Server Startup
  2. Browser Page Load
  3. Login & Authentication
  4. Session Initialization
  5. Sidebar & Navigation
  6. Context Switching
  7. Permissions
  8. Screen Navigation
  9. Logout
  10. Multi-Tenant Support
  11. Route Registry

1. Server Startup

1.1 Entry Point

The application server is a Node.js / Express process started from server.js.

node server.js
Load .env (dotenv)
src/config/db.js — read tenants.json, build mysql2 connection pools
Register all API routes (/api/...)
express.static(__dirname) — serve frontend files
app.listen(3000) — ready for connections

1.2 Tenant Configuration

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.

// .env (example) DB_HOST=localhost DB_USER=smeco DB_PASSWORD=secret DB_NAME=smeco PORT=3000

1.3 Connection Pool

One mysql2 pool is created per tenant at startup. Pools are configured with:

SettingValuePurpose
connectionLimit10 (default)Max simultaneous connections
enableKeepAlivetruePrevents idle connection drops
keepAliveInitialDelay10 000 msFirst keepalive ping after 10 s
idleTimeout300 000 msRetire idle connections after 5 min (clean COM_QUIT)

1.4 Graceful Shutdown

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.

2. Browser Page Load

2.1 index.html

The browser fetches index.html from the Express static middleware. The page contains two top-level sections:

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:

window.addEventListener('load', App.initTenant);

2.2 initTenant() — Tenant Branding

initTenant() runs first and performs three tasks before delegating to initApp():

  1. Tenant detection — reads ?tenant=xxx from the URL query string and persists it to sessionStorage as tenantId. If absent, defaults to "smeco".
  2. Branding fetch — calls GET /api/public/config/:tenantId (no auth required). Applies title, logo, splash background color, and primary button color from the response.
  3. Login form listener — attaches the submit event to #login-form, calling performLogin().
[Screenshot: Login screen with SMECO branding — title "System Login", username and password fields, Sign In button]

3. Login & Authentication

3.1 initApp() — Session Check

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.

3.2 performLogin()

When the user submits the login form:

  1. The Sign In button is disabled to prevent double-submission.
  2. Any previous error message is cleared.
  3. A POST request is sent to the server.

Route: POST /api/auth/login

Body: { username, password }

Headers: X-Tenant-ID is automatically included by safeFetch.

⛔ Server-Side Validation Gates (auth.js):
  1. Input check — username and password must be present → 400 if missing
  2. User lookup — username must exist in users table → 401 if not found
  3. Password hash — bcrypt compare must pass → 401 if wrong
  4. Account enabledusers.enabled = 1 must be true → 403 if disabled

3.3 Successful Login Response

On 200 OK, the server returns a user object:

{ "user": { "id": 1, "username": "Admin", "full_name": "System Administrator", "dept_id": 1, "role_id": 1, "work_id": null, "workorder_name": null } }

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).

3.4 Failed Login

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.

4. Session Initialization

4.1 Layout Switch

Once a valid currentUser is confirmed in sessionStorage, initApp():

  1. Hides #login-overlay
  2. Shows #main-layout (removes the hidden class)
  3. Calls setupSidebarFooter(), setupPasswordModal(), and attachStaticListeners()

4.2 populateSidebarDropdowns()

Two API calls are made in parallel:

CallRouteReturns
ContextGET /api/auth/context/:userIdUser's available departments and roles
PackagesGET /api/packages/activeList of active work packages

The sidebar dropdowns are then populated:

If 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.

4.3 Permissions Fetch

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_permissionspermissions 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.

// sessionStorage after successful init tenantId → "smeco" currentUser → "{id, username, full_name, dept_id, role_id, work_id, ...}" userPermissions → '["buildings.view","buildings.manage","users.view",...]' screenNames → '{"building-maintenance":"Buildings","raceway-maintenance":"Raceways",...}'

5.1 loadNavigation(deptId, roleId)

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.

ℹ️ Current behaviour: The nav query filters by 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.

5.2 Accordion Sidebar Structure

Screens are grouped by their module. Each module becomes a collapsible accordion section. Screens with no module assignment render flat (no accordion wrapper).

// Sidebar HTML structure generated by loadNavigation() <ul id="nav-list"> <li class="nav-group-header" data-group="nav-group-electrical"> <span class="nav-group-label">Electrical</span> <span class="nav-group-arrow">▶</span> </li> <ul class="nav-group-items" id="nav-group-electrical" style="display:none"> <li class="nav-item" data-route="building-maintenance">Buildings</li> <li class="nav-item" data-route="raceway-maintenance">Raceways</li> ... </ul> </ul>

5.3 First Screen Auto-Navigate

After the nav list is built, the first screen in the returned array is automatically navigated to with a 100 ms delay:

setTimeout(() => navigate(screens[0].route), 100)

If no screens are returned (no assignments for this department/role), a "No screens found" welcome message is shown in the workspace instead.

[Screenshot: Full application with sidebar showing accordion nav groups (collapsed), user name in footer, Department/Role/Package dropdowns at top of sidebar]

5.4 Sidebar Layout

The sidebar contains three distinct zones:

ZoneElementContents
Header.sidebar-header"Maintenance" title
Context.user-contextDepartment, Role, and Active Package dropdowns
Nav Menu#nav-listAccordion module groups + screen links
Footer#nav-footerLogged-in user's full name + Sign Out link

6. Context Switching

6.1 handleNavContextChange()

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:

  1. Updates dept_id, role_id, work_id, and workorder_name on the currentUser session object
  2. Calls fetchAndStorePermissions() to refresh the permission set for the new role
  3. Calls loadNavigation(deptId, roleId) to rebuild the sidebar nav for the new department
  4. Navigates to the first screen of the new context
✓ No full page reload. Context switching is fully in-page. The sidebar rebuilds, permissions refresh, and the workspace loads the first screen of the new context — all without losing browser state.

6.2 Active Package (Work Order)

The 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.

ℹ️ Direct-write screens (buses, generators, loads, raceways, cable register, buildings) do NOT require a work order — they write directly to the database without staging.

7. Permissions

7.1 Architecture

Permissions are enforced at two levels:

LevelMechanismWhere
ServercheckPermission("perm.name") middlewareEvery API route handler
ClienthasPermission("perm.name") functionScreen JS files — show/hide buttons

7.2 Permission Names in Use

Common permissions that gate both UI elements and API access:

buildings.viewMost electrical read endpoints
buildings.manageAll electrical write endpoints
users.viewAuth context, nav, change-password
users.manageUser CRUD
logs.viewTransaction log viewer
packages.manageWork order management

7.3 safeFetch Header Injection

Every API call made by the frontend goes through safeFetch(), which automatically injects three headers:

X-User-ID → user.id (from sessionStorage currentUser) X-Tenant-ID → tenantId (from sessionStorage, default "smeco") X-Workorder-Name → user.workorder_name (if set)

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.

8.1 navigate(route)

Clicking a sidebar nav item calls navigate(routeName). The function:

  1. Sets #workspace-content to a "Loading…" placeholder
  2. Looks up the route in the Route Registry (a static map in app.js)
  3. Dynamically imports the screen's JS module: import(path)
  4. Derives the render function name from the route string — e.g. "building-maintenance"renderBuildingMaintenance
  5. Calls module[funcName](workspace), passing the workspace DOM element
// Route → function name derivation "building-maintenance"renderBuildingMaintenance "cable-register-maintenance"renderCableRegisterMaintenance "link-routing-maintenance"renderLinkRoutingMaintenance
⚠️ Route Registry requirement: Every screen JS file must be registered in the routeRegistry object inside navigate() in src/modules/app/app.js. Unregistered routes render a 404 panel in the workspace.

8.2 Registered Screen Modules

As of May 2026 the following screens are registered, grouped by module:

Admin

admin-reportsAdmin reporting
module-maintenanceModule CRUD
department-screen-maintenanceDept ↔ Screen assignments
my-listsUser list management
package-maintenanceWork package CRUD
package-typesPackage type codes
permission-maintenancePermission CRUD
record-codes-maintenanceRecord code registry
role-permission-maintenanceRole ↔ Permission matrix
screen-maintenanceScreen CRUD + module filter
system-monitorDB pool + server stats
transaction-logsAudit log viewer
user-maintenanceUser CRUD
verification-maintenanceVerification records
construction-queueConstruction queue
dashboard-engineeringEngineering dashboard
report-maintenanceReport definitions

Electrical

building-maintenanceBuildings + Rooms
room-maintenanceRoom CRUD
raceway-maintenanceRaceway CRUD
raceway-linksRaceway ↔ connection topology
raceway-cablesCables per raceway view
raceway-analysisRaceway fill analysis
raceway-utilityData quality checks
raceway-problemsRaceway problem viewer
link-pathwaysPathway analysis (DFS)
link-routing-maintenanceCable routing (Links)
materials-maintenanceCable / Tray / Conduit types
cable-register-maintenanceCable register CRUD
cable-utilityShared-room cable utility
cable-analysisCable problem analysis
cable-problemsCable problem viewer
cable-selectorCable type selector chart
equipment-maintenanceEquipment CRUD
bus-maintenanceElectrical buses
generator-maintenanceGenerator records
load-maintenanceLoad records
loadflow-crdLoadflow circuit diagram
loadflow-sldLoadflow single-line diagram
loadflow-linkerLoad ↔ cable linker
scenario-maintenanceScenarios (deferred)
electrical-reportsElectrical reports

Documents

document-maintenanceDocument library
document-links-maintenanceDocument ↔ record links
document-type-maintenanceDocument type codes

Documentation

documentation-launchpadIn-app documentation

9. Logout

The Sign Out link in the sidebar footer calls logout():

  1. Reads tenantId from sessionStorage (preserves tenant across logout)
  2. Calls sessionStorage.clear() — removes currentUser, userPermissions, screenNames
  3. Re-sets tenantId so the branding is restored on the next login
  4. Calls window.location.reload() — triggers initTenant()initApp()showLogin()

10. Multi-Tenant Support

The application supports multiple tenants via a single server instance. Each tenant has an isolated database pool and its own UI branding.

10.1 Selecting a Tenant

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.

http://localhost:3000 → tenant: smeco (default) http://localhost:3000?tenant=training → tenant: training

10.2 Tenant Configuration

Each tenant entry in src/config/tenants.json defines:

KeyDescription
db.host / user / password / databaseMySQL/MariaDB connection credentials
ui.titlePage title shown on login screen
ui.logoUrlURL of logo image (leave blank for none)
ui.splashBackgroundLogin page background color
ui.buttonColorCSS primary color override
licensed_modulesArray of module codes licensed for this tenant
ℹ️ Training tenant: A separate training tenant can be configured in tenants.json pointing to an isolated database. Access via ?tenant=training in the URL.

11. Adding a New Screen

To add a new screen to the application, four steps are required:

Step 1 — Create the screen JS module

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:

export async function renderMyScreen(workspace) { workspace.innerHTML = `<h2>My Screen</h2>` }

Step 2 — Register the route in app.js

Add an entry to the routeRegistry object inside navigate() in src/modules/app/app.js:

"my-screen": "/src/modules/electrical/screens/my-screen.js",

Step 3 — Insert the screen record in the database

INSERT INTO `screens` (`route`, `name`, `description`, `module_id`, `enabled`, `sortseq`) VALUES ( 'my-screen', 'My Screen', 'Description here', (SELECT id FROM `modules` WHERE code = 'electrical'), 1, 100 ) ON DUPLICATE KEY UPDATE name = VALUES(name);

Step 4 — Assign the screen to a department

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.

⚠️ screens table schema: The table uses route (not path), name (not label), and sortseq (not sort_order). There is no icon column and no permission column.