MSP Field Ops Console
Knowledge Base · MSP Console Suite

MSP Field Ops Console

A real-time, role-aware service desk for MSP technicians. Surfaces the active ticket queue from ConnectWise PSA, wires every remediation action through a backend proxy to 17 vendor APIs, and guides techs through a structured 6-tab workflow from Intake to Close.

17 Vendor Integrations L1–L4 RBAC 30s Auto-Refresh Proxy-Ready 12 Audit Fixes Applied CW Manage: No CSAT API
01

What Is This Console

Purpose, scope, and operating modes

The MSP Field Ops Console is a single-screen operations dashboard purpose-built for MSP service desk technicians. It aggregates the active ticket queue, vendor health, client context, and live audit log into one interface, then provides a structured 7-tab workflow for every ticket — from initial intake through root-cause investigation, remediation, verification, and close.

Core Design Principle

Every action in the console that changes a ticket state — creating, closing, escalating, reassigning, adding notes — routes through the postAction() function. This gives the proxy a single intercept point for logging, auth, and fan-out to vendor APIs. In demo mode (FETCH_ENABLED=false) every postAction() call falls through to a mock Promise that resolves after a simulated latency, so the console is fully exercisable without a backend.

Operating Modes
Demo Mode Default state. FETCH_ENABLED = false. All data is served from in-memory arrays (TICKETS, CLIENTS, VENDORS, AUDIT_LOG). Every postAction() and fetchWithFallback() call returns a mock response after a randomised delay. The API badge shows ◌ MOCK. Fully functional for training and review.
Live Mode Set FETCH_ENABLED = true and point API_BASE at your proxy. The doRefresh() function polls /api/tickets, /api/alerts, and /api/client-health. Every action POST hits /api/action. The API badge switches to ◉ LIVE.
02

Intended Audience

Who uses this console and at what role level

RoleLevelVisible FeaturesRestricted Features
L1 TechnicianL1Full ticket queue, workflow tabs 1–7, safe remediation actions, notes, KB search, dashboard viewIsolate Endpoint, Block IP/Domain, Advanced Triage section, L4 Profit Widget
L2 Senior TechL2All L1 + destructive remediation actions (Isolate Endpoint, Block IP/Domain)L4 Profit Widget, some L3/L4 advanced triage UI
L3 EngineerL3All L2 + Advanced Triage section (IOC Hunt, Mem Forensic, Event Timeline, Net Isolate)L4 Profit Widget
L4 Lead EngineerL4All L3 + L4 Profitability & EHR widget on dashboardNothing — full access
03

Architecture

Data flow, proxy pattern, and state management

Proxy-First Architecture

