$ ~/krawczyk.city/kb man soar-action-console --full
Demo Mode Active v1.0 · Mar 2026 7 Nav Views 20 Actions 8 Platforms 5 Report Types 18 Incident Pool 3 Scenarios
20
Wired Actions
8
API Integrations
7
Nav Views
5
Report Types
01 // What It Is
Console Purpose
The MSP Command Center SOAR Action Console is a single-file, browser-based security operations and automation dashboard built for MSP engineers managing multi-client environments at national scale. It combines real-time incident triage, context-aware action dispatch, and a full audit trail in one interface — no server required.

The console sits at L1 of the MSP AI Operations Platform — the human-in-the-loop layer where engineers review AI-triaged incidents and fire pre-wired automations against 8 integrated platforms. Every action is logged, every decision is traceable.
Who Uses It
SOC / NOC engineers on shift — select an active incident, see which of the 20 actions are context-matched to that incident type, fire one, watch the execution modal step through the API calls, and the audit log captures it. L1 triage, L2 escalation, L3 investigation actions all live here. The scenario switcher lets team leads simulate crisis conditions for training without touching production data.
02 // Architecture
File Structure
Single HTML file — zero build step
All JS inline — no bundler
Chart.js via cdnjs CDN
Google Fonts via CDN
No React, no framework
State Model
var STATE — global object
lastKpis, incidents, platforms
selectedAlert — current focus
auditLog — array, 50 cap
refreshTimer — 30s auto
Data Flow
loadData() → fetchMock × 5
Promise.allSettled → merge KPIs
renderKPIs + renderIncidents
renderPlatforms + renderAuditLog
Auto-refresh every 30 seconds
Action Flow
Click incident → selectAlert()
Context banner + MATCH badges
Click action → runAction(fn)
runModal() → step sequence
addLog() → audit trail
View System
navTo(page) → show/hide panels
#body (automation) vs #view-container
refreshViewStats() on each view
7 views including automation
All tiles clickable → dispatch
Report System
showReport(type) → report modal
5 report types, all rendered JS
Live data from STATE.lastKpis
window.print() → PDF export
Print CSS — clean black/white
03 // Layout & Navigation
Region
Width
Contents
Rail (top bar)
48px
Brand name, 7 nav tabs with active highlight, scenario selector, live clock, Refresh button. Rail badge shows alert count. Cyan gradient line animates on bottom edge.
KPI Strip
~55px
6 metrics in a 3-col grid: Active Alerts, Endpoints, Threats, Backup Fails, VPN Down, Patch Compliance. Updates on every data refresh. Color-coded by status.
Left column (lcol)
320px
Incident queue. Active incidents from pool, severity color stripe on left edge, click to select and load context. Header shows active count.
Center column (ccol)
flex: 1
Context banner (selected incident) + 6-panel action grid in 2 columns. Actions highlight with MATCH badge when relevant to selected incident tags.
Right column (rcol)
340px
Platform API status cards (8 platforms), refresh timer progress bar, real-time audit log. Log shows last 30 entries with color-coded dot and timestamp.
View Container
full width
Replaces the 3-column body when a non-automation nav tab is clicked. Each of 6 views: stat cards + data tables. All stat cards are clickable — dispatch to automation + pre-select incident.
Nav Tab
Shortcut
What it shows
Overview
navTo('overview')
4 KPI tiles (critical alerts, open tickets, endpoints, patch %), platform status table, top clients by alert volume.
Endpoints
navTo('endpoints')
Online/Offline counts, Patch Pending, Disk Warning — all from live STATE. Device table with top issues. Clickable rows pre-select incidents.
Security
navTo('security')
Active Threats, Quarantined Files, MFA Gaps, Phishing Blocked. Recent detection log. Identity gaps table by client.
Network
navTo('network')
Managed Devices, VPN Tunnels Down, Bandwidth Alerts. VPN tunnel status table. Bandwidth top consumers (Gbps scale).
⚡ Automation
navTo('automation')
Default view. 3-column layout: incidents / action panels / platforms + audit log. This is the primary working view.
Backup
navTo('backup')
Jobs Succeeded/Failed/Missed, Protected Data (847 TB). Failed jobs table with root causes. All from live STATE.lastKpis.
Reports
navTo('reports')
6 report cards — click any to open full formatted report modal. Includes Monthly Health, SLA, Security, Backup, Patch, Generate New.
04 // KPI Strip
Metric
Normal
Crisis
Source & Notes
Active Alerts
42–68
140–180
Derived from actual incident count after loadData(). Synced to left column — what you see in the queue matches this number.
Endpoints
9,480–9,520
9,340–9,480
Ninja RMM mock payload. Used in Endpoints view (ep-online = endpoints − offline). Drives patch pending calc.
Threats
3–7
18–26
SentinelOne mock. Drives Security view sec-threats card and security incident count badge in rail.
Backup Fails
6–14
28–44
Backup agent mock. bkpFail feeds bk-fail card in Backup view and Backup Health Report.
VPN Down
0–2
6–12
FortiGate mock. Feeds net-vpndown card and Network report VPN tunnel count.
Patch Compliance
91–96%
61–74%
Ninja RMM mock patchCompliance field. Drives Patch Compliance report derived counts (pending = endpoints × (1 − pct/100)).
05 // Incident Queue
How it works
The incident pool (INCIDENTS_POOL) contains 18 pre-defined incidents. On each loadData() call, the pool is sliced based on the active scenario — 8 in normal, 18 in crisis, 4 in quiet. In crisis mode the pool is shown in full, in-order (most severe first). In other modes it's shuffled randomly so each refresh shows a different combination.

