The CW Automate Ops Console replaces direct portal monitoring for MSP operations teams managing RMM agent fleets. It combines agent inventory, patch compliance, alert feeds, script execution, and monitor trigger counts into a single always-on interface. Engineers can search, filter, and drill into any agent to see system metrics, run remote commands, and view the event timeline without switching tabs.
The console uses a three-column layout: left (API health, patch donut, client breakdown), center (agent inventory table + 24h check-in chart), and right (script runner, alert feed, top monitors). A slide-out drawer provides full agent detail and remote command output.
| Filename | dashboard-stack-cw-automate.html |
| Suite | MSP · CMD Design Suite — Stack Intelligence Layer |
| Fonts | Orbitron (display), Share Tech Mono (mono), Rajdhani (body) |
| Mode | Demo — const DEMO = true in JS |
| Live Activation | Set DEMO = false, uncomment apiFetch() production block |
| Refresh Interval | 30 seconds — startAutoRefresh(30000) |
| Vendor API | ConnectWise Automate REST API v1 |
| API Base Path | https://your-server.com/automate/api/v1 |
| Auth Method | POST /apitoken → Bearer token (24h); header: Authorization: CWA token=<bearer> |
| State Persistence | In-memory only — filter, sort, selection reset on refresh |
| KTC Home Bar | Removed — style block, div, and body{padding-top:36px} all stripped |
| Data Source | Status | Live API Endpoint |
|---|---|---|
| Agent list | Demo | GET /automate/api/v1/computers |
| Agent detail | Demo | GET /automate/api/v1/computers/{id} |
| Open alerts | Demo | GET /automate/api/v1/alerts |
| Client list | Demo | GET /automate/api/v1/clients |
| Scripts catalog | Demo | GET /automate/api/v1/scripts |
| Script history | Demo | GET /automate/api/v1/commands |
| Patch status | Demo | GET /automate/api/v1/patches |
| Monitor triggers | Demo | GET /automate/api/v1/monitors |
| Run script on agent | Proxy Needed | POST /automate/api/v1/computers/{id}/scripts/{scriptId} |
| Remote commands | Proxy Needed | POST /automate/api/v1/computers/{id}/commands |
| Acknowledge alert | Proxy Needed | POST /automate/api/v1/alerts/{id}/ack |
| Schedule script | Proxy Needed | POST /automate/api/v1/schedules |
| Create ticket | CW Manage API | CW Manage: POST /v4_6_release/apis/3.0/service/tickets (separate system) |
| Network isolation | No API | No CW Automate endpoint — requires EDR/firewall integration |
| Health probe | Derived | No /health endpoint — liveness inferred from /computers response |
| Identifier | Type | Purpose |
|---|---|---|
DEMO | Boolean | Master mock/live switch. true = serve mock data. Set false + uncomment production fetch block to go live. |
agents | Array | Working copy of all agents. Built by buildAgents() on first load, then mutated by state-change simulations. |
ackedAlerts | Set | Alert IDs that have been acknowledged. Session-only (not persisted). |
agentFilter | String | Active filter for agent table: all | online | offline | alert | patch |
agentSort | Object | { field, dir } — current sort column and direction. |
hist | Object | Sparkline history arrays for each KPI (max 12 samples, sliding window). |
execHistory | Array | Script execution log (max 8 entries). Displayed in exec-log panel. |
loadDashboard() | Function | Suite entry point. Called on DOMContentLoaded. Builds agents if empty, calls refreshAll(). |
refreshAll() | Async Function | DEMO/live branch controller. In demo: mutates state + calls all render functions. In live: fetches from proxy. |
startAutoRefresh(30000) | Function | Sets 30s setInterval on refreshAll() and a 1s countdown on the header label. |
apiFetch(endpoint, mockFn) | Async Function | Unified fetch wrapper. Production block (commented out) calls /api/automate + endpoint with timeout. Falls back to mockFn(). |
A 44px fixed header containing the CW logo mark, title, and right-side controls. The right side includes two simulation buttons (Demo/testing only), a refresh indicator with pulsing green dot and countdown label, and a timestamp of the last sync. Clicking the dot/label triggers refresh() (alias for refreshAll()).
| Control | Function | Live Behavior |
|---|---|---|
+ Agent Event | simulateAgent() | Demo only — randomly mutates an agent’s state and fires a toast |
⚡ Simulate Outage | simulateOutage() | Demo only — sets all agents for a random client to offline |
| Pulsing dot + label | refresh() | Manual refresh; triggers full refreshAll() cycle |
| Timestamp | Updated in refreshAll() | Shows @ HH:MM:SS of last successful data refresh |
Six KPI cards in a 6-column grid. Each has a large number, a sparkline chart (last 12 polls), a label, and a delta badge. Hovering reveals the API endpoint tooltip. Clicking opens a detail drawer with the full endpoint, trend chart, and a sample JSON response. All six sparklines are maintained in the hist sliding window (12 samples max).
| KPI | Element ID | Live API Endpoint | Notes |
|---|---|---|---|
| Total Agents | kTotal | GET /computers | Total count from response pagination; delta shows daily new enrollments |
| Online | kOnline | GET /computers?condition=online eq 1 | Corrected from ?online=true (invalid CW Automate param) |
| Offline | kOffline | GET /computers?condition=online eq 0 | Corrected from ?online=false |
| Open Alerts | kAlerts | GET /alerts | Count of open, unacknowledged alerts |
| Patch Pending | kPatches | GET /patches?condition=status eq "pending" | Corrected from ?status=pending |
| Scripts Run/24h | kScripts | GET /commands | Corrected from non-existent /scripts/history |
Left column, top. Displays six API endpoint health rows showing name, latency in ms, and a status pill (ok/warn/deg/dn). In demo mode all latencies are randomly generated on each refresh. Clicking a row opens the API detail drawer showing P50/P95/P99 latency, a 10-bar request history, and the auth endpoint reference.
/health endpoint in CW Automate. The health check previously probed /automate/api/v1/health which does not exist. After the audit fix, liveness is inferred from the /computers response status. Status pills reflect mock latency in demo mode; in live mode they should reflect actual response time from the fetch wrapper.Left column, middle. A donut chart SVG with four segments (Current, Pending, Critical, Outdated) and a legend list. The center text shows the overall compliance percentage. In demo mode, patch states are assigned randomly to each generated agent. When live, this aggregates from GET /automate/api/v1/patches.
| Segment | Color | Source Field | CSS Class |
|---|---|---|---|
| Current | Green | patch === 'current' | pb-ok |
| Pending | Orange | patch === 'pending' | pb-pend |
| Critical | Red | patch === 'critical' | pb-crit |
| Outdated | Amber | patch === 'outdated' | pb-warn |
Left column, bottom (scrollable). Horizontal bar chart showing agent count per client, sorted descending. Bars are normalized to the largest client. In demo mode, 7 mock clients are generated (Acme Corp, Harbor Tech, Sunrise MSP, BlueWave Inc, Redstone IT, Cascade Health, Nordic Systems) with 5–15 agents each. Live: GET /automate/api/v1/clients.
Center column, main panel. Sortable, filterable table of all agents. A search box filters by name, client, or OS. Filter chips narrow by status (Online/Offline/Alert) or patch state (Patch). Column headers for Agent, Client, Status, and Last Seen are sortable (ascending/descending). Clicking a row opens the agent detail drawer.
| Column | Field | Notes |
|---|---|---|
| Agent | a.name | Bold; sortable |
| Client | a.client | Sortable |
| OS | a.os | Abbreviated for space: Win 11, Mac, Ub. |
| Status | a.status | Colored dot + label: online (green), alert (amber), offline (red). Row fades to 55% opacity if offline. |
| Patch | a.patch | Colored badge: Current/Pending/Critical/Outdated |
| Last Seen | a.lastSeen | Relative time string; sortable by a.lastSeenMin |
Bottom of the center panel (fixed height). 24 vertical bars representing agent check-ins per hour. Business hours (8–17) generate higher simulated counts (180–480). Bars above 70% of peak are highlighted in cyan. Hovering a bar shows the count in a tooltip. Peak hour is shown in the panel header. Generated fresh on each refresh by mActivity(). In live mode, this should be sourced from the /computers or /commands endpoint with timestamp aggregation.
Right column, top. A two-dropdown + two-button interface for running and scheduling scripts. Script dropdown has 8 built-in options. Target dropdown selects scope: All Online Agents, Selected Client, or Selected Agent. An execution log below shows the last 8 script runs with time, name, and status (running… / ok / fail).
| Control | Function | Live API Call |
|---|---|---|
| ▶ Run Script | runScript() | POST /automate/api/v1/computers/{id}/scripts/{scriptId} — corrected from POST /scripts/{id}/run |
| ⏰ Schedule | scheduleScript() | POST /automate/api/v1/schedules — corrected from POST /scripts/{id}/schedule |
| Exec log | logExec(name, status) | Visual only — populated from execHistory array; no separate API call |
POST /computers/{id}/scripts/{scriptId} endpoint per computer. There is no batch script-run endpoint in the CW Automate API.Right column, middle (scrollable, grows to fill). Colored left-border alert cards sourced from mAlerts(agents). Three severity levels: 1=red (critical), 2=orange (warn), 3=amber (info). Three actions appear on hover: Ack, → Ticket, Dismiss.
| Button | Original (Wrong) | Corrected |
|---|---|---|
| Ack | PATCH /alerts/{id} | POST /automate/api/v1/alerts/{id}/ack |
| → Ticket | POST /service/tickets (CW Manage) | CW Manage: POST /v4_6_release/apis/3.0/service/tickets — documented as limitation; not a CW Automate endpoint |
| Dismiss | Visual only | Visual only — removes from DOM, no API call |
Right column, bottom. Six monitor trigger rows with name, a normalized horizontal bar, and count. Generated by mMonitors() with fixed labels and random counts. In live mode: GET /automate/api/v1/monitors. The monitors endpoint is valid in CW Automate v1 API.
A 440px slide-in panel from the right, opened by clicking any agent row or KPI card. Overlays the UI with a dim background. The agent drawer shows API calls made, system info grid, remote command buttons, and a recent event timeline. KPI drawers show endpoint detail and sparkline trend.
| Section | Content | API Source |
|---|---|---|
| API Calls Made | Shows 3 GET calls: computer detail, patches, alerts for this agent | GET /computers/{id}, /computers/{id}/patches, /computers/{id}/alerts |
| System Info | Status, Uptime, Agent Ver, Location, CPU%, RAM%, Disk%, Patch badge | From agents array in memory |
| Remote Commands | Ping, Get Services, Screenshot, Sys Info, Reboot, Isolate | POST /automate/api/v1/computers/{id}/commands |
| Recent Timeline | 2–4 timestamped events per agent | Built by buildAgentTimeline() |
| Button | Online Agent | Offline Agent | Live API Call |
|---|---|---|---|
| Left button | Isolate (red) | Repair Agent (default) | Isolate: no CW Automate endpoint — documented limitation. Repair: POST /automate/api/v1/commands |
| Run Script | runScriptOn(id) | runScriptOn(id) | POST /automate/api/v1/computers/{id}/scripts/1 |
| Open in Control | openControl(id) | openControl(id) | Opens ConnectWise Control (ScreenConnect) — no REST API call; URL-based launch |
https://your-server.com/automate/api/v1. Swagger UI: /automate/api/v1/swagger/ui/index.{"UserName": "…", "Password": "…", "TwoFactorPasscode": "…"}. Returns token valid for 24 hours. All subsequent requests use Authorization: CWA token=<bearer>.condition query parameter for filtering. Pagination via page and pagesize params. Response includes totalCount in header.?online=true (a boolean string) which is not a valid CW Automate API parameter. Correct syntax uses the condition parameter with Automate query language: online eq 1.POST /scripts/{id}/run which is not a valid CW Automate endpoint.POST /automate/api/v1/commands without the computer ID sub-path.condition filtering. Response includes alert ID, severity, computer name, and message.PATCH /alerts/{id} which is not the correct CW Automate acknowledge verb. The CW Automate API uses a /ack sub-action via POST.condition=status eq "pending".GET /scripts/history which does not exist in the CW Automate API. Execution records are in /commands.condition=lastRunResult eq "fail" or similar.POST /scripts/{id}/schedule which is not a valid CW Automate endpoint.POST https://your-manage-server.com/v4_6_release/apis/3.0/service/tickets. This requires a separate CW Manage API key (clientId + public/private key pair). The alert action toast now documents this limitation explicitly.| # | Location | Original | Fixed |
|---|---|---|---|
| 1 | CSS & HTML | #ktc-demo-bar style block, div, body{padding-top:36px} | Removed entirely |
| 2 | apiFetch() call | /automate/api/v1/health (does not exist) | Replaced with /computers probe; limitation documented in comment |
| 3 | mApiHealth() | Silently probed non-existent /health | Added code comment documenting no /health endpoint exists |
| 4 | KPI endpoint tags (HTML) | ?online=true / ?online=false | ?condition=online eq 1/0 |
| 5 | showKpiDrawer() map | ?online=true / ?online=false | ?condition=online%20eq%201/0 |
| 6 | repairAgent() | POST /computers/{id}/repair (no such endpoint) | POST /automate/api/v1/commands + limitation comment |
| 7 | isolateAgent() | POST /computers/{id}/isolate (no such endpoint) | Toast now says “via EDR/firewall (not a CW Automate endpoint)” + comment |
| 8 | runScriptOn() | POST /scripts/1/run (wrong path) | POST /automate/api/v1/computers/{id}/scripts/1 |
| 9 | scheduleScript() | POST /scripts/{id}/schedule (wrong path) | POST /automate/api/v1/schedules |
| 10 | ackAlert() | PATCH /alerts/{id} (wrong verb) | POST /automate/api/v1/alerts/{id}/ack |
| 11 | createTicket() | POST /service/tickets (CW Manage endpoint, not Automate) | Toast documents limitation: “via CW Manage API (not a CW Automate endpoint)” |
| 12 | remoteCmd() output display | POST /automate/api/v1/commands (missing computer ID sub-path) | POST /automate/api/v1/computers/{agentId}/commands |
CW Automate uses a two-step token-based auth. First, POST credentials to get a Bearer token. Then include it in all subsequent requests. The token format differs from standard Bearer — it uses the CWA token= prefix in the Authorization header.
| Auth type | CW Automate proprietary token (NOT standard Bearer) |
| Header format | Authorization: CWA token=<token> |
| Token lifetime | 24 hours; must be refreshed via POST /apitoken |
| Token storage | Proxy env vars: CWA_USER, CWA_PASS; never in HTML |
| 2FA | Optional; pass empty string if not required |
The frontend calls /api/automate/*. The proxy injects auth headers and forwards to the CW Automate server. Token refresh should be handled server-side with a cached token renewed before expiry.
Isolate button in the drawer was originally wired to a non-existent POST /computers/{id}/isolate endpoint. In production, isolation must be handled by an integrated EDR (Huntress, Defender for Endpoint) or network infrastructure (VLAN reassignment, firewall rule push). The toast message now clearly documents this limitation./health or /ping endpoint. Liveness must be inferred from the response status of a lightweight read endpoint like GET /computers?pagesize=1. The API health panel uses this approach after the audit fix./commands endpoint, not a dedicated /scripts/history sub-path. The Scripts Run/24h KPI was referencing a non-existent endpoint. Filter GET /commands by date range for daily execution counts./computers, /alerts, /patches) return paginated results. Default page size is typically 100. For MSPs with 1000+ agents, the proxy must page through all results using ?page=N&pagesize=100 to build a complete dataset. The current frontend assumes the proxy returns a complete flat array.POST /computers/{id}/scripts/{scriptId} for each computer. This must be implemented as a server-side loop, not a single API call.const DEMO = false.- Provision a server-side proxy (Node.js/Express, Cloudflare Worker, or equivalent)
- Set environment variables:
CWA_USER,CWA_PASS,AUTOMATE_URL(e.g.https://your-server.com) - Implement token acquisition:
POST /automate/api/v1/apitoken→ cacheCWAIISToken - Implement token refresh logic (before 24h expiry)
- Implement proxy route:
/api/automate/*→AUTOMATE_URL/automate/api/v1/*withAuthorization: CWA token=<token> - Handle pagination:
/computerswith 100+ agents requires looping?page=N&pagesize=100 - Set CORS headers to allow requests from the console’s serving origin
- Test
GET /api/automate/computers→ verify agent count matches Automate portal - Test
GET /api/automate/alerts→ verify alert list matches open alerts in Automate
- Open
dashboard-stack-cw-automate.htmland locateconst DEMO = truenear the bottom of the script block - Set
const DEMO = false - In
apiFetch(), uncomment the production block (8 lines starting withtry {) - Comment out (or delete) the demo fallback
return new Promise(resolve => setTimeout(...))block - Reload — the header label should show “Live” instead of “Demo”
- Verify agent count in the Total Agents KPI matches the Automate portal
- Verify the client breakdown bars reflect real client names
- Test clicking an agent row → confirm API calls section in drawer shows real latency
- Test Ack on an alert → confirm the POST call returns 200/204 from the proxy
| Feature | Change When Live |
|---|---|
| KPI counters | Real agent, alert, and patch counts from Automate |
| Agent table | Real device names, client names, OS versions, patch states |
| Alert feed | Real open alerts from GET /alerts |
| Client breakdown | Real client list and agent counts from GET /clients |
| Patch donut | Real patch status distribution |
| Ack alert | Calls POST /alerts/{id}/ack — real state change in Automate |
| Run Script | Calls POST /computers/{id}/scripts/{scriptId} — real execution |
| Remote commands | Calls POST /computers/{id}/commands — real remote execution |
| 30s auto-refresh | Pulls fresh data from all endpoints every 30 seconds |
| Field / Location | Default | Purpose |
|---|---|---|
const DEMO | true | Master mock/live switch. Set false + uncomment fetch block. |
startAutoRefresh(30000) | 30 000 ms | Refresh interval. Increase to 60 000 for busy Automate instances. |
CLIENTS array | 7 mock clients | In production, sourced from GET /clients. Change mock names to match real clients for demo. |
| Agent pool size | 5–15 per client | buildAgents() — rnd(5,16). Adjust range for density. |
| Online ratio | 82% online | R() > 0.18 in buildAgents(). 0.18 = 18% offline rate. |
| Alert ratio | 22% of online agents | R() > 0.78 in buildAgents(). Tune for demo realism. |
| Sparkline samples | 12 | if(arr.length>12)arr.shift() in rKPI(). Increase for longer trend window. |
| Exec log max | 8 entries | if(execHistory.length>8) execHistory.pop(). Increase for more history. |
| Activity chart hours | 24 | Array.from({length:24}...) in mActivity(). |
| Peak hour coloring | 70% of max | v>max*.7 in rActivity(). Bars above this threshold turn cyan. |
| Agent table columns | 6 columns | Agent, Client, OS, Status, Patch, Last Seen. Add columns in renderAgents() and corresponding <th> elements. |
| Drawer width | 440px | CSS .drawer{width:440px}. Widen for more detail. |
| Toast duration | 3000ms | setTimeout(...)},3000) in toast() function. |
| Fetch timeout | 8000ms | AbortSignal.timeout(8000) in apiFetch() production block. Adjust for slow Automate servers. |
Cause: loadDashboard() or refreshAll() was not called, or the agents array was empty when rKPI() ran.
Fix (demo): Check browser console for JS errors. Confirm buildAgents() returns a non-empty array. Verify DOMContentLoaded listener calls loadDashboard().
Fix (live): Confirm the proxy returns valid JSON from /api/automate/computers. Check that the response has a results array.
Cause: Active filter plus search text yields no matches, or agents array is empty.
Fix: Click the “All” filter chip. Clear the search box. Call renderAgents() from the console to verify the agents array is populated. In live mode, log agents in the console after a refreshAll() call to confirm data shape matches expectations.
Cause: The CW Automate alert ID format from the mock (ALT-3000) differs from real Automate alert IDs (numeric). Or the proxy is not routing POST /alerts/{id}/ack correctly.
Fix: Verify the alert object from GET /alerts includes an id field (integer). Update the alert rendering to use the real ID. Confirm the proxy routes POST /api/automate/alerts/{id}/ack → POST /automate/api/v1/alerts/{id}/ack.
Cause: In live mode, the script ID from the select dropdown (value="1" through "8") are mock IDs that do not correspond to real scripts in the Automate instance.
Fix: In production, populate the script dropdown from GET /automate/api/v1/scripts and use real script IDs. The target computer must also be selected from real agent IDs from the agents array.
Cause: Bearer token is expired, or the Authorization: CWA token= header format is wrong (standard Bearer prefix does not work with CW Automate).
Fix: Re-acquire the token via POST /automate/api/v1/apitoken. Confirm the header format is exactly CWA token=<tokenValue> (note: not Bearer). Implement token refresh in the proxy before the 24h expiry.
Cause: openAgentDrawer(id) could not find the agent in the agents array (ID mismatch), or the drawer/overlay elements are missing from the DOM.
Fix: Confirm agents.find(x => x.id === id) returns a truthy value. In live mode, agent IDs from the API must be stored as numeric values matching what is passed in row onclick. Check browser console for “Cannot read properties of undefined” errors.
Cause: startAutoRefresh() was not called, or the DOMContentLoaded event did not fire (e.g. script was blocking).
Fix: Confirm the script block ends with a document.addEventListener('DOMContentLoaded', ...) listener that calls both loadDashboard() and startAutoRefresh(30000). Manually call refreshAll() from the browser console to test the data cycle in isolation.
Cause: mPatches(agents) returned all zeroes (all agents have the same patch state), or total is zero causing a divide-by-zero in percentage calculation.
Fix (demo): Verify the PATCH constant array contains all four values and buildAgents() assigns random patch states. Confirm agents is not empty. Fix (live): Confirm the proxy returns patch records from /patches with a valid status field matching current|pending|critical|outdated.