KNOWLEDGE BASE // MSP CONSOLE v2
v2 · 2026
KNOWLEDGE BASE // MSP CONSOLE v2
MSP Console v2
A single-file client security and compliance dashboard for MSP engineers. Aggregates Microsoft Entra ID, Intune, and on-premise infrastructure data into a unified cockpit with live event feeds, AI analysis, PDF reporting, and a full credential management layer.
9 Nav Sections
WebSocket Live Feed
AI Analyst (Anthropic API)
4-Page jsPDF Report
01Overview

MSP Console v2 is a self-contained single-file HTML dashboard purpose-built for MSP engineers managing a specific client environment. The demo client is "Meridian Financial Group" but every reference is hardcoded and replaceable. All logic runs client-side with no build process required.

The console operates in two modes: Demo Mode (default, all data is static/simulated) and Live Mode (real data via a backend proxy and WebSocket connection). Switching between them requires no code changes beyond toggling Live Mode in Settings and providing a backend URL.

Design Philosophy
The file is intentionally self-contained to reduce deployment surface. One file, one URL, no build artifacts, no npm. This trades bundle optimization for operational simplicity, which matters in MSP environments where the person deploying it may not be a developer.
Primary Audience

MSP L2/L3 engineers running monthly security reviews, account managers preparing QBR materials, and team leads monitoring live client environments. The AI analyst layer lowers the barrier for L1 engineers interpreting complex security posture data.

Rebranding for a New Client

Find and replace Meridian Financial Group throughout the file. The client name appears in the header badge, the risk banner label, the PDF report cover, and several hardcoded demo data strings. All other data flows from the state object.

02Architecture & Dependencies

The file loads three CDN scripts and two Google fonts. All other logic is vanilla JavaScript inside a single <script> block. There is no module system, no transpilation, and no state management library.

DependencyVersionSourceRequired For
Chart.jslatestcdn.jsdelivr.net/npm/chart.jsSafety score trend line, MFA trend line, radar chart (control coverage). All canvas charts.
jsPDF2.5.1cdnjs.cloudflare.comPDF report generation. Page layout, text, shapes, and image embedding.
jsPDF-AutoTable3.8.2cdnjs.cloudflare.comMetrics detail table on Page 3 of the PDF report. Must load after jsPDF.
Inter400,600,700,800Google FontsPrimary UI font throughout the dashboard.
JetBrains Mono400,700Google FontsAll metric values, code labels, timestamps, nav badges.
CDN Dependency Risk
If any CDN is unreachable at page load, Chart.js charts will be blank and PDF generation will silently fail. For production deployments, self-host all three scripts. Place them alongside the HTML file and update the <script src> paths to relative URLs.

Content Security Policy

A <meta http-equiv="Content-Security-Policy"> tag is embedded in the head. Key directives:

default-src'self' — blocks all unlisted origins by default.
script-srccdn.jsdelivr.net and cdnjs.cloudflare.com for Chart.js and jsPDF. 'unsafe-inline' is required for the inline script block.
connect-srchttps://api.anthropic.com only. This is the only domain the AI module may call. If you change AI providers, update this directive or the API call will be blocked.
img-src'self' data: — allows the PDF chart export which generates a data: URI from the canvas.

Page Structure

The layout is a CSS grid with a 196px fixed sidebar and a fluid content area (main { grid-template-columns: 196px 1fr }). The content area uses overflow-y: auto on a #contentBody div. Navigation switches sections by calling loadSection(name) which writes HTML into #contentBody via the sectionMap registry.

03State Object

All runtime data lives in a single const state object. This is the source of truth for every render function. The WebSocket handlers, CSV import pipeline, and score calculator all mutate this object and then trigger re-renders.