Clicking an incident calls selectAlert(id) which sets STATE.selectedAlert, re-renders the context banner, and re-renders all 20 action buttons with MATCH badges on buttons whose tags overlap the incident's tags. Actions without a match are still visible but not highlighted.
ID
Severity
Incident
Tags
i1
CRIT
Ransomware Activity Detected — MHC-HOSP-SRV-04, Meridian Healthcare, Chicago IL
security · endpoint · ransomware
i2
CRIT
FortiGate VPN Cluster Failover — us-central-ha, Nexus Logistics Corp, Dallas TX
network · vpn
i3
CRIT
Domain Controller Unreachable — VFP-DC-02, Vanguard Financial Partners, New York NY
endpoint · server
i4
CRIT
Mass MFA Bypass Attempt — Azure AD, 847 failed auth, 12 states, Continental Retail Group
security · cloud
i5
CRIT
Lateral Movement Detected — AMG-MFG-WS-19, Atlas Manufacturing Group, Columbus OH
security · endpoint
i6
CRIT
Firewall Policy Drift — 3 sites, Summit Energy Holdings, Phoenix / Denver / Austin
network · security
i7
WARN
Backup Failure — CMC-SQL-CLUSTER-01, Cascadia Medical Center, Seattle WA
backup
i8
WARN
SAN Storage at 94% — TDS-SAN-07, TerraData Solutions, Atlanta GA
endpoint · server · performance
i9–i14
WARN
Core Switch Offline, SSL Cert Expiring, M365 CA Gap, Patch Compliance Below Threshold, BGP Route Flap, Backup Agent Offline 18 Devices
mixed
i15–i18
INFO/OK
Threat Quarantined FP, Scheduled Maintenance, EDR Scan Complete, Spam Surge
mixed
06 // Action System — All 20 Actions
Context Matching
Every action has a tags array. When an incident is selected, actions whose tags overlap the incident's tags get a MATCH badge and glow highlight. Tags include: all (always matches), security, endpoint, ransomware, network, vpn, server, backup, performance, cloud. Actions marked danger:true show a red hover state and are intended to require confirmation before live wiring.
Incident Actions
ConnectWise PSA · Ninja RMM
🎫Create TicketPSA
👤Assign EngineerPSA
🔺Escalate IncidentPSA
Close AlertRMM
Security Actions
SentinelOne · Huntress
🔒Isolate EndpointDANGER
Terminate ProcessDANGER
🔍Initiate EDR ScanS1
📤Release from QuarantineS1
Network Actions
Ninja RMM · Auvik · FortiGate
Restart DeviceRMM
🚫Disable Switch PortDANGER
📡Run Network ScanAuvik
🔗Restart VPN TunnelFortiGate
Firewall Actions
FortiGate
🚷Block IP AddressDANGER
👁Disable VPN UserDANGER
📋Pull Firewall LogsFG
🔄Apply Policy UpdateFG
Backup Actions
Axcient · Cove · Datto
Run Backup JobBKP
Verify BackupBKP
Restore File or SystemBKP
📊Backup Health ReportBKP
Remediation
Ninja RMM · ConnectWise · Internal
📜Run Remediation ScriptRMM
Force Patch CycleRMM
🖨Generate Health ReportINT
📧Notify ClientCW
07 // Modal Execution Engine
runModal() — How Every Action Executes
Every action calls runModal(title, steps[], successMsg). The modal shows a step-by-step execution trace: each step transitions from waiting → running… → ✓ done with a simulated duration. A progress bar fills across the top. On completion, a success message appears and the ✓ Complete button shows.