The console never calls vendor APIs directly. All live data flows through a single backend proxy at API_BASE (default: http://localhost:3001). The proxy holds vendor credentials, fans out to NinjaRMM, SentinelOne, ConnectWise, Huntress, etc., and returns a normalised JSON response. This keeps secrets off the client, enables audit logging, and means the frontend only ever needs a single x-api-key header.

Two Core Fetch Wrappers
fetchWithFallback(path, fn) GET requests. Tries API_BASE + path with x-api-key header and an 8s abort timeout. On any failure (network error, timeout, non-2xx) falls back silently to fn(), which returns mock data. Used for read operations: tickets, alerts, client health.
postAction(payload, fn) POST to /api/action. Sends {ticketId, action, api, tech, ts, ...fields} as JSON. Returns {ok:bool, result:string, ts:string}. On failure falls back to fn() which returns a mock success. Used for every state-changing operation.
Initialisation Flow
  1. 1
    DOMContentLoaded → loadDashboard()
    Restores saved theme from localStorage, then calls loadDashboard(). This calls renderAll() immediately for a fast first paint, then starts startAutoRefresh(30000).
  2. 2
    startAutoRefresh(30000)
    Clears any existing interval timer, then sets a 30-second interval calling refreshAll(). The timer reference is stored in _refreshTimer to prevent duplicate timers on repeated calls.
  3. 3
    refreshAll() → doRefresh()
    The suite-standard wrapper. Delegates to doRefresh() which is the actual DEMO/LIVE branch implementation. In demo mode, falls through to mock data. In live mode, polls all three proxy endpoints.
  4. 4
    doRefresh() — three parallel fetches
    Calls fetchWithFallback('/api/tickets?level=N'), fetchWithFallback('/api/alerts'), and fetchWithFallback('/api/client-health'). Maps real responses into TICKETS, CLIENTS arrays. Then calls renderAll().
  5. 5
    renderAll() — six render functions
    Calls renderRailStats(), renderTickerFeed(), renderTicketQueue(), renderVendorSidebar(), renderDashWidgets(), renderRightSidebar(). All read only from state arrays — purely presentational.
Proxy Endpoint Contract
Expected proxy endpoints — implement in your server.js
// READ endpoints (GET)
GET  /api/tickets?level=N        → [{id,p,subj,client,sla,age,assignee,cat,board},…]
GET  /api/alerts                 → {critical,warning,slaBreaches,atRisk,bkpFails,envScore}
GET  /api/client-health          → [{name,score,risk,devices,alerts,backupFails,patchPct,open},…]

// WRITE endpoint (POST) — action fan-out
POST /api/action                 → {ok:bool, result:string, ts:string}

// Action types handled by proxy:
//   create-ticket    → CW Manage POST /service/tickets
//   close-ticket     → CW Manage PATCH /service/tickets/{id} status=Closed
//   add-note         → CW Manage POST /service/tickets/{id}/notes
//   escalate-ticket  → CW Manage PATCH /service/tickets/{id} assignedTo+priority
//   reassign-ticket  → CW Manage PATCH /service/tickets/{id} assignedTo
//   run-diag         → NinjaRMM + S1 + CW Automate + Auvik (multi-vendor read)
//   vendor-diag      → single-vendor read by api field
//   S1 Full Scan     → S1 POST /web/api/v2.1/agents/actions/scan
//   Huntress Scan    → Huntress POST /v1/agents/{id}/scan
//   Restart Service  → CW Automate POST /cwa/api/v1/scripts/run
//   Ninja Push Patch → NinjaRMM POST /v2/device/{id}/maintenance-windows
//   Isolate Endpoint → S1 POST /web/api/v2.1/agents/actions/disconnect (L2+)
//   Reset Password   → Entra ID PATCH /v1.0/users/{id} passwordProfile
//   Verify Backup    → Acronis GET /api/2/resources/{id} lastBackup status
//   Block IP/Domain  → Umbrella POST /admin/v2/destinationlists/{id}/destinations
//   Quarantine Email → Mimecast POST /api/email/hold-summary/create-hold
04

Audit Summary

12 findings across every button, action, and data-fetching call

Audit Complete — 12 Issues Found, All Fixed

No KTC home bar was present. The proxy wrapper pattern (fetchWithFallback, postAction) was already in place and well-implemented. Issues were concentrated in toast-only actions that bypassed the existing proxy wiring, four missing suite-pattern functions, and one vendor API limitation that was silently misrepresented in the UI.

IDSeverityItemStatus
F-01MEDIUMMissing suite pattern: loadDashboard(), startAutoRefresh(), refreshAll() — bare setInterval(doRefresh,30000) used insteadFixed All three added; init uses loadDashboard()
F-02HIGHQuick Action "Escalate" button fired showToast() only — bypassed existing execEscalate() function entirelyFixed onclick → execEscalate()
F-03HIGHQuick Action "Close Tkt" fired showToast() only — bypassed confirm-close modal and postAction() flowFixed onclick → prepareCloseTicket()
F-04MEDIUM"Sync APIs" button fired showToast() only — did not trigger any actual fetch cycleFixed onclick → doRefresh()
F-05MEDIUMexecEscalate() was toast + audit-log only — no postAction() call, ticket not updated in CW ManageFixed Wired through postAction({action:'escalate-ticket'})
F-06HIGHcreateTicket() was toast + audit-log only — no CW Manage ticket creation ever occurredFixed Wired through postAction({action:'create-ticket'})
F-07MEDIUMrunDiag() was a hardcoded setTimeout animation — no proxy call, no vendor dataFixed Wrapped in postAction({action:'run-diag'}); animation is now the fallback
F-08MEDIUMrunVendorDiag() was dual-toast with 1.2s delay — no proxy call, no vendor dataFixed Wired through postAction({action:'vendor-diag',api:id})
F-09LOWL3/L4 Advanced Triage: all 4 act-btns (IOC Hunt, Mem Forensic, Event Timeline, Net Isolate) were toast-onlyFixed All route through execAction() with correct vendor api
F-10LOWTicket header "Assign →" button fired toast only — no CW Manage reassignmentFixed onclick → execReassign() → postAction({action:'reassign-ticket'})
F-11INFOCSAT — UI implied a CSAT survey API call. ConnectWise Manage REST API has no CSAT endpointDocumented Limitation comment added to submitNoteAndClose()
F-12INFODashboard widget vendor rows fire showToast("Opening X console…") — these are navigation affordances, not API callsNo change Correct behaviour — they launch vendor sub-consoles via v.link
05

Fixes Applied

What changed and why

Suite Pattern — loadDashboard / startAutoRefresh / refreshAll

The existing doRefresh() function already contained the full DEMO/LIVE fetch logic. The suite pattern was added as a thin wrapper layer. loadDashboard() calls renderAll() immediately for instant first paint, then starts the 30-second cycle. refreshAll() delegates to doRefresh() — no logic was duplicated.

New pattern — init and refresh cycle
function loadDashboard(){
  renderAll();                 // instant first paint from mock data
  startAutoRefresh(30000);    // start 30s cycle
}

function startAutoRefresh(ms){
  if(_refreshTimer) clearInterval(_refreshTimer);
  _refreshTimer = setInterval(function(){ refreshAll(); }, ms||30000);
}

async function refreshAll(){
  // DEMO: falls through to mock in doRefresh
  // LIVE: polls /api/tickets + /api/alerts + /api/client-health
  await doRefresh();
}
Three Toast-Only Quick Actions Wired

The three Quick Action sidebar buttons that were firing toasts without doing anything real now route to their correct functions. "Escalate" → execEscalate() which POSTs through postAction(). "Close Tkt" → prepareCloseTicket() which opens the confirm-close modal with the resolution note template. "Sync APIs" → doRefresh() which actually polls the proxy.

execEscalate, createTicket, runDiag, runVendorDiag Wired

All four functions now call postAction() with a typed action field that the proxy can route to the correct vendor. Each retains its original mock behaviour as the fallback — the animated diagnostic sweep in runDiag(), for example, now runs as the fallback response when the proxy is not live.

execReassign Added (New Function)

A new execReassign() function replaces the toast on the "Assign →" ticket header button. It opens a technician picker modal and calls submitReassign() which POSTs {action:'reassign-ticket', assignee} to the proxy. The proxy maps this to ConnectWise Manage PATCH /v4_6_release/apis/3.0/service/tickets/{id}.

06

Vendor API Limitations

Genuine constraints that affect what the console can do via API

ConnectWise Manage — No CSAT Survey Endpoint

The ConnectWise Manage REST API has no endpoint to trigger a CSAT (Customer Satisfaction) survey. The "CSAT survey triggered" text in the close workflow and the checklist item "CSAT survey triggered" are workflow reminders only. CSAT surveys are delivered automatically by the ConnectWise Client Portal when a ticket is closed — this happens server-side based on Admin → Setup Tables → Survey configuration, not via a REST call from this console. The close button label "Close Ticket & Send CSAT" is aspirational UI copy; no programmatic CSAT call is made.

SentinelOne — Network Isolation Requires threatId

The Isolate Endpoint action (L2+) routes to S1 POST /web/api/v2.1/agents/actions/disconnect. This requires the agent's uuid (not the ticket ID). The proxy must resolve the affected device's S1 agent UUID from the ticket context before calling the S1 API. If no agent UUID is available, the action will fail gracefully and return an error to the execution log.

ConnectWise Automate — Restart Service Requires Script ID

The "Restart Service" action routes to CW Automate POST /cwa/api/v1/scripts/run. The proxy must know the specific script ID in the Automate library that performs the service restart. There is no generic "restart service" command — the proxy needs a pre-created automation script. Document the script ID in your proxy's environment config.

Entra ID — Reset Password via Graph API Requires Permissions

The "Reset Password" action routes to Microsoft Graph API PATCH /v1.0/users/{id} passwordProfile. This requires the proxy's service principal to have Directory.AccessAsUser.All or UserAuthenticationMethod.ReadWrite.All permissions in the tenant. Temporary access passes (TAPs) require TemporaryAccessPassAuthenticationMethod.ReadWrite.All.

Tech CSAT Score (4.8★) — Static Demo Only

The CSAT score shown in the technician profile in the left sidebar is a static demo value. ConnectWise Manage does not expose a per-technician CSAT score via the REST API. CSAT data is available only via the Reporting API or direct database query, both of which require separate integration not covered by the standard REST endpoints.

07

Header Rail

Top-bar elements — stats, role switcher, controls

Brand / LogoStatic. Displays "FIELD · OPS / FIELD OPS CONSOLE". No actions.
Global SearchSearches TICKETS array by subject, ID, or client name. Enter key opens the matching ticket in the ticket view. No backend call — searches local array only in demo mode; activates when proxy returns real ticket data.
Critical rb-critP1 ticket count + random offset (R(2,4)) in demo mode. In live mode, reads freshAlerts.critical from /api/alerts.
Warnings rb-warnRandom R(10,18) in demo. From freshAlerts.warning in live mode.
Open Tkts rb-tktLive count of TICKETS array length. Updates on every renderRailStats() call.
SLA Breach rb-slaCount of tickets where sla === 'breach'.
At Risk rb-riskCount of clients where risk === 'red'.
Bkp Fails rb-bkpSum of backupFails across all clients.
Env Score rb-envRandom R(74,88)% in demo. From freshAlerts.envScore in live mode.
Role Switcher (L1–L4)Calls setRole(N). Updates document.body.className to lN, triggering the RBAC CSS visibility system. Rebuilds the action grid if a ticket is open.
API Badge rb-api◌ MOCK when FETCH_ENABLED=false. Switches to ◉ LIVE (green) when live data arrives from /api/alerts.
↻ RefreshCalls doRefresh(). Also triggered by Ctrl+R.
08

Alert Ticker

Scrolling live-event feed below the header rail

A horizontally scrolling CSS-animated ticker (@keyframes tscrl, 55s cycle) showing 10 alert items. In demo mode the items are a static hardcoded array in renderTickerFeed(). Clicking any item shows a toast with the full text. The ticker pauses on hover. Items duplicate themselves to fill the scroll width.

Live Mode — Activates When Proxy Is Live

In live mode the proxy can return a /api/ticker feed. The current implementation uses a static array. When proxy is live, update renderTickerFeed() to call fetchWithFallback('/api/ticker', fallbackFn) and map the response.

09

Alert Banner

Full-width critical strip above the work area

A collapsible red banner at the top of the workspace grid area. Only visible when P1 tickets are in the queue (zone.classList.contains('has-alert')). Auto-updates via updateBanner() on every renderRailStats() call. Shows count of P1 tickets and SLA breaches with a truncated summary of active P1 subjects. Dismiss button removes the has-alert class from the zone div, hiding it until the next render cycle detects new P1s.

10

Left Sidebar

Tech profile, ticket queue, quick actions, vendor console links

Tech Profile

Shows the current technician's avatar initials, name, level badge, and CSAT score. Updates dynamically when the role switcher changes. Four hardcoded demo personas: Arnold K. (L1), Maria S. (L2), Dev P. (L3), Lead Eng. (L4). In live mode the proxy can return the authenticated technician's data from ConnectWise Manage GET /v4_6_release/apis/3.0/system/members/{id}.

Ticket Queue

Rendered by renderTicketQueue(). Shows up to ~8 tickets in a scrollable list. Each row shows priority badge (P1–P4), truncated subject, client name, SLA indicator (✓/!/⚠ with blink animation on breach), and age. Clicking a row calls openTicket(idx) which switches the centre area to ticket view. Active ticket row is highlighted with the .active class.

Queue Filters
ALLShows all tickets. Badge shows total count.
P1Filters to priority p1 only. Badge shows P1 count.
P2Filters to priority p2 only. Badge shows P2 count.
MINEFilters to tickets where assignee === TECH.name.
Quick Actions (Post-Fix)
ButtonBefore FixAfter Fix
+ New TicketopenNewTicket() — correctNo change needed
⬆ EscalateshowToast() onlyexecEscalate() → postAction()
⚡ Run DiagrunDiag() — correctNow wired through postAction()
✓ Close TktshowToast() onlyprepareCloseTicket() → modal → postAction()
📖 KB SearchopenKB() — correctNo change needed
⟳ Sync APIsshowToast() onlydoRefresh() → actual fetch cycle
Vendor Console Links

Rendered by renderVendorSidebar() from the VENDORS array (17 entries). Each row shows status dot, icon, name, category, and latency. Clicking opens the Vendor Detail Pane (openVendorPane(v)) which shows API status, latency bars, and a button to open the vendor's sub-console HTML file in a new tab.

11

Dashboard View

Default centre area — 3-column widget grid

The default centre view is a 3-column CSS grid of glassmorphism widget cards. Replaced by ticket view when a ticket is opened. Returns when closeTicket() is called.

WidgetSpanData SourceLive Status
Operations Summaryspan 3renderRailStats() — mirrors rail KPIsLives on proxy when FETCH_ENABLED=true
Backup Healthcol 1Static bkpData array in renderDashWidgets()Activates when proxy is live
Security & Threat Intelcol 2Static secData array in renderDashWidgets()Activates when proxy is live
Network / RMMcol 3Static netData array in renderDashWidgets()Activates when proxy is live
Client Health Overviewspan 2CLIENTS array — bar chart per clientLives on /api/client-health
L4 Profitability & EHRcol 1 (L4 only)CLIENTS array + random margin R(18,42)L4 only — activates when proxy is live
API Statuscol 3VENDORS array first 10 entriesActivates when proxy is live
12

Ticket View

Activated by clicking any ticket in the queue

openTicket(idx) hides the dashboard grid, shows #ticket-view, and populates all elements from TICKETS[idx]. The ticket header shows ID, priority badge, subject, client, SLA status, and the assignee button. Below it are the 6-step progress indicator and the 7-tab workflow nav.

Ticket Header Controls (Post-Fix)
✕ CloseReturns to dashboard via closeTicket(). Does not close the ticket — just navigates back.
Priority badgeStatic display. Colour-coded P1 (red) / P2 (orange) / P3 (blue) / P4 (dim).
SLA indicatorBlinking red animation on breach. Live SLA data from /api/ticket/:id when proxy is live.
{Assignee} ▸Fixed (F-10): Calls execReassign() which opens a technician picker modal and POSTs {action:'reassign-ticket'} via submitReassign(). Proxy maps to CW Manage PATCH /service/tickets/{id}.
Notes Area (Bottom of Ticket View)
✨ Auto-SuggestCalls autoSuggestNote() which generates a timestamp-stamped triage note from the active ticket fields. Populates the textarea. No API call.
⎘ CopyCopies textarea content to clipboard via navigator.clipboard. No API call.
↵ Save NoteCalls saveNote() which adds the note text to AUDIT_LOG and shows a toast. Note: This is a local audit-log-only save. The full note-to-CW flow goes through the Confirm-Close modal's submitNoteAndClose() which POSTs {action:'add-note'} to the proxy.
Suggestion chipsPrefab note phrases (Confirmed with client, S1 clean, Service restart, Escalated, Root cause, KB reference) that call insertNote(txt) to append text to the textarea.
13

Right Sidebar

Client context, related cases, vendor health, live audit log

Client ContextShows the selected client's health score (big number, colour-coded), four bar metrics (Patch %, Backup fails, Alert count, Device count), and tags. Updates when a ticket is opened (loadClientContext(idx)) or when a client health row is clicked in the dashboard.
Related CasesStatic RELATED_CASES array: two prior resolved tickets and one KB article for the same client. In live mode the proxy can return related tickets from CW Manage GET /v4_6_release/apis/3.0/service/tickets?conditions=company/name='X' AND status/name='Closed'&pageSize=3.
Vendor HealthFirst 8 entries of VENDORS array with status dots. Clicking opens the Vendor Detail Pane. In live mode the proxy pings each vendor's health endpoint and returns status/latency.
Live Audit LogAUDIT_LOG array rendered as a timestamped list. Prepended by every action (ticket open, note save, action execute, escalation, close). A live-sim interval injects random events every 22–40 seconds via simEvent(). Max 30 entries — oldest are trimmed.
14

Tab 01 — Intake

Ticket details and intake checklist

Displays the ticket's core fields (ID, priority, client, category, board, assignee, age, SLA) in an info grid. Followed by a 6-item intake checklist. Checklist items toggle .done state on click via toggleChk(el) — purely client-side, not persisted to CW Manage. Hotkey: press 1 while ticket is open.

15

Tab 02 — Triage

Three-Pass System checklist and diagnostic snapshot

Implements the Three-Pass Triage methodology: Pass 1 (confirm symptom, quick win check), Pass 2 (cross-reference NinjaRMM, SentinelOne, Huntress, Mimecast), Pass 3 (deep-dive for unresolved P1/P2). The diagnostic output terminal shows real-time results from runDiag(). L3/L4-only Advanced Triage section (visible when body has .l3 or .l4 class) provides four direct action buttons now properly wired through execAction(). Hotkey: 2.

16

Tab 03 — Investigate

Vendor diagnostic cards and investigation notes terminal

Shows four vendor diagnostic cards (NinjaRMM, SentinelOne, CW Automate, Auvik) with live data fields. Each card has a "▶ Run Query" button that now calls runVendorDiag(id, nm), which POSTs {action:'vendor-diag', api:id} to the proxy and refreshes the card rows with the returned data. The investigation notes terminal below is a pre-populated mock showing a multi-vendor query sequence — in live mode this is replaced by real proxy response lines. Hotkey: 3.

17

Tab 04 — Remediate

Action grid, execution log, change management checklist

The action grid renders from the ACTIONS array, filtered by role. L1 sees 7 actions (non-destructive). L2+ sees all 9 including Isolate Endpoint and Block IP/Domain. Each button calls execAction(nm, api). Hotkey: 4.

Destructive Action Warning

"Isolate Endpoint" (SentinelOne network isolation) and "Block IP/Domain" (Umbrella + SonicWall) are irreversible without a change approval process. The L2+ warning strip is shown above the grid when role is L2 or higher. Both actions require the proxy to have valid agent UUID / destination list ID resolved from the ticket context.

18

Tab 05 — Verify / Doc

Verification checklist, documentation checklist, status terminal

Two checklists: Verification (4 items — client confirmation, 15-minute hold, vendor dashboard health, downstream check) and Documentation (5 items — root cause, resolution steps, KB article, time entries in CW PSA, client communication). Both are client-side toggle only. The status terminal at the bottom is populated in live mode by a final proxy health-check query. Hotkey: 5.

19

Tab 06 — Close / Escalate

Escalation card and confirm-close modal flow

Escalation Card

Two dropdowns: "Escalate To" (L2/L3/L4/Vendor TAC/Security Team) and "Reason". The "⬆ ESCALATE TICKET" button calls execEscalate() which reads the dropdown values and POSTs {action:'escalate-ticket', escalateTo, reason}. Proxy maps to CW Manage PATCH /service/tickets/{id} updating assignedTo and priority fields.

Confirm-Close Modal Flow
  1. 1
    ✓ CLOSE TICKET & SEND CSAT
    Calls prepareCloseTicket(). Computes elapsed time since ticketOpenTime. Builds a resolution note template and injects it into the modal textarea with the current timestamp.
  2. 2
    Resolution note modal opens
    Editable textarea with 4000-character limit (CW Manage note length limit). Live character counter with warn/danger colour thresholds at 3800/4000. "↺ Revert to Template" resets to the auto-generated text.
  3. 3
    ↵ Submit & Close Ticket
    Calls submitNoteAndClose(). Two sequential postAction() calls: {action:'add-note', text:finalNote} then {action:'close-ticket', status:'Closed', doneFlag:true}. Both must succeed for the ticket to be removed from the local queue.
  4. 4
    CSAT — workflow reminder only
    The "CSAT survey sent" toast is informational. ConnectWise Manage REST API has no CSAT endpoint. CSAT delivery is handled server-side by CW Client Portal rules.

Keyboard: Esc closes the modal. Enter (when focus is not in the textarea) submits.

20

Tab 07 — History

Activity timeline for the open ticket

A vertical timeline rendered from HISTORY_ITEMS. Each item shows a timestamp, dot state (done/active/pending/warn), title, and sub-text. In demo mode these are static demo entries for TKT-10087. In live mode the proxy returns the ticket's full activity history from CW Manage GET /v4_6_release/apis/3.0/service/tickets/{id}/notes combined with S1/Ninja action logs. Hotkey: 7.

21

Remediation Actions

All 9 actions — vendor API mapping and RBAC visibility

Actionapi fieldVendor EndpointMin RoleDestructive
Restart Servicecw-autoCW Automate: POST /cwa/api/v1/scripts/run {scriptId, computerId}L1No
S1 Full Scans1SentinelOne: POST /web/api/v2.1/agents/actions/scan (agentUUIDs array)L1No
Huntress ScanhuntressHuntress: POST /v1/agents/{id}/scan — triggers deep threat huntL1No
Ninja Push PatchninjaNinjaRMM: POST /v2/device/{id}/maintenance-windows — force patch cycleL1No
Reset Passwordcw-autoMicrosoft Graph: PATCH /v1.0/users/{id} passwordProfile (via Entra ID)L1No
Verify BackupacronisAcronis: GET /api/2/resources/{id} — last backup job statusL1No
Quarantine EmailmimecastMimecast: POST /api/email/hold-summary/create-hold (messageId)L1No
Isolate Endpoints1SentinelOne: POST /web/api/v2.1/agents/actions/disconnect (agentUUIDs)L2+YES
Block IP/DomainumbrellaUmbrella: POST /admin/v2/destinationlists/{id}/destinations + SonicWall: POST /api/sonicos/network/dns/snwl/{id}L2+YES
execAction() Flow

Every action button calls execAction(nm, api). The function: (1) puts the button in a loading state (.running), (2) appends a timestamped entry to the execution log terminal, (3) calls postAction({ticketId, action:nm, api, tech, ts}), (4) on success renders the result in green, prepends to AUDIT_LOG, (5) restores button state. The proxy receives the action string and api field and routes to the correct vendor endpoint.

22

Proxy Architecture

What the backend must implement to make every action live

API_BASEDefault: http://localhost:3001. Set to your proxy URL in the config block near the top of the script section.
API_KEYDefault: 'super-secret-uuid-1234-5678'. Sent as x-api-key header on every request. Replace with a real secret and match in your proxy's environment config.
FETCH_ENABLEDSet to true when your proxy is running. Controls the DEMO/LIVE branch in both fetchWithFallback() and postAction().
FETCH_TIMEOUTDefault: 8000ms. Uses AbortController — if the proxy doesn't respond in 8s, the call times out and falls back to mock data.
Minimum Proxy Endpoints to Implement
Method + PathPurposeFan-out Target
GET /api/tickets?level=NTicket queue for role NCW Manage GET /service/tickets (filter by board and assignedTo)
GET /api/alertsRail KPI statsAggregate from S1 threats, NinjaRMM alerts, CW SLA engine
GET /api/client-healthClient health barsCW Manage GET /company/companies + NinjaRMM device counts + Acronis
POST /api/actionAll state-changing operationsRouted by action field — see architecture section code block
23

Vendor Catalogue

All 17 vendors in the VENDORS array — category, API status, link target

IDVendorCategoryDemo StatusSub-Console Link
cw-psaConnectWise PSAPSAOK · 82msconnectwise-psa.html
cw-autoCW AutomateRMMOK · 95msconnectwise-automate.html
ninjaNinjaRMMRMMOK · 68msninjarmm.html
s1SentinelOneEDROK · 114mssentinelone.html
huntressHuntressMDROK · 88mshuntress.html
mimecastMimecastEmail SecWARN · 210msmimecast.html
umbrellaCisco UmbrellaDNS SecOK · 77msumbrella.html
acronisAcronisBackupOK · 134msacronis.html
axcientAxcientBDRCRIT · offlineAxcientBackupConsole-Demo.html
coveCove (N-able)BackupOK · 102mscove.html
dattoDatto RMMRMMOK · 91msdatto.html
auvikAuvikNetworkOK · 73msauvik.html
merakiMerakiNetworkOK · 88msmeraki-dashboard.html
sonicwallSonicWallFirewallWARN · 188mssonicwall-msp.html
rocketRocketCyberSOCOK · 99msrocketcyber.html
scalepadScalePadAssetsOK · 112msscalepad.html
8x88x8 CPaaSVoiceOK · 156ms8x8.html
24

RBAC — L1 to L4

CSS-driven visibility system — body.lN class controls element display

Role visibility is entirely CSS-driven. When setRole(N) is called, document.body.className is set to 'lN'. Elements tagged with RBAC classes are shown/hidden by cascade rules defined in the style block.

CSS ClassVisible WhenUsage
.l4-onlybody.l4 onlyL4 Profitability widget, L4-exclusive actions
.l3-upbody.l3, body.l4L3+ visible elements
.l3-l4-onlybody.l3, body.l4Advanced Triage section in Tab 2
.l2-upbody.l2, body.l3, body.l4Destructive action warning strip, L2+ context
.l3-l4-flexbody.l3, body.l4 (display:flex)Flex-layout L3/L4 elements
Client-Side RBAC Only

This RBAC system is purely cosmetic — it controls UI visibility only. Any tech can switch to L4 in the browser. For production, role enforcement must be done at the proxy: the proxy receives tech and level in every postAction() payload and must reject destructive actions from low-level techs. Do not rely on the CSS visibility system as a security boundary.

25

Refresh Cycle

30-second auto-refresh, manual trigger, and response mapping

Auto-refresh30-second setInterval via startAutoRefresh(30000). Timer stored in _refreshTimer. Calling startAutoRefresh() again safely clears the previous timer before setting a new one.
Manual refresh↻ Refresh button in the topbar calls doRefresh(). Ctrl+R also triggers it. "Sync APIs" Quick Action now also calls doRefresh().
Response mappingIf the proxy returns a different field name than the frontend expects (e.g. ticketId vs id, company vs client), doRefresh() normalises via the mapping layer in each fetch block using || fallbacks.
Live indicatorWhen FETCH_ENABLED=true and /api/alerts returns successfully, the API badge updates from ◌ MOCK to ◉ LIVE and all rail stats reflect live values.
26

Activation Checklist

Ordered steps to go live from demo mode

  1. 01
    Deploy backend proxy on Node/Express (or equivalent)
    Implement the four endpoints: GET /api/tickets, GET /api/alerts, GET /api/client-health, POST /api/action. Start with ConnectWise Manage integration — it covers ticket CRUD, notes, and reassignment. All other vendor fan-outs can be added incrementally.
  2. 02
    Configure API_BASE, API_KEY, FETCH_ENABLED
    In the config block near the top of the script section: set API_BASE to your proxy URL, set API_KEY to your proxy's shared secret (match in your proxy .env), set FETCH_ENABLED = true.
  3. 03
    Register ConnectWise clientId at developer.connectwise.com
    The CW Manage REST API requires a clientId header (UUID) on every request. Register your proxy application at the developer portal to obtain it. Without it, all CW Manage calls return 401.
  4. 04
    Implement action router in proxy by action string
    The proxy receives {action:'S1 Full Scan', api:'s1', ticketId, tech}. Route by action string to the correct vendor endpoint. Start with the non-destructive actions (scan, verify backup, diag), then gate destructive ones (isolate, block) behind role verification.
  5. 05
    Resolve device/agent identifiers from ticket context
    Several actions need more than a ticket ID — S1 scan needs an agent UUID, CW Automate restart needs a computerId, Acronis verify needs a resource ID. Your proxy must look up these identifiers from NinjaRMM or CW Manage using the client name and device name from the ticket.
  6. 06
    Verify in browser — confirm API badge flips to ◉ LIVE
    Load the console, check DevTools → Network for the three GET calls on load. Confirm 200 responses. The badge in the top-right should switch from ◌ MOCK to ◉ LIVE. Rail stats should reflect real ticket and alert counts.
  7. 07
    Test ticket close flow end-to-end
    Open a test ticket, navigate to Tab 6, click "✓ CLOSE TICKET & SEND CSAT", review the modal, submit. Verify in CW Manage that the ticket has a resolution note and status of Closed. Verify the ticket disappears from the queue on the next refresh cycle.
  8. 08
    Enforce role at proxy level — do not rely on CSS RBAC
    The CSS visibility system is UI-only. Configure your proxy to reject action:'Isolate Endpoint' or action:'Block IP/Domain' when the tech field resolves to an L1 role in your CW Manage system/members data. Log all action attempts with the tech identifier regardless of outcome.
27

Troubleshooting

Common issues and resolutions

API badge stays ◌ MOCK after setting FETCH_ENABLED=true
  • Check that API_BASE points to your running proxy (default: http://localhost:3001).
  • Open DevTools → Network. The three GET calls should show. If they fail with CORS errors, add Access-Control-Allow-Origin: * headers to your proxy, or serve the dashboard from the same origin.
  • If you get a 401, verify the API_KEY value matches the secret in your proxy .env.
  • The badge only flips to LIVE when /api/alerts returns successfully. Even if /api/tickets works, the badge won't update until alerts succeed too.
postAction() returns ok:false after ticket close attempt
  • Check the proxy logs for which of the two sequential POSTs failed (add-note vs close-ticket).
  • The 4000-character note limit is enforced client-side before the POST — check the character counter in the modal. If it shows danger (red), the note was too long but was still submitted; add server-side length validation in your proxy.
  • CW Manage's POST /service/tickets/{id}/notes requires the ticket to be in an open state. If the ticket was already closed externally, the note POST will fail.
Isolate Endpoint or Block IP/Domain fails silently
  • These actions require the proxy to resolve a device identifier (S1 agent UUID for Isolate, Umbrella destination list ID for Block). If the proxy can't resolve the identifier from ticket context, it returns {ok:false, error:'agent not found'}.
  • Ensure your proxy has a lookup table or query path from ticket client name → NinjaRMM device → S1 agent UUID.
  • S1 network isolation also requires the S1 API token to have the Endpoints → Disconnect permission on the correct scope (account or group level).
Quick Action "Escalate" or "Close Tkt" does nothing when no ticket is open
  • Both execEscalate() and prepareCloseTicket() guard on ACTIVE_TKT !== null. If no ticket is open in the ticket view, they show a "⚠ No active ticket" toast and return early. Open a ticket first from the queue, then use the quick actions.
runDiag() shows mock animation even with proxy live

The animated mock steps are the fallback inside postAction()'s second argument. When the proxy IS live, the animation still runs while the real query executes — the proxy result (result.lines array) is applied afterwards only if FETCH_ENABLED=true AND result.lines is present. Your proxy must return a lines array in the response shape {ok:true, lines:[{cls:'t-g', txt:'…'}, …]} for the real data to replace the animation output.

MSP Field Ops Console · 17 Vendors · 9 Actions · L1–L4 RBAC