const state // Full shape
const state = {
  score:         88,    // 0–100 safety score (recomputed by score heuristic)
  grade:         'A',   // 'A+' | 'A' | 'B' | 'C' | 'D'
  mfaPct:        88,    // % of users with MFA enabled (0–100)
  nonCompliant:  2,     // count of Intune noncompliant devices
  activeUsers:   142,   // total Entra ID licensed users
  stalePasswords:12,    // accounts with passwords 90+ days old
  lockedOut:     0,     // accounts currently locked out (real-time)
  riskySignIns:  0,     // risky sign-ins in current window

  criticalItems: [       // "Must Fix This Week" list
    {
      main:        'MFA disabled on 6 finance/ops accounts',
      sub:         'T. Williams, A. Patel, M. Chen + 3 others',
      action:      'https://entra.microsoft.com/...',
      actionLabel: 'Open Entra MFA →',
      importSource:'lockouts' // optional — set by CSV import for deduplication
    }
  ],

  warningItems: [        // "Fix This Month" list — same shape as criticalItems
  ],

  activityLog:  [],     // array of { msg, level, time } — max 60 entries, newest first
  lastSync:     new Date(),
  lastSyncMs:   Date.now(),
}
Mutation Pattern
All state mutations are direct property assignments (e.g. state.mfaPct = 92). There is no setter or reactive system. After mutating state, you must manually call the relevant render functions or the UI will not update. The WebSocket handlers in const handlers follow the correct pattern: mutate state, then call animateCount(), renderPriorities(), or addActivity() as appropriate.

The trendData object holds 12-month historical arrays for the safety score and MFA trend charts. These are separate from state because they are chart-only data that the live event handlers never need to mutate.

04Dashboard Section

The Dashboard is the default landing section, rendered by renderDashboard(). It contains five visual zones that give the shift engineer a complete situational picture without needing to open any other section.

ComponentFunctionNotes
Risk Banner SVG ring gauge with letter grade, score, kill-chain stats, and priority counts Ring color: green ≥ 85, yellow ≥ 70, red below. Hovering the ring shows a flyout popover with score breakdown. Ring updates live via updateRing(score).
Summary Grid 5 stat cards: MFA Coverage, Non-Compliant Devices, Active Users, Stale Passwords, Locked Out Cards are sticky at top: 0 so they remain visible while scrolling. Each card is clickable to navigate to its related section. Values animate via animateCount() on live updates.
Priority Boxes Critical (Must Fix This Week) and Warning (Fix This Month) item lists with remediation action links Items are rendered from state.criticalItems and state.warningItems. Clicking an item toggles expanded state to reveal the action link. Critical box has a pulsing red border animation. Max 10 items displayed per box.
Live Event Feed Chronological log of incoming WebSocket events and system activity Rendered from state.activityLog by renderActivityFeed(). Max 60 entries stored, top 20 shown. Each entry has a color-coded dot: crit (red), warn (yellow), ok (green), info (cyan).
Charts Safety Score 12-month trend line + Risk Radar (6-axis control coverage) Both are Chart.js instances. Initialized in initCharts() called via setTimeout(initCharts, 50) after the section renders. The trend chart is also captured as a PNG and embedded in PDF Page 4.

Score Ring Color Logic

A / A+
85 – 100
B / C
70 – 84
D / F
0 – 69
05Navigation Sections

The sidebar has 9 nav items. All sections except Dashboard, Credentials, Import, and Audit Log are rendered by simple functions that return an HTML string. loadSection(name) writes the string to #contentBody and handles the active state on the nav item.

SectionKeyRender FunctionNav Badge
DashboarddashboardrenderDashboard()None
UsersusersrenderUsers()WARN count of users without MFA (hardcoded: 6)
DevicesdevicesrenderDevices()CRIT count of noncompliant devices — updates via updateBadge('nb_devices', state.nonCompliant)
Sign-InssigninsrenderSignIns()OK count of risky sign-ins — updates live
TrendstrendsrenderTrends()None
InfrastructureinfrarenderInfraSection()! hidden until live infra data arrives
CredentialscredentialsrenderCredentials()count hidden until credentials saved
Import CSVsimportrenderImportSection()count hidden until files queued
Audit LogauditlogrenderAuditLog(body)Count of stored AI query log entries

Sign-Ins Section

The Sign-Ins section is unique: it filters state.activityLog for entries that include the strings 'Sign-in' or 'Risky'. This means it is not a separate data store — it is a filtered view of the live feed. In production with real WebSocket data, this section will populate automatically as events arrive.

Infrastructure Section

The Infrastructure section renders four monitoring cards: Active Directory, Server Health, SQL Server, and File Server. In demo mode it uses a hardcoded infraDemo object. In Live Mode it polls GET /infra on the configured backend URL. Each card shows metrics with color-coded thresholds using the valC(value, suffix, goodFn, warnFn) helper. Metrics with no good/warn function default to muted display.