Steps are objects: {id, icon, label, dur}. Duration is in milliseconds. In demo mode, dur drives a setTimeout. In live mode, the step can carry a fn property (a Promise-returning function) that the modal awaits before advancing. Close with the ✓ Complete button or press Escape.
// How a step sequence looks — aIsolate as example window.aIsolate = function() { runModal('Isolate Endpoint — SentinelOne', [ {id:'a', icon:'🔍', label:'Locate device in S1 console', dur:500}, {id:'b', icon:'🔒', label:'Apply network isolation policy', dur:1200}, {id:'c', icon:'📸', label:'Capture memory snapshot', dur:1000}, {id:'d', icon:'🎫', label:'Auto-create PSA ticket', dur:500}, {id:'e', icon:'✓', label:'Confirm isolation active', dur:400} ], 'Endpoint isolated · SentinelOne'); };
08 // Audit Log
Log Type
Color
When it fires
ok
● GREEN
Action completed successfully. Data refresh successful. Engineer completes modal.
err
● RED
API timeout or rejection. Any of the 5 mock APIs returning a simulated error (~5% rate per refresh).
run
● YELLOW
In-progress. "Connecting to APIs…" at init. Running action steps.
info
● CYAN
System events: console initialized, scenario switched, manual refresh triggered.
09 // View System — Clickable Tiles
All stat tiles are clickable
Every stat card across all 6 non-automation views fires an onclick that dispatches to the Automation view and pre-selects a relevant incident. For example: clicking Offline (Endpoints view) navigates to Automation and pre-selects the "Domain Controller Unreachable" incident. Jobs Succeeded (Backup view) opens the Backup Health Report directly. This creates a natural triage flow — spot the problem in a view, click it, land in Automation ready to act.
Tile
View
onclick behavior
Critical Alerts
Overview
→ Automation, pre-selects i1 (Ransomware Activity · Meridian Healthcare)
Managed Endpoints
Overview
→ Endpoints view (navTo)
Patch Compliance
Overview
→ Opens Patch Compliance report modal directly (showReport('patch'))
Offline (count)
Endpoints
→ Automation, pre-selects i3 (VPN / offline device incident)
Patch Pending
Endpoints
→ Automation, pre-selects i12 (Patch Compliance Below Threshold)
Active Threats
Security
→ Automation, pre-selects i1 (Critical threat · Meridian Healthcare)
VPN Tunnels Down
Network
→ Automation, pre-selects i2 (FortiGate VPN Failover · Nexus Logistics)
Jobs Failed
Backup
→ Automation, pre-selects i7 (Backup Failure · Cascadia Medical)
Jobs Succeeded
Backup
→ Opens Backup Health Report modal (showReport('backup'))
10 // Report System
Report
Period
Sections & Live Data
Monthly Health
Mar 2026
8-KPI row (alerts, tickets, patch%, backup fails, endpoints, threats, 847 TB, 4,240 net devices), 10-client health bar chart, 7-incident log with states, 6-platform API status. Endpoints and tickets pull from live STATE.
SLA Compliance
Q1 2026
Two KPI rows (response %, breaches, avg response, resolution % + total tickets, first response time, resolution time, tickets/engineer/day), 8-client table with real ticket volumes, 5-breach detail events with timestamps.
Security Summary
Last 30d
Two KPI rows (38 threats, 312,480 phishing blocked, 94,200 firewall hits, 0 breaches), 8-incident log with states and device names, 5-client identity remediation table.
Backup Health
Last 7d
Two KPI rows (jobs succeeded, fails, 847 TB, success rate, 7-day total, restore test pass, avg full backup time, restore success %). 10-client success rate bars. 5-device failed job table. Counts derive from STATE.lastKpis.jobsRan.
Patch Compliance
Current
Two KPI rows (overall %, pending endpoints, patched endpoints, critical CVE exposure). 11-client compliance bars. 5-CVE table with KB numbers. Pending = endpoints × (1 − patchPct/100) — live-calculated.
11 // Scenario Engine
Normal
Incidents: 8 shown
Alerts: 42–68
Endpoints: 9,480–9,520
Patch: 91–96%
Backup Fails: 6–14
VPN Down: 0–2
Pool shuffled randomly each refresh
Crisis
Incidents: All 18 shown
Alerts: 140–180
Endpoints: 9,340–9,480
Patch: 61–74%
Backup Fails: 28–44
VPN Down: 6–12
Pool shown in severity order, no shuffle
Quiet
Incidents: 4 shown (filtered)
Alerts: 8–14
Endpoints: 9,490–9,520
Patch: 96–99%
Backup Fails: 1–3
VPN Down: 0
No crit incidents shown, pool shuffled
12 // Platform Integrations
🖥
Ninja RMM
Endpoints · Alerts · Scripts
SentinelOne
EDR · Isolation · Threats
🛡
FortiGate
Firewall · VPN · Policy
🌐
Auvik
Network · Topology · Ports
🎫
ConnectWise
PSA · Tickets · Contacts
🔍
Huntress
Threat Detection · IR
Azure / M365
Identity · Entra · Copilot
💾
Backup Agent
Axcient · Cove · Datto
13 // Mock API System
fetchMock vs fetchReal
In demo mode, all data comes from fetchMock(endpoint, label) — a function that returns a Promise resolving in 120–680ms with a generateMockPayload() object. It has a 5% random failure rate to simulate real API timeouts (which then appear as red entries in the audit log).

