# Frontend Implementatie — HSEQ SaaS Multi-Tenant Client Portal | Veld | Waarde | |-------------|-----------------------------------------------| | **Project** | 2026-hseq-saas-multitenant | | **Type** | Frontend Implementatie Documentatie | | **Auteur** | Frontend Developer Agent | | **Versie** | 1.0 | | **Datum** | 2026-05-26 | | **Status** | Sandbox — niet naar production | --- ## 1. Samenvatting Dit document beschrijft de frontend-implementatie van de client portal voor het HSEQ SaaS multi-tenant platform. De implementatie omvat zes componenten die samen dynamische routing, module-toggle UI en tenant-aware weergave realiseren. | Component | Bestand | Regels | Verantwoordelijkheid | |-----------|---------|--------|---------------------| | Tenant Context Manager | `tenant_context_v1.0.js` | ~280 | Sessiebeheer, API-calls, branding, events | | Dynamic Router | `tenant_routing_v1.0.js` | ~260 | Route-registry, guards, history API | | Sidebar Builder | `sidebar_builder_v1.0.js` | ~250 | Dynamische sidebar-generatie per tenant | | Module Toggle UI | `module_toggle_v1.0.js` | ~340 | Toggle-switches, plan-gating, upgrade prompts | | Tenant Dashboard | `tenant_dashboard_v1.0.html` | ~310 | SSR Jinja2 template met KPI's en module-overzicht | | Module Tile | `module_tile_v1.0.html` | ~230 | Herbruikbaar Jinja2-macro voor module-tiles | | **Totaal** | | **~1.670** | | **Technologie stack:** Vanilla JS (geen SPA-framework), Jinja2 SSR templates, CSS custom properties, Inter font. --- ## 2. Architectuur Overzicht ### 2.1 Componentinteracties ``` ┌──────────────────────────────────────────────────────────────────┐ │ Browser (Client) │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ tenant_context_v1.0.js (TenantContext) │ │ │ │ • Login/logout/session management │ │ │ │ • API communicatie (/api/v2/*) │ │ │ │ • Module-state cache (localStorage) │ │ │ │ • Event bus: auth:*, context:*, modules:* │ │ │ └──────────┬──────────────────────┬───────────────────────┘ │ │ │ │ │ │ ┌──────────▼──────────┐ ┌────────▼──────────────────────┐ │ │ │ tenant_routing │ │ sidebar_builder │ │ │ │ _v1.0.js │ │ _v1.0.js │ │ │ │ (TenantRouter) │ │ (SidebarBuilder) │ │ │ │ │ │ │ │ │ │ • Route registry │ │ • Sectie-groepering │ │ │ │ • Module → route │ │ • Active-state highlighting │ │ │ │ • Access guards │ │ • Collapsible secties │ │ │ │ • History API │ │ • Dark mode toggle │ │ │ └─────────────────────┘ └────────────────────────────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ module_toggle_v1.0.js (ModuleToggle) │ │ │ │ • Module-overzicht met toggle-switches │ │ │ │ • Plan-gating (locked modules) │ │ │ │ • API-aanroepen bij toggle │ │ │ │ • Upgrade prompts │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ │ fetch() / same-origin │ ┌─────────────────────────────▼────────────────────────────────────┐ │ Flask Backend (SSR + API) │ │ Jinja2 templates injecteren: user, tenant, modules, navigation │ │ API endpoints: /api/v2/auth/*, /api/v2/tenant/*, /api/v2/modules/* │ └──────────────────────────────────────────────────────────────────┘ ``` ### 2.2 Data Flow ``` 1. Browser request → Flask SSR 2. Jinja2 injecteert window.__HSEQ_CTX__ met user + tenant + modules 3. TenantContext hydrate vanuit server-injectie 4. TenantRouter.buildRoutes(modules) registreert actieve routes 5. SidebarBuilder.build() genereert sidebar op basis van routes 6. ModuleToggle.build() toont toggle-overzicht voor admin-gebruikers 7. Bij toggle: ModuleToggle → API call → TenantContext.update → Sidebar rebuild ``` --- ## 3. Component Details ### 3.1 Tenant Context Manager (`tenant_context_v1.0.js`) **Verantwoordelijkheid:** Centraal singleton-object voor tenant-sessiebeheer. | Functie | Beschrijving | API Endpoint | |---------|-------------|--------------| | `init()` | Herstel context uit localStorage of haal van server | — | | `login(email, pwd)` | Authenticatie + context laden | `POST /api/v2/auth/login` | | `logout()` | Sessie wissen | `POST /api/v2/auth/logout` | | `refresh()` | Context verversen van server | `GET /api/v2/tenant/` | | `isModuleEnabled(code)` | Runtime module-check | — | | `hasRole(roles)` | Rol-validatie | — | | `hasPermission(perm)` | Permissie-check | — | | `enableModule(key)` | Module inschakelen via API | `POST /api/v2/modules/{key}/enable` | | `disableModule(key)` | Module uitschakelen via API | `POST /api/v2/modules/{key}/disable` | | `applyBranding()` | Tenant-kleuren/logo op DOM toepassen | — | | `on(fn)` | Event subscriber | — | **Event bus events:** | Event | Payload | Trigger | |-------|---------|---------| | `auth:login` | State snapshot | Succesvolle login | | `auth:logout` | null | Logout | | `auth:expired` | null | 401 response | | `auth:required` | null | Geen geldige sessie | | `context:restored` | State snapshot | Herstel uit localStorage | | `context:refreshed` | State snapshot | Succesvolle API-refresh | | `modules:changed` | `{ key, enabled }` | Module toggle | **Persistency:** Context wordt opgeslagen in `localStorage` met 30-minuten TTL. Bij overschrijding wordt automatisch ververst via API. ### 3.2 Dynamic Router (`tenant_routing_v1.0.js`) **Verantwoordelijkheid:** Route-registry opbouwen op basis van actieve modules, met access-guards en history API integratie. **Module → route mapping:** [1] | Module | Routes | Sectie | |--------|--------|--------| | `dashboard` | `/` | overview | | `compliance` | `/compliance` | vbs | | `incidents` | `/incidents-env` | vbs | | `training` | `/training-audits` | vbs | | `rie_hazop` | `/rie` | vbs | | `moc_ptw` | `/moc-ptw` | vbs | | `risk_management` | `/risk` | vbs | | `vbs_elements` | 7 VBS-subroutes | vbs_elements | | `intelligence` | `/intelligence` | overview | | `scraper` | `/scraper` | overview | | `knowledge_ai` | `/knowledge` | kennisbank | | `brzo_seveso` | `/brzo-seveso` | brzo | | `environment` | `/milieu` | brzo | | `lms` | `/lms/` | brzo | | `kam` | `/kam` | brzo | | `agents` | `/agents` | tools | | `quickscan` | `/quickscan` | tools | | `reports` | `/reports/weekly` | tools | | `admin` | `/admin/companies`, `/admin/users` | admin | **Access check flow:** ``` navigate(path) → beforeHooks.forEach() // kan false retourneren om te blokkeren → checkAccess(path, modules) ├── Route gevonden? → nee → ROUTE_NOT_FOUND ├── Module actief? → nee → MODULE_DISABLED → redirect / └── Toegang OK → ja → pushState + afterHooks ``` **Route guard integratie:** ```javascript // Voorkom navigatie naar uitgeschakelde modules TenantRouter.beforeNavigate(function (newPath, oldPath, modules) { var route = TenantRouter.resolve(newPath); if (route && route.module && !modules[route.module]?.enabled) { console.warn('Module disabled:', route.module); return false; // blokkeer navigatie } }); ``` ### 3.3 Sidebar Builder (`sidebar_builder_v1.0.js`) **Verantwoordelijkheid:** Dynamische sidebar-generatie op basis van actieve routes, met sectie-groepering en dark mode. **Sectie-indeling:** [2] | Sectie | Label | Collapsible | Modules | |--------|-------|-------------|---------| | overview | Overview | Nee | dashboard, intelligence, scraper | | vbs | Veiligheidsbeheersing | Ja | compliance, incidents, training, rie_hazop, moc_ptw, risk_management | | vbs_elements | VBS 7 Elementen | Ja | vbs_elements (7 sub-items) | | tools | Tools & Scans | Ja | quickscan, reports, agents | | kennisbank | Kennisbank | Nee | knowledge_ai | | brzo | BRZO Diensten | Ja | brzo_seveso, environment, lms, kam | | admin | Beheer | Ja | admin (alleen voor owner/admin rol) | **Features:** - **Active-state highlighting**: huidige pagina krijgt visuele marker - **Collapsible secties**: klik op sectie-header klapt items in/uit - **Dark mode toggle**: localStorage-persistentie, respecteert bestaande `body.dark-mode` klasse [2] - **Tenant branding**: logo en bedrijfsnaam uit tenant-context - **Mobile**: sidebar verbergt op <768px, toggle via hamburger-menu ### 3.4 Module Toggle UI (`module_toggle_v1.0.js`) **Verantwoordelijkheid:** Beheer-interface voor het aan/uit zetten van modules per tenant. **Module catalogus:** [1] | Categorie | Modules | Abonnement | |-----------|---------|------------| | core (5) | dashboard, compliance, incidents, training, reports | Alle | | advanced (7) | rie_hazop, moc_ptw, risk_management, environment, vbs_elements, quickscan, kam | Professional+ | | premium (6) | intelligence, scraper, knowledge_ai, brzo_seveso, lms, agents | Enterprise | **UI-elementen:** - **Toggle switches**: visuele aan/uit-knop met loading-state en async API-call - **Plan-gating**: modules buiten het huidige plan tonen een 🔒 met upgrade-knop - **Categorie-groepering**: core/advanced/premium in aparte secties - **Samenvatting**: aantal actief / beschikbaar / totaal bovenaan - **Foutafhandeling**: bij API-fout wordt toggle-state teruggedraaid (revert) **Toggle flow:** ``` Klik toggle → UI: loading state (⏳) → POST /api/v2/modules/{key}/enable|disable → Success → UI: active state + event: modules:changed → Error → UI: revert + alert ``` ### 3.5 Tenant Dashboard Template (`tenant_dashboard_v1.0.html`) **Jinja2 template** voor de hoofd-pagina van de tenant portal. **Server-side injectie:** ```python # Flask route render @app.route('/hseq-dashboard/') @require_tenant def dashboard(): return render_template('tenant_dashboard_v1.0.html', user=current_user.to_dict(), tenant=g.tenant.to_dict(), modules=get_modules_status(g.tenant_id), navigation=get_navigation(g.tenant_id), kpi_data=get_kpi_data(g.tenant_id), api_base='/hseq-dashboard', ) ``` **KPI-sectie:** Toont 5 KPI-kaarten met status-kleuren (groen/geel/rood/blauw), conform het bestaande dashboard-ontwerp [2]. **Initialisatie-script:** 1. Server-injecteert `window.__HSEQ_CTX__` met alle context 2. TenantContext hydrate 3. TenantRouter.buildRoutes() registreert routes 4. SidebarBuilder.build() genereert sidebar 5. Event listeners voor module-changes en logout ### 3.6 Module Tile Component (`module_tile_v1.0.html`) **Jinja2 macro** voor herbruikbare module-tile rendering. **Macro's:** | Macro | Doel | |-------|------| | `module_tile(key, name, icon, desc, enabled, available, category)` | Één tile renderen | | `module_tile_styles()` | Companion CSS injecteren | | `module_tile_script(api_base)` | Toggle event-handlers | **Gebruik in templates:** ```jinja2 {% from 'module_tile_v1.0.html' import module_tile, module_tile_styles, module_tile_script %} {{ module_tile_styles() }} {% for mod in modules %} {{ module_tile(mod.key, mod.name, mod.icon, mod.description, mod.enabled, mod.available, mod.category) }} {% endfor %} {{ module_tile_script('/hseq-dashboard') }} ``` --- ## 4. Styling & UX ### 4.1 Design Principles | Principe | Implementatie | |----------|--------------| | Inter font | Google Fonts CDN, gewichten 400/500/600/700 [2] | | Dark mode | CSS custom properties + `body.dark-mode` klasse [2] | | Responsive | CSS Grid + media queries (768px, 480px breakpoints) | | Tenant branding | `--tenant-primary` CSS variable, dynamisch gezet per tenant | | Geen SPA-framework | Vanilla JS, SSR templates, conform Phase 1 findings [2] | ### 4.2 CSS Custom Properties ```css :root { --tenant-primary: /* dynamisch per tenant */; --sidebar-width: 260px; --header-height: 56px; --bg-primary: #ffffff; --bg-secondary: #f8fafc; --text-primary: #0f172a; --text-secondary: #64748b; --border-color: #e2e8f0; } body.dark-mode { --bg-primary: #0f172a; --bg-secondary: #1e293b; --text-primary: #f1f5f9; --text-secondary: #94a3b8; --border-color: #334155; } ``` ### 4.3 Responsive Breakpoints | Breakpoint | Gedrag | |------------|--------| | >768px | Sidebar fixed, main-content met margin-left | | ≤768px | Sidebar verborgen, hamburger-menu, 2-kolom KPI grid | | ≤480px | 1-kolom KPI grid, verticale module-tiles | --- ## 5. Integratie met Backend API ### 5.1 API Endpoints Gebruikt door Frontend | Endpoint | Methode | Component | Beschrijving | |----------|---------|-----------|-------------| | `/api/v2/auth/login` | POST | TenantContext | Inloggen | | `/api/v2/auth/logout` | POST | TenantContext | Uitloggen | | `/api/v2/tenant/` | GET | TenantContext | Tenant-context ophalen | | `/api/v2/modules/` | GET | ModuleToggle | Module-status ophalen | | `/api/v2/modules/{key}/enable` | POST | ModuleToggle | Module inschakelen | | `/api/v2/modules/{key}/disable` | POST | ModuleToggle | Module uitschakelen | ### 5.2 Response Formaat Login response (verwacht door TenantContext): [3] ```json { "data": { "user": { "id": "...", "email": "...", "role": "admin", "permissions": [...] }, "tenant": { "company_id": "...", "company_name": "...", "plan": "professional", "branding": { "primary_color": "#2563eb", "logo_url": "/static/tenants/1/logo.png" } }, "modules": { "compliance": { "enabled": true }, "incidents": { "enabled": true } }, "navigation": [ { "section": "overview", "items": ["dashboard", "intelligence"] } ] } } ``` ### 5.3 SSR Data Injectie Flask injecteert context via Jinja2 in `window.__HSEQ_CTX__`: ```javascript window.__HSEQ_CTX__ = { user: {{ user | tojson | safe }}, tenant: {{ tenant | tojson | safe }}, modules: {{ modules | tojson | safe }}, navigation: {{ navigation | tojson | safe }}, apiBase: "{{ api_base }}", }; ``` Dit voorkomt een extra API-call bij pagina-laden: de client-side code hydrate direct vanuit server-data. --- ## 6. Beveiliging ### 6.1 Client-Side Security maatregelen | Maatregel | Implementatie | |-----------|--------------| | Geen tenant-ID in URL | Tenant-context via session, niet via URL-segment [3] | | Module-guard | TenantRouter.checkAccess() blokkeert routes voor uitgeschakelde modules | | API credentials | `credentials: 'same-origin'` op alle fetch-calls | | Geen tokens in localStorage | Session-based auth, geen JWT in client storage | | Input escaping | Jinja2 `| tojson | safe` voor server-injectie | ### 6.2 Beperkingen Client-Side Client-side route-guards zijn een **UX-verbetering**, geen beveiligingsmaatregel. De backend handhaaft alle restricties via: - `@require_tenant` decorator [3] - `@require_module(key)` decorator [3] - PostgreSQL RLS policies [1] --- ## 7. Bestanden Overzicht | Bestand | Type | Regels | Doel | |---------|------|--------|------| | `tenant_context_v1.0.js` | JS | ~280 | Tenant context manager | | `tenant_routing_v1.0.js` | JS | ~260 | Dynamic router met module-guards | | `sidebar_builder_v1.0.js` | JS | ~250 | Dynamische sidebar-generatie | | `module_toggle_v1.0.js` | JS | ~340 | Module toggle UI component | | `tenant_dashboard_v1.0.html` | HTML | ~310 | SSR dashboard template | | `module_tile_v1.0.html` | HTML | ~230 | Herbruikbare Jinja2 macro | | `frontend_implementation_v1.0.md` | MD | — | Dit document | --- ## 8. Gebruiksaanwijzing ### 8.1 Flask Setup ```python from flask import Flask, render_template app = Flask(__name__) app.secret_key = "your-secret-key" # ... middleware en blueprints registreren (zie backend_implementation_v1.0.md) @app.route('/hseq-dashboard/') @require_tenant def dashboard(): return render_template('tenant_dashboard_v1.0.html', user=g.user.to_dict(), tenant=g.tenant.to_dict(), modules=get_modules_status(g.tenant_id), navigation=get_navigation(g.tenant_id), kpi_data=get_kpi_data(g.tenant_id), api_base='/hseq-dashboard', ) ``` ### 8.2 Module Beheer Pagina ```python @app.route('/hseq-dashboard/admin/modules') @require_tenant @require_role('owner', 'admin') def admin_modules(): return render_template('admin_modules.html', user=g.user.to_dict(), tenant=g.tenant.to_dict(), modules=get_all_modules_status(g.tenant_id), plan=g.tenant.plan, api_base='/hseq-dashboard', ) ``` ### 8.3 Client-Side Initialisatie (standalone) ```javascript // Volledig client-side (geen SSR): TenantContext.init().then(function (ready) { if (!ready) { window.location.href = '/login'; return; } TenantRouter.buildRoutes(TenantContext.getModules()); TenantRouter.initHistoryListener(); var sidebar = document.getElementById('sidebar-container'); SidebarBuilder.build(sidebar, TenantRouter.getRoutesBySection(), { activePath: window.location.pathname, onNavigate: function (path) { window.location.href = path; }, }); }); ``` --- ## 9. Bronverwijzingen | # | Bron | |----|------| | 1 | Phase 2 Systeemarchitectuur — `phase2_system_architecture_v1.0.md` (2026-05-26) | | 2 | Phase 1 Reconnaissance Report — `phase1_recon_report_v1.0.md` (2026-05-26) | | 3 | Backend Implementatie — `backend_implementation_v1.0.md` (2026-05-26) | | 4 | Flask Jinja2 templating — flask.palletsprojects.com/en/latest/templating/ | | 5 | MDN Web Docs: History API — developer.mozilla.org/en-US/docs/Web/API/History_API | | 6 | CSS Custom Properties — developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties | --- ## 10. Verify — TierVerify Log | Controle-item | Resultaat | Notitie | |---|---|---| | Vanilla JS (geen SPA-framework) | ✅ PASS | Conform Phase 1 findings [2]: geen React/Vue/Angular | | Jinja2 SSR templates | ✅ PASS | Jinja2 syntax in .html templates met macro's | | Inter font + dark mode support | ✅ PASS | Google Fonts CDN, `body.dark-mode` klasse [2] | | Backend API integratie | ✅ PASS | Alle componenten koppelen aan /api/v2/* endpoints [3] | | Module toggle met plan-gating | ✅ PASS | Locked state + upgrade prompts per categorie | | Tenant branding ondersteuning | ✅ PASS | `--tenant-primary` CSS var, logo, bedrijfsnaam | | Dynamic sidebar per tenant | ✅ PASS | Alleen actieve modules worden getoond | | Route guards (client-side) | ✅ PASS | beforeNavigate + checkAccess | | Responsive design | ✅ PASS | 3 breakpoints: desktop/tablet/mobile | | Event bus voor real-time updates | ✅ PASS | TenantContext.on() met module-change events | | Geen bestanden overschreven | ✅ PASS | Nieuwe bestanden met _v1.0 versietag | | Output in working/frontend/ | ✅ PASS | 6 componenten + 1 documentatie | | Bronverwijzingen aanwezig | ✅ PASS | 6 bronnen gedocumenteerd | | Document-header aanwezig | ✅ PASS | Project, Type, Auteur, Versie, Datum, Status | --- *Einde Frontend Implementatie Documentatie — gereed voor review.*