06Risk Score Model

The safety score is a proprietary MSP heuristic, not Microsoft Secure Score or NIST 800-53. It is computed fresh every time the CSV import applies data. The formula deducts points from 100 based on five signals from the state object.

Score Heuristic // importApplyToDashboard()
const mfaDeduct    = Math.max(0,  Math.round((100 - state.mfaPct) * 0.5));
const devDeduct    = Math.min(20, state.nonCompliant * 7);
const lockedDeduct = Math.min(10, state.lockedOut * 3);
const staleDeduct  = Math.min(8,  Math.round(state.stalePasswords * 0.3));
const riskDeduct   = Math.min(10, state.riskySignIns * 2);
const rawScore     = 100 - mfaDeduct - devDeduct - lockedDeduct - staleDeduct - riskDeduct;
const newScore     = Math.max(0, Math.min(100, rawScore));
SignalFormulaMax Deduction
MFA Coverage(100 - mfaPct) * 0.550 pts (if 0% MFA)
Non-Compliant DevicesnonCompliant * 720 pts cap
Locked Out AccountslockedOut * 310 pts cap
Stale PasswordsstalePasswords * 0.38 pts cap
Risky Sign-InsriskySignIns * 210 pts cap
GradeScore Range
A+90 – 100
A80 – 89
B70 – 79
C60 – 69
Dbelow 60
Score Only Recalculates on CSV Import
The score heuristic runs inside importApplyToDashboard(). Live WebSocket events update individual state fields and display values but do NOT recompute the score. If you want the score to recompute on live events, add the heuristic formula as a shared recomputeScore() function and call it inside the relevant handlers entries.
07Real-Time Module (RT)

The RT object manages the WebSocket connection. In demo mode, RT.connect() calls itself without a URL argument and simulates events via setTimeout. In production, call RT.connect('wss://your-backend.azurewebsites.net').

Going Live // Boot section at bottom of script
/*
 * TO GO LIVE:
 *   1. Uncomment: <script src="https://cdn.socket.io/4.7.5/socket.io.min.js">
 *   2. Change: RT.connect('wss://your-backend.azurewebsites.net');
 */
RT.connect(); // currently runs in demo mode

Event Handlers

The const handlers object maps WebSocket event names to handler functions. Each handler mutates the relevant state field, updates the relevant DOM element, and fires a toast notification.

EventPayload FieldsWhat It Does
update:mfacoveragePct, disabled, userDisplayName, userIdUpdates state.mfaPct, animates the MFA stat card, adds a critical item if MFA was disabled on a user.
update:devicecompliance, deviceName, deviceIdIncrements or decrements state.nonCompliant, flashes the devices card, updates the nav badge.
update:riskyUseruserDisplayName, riskLevelIncrements state.riskySignIns, logs to activity feed, updates sign-ins nav badge.
update:signInstatus, userDisplayName, locationLogs sign-in event to activity feed. Fires a toast only if status is 'failure'.
update:scorescore, gradeUpdates state.score and state.grade, animates the score display, re-draws the ring, pulses the client name badge.

Sync Status Indicator

The header shows a sync dot and timestamp driven by updateSyncStatus(), called every 10 seconds via setInterval. The dot transitions through three states based on time since last markSynced() call:

fresh (green)Last sync less than 5 minutes ago. Normal operation.
stale (yellow)5 to 15 minutes. Refresh button gets a pulsing red border animation.
error (red)Over 15 minutes. Data is considered stale — check WebSocket connection.
08AI Analyst Module

The AI panel is a fixed bottom-right chat widget labeled "Grok Assistant." It calls the Anthropic API (api.anthropic.com/v1/messages) directly from the browser. The CSP connect-src allows only this domain. The model used is configurable in askAI().

API Key Exposure Risk
The Anthropic API key is stored in the askAI() function as a plain string in the HTML file. Anyone with access to the file can extract it. In production, proxy AI requests through your backend at /ai/ask and inject the key server-side. Never ship this file with a real API key to a client-accessible URL.

Quick Chips