fetchReal(url, label, opts) is the live equivalent — already defined and waiting. To go live, replace fetchMock(API.ninja, 'ninja') with fetchReal(API.ninja, 'ninja', {'headers': {'Authorization': 'Bearer KEY'}}) in loadData(). Then write a field-mapping function to normalize the live response into the same shape as the mock payload.
// var API endpoints — replace placeholder URLs with real ones var API = { ninja: 'https://api.ninjarmm.com/v2/alerts/active', sentinel: 'https://usea1-0123.sentinelone.net/web/api/v2.1/threats', fortigate: 'https://fortigate.yourdomain.com/api/v2/monitor/system/status', auvik: 'https://auvikapi.us1.my.auvik.com/v1/inventory/device/info', connectwise: 'https://yourinstance.connectwise.com/v4_6_release/apis/3.0/service/tickets', huntress: 'https://api.huntress.io/v1/incidents', backup: 'https://api.backup.yourdomain.com/v1/jobs/status' }; // To go live — swap one at a time, start with Ninja: // BEFORE: fetchMock(API.ninja, 'ninja'), // AFTER: fetchReal(API.ninja, 'ninja', { headers: { 'Authorization': 'Bearer YOUR_KEY' } }),
14 // Deployment
Path
Auth
Notes
SharePoint (Recommended)
SP SESSION
Upload as .aspx. Add <%@ Page Language="C#" %> before DOCTYPE. Remove Google Fonts link, replace with Segoe UI, Consolas, system-ui. Add type="text/javascript" to all script tags. Save UTF-8 no BOM, LF endings. Zero auth code — SP session handles it.
krawczyk.city
MSAL.JS
Drop in demos/ or icons/ folder. Use MSAL.js to acquire SP token for cross-origin API calls. Keep mock mode for public demo — never put real API keys in a public file.
Azure Static Web App
EASY AUTH
Deploy via GitHub Actions. Add Easy Auth (Entra) as access gate. Use Azure Function proxy to hold API keys — console calls /api/ninja-alerts, function calls Ninja on your behalf. Best for multi-client portals.
CDN Block on SharePoint
SharePoint may block cdnjs.cloudflare.com via tenant Content Security Policy. Download Chart.js 4.4.1 (chart.umd.min.js) and upload to the same SP document library. Update the <script src> to the SP relative path.
Inline Script Blocked
If the page loads but panels are blank and DevTools shows CSP errors, SharePoint's CSP is blocking inline <script> blocks. Move all JS to a separate .js file uploaded to the library and reference it with a typed <script type="text/javascript" src="..."></script> tag.
15 // API Wiring — Priority Order
#
Platform
Variable
Required API Scope
Go-Live Priority
1
Ninja RMM
API.ninja
API key — Devices:Read, Alerts:Read, Scripts:Write
FIRST
2
SentinelOne
API.sentinel
API token — threats.read, agents.disconnect, threats.mitigate
SECOND
3
ConnectWise
API.connectwise
API key pair — service/tickets:write, company/contacts:read
THIRD
4
FortiGate
API.fortigate
REST API key — system/status, vpn/ipsec, firewall/policy:write
FOURTH
5
Auvik
API.auvik
Basic auth — inventory.read, alerts.read, device.manage
LATER
6
Huntress
API.huntress
API key — incidents.read, hosts.read
LATER
7
Backup Agent
API.backup
Vendor-specific — Axcient, Cove, or Datto REST key
LATER
16 // Action Wiring — How to Add Live Calls
The Pattern — Add fn to a step
Each window.aXxx function passes a steps array to runModal(). To make a step fire a real API call, add an optional fn property that returns a Promise. Modify the next() function inside runModal() to check for step.fn and await it before advancing. The modal progress stays accurate — the step hangs on "running…" until the Promise resolves or rejects.
// STEP 1 — Add fn property to the execution step {id:'b', icon:'🔒', label:'Apply isolation policy', dur:1200, fn: () => fetch(API.sentinel + '/web/api/v2.1/agents/actions/disconnect', { method: 'POST', headers: { 'Authorization': 'ApiToken YOUR_S1_TOKEN', 'Content-Type': 'application/json' }, body: JSON.stringify({ filter: { ids: [STATE.selectedAlert.agentId] } }) }) }, // STEP 2 — Patch next() inside runModal() to await fn: var work = s.fn ? s.fn() : Promise.resolve(); work.then(function() { document.getElementById('ms-'+s.id).textContent = '✓ done'; idx++; setTimeout(next, 150); }).catch(function(err) { document.getElementById('ms-'+s.id).textContent = '✕ failed'; addLog('Action error: ' + err.message, 'Action Console', 'err'); }); // DANGER ACTIONS — always add a confirm step before fn fires: {id:'confirm', icon:'⚠', label:'Awaiting engineer confirmation', dur:0, fn: () => new Promise((resolve, reject) => { if (confirm('Confirm: Isolate endpoint? This cuts network access.')) resolve(); else reject(new Error('Cancelled by engineer')); }) },
17 // SharePoint Deployment Steps
Create Entra security group SOAR-Console-Access. Members: SOC/NOC engineers, on-call leads. Owners: MK + backup. Type: Security (not M365).
Upload soar-action-console-fixed.html to a SharePoint document library. Assign Read access to the Entra group.
Add <%@ Page Language="C#" %> as the very first line of the file, before <!DOCTYPE html>.
Remove the Google Fonts <link> tag. Replace font variables in CSS with Segoe UI, Consolas, system-ui.
Add type="text/javascript" to all <script> tags. Save file as UTF-8 without BOM, LF line endings.
Rename file to .aspx extension in the SP library.
If CDN blocked: download chart.umd.min.js locally, upload to SP library, update <script src> to relative path.
Open from an engineer's browser. Verify: KPI strip populates, 8+ incidents load in left column, all nav views render, action modal fires on button click.
Verify Automation is set as default active nav tab. Scenario selector shows NORMAL on load. Auto-refresh triggers at 30s.
Conduct one CRISIS scenario drill with the team before wiring any live API keys.
18 // Data Schema Reference
Incident Object
id — string, unique (i1–i18)
icon — Unicode character
title — short incident label
sub — platform · device · location
client — client name string
sev — crit / warn / info / ok
tags — array of tag strings
INCIDENTS_POOL18 items
Action Object
icon — Unicode character
label — button display name
hint — sub-label (platform/method)
vendor — platform name
tags — match tags array
fn — window function name string
danger — boolean (optional)
ACTION_DEFS6 categories20 actions
KPI / STATE Object
alerts — active alert count
endpoints — total managed
offline — offline count
threats — active S1 threats
backupFails — job fail count
vpnDown — tunnel down count
patchPct — compliance %
tickets — PSA open tickets
jobsRan — backup jobs run
STATE.lastKpisDrives all views
Audit Log Entry
msg — log message string
src — source label (System / Action Console / User)
type — ok / err / run / info
ts — HH:MM:SS timestamp

Capped at 50 entries. Rendered via renderAuditLog(). Written by addLog(msg, src, type). Called on every action completion, refresh, and system event.
STATE.auditLogMax 50
19 // Keyboard Shortcuts
Shortcut
Action
Escape
Close the action modal if open. Close the report modal if open. Closes whichever is showing.
Ctrl / Cmd + R
Trigger manual data refresh (doRefresh()). Equivalent to clicking the Refresh button in the rail. Logs "Manual refresh triggered" to audit log.
Click overlay
Clicking the dark background behind the report modal closes it. Action modal requires the ✓ Complete button.
20 // Known Items & Notes
Demo Mode — All Data is Simulated
All KPI numbers, incident titles, client names, and platform statuses are generated by generateMockPayload(). They are realistic in scale (9,500 endpoints, 847 TB backup, 312k phishing blocked) but not live. No real APIs are called until fetchReal() replaces fetchMock() in loadData().
DANGER Actions — No Live Gate Yet
Actions marked danger:true (Isolate, Terminate Process, Block IP, Disable VPN, Disable Switch Port) show red hover styling but have no confirmation gate in demo mode. Before wiring any of these to live APIs, a confirm dialog or second modal approval step must be added inside the action function. Do not wire destructive actions without this.
API Keys — Never in the HTML File
The var API object holds endpoint URLs only. API keys must be injected at runtime from a config.json (SP path with restricted permissions), an Azure Function proxy, or MSAL token flow. Never commit keys to git or embed them in the HTML file.
Refresh Timer
Auto-refresh fires every 30 seconds via startRefreshTimer(). A progress bar in the right column fills left-to-right over 30s then resets. The timer restarts after every manual refresh. No data persists between refreshes — STATE is fully rebuilt from mock/live APIs on each cycle.
Fully Stateless — No localStorage
The console uses no localStorage or sessionStorage. All data lives in var STATE for the duration of the browser session. This means no persistence concerns on SharePoint and no cross-tab state conflicts. Refresh the page and everything resets to a fresh load.
Mobile Layout
Below 768px, a bottom tab bar replaces the top rail nav. Four tabs: Incidents / Actions / Log / Menu. The 3-column body becomes single-panel tab switching. The Menu tab opens a drawer with all 7 nav items. Desktop layout is unchanged.
Adding Custom Scenarios
Add a new <option value="custom"> to the scenario selector dropdown in the rail HTML. Then add a case in generateMockPayload() for the new scenario string, and a filter clause in loadData() for the incident pool. The changeScenario(val) function picks it up automatically and calls loadData().