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.
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.
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.
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.
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.
Intended Audience
Who uses this console and at what role level
| Role | Level | Visible Features | Restricted Features |
|---|---|---|---|
| L1 Technician | L1 | Full ticket queue, workflow tabs 1–7, safe remediation actions, notes, KB search, dashboard view | Isolate Endpoint, Block IP/Domain, Advanced Triage section, L4 Profit Widget |
| L2 Senior Tech | L2 | All L1 + destructive remediation actions (Isolate Endpoint, Block IP/Domain) | L4 Profit Widget, some L3/L4 advanced triage UI |
| L3 Engineer | L3 | All L2 + Advanced Triage section (IOC Hunt, Mem Forensic, Event Timeline, Net Isolate) | L4 Profit Widget |
| L4 Lead Engineer | L4 | All L3 + L4 Profitability & EHR widget on dashboard | Nothing — full access |
Architecture
Data flow, proxy pattern, and state management
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.
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.
/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.
- 1DOMContentLoaded → loadDashboard()Restores saved theme from localStorage, then calls
loadDashboard(). This callsrenderAll()immediately for a fast first paint, then startsstartAutoRefresh(30000). - 2startAutoRefresh(30000)Clears any existing interval timer, then sets a 30-second interval calling
refreshAll(). The timer reference is stored in_refreshTimerto prevent duplicate timers on repeated calls. - 3refreshAll() → 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. - 4doRefresh() — three parallel fetchesCalls
fetchWithFallback('/api/tickets?level=N'),fetchWithFallback('/api/alerts'), andfetchWithFallback('/api/client-health'). Maps real responses into TICKETS, CLIENTS arrays. Then callsrenderAll(). - 5renderAll() — six render functionsCalls
renderRailStats(),renderTickerFeed(),renderTicketQueue(),renderVendorSidebar(),renderDashWidgets(),renderRightSidebar(). All read only from state arrays — purely presentational.
// 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
Audit Summary
12 findings across every button, action, and data-fetching call
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.
| ID | Severity | Item | Status |
|---|---|---|---|
| F-01 | MEDIUM | Missing suite pattern: loadDashboard(), startAutoRefresh(), refreshAll() — bare setInterval(doRefresh,30000) used instead | Fixed All three added; init uses loadDashboard() |
| F-02 | HIGH | Quick Action "Escalate" button fired showToast() only — bypassed existing execEscalate() function entirely | Fixed onclick → execEscalate() |
| F-03 | HIGH | Quick Action "Close Tkt" fired showToast() only — bypassed confirm-close modal and postAction() flow | Fixed onclick → prepareCloseTicket() |
| F-04 | MEDIUM | "Sync APIs" button fired showToast() only — did not trigger any actual fetch cycle | Fixed onclick → doRefresh() |
| F-05 | MEDIUM | execEscalate() was toast + audit-log only — no postAction() call, ticket not updated in CW Manage | Fixed Wired through postAction({action:'escalate-ticket'}) |
| F-06 | HIGH | createTicket() was toast + audit-log only — no CW Manage ticket creation ever occurred | Fixed Wired through postAction({action:'create-ticket'}) |
| F-07 | MEDIUM | runDiag() was a hardcoded setTimeout animation — no proxy call, no vendor data | Fixed Wrapped in postAction({action:'run-diag'}); animation is now the fallback |
| F-08 | MEDIUM | runVendorDiag() was dual-toast with 1.2s delay — no proxy call, no vendor data | Fixed Wired through postAction({action:'vendor-diag',api:id}) |
| F-09 | LOW | L3/L4 Advanced Triage: all 4 act-btns (IOC Hunt, Mem Forensic, Event Timeline, Net Isolate) were toast-only | Fixed All route through execAction() with correct vendor api |
| F-10 | LOW | Ticket header "Assign →" button fired toast only — no CW Manage reassignment | Fixed onclick → execReassign() → postAction({action:'reassign-ticket'}) |
| F-11 | INFO | CSAT — UI implied a CSAT survey API call. ConnectWise Manage REST API has no CSAT endpoint | Documented Limitation comment added to submitNoteAndClose() |
| F-12 | INFO | Dashboard widget vendor rows fire showToast("Opening X console…") — these are navigation affordances, not API calls | No change Correct behaviour — they launch vendor sub-consoles via v.link |
Fixes Applied
What changed and why
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.
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(); }
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.
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.
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}.
Vendor API Limitations
Genuine constraints that affect what the console can do via API
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.
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.
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.
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.
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.
Header Rail
Top-bar elements — stats, role switcher, controls
rb-critP1 ticket count + random offset (R(2,4)) in demo mode. In live mode, reads freshAlerts.critical from /api/alerts.rb-warnRandom R(10,18) in demo. From freshAlerts.warning in live mode.rb-tktLive count of TICKETS array length. Updates on every renderRailStats() call.rb-slaCount of tickets where sla === 'breach'.rb-riskCount of clients where risk === 'red'.rb-bkpSum of backupFails across all clients.rb-envRandom R(74,88)% in demo. From freshAlerts.envScore in live mode.setRole(N). Updates document.body.className to lN, triggering the RBAC CSS visibility system. Rebuilds the action grid if a ticket is open.rb-api◌ MOCK when FETCH_ENABLED=false. Switches to ◉ LIVE (green) when live data arrives from /api/alerts.doRefresh(). Also triggered by Ctrl+R.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.
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.
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.
Left Sidebar
Tech profile, ticket queue, quick actions, vendor console links
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}.
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.
assignee === TECH.name.| Button | Before Fix | After Fix |
|---|---|---|
| + New Ticket | openNewTicket() — correct | No change needed |
| ⬆ Escalate | showToast() only | execEscalate() → postAction() |
| ⚡ Run Diag | runDiag() — correct | Now wired through postAction() |
| ✓ Close Tkt | showToast() only | prepareCloseTicket() → modal → postAction() |
| 📖 KB Search | openKB() — correct | No change needed |
| ⟳ Sync APIs | showToast() only | doRefresh() → actual fetch cycle |
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.
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.
| Widget | Span | Data Source | Live Status |
|---|---|---|---|
| Operations Summary | span 3 | renderRailStats() — mirrors rail KPIs | Lives on proxy when FETCH_ENABLED=true |
| Backup Health | col 1 | Static bkpData array in renderDashWidgets() | Activates when proxy is live |
| Security & Threat Intel | col 2 | Static secData array in renderDashWidgets() | Activates when proxy is live |
| Network / RMM | col 3 | Static netData array in renderDashWidgets() | Activates when proxy is live |
| Client Health Overview | span 2 | CLIENTS array — bar chart per client | Lives on /api/client-health |
| L4 Profitability & EHR | col 1 (L4 only) | CLIENTS array + random margin R(18,42) | L4 only — activates when proxy is live |
| API Status | col 3 | VENDORS array first 10 entries | Activates when proxy is live |
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.
closeTicket(). Does not close the ticket — just navigates back./api/ticket/:id when proxy is live.execReassign() which opens a technician picker modal and POSTs {action:'reassign-ticket'} via submitReassign(). Proxy maps to CW Manage PATCH /service/tickets/{id}.autoSuggestNote() which generates a timestamp-stamped triage note from the active ticket fields. Populates the textarea. No API call.navigator.clipboard. No API call.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.insertNote(txt) to append text to the textarea.Right Sidebar
Client context, related cases, vendor health, live audit log
loadClientContext(idx)) or when a client health row is clicked in the dashboard.GET /v4_6_release/apis/3.0/service/tickets?conditions=company/name='X' AND status/name='Closed'&pageSize=3.simEvent(). Max 30 entries — oldest are trimmed.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.
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.
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.
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.
"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.
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.
Tab 06 — Close / Escalate
Escalation card and confirm-close modal flow
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.
- 1✓ CLOSE TICKET & SEND CSATCalls
prepareCloseTicket(). Computes elapsed time sinceticketOpenTime. Builds a resolution note template and injects it into the modal textarea with the current timestamp. - 2Resolution note modal opensEditable 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↵ Submit & Close TicketCalls
submitNoteAndClose(). Two sequentialpostAction()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. - 4CSAT — workflow reminder onlyThe "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.
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.
Remediation Actions
All 9 actions — vendor API mapping and RBAC visibility
| Action | api field | Vendor Endpoint | Min Role | Destructive |
|---|---|---|---|---|
| Restart Service | cw-auto | CW Automate: POST /cwa/api/v1/scripts/run {scriptId, computerId} | L1 | No |
| S1 Full Scan | s1 | SentinelOne: POST /web/api/v2.1/agents/actions/scan (agentUUIDs array) | L1 | No |
| Huntress Scan | huntress | Huntress: POST /v1/agents/{id}/scan — triggers deep threat hunt | L1 | No |
| Ninja Push Patch | ninja | NinjaRMM: POST /v2/device/{id}/maintenance-windows — force patch cycle | L1 | No |
| Reset Password | cw-auto | Microsoft Graph: PATCH /v1.0/users/{id} passwordProfile (via Entra ID) | L1 | No |
| Verify Backup | acronis | Acronis: GET /api/2/resources/{id} — last backup job status | L1 | No |
| Quarantine Email | mimecast | Mimecast: POST /api/email/hold-summary/create-hold (messageId) | L1 | No |
| Isolate Endpoint | s1 | SentinelOne: POST /web/api/v2.1/agents/actions/disconnect (agentUUIDs) | L2+ | YES |
| Block IP/Domain | umbrella | Umbrella: POST /admin/v2/destinationlists/{id}/destinations + SonicWall: POST /api/sonicos/network/dns/snwl/{id} | L2+ | YES |
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.
Proxy Architecture
What the backend must implement to make every action live
http://localhost:3001. Set to your proxy URL in the config block near the top of the script section.'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.true when your proxy is running. Controls the DEMO/LIVE branch in both fetchWithFallback() and postAction().AbortController — if the proxy doesn't respond in 8s, the call times out and falls back to mock data.| Method + Path | Purpose | Fan-out Target |
|---|---|---|
| GET /api/tickets?level=N | Ticket queue for role N | CW Manage GET /service/tickets (filter by board and assignedTo) |
| GET /api/alerts | Rail KPI stats | Aggregate from S1 threats, NinjaRMM alerts, CW SLA engine |
| GET /api/client-health | Client health bars | CW Manage GET /company/companies + NinjaRMM device counts + Acronis |
| POST /api/action | All state-changing operations | Routed by action field — see architecture section code block |
Vendor Catalogue
All 17 vendors in the VENDORS array — category, API status, link target
| ID | Vendor | Category | Demo Status | Sub-Console Link |
|---|---|---|---|---|
| cw-psa | ConnectWise PSA | PSA | OK · 82ms | connectwise-psa.html |
| cw-auto | CW Automate | RMM | OK · 95ms | connectwise-automate.html |
| ninja | NinjaRMM | RMM | OK · 68ms | ninjarmm.html |
| s1 | SentinelOne | EDR | OK · 114ms | sentinelone.html |
| huntress | Huntress | MDR | OK · 88ms | huntress.html |
| mimecast | Mimecast | Email Sec | WARN · 210ms | mimecast.html |
| umbrella | Cisco Umbrella | DNS Sec | OK · 77ms | umbrella.html |
| acronis | Acronis | Backup | OK · 134ms | acronis.html |
| axcient | Axcient | BDR | CRIT · offline | AxcientBackupConsole-Demo.html |
| cove | Cove (N-able) | Backup | OK · 102ms | cove.html |
| datto | Datto RMM | RMM | OK · 91ms | datto.html |
| auvik | Auvik | Network | OK · 73ms | auvik.html |
| meraki | Meraki | Network | OK · 88ms | meraki-dashboard.html |
| sonicwall | SonicWall | Firewall | WARN · 188ms | sonicwall-msp.html |
| rocket | RocketCyber | SOC | OK · 99ms | rocketcyber.html |
| scalepad | ScalePad | Assets | OK · 112ms | scalepad.html |
| 8x8 | 8x8 CPaaS | Voice | OK · 156ms | 8x8.html |
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 Class | Visible When | Usage |
|---|---|---|
| .l4-only | body.l4 only | L4 Profitability widget, L4-exclusive actions |
| .l3-up | body.l3, body.l4 | L3+ visible elements |
| .l3-l4-only | body.l3, body.l4 | Advanced Triage section in Tab 2 |
| .l2-up | body.l2, body.l3, body.l4 | Destructive action warning strip, L2+ context |
| .l3-l4-flex | body.l3, body.l4 (display:flex) | Flex-layout L3/L4 elements |
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.
Refresh Cycle
30-second auto-refresh, manual trigger, and response mapping
setInterval via startAutoRefresh(30000). Timer stored in _refreshTimer. Calling startAutoRefresh() again safely clears the previous timer before setting a new one.doRefresh(). Ctrl+R also triggers it. "Sync APIs" Quick Action now also calls doRefresh().ticketId vs id, company vs client), doRefresh() normalises via the mapping layer in each fetch block using || fallbacks.FETCH_ENABLED=true and /api/alerts returns successfully, the API badge updates from ◌ MOCK to ◉ LIVE and all rail stats reflect live values.Activation Checklist
Ordered steps to go live from demo mode
- 01Deploy 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. - 02Configure API_BASE, API_KEY, FETCH_ENABLEDIn the config block near the top of the script section: set
API_BASEto your proxy URL, setAPI_KEYto your proxy's shared secret (match in your proxy.env), setFETCH_ENABLED = true. - 03Register ConnectWise clientId at developer.connectwise.comThe CW Manage REST API requires a
clientIdheader (UUID) on every request. Register your proxy application at the developer portal to obtain it. Without it, all CW Manage calls return 401. - 04Implement action router in proxy by action stringThe proxy receives
{action:'S1 Full Scan', api:'s1', ticketId, tech}. Route byactionstring to the correct vendor endpoint. Start with the non-destructive actions (scan, verify backup, diag), then gate destructive ones (isolate, block) behind role verification. - 05Resolve device/agent identifiers from ticket contextSeveral 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.
- 06Verify in browser — confirm API badge flips to ◉ LIVELoad the console, check DevTools → Network for the three GET calls on load. Confirm 200 responses. The badge in the top-right should switch from
◌ MOCKto◉ LIVE. Rail stats should reflect real ticket and alert counts. - 07Test ticket close flow end-to-endOpen 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.
- 08Enforce role at proxy level — do not rely on CSS RBACThe CSS visibility system is UI-only. Configure your proxy to reject
action:'Isolate Endpoint'oraction:'Block IP/Domain'when thetechfield resolves to an L1 role in your CW Manage system/members data. Log all action attempts with the tech identifier regardless of outcome.
Troubleshooting
Common issues and resolutions
- Check that
API_BASEpoints 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_KEYvalue matches the secret in your proxy.env. - The badge only flips to LIVE when
/api/alertsreturns successfully. Even if/api/ticketsworks, the badge won't update until alerts succeed too.
- 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}/notesrequires the ticket to be in an open state. If the ticket was already closed externally, the note POST will fail.
- 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).
- Both
execEscalate()andprepareCloseTicket()guard onACTIVE_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.
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.