Four quick-access chips send preset queries without typing: mfa gaps, top risks, fix plan, and score. Each chip value is passed to askAI(query) which assembles a context-rich system prompt from the current state object before calling the API.

AI Offline Mode

The header button "AI Offline: OFF" toggles window._aiOffline. When active, askAI() returns immediately without making any API call and shows a local message instead. The state persists to localStorage as xai_msp_ai_offline. Use this during client meetings or any context where external API traffic is inappropriate.

AI Panel Toggle

The "AI Analyst: ON/OFF" header button controls panel visibility. The preference is saved to localStorage under the key AI_PREF_KEY. The panel is hidden on viewports below 680px and accessible instead via the Settings modal.

Context Injection

Every AI query prepends a system prompt built from live state values. This gives the model accurate numbers for the current client session without requiring it to hallucinate or estimate. The system prompt includes: client name, current score and grade, MFA percent, non-compliant device count, locked out count, stale passwords, risky sign-ins, and the text of all critical and warning items.

09Credentials Manager

The Credentials section stores on-premise server credentials used by the backend proxy to connect to AD, SQL, File, and utility servers for the Infrastructure section. The security model is carefully designed to minimize credential exposure risk.

Security Model
Passwords are never written to localStorage or sessionStorage. They live only in a JavaScript Map (credSession) in memory and are cleared when the tab closes. Only display metadata (name, hostname, username, roles, status) is persisted to localStorage. If a backend is configured, passwords are POST'd over HTTPS to /credentials where they are encrypted AES-256 server-side.

Credential Object Shape

credSession Map value (memory only)
{
  id:       'cr_m8k2q9xa',    // generated by credUid()
  name:     'Primary DC',
  hostname: 'dc01.contoso.local',
  username: 'DOMAIN\\svc_msp',
  password: '••••••••',        // in memory only — never persisted locally
  roles:    ['dc', 'server']
}

// localStorage stores this shape — NO password field:
{
  id:         'cr_m8k2q9xa',
  name:       'Primary DC',
  hostname:   'dc01.contoso.local',
  username:   'DOMAIN\\svc_msp',
  roles:      ['dc', 'server'],
  addedAt:    1711123456789,
  lastStatus: 'ok',   // 'ok' | 'fail' | null
  lastTest:   1711123500000
}

Server Roles

Each credential can be tagged with one or more roles that determine which Infrastructure section panels it feeds data to:

dcDomain Controller — feeds Active Directory panel (locked out, stale passwords, kerberoastable, pwd-never-expires, replication failures).
sqlSQL Server — feeds SQL panel (connections, blocking chains, memory, overdue backups).
fileFile Server — feeds File Server panel (open-access shares, risky share enumeration).
serverUtility Server — feeds Server Health panel (CPU, RAM, stopped services, critical event log count).

Test Connection

testCredConnection(id) calls GET /credentials/{id}/test on the configured backend. The backend attempts a connection using the stored (encrypted) password and returns { reachable: true|false }. The card border and status badge update accordingly. This requires a backend URL to be set in Settings.

10CSV Import Pipeline

The Import CSVs section provides a drag-and-drop interface for applying monthly maintenance script outputs directly to the dashboard. Files are parsed entirely in the browser — no data is transmitted anywhere. This is the primary mechanism for updating state when Live Mode is not connected.

Workflow

  1. 01
    Drop or browse CSV files. Multiple files can be queued at once. The drop zone accepts .csv only. The queue displays each file name with a size label.
  2. 02
    Click "Parse and Preview." importParseAndPreview() reads each file using FileReader, splits by newlines, parses headers, and counts rows. Delta chips appear below the drop zone showing what will change.
  3. 03
    Click "Apply to Dashboard." importApplyToDashboard() maps each file to the CSV_SCHEMA entries, writes row counts to state fields, recalculates the score, and navigates to the Dashboard.

CSV Schema Map

The CSV_SCHEMA object maps filename patterns to state fields. Filenames are matched case-insensitively. Unknown files are accepted in the queue but produce no state changes.

Expected FilenameState Field UpdatedSpecial Logic
LockedOutUsers.csvstate.lockedOutAlso rebuilds state.criticalItems entries tagged importSource: 'lockouts'. First 5 usernames shown in the item sub-text.
Kerberoastable.csvstate (warning item)Rebuilds state.warningItems entries tagged importSource: 'kerb'. First 3 account names shown.
OldPasswords.csvstate.stalePasswordsAlso rebuilds stale password warning item tagged importSource: 'stale'.
Other mapped filesVaries by CSV_SCHEMA configRow count written to the mapped state.field. No special item logic.
Deduplication Guard
Before adding a new import-derived item to criticalItems or warningItems, the code filters out any existing item with the same importSource tag. This prevents duplicate entries when you re-import updated files without refreshing the page.

After a successful apply, file names (not content) are saved to localStorage as lastImportNames. This drives the "Last import:" hint text shown inside the drop zone on subsequent visits.

11PDF Report

The "PDF Report" header button generates a 4-page A4 PDF using jsPDF and jsPDF-AutoTable. The report is generated entirely client-side from the current state object and immediately downloaded. The filename is MSP-Security-Report-{ClientName}-{Month}-{Year}.pdf.

PageContent
Page 1 — Cover Client name, report period, generation date, classification. Large grade letter, score value with progress bar. 4 stat tiles: MFA Coverage, Locked-Out Users, Non-Compliant Devices, Risky Sign-Ins. Confidentiality strip at bottom.
Page 2 — Executive Summary 5-tile KPI row. Introductory narrative paragraph (named client, period, data sources). Red critical items box with all state.criticalItems. Yellow moderate items box with all state.warningItems. Automatic page break if boxes overflow.
Page 3 — Metrics Detail 17-row AutoTable covering Identity/MFA, Active Directory, Endpoint/Intune, M365/Exchange, Conditional Access, and App Registrations. Each row has Category, Metric, Value (live from state), and Status ([OK] or [!] color-coded). Column widths total exactly PW - 2*M = 174mm.
Page 4 — Trends and Recommendations Safety score trend chart embedded as PNG (captured from the live canvas via an off-screen composite). 4-item recommendations table with priority, timeframe, effort, action, and guidance columns. Footer on every page via didDrawPage callback.
Chart Export Requirement
The trend chart PNG on Page 4 is captured from the live #trendChart canvas element. If you generate the PDF without having visited the Dashboard section first (so the chart was never rendered), the PDF will show a placeholder text line instead of the chart image. Always navigate to Dashboard before generating the PDF.

Chart White-Background Composite

The dark-themed chart canvas is composited onto a white offscreen canvas before embedding. This is done by creating a new canvas at 2x pixel density, drawing a white fillRect, then drawing the live canvas on top. This produces a sharp, print-appropriate chart without modifying the live Chart.js instance or causing any visual flash in the UI.

12Audit Log

Every AI query submitted through the chat panel is logged to IndexedDB (msp_console_audit, version 2). The prompt text is encrypted with AES-GCM using a session-only CryptoKey before the write. The key is non-extractable and lives only in memory — it is generated fresh on each page load via the Web Crypto API.

What Is Logged

timestampISO 8601 string, stored in plaintext for TTL sweep efficiency.
ivAES-GCM initialization vector, base64-encoded.
ctEncrypted ciphertext of the prompt, base64-encoded.
offlineBoolean. true if the query was submitted while AI Offline Mode was active (meaning no API call was made).

TTL and Auto-Expiry

On every page load, _purgeExpiredLogs() runs an IndexedDB cursor sweep on the by_ts index and deletes any entry with a timestamp older than LOG_TTL_DAYS (default 90 days). To change the retention window, edit const LOG_TTL_DAYS = 90 near the top of the audit section in the script.

Export and Clear

The Audit Log section renders Export CSV and Clear Logs buttons. Export decrypts each entry (prompts decrypt successfully only in the same browser session using the same in-memory key) and generates a CSV download. Entries from a previous session will decrypt as [encrypted — key unavailable] because the key is never persisted. Clear Logs deletes all IndexedDB entries and resets the nav badge.

Why Encrypt Local Logs
The audit log exists to demonstrate to clients that AI queries are tracked and governed. The AES-GCM layer ensures that even if an engineer exports the raw IndexedDB, prompt text is not readable without the in-memory key from that specific session. It is a defence-in-depth measure, not a replacement for proper data governance.
13Settings & Configuration

The Settings modal is opened via the ⚙ header button. It controls four areas. Settings are persisted to localStorage under the liveSettings key.

Live Infrastructure Monitoring

SettingIDNotes
Live ModeliveModeToggleEnables polling the backend for real AD / SQL / server data. Required for Infrastructure section to show live data.
Backend URLbackendUrlBase URL of your backend proxy, e.g. https://your-backend:3000. Used by liveFetch(path) for all infrastructure API calls.
API KeybackendApiKeyAdded as x-api-key header on every liveFetch call. Stored in liveSettings in localStorage.
Poll IntervalpollIntervalOptions: 1 min, 5 min (default), 15 min. Controls how often the dashboard pulls a full infra snapshot.

Source Toggles

Four toggles control which infrastructure data sources are polled: AD, SQL, File Server, and Server Health. Disabled sources show "Disabled" in the relevant Infrastructure card header and do not trigger API calls for that data type.

AI and Privacy

A toggle controls AI panel visibility (same effect as the header button). Settings also contains the AI Offline Mode toggle as an alternative to the header button.

localStorage Keys Reference

xai_msp_liveSerialized liveSettings object (backend URL, API key, poll interval, source toggles).
xai_msp_cred_metaArray of credential metadata objects. No passwords.
xai_msp_ai_offline'1' or '0' — AI offline mode preference.
xai_msp_ai_pref'1' or '0' — AI panel visibility preference.
xai_msp_lastimportArray of filenames from the last successful CSV import (names only, no content).
14Security & Privacy Design

The console was built with several specific security and accessibility decisions that are worth understanding before deployment or modification.

Credentials Security
Strong

Passwords exist only in the credSession in-memory Map. They are cleared on tab close. The CSP connect-src prevents credential data from being sent to any URL except the configured backend. Metadata in localStorage carries no sensitive fields.

AI Privacy Controls
Review Needed

AI Offline Mode blocks all Anthropic API calls. The audit log captures every query with AES-GCM encryption. The privacy footer states that all data is processed locally and AI queries are the only external call. The xAI privacy policy link references Anthropic-equivalent terms.

Accessibility Features

Skip LinkVisually hidden a.skip-link becomes visible on keyboard focus. Jumps to #contentBody. Required for keyboard-only users who would otherwise tab through the entire sidebar on every page.
Focus Ring:focus-visible shows a 2px cyan outline. :focus is suppressed to avoid ugly rings on mouse clicks. Nav items get a left-bar indicator instead of a full outline.
ARIA LabelsAll interactive elements have aria-label. The connection pill has role="status". The sync status has aria-live="polite". Nav items have role="button" and tabindex="0".
Reduced Motion@media (prefers-reduced-motion: reduce) disables the starfield entirely, suppresses all pulsing animations, and kills the toast slide animations. Hover transitions are preserved as they are user-initiated.
Forced Colors@media (forced-colors: active) hides the starfield layer to prevent it from interfering with high-contrast mode rendering.
WCAG Color--muted: #b8b8b8 is WCAG AA compliant on #000 background. --muted2: #888 is flagged in comments as decorative-only and should not carry readable text.

Starfield Performance

50 star elements are generated by JavaScript in initStarfield(). Each star uses CSS custom properties (--star-opacity, --star-dx, --star-dy) to randomize appearance without generating 50 separate keyframe rules. The will-change: transform, opacity declaration promotes each star to its own GPU compositor layer, preventing layout thrash during animation.

15Deployment

Go-Live Checklist

  1. 01
    Replace the client name. Find and replace all instances of Meridian Financial Group with your client name. Also update the HTML <title> tag.
  2. 02
    Self-host the CDN dependencies. Download Chart.js, jsPDF 2.5.1, and jsPDF-AutoTable 3.8.2. Place them alongside the HTML file. Update the three <script src> tags to relative paths.
  3. 03
    Update the CSP if changing script hosts. If you host scripts on a different domain, add it to the script-src directive in the CSP meta tag. If you add a backend proxy URL, add it to connect-src.
  4. 04
    Move the Anthropic API key to a backend proxy. Replace the direct Anthropic fetch in askAI() with a POST to /ai/ask on your backend. The backend injects the API key. Update connect-src to your backend domain.
  5. 05
    Set up the backend for Live Mode. The console expects a backend that responds to: GET /infra, GET /credentials/{id}/test, POST /credentials, DELETE /credentials/{id}. Wire the WebSocket server to push events matching the handler names documented in Section 07.
  6. 06
    Uncomment the socket.io script tag. The <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"> tag is commented out in the source. Uncomment it and add cdn.socket.io to the CSP script-src.
  7. 07
    Update RT.connect() with your WebSocket URL. Change the boot call at the bottom of the script from RT.connect() to RT.connect('wss://your-backend.azurewebsites.net').
  8. 08
    Update the demo data in state. Replace state.criticalItems and state.warningItems with the client's actual current findings. Replace trendData.safety and trendData.mfa with 12-month historical actuals from the client's Microsoft 365 tenant.
  9. 09
    Serve from an authenticated URL. This file contains API endpoint references, a backend URL, and potentially an API key. It must not be publicly accessible. Serve from an authenticated internal host, VPN-gated URL, or behind HTTP Basic Auth at minimum.
  10. 10
    Test PDF generation on the target machine. Navigate to Dashboard, wait for charts to render, then click PDF Report. Verify the chart image appears on Page 4. Test that the AutoTable metrics render correctly with your actual state values.
16FAQ
Q The radar chart or trend chart is blank when loading the dashboard.

Chart.js was not loaded from the CDN before initCharts() was called. This usually means the CDN request timed out or failed. Open the browser console — a Chart.js 404 or failed fetch will be visible. Self-host Chart.js as a local file to eliminate this dependency entirely.

Q The PDF report shows "[Chart unavailable]" instead of the trend chart image.

The trend chart canvas (#trendChart) must exist in the DOM and be rendered when the PDF button is clicked. The chart only renders when the Dashboard section is active. Navigate to the Dashboard section first, wait for the chart to draw, then click PDF Report. If you clicked PDF from another section, the canvas does not exist in the DOM and the chart capture fails gracefully with the placeholder text.

Q My credentials disappear after I close and reopen the tab.

This is by design. Passwords are memory-only and cleared on tab close. Credential metadata (name, hostname, username, roles) should persist in localStorage across sessions. If metadata is also missing, check that localStorage is not blocked in your browser. In private/incognito mode, localStorage is cleared on tab close — use a regular browser session for the console.

Q The score did not change after applying the CSV import.

The score heuristic only runs if at least one of the five score signals (mfaPct, nonCompliant, lockedOut, stalePasswords, riskySignIns) was updated by the imported files. If none of your imported files map to those fields in CSV_SCHEMA, the score will not change. Check the toast notification after applying — it shows the count of fields updated and the old/new score values.

Q The AI chat returns a generic response rather than using live data.

The AI system prompt is built from the current state object at the time the query is sent. If the state is still showing demo defaults (score 88, 142 users, etc.) it means no CSV import has been applied and no live WebSocket data has arrived. Apply a CSV import first to update state with real client values, then query the AI again for data-relevant responses.

Q Live Mode is enabled but the Infrastructure section shows "Enable live mode to scan."

Live Mode being toggled ON does not immediately trigger a fetch. The first poll runs at the next interval tick or when you manually click Refresh. Click the Refresh button in the header to force an immediate liveFetch('/infra'). If it still fails, check the backend URL in Settings and verify the backend is reachable from your network. The browser console will show the failed fetch with a specific error.

Q The audit log decrypts to "[encrypted — key unavailable]" for most entries.

The AES-GCM key is session-only and generated fresh on each page load. Entries from previous sessions cannot be decrypted in a new session — the key that encrypted them no longer exists. Only entries from the current session (same page load, not just same tab) can be decrypted. This is intentional. If you need readable long-term logs, add a server-side logging endpoint and POST queries there with the key stored server-side.

Q How do I add a new section to the sidebar?

Add a .nav-item div to the <aside> with a data-section="mysection" attribute and any desired nav badge. Then register a render function: sectionMap['mysection'] = () => '<div>...content...</div>'. The loadSection() function will find it automatically via the sectionMap. If your section needs post-render DOM initialization (like the Import section's drop zone), return the HTML string from the map entry and call setTimeout(initMySection, 50) before returning.