The L1 Triage Console is a sub-page of the KrawTech Automation Platform. It does not include standalone authentication or its own navigation — both are injected by the parent platform's nav.js. The console exists at the top of the dispatch pipeline: a human engineer sees a Copilot-surfaced queue of tickets, reads AI-generated context, and with a single button press approves or cancels an automated action.
No engineer needs to log into ConnectWise Manage, NinjaRMM, or SentinelOne directly. All vendor API calls are proxied through a local Node.js server that holds credentials. The browser communicates only with that proxy.
Current state: All UI is fully built. Buttons, tabs, the dispatch bar, and stat cards render correctly in demo mode. All API calls are written, fully commented, and ready to activate — they are intentionally behind a proxy-offline guard that falls through to realistic demo data until the proxy is running.
The console follows a browser → local proxy → vendor API pattern used across the entire KrawTech suite. The browser never holds vendor credentials. All secrets live in the proxy's environment variables.
Four stat cards show real-time queue health. In demo mode they display hardcoded values with a random drift applied every 30 seconds. When the proxy is live, they are populated by loadDashboard() on initial load and re-populated by refreshAll() every 30 seconds.
| Card | Value | Real Data Source |
|---|---|---|
| Critical / High | Count of P1+P2 tickets | GET /cw/service/tickets?conditions=priority/name="Critical" OR priority/name="High" |
| Awaiting Triage | Open tickets in "New" or "Needs Triage" status | GET /cw/service/tickets?conditions=status/name="New" |
| In-Flight | Tickets in "In Progress" or "Dispatched" | GET /cw/service/tickets?conditions=status/name="In Progress" |
| Resolved Today | Tickets closed today | GET /cw/service/tickets?conditions=dateClosedSince=[today]&status/name="Closed" |
.loading on the card). This is stripped when applyStats() is called successfully.
The pipeline strip shows the four layers of the KrawTech automation stack. L1 is always active (highlighted). This is a static visual element — it reflects architecture, not real-time runbook state. When runbook execution status is added in a future version, the active step will advance dynamically based on a runbook progress endpoint.
| Step | Layer | Description | Real-time? |
|---|---|---|---|
| 1 | L1 // Triage | Human review and approval | Always active |
| 2 | Intent Dispatch | Copilot → L2 intent routing | Static — future: runbook status API |
| 3 | Task Routing | L2 → L3 task assignment | Static — future: runbook status API |
| 4 | Execution Dispatch | L3 → L4 vendor execution | Static — future: runbook status API |
Three tab views — Triage, In-Flight, and Resolved — each rendered by renderTickets(listId, tickets). Tickets are generated from the API response shape or, in demo mode, from the DEMO object which mirrors that shape exactly.
Each ticket card contains: severity badge, ticket ID, age, title, client name, Copilot suggestion bar, and 1–3 action buttons. Severity classes drive the left accent stripe color and badge color.
| Class | Color | CW Priority Mapping |
|---|---|---|
| .sev-critical | Red | Priority: Critical (P1) |
| .sev-high | Orange | Priority: High (P2) |
| .sev-medium | Yellow | Priority: Medium (P3) |
| .sev-low | Cyan | Priority: Low (P4) |
| .sev-info | Blue | Informational / monitoring |
| .sev-done | Green | Resolved / closed |
GET /v4_6_release/apis/3.0/service/tickets — filtered by status and priority. The mapCWTickets() function (in proxy or browser layer) transforms the CW response shape into the internal ticket format consumed by renderTickets().
Each ticket contains a cyan-bordered .copilot-bar showing an AI-generated triage suggestion. In demo mode this text is hardcoded in the DEMO object. In live mode, the Copilot suggestion is either:
Option A — Pre-generated: stored as a ticket note in CW Manage (tagged with a Copilot prefix) and retrieved with GET /service/tickets/{id}/notes.
Option B — On-demand: the proxy calls an LLM (Claude via Anthropic API) with ticket context and returns a suggestion inline at ticket load time.
The current build uses Option A in demo mode (hardcoded) and routes to the proxy's /copilot/suggest endpoint in live mode.
The bottom dispatch bar accepts free-text commands. On Enter or clicking DISPATCH, sendDispatch(text) fires. It appends the command as an internal note on the most recently active ticket via POST /cw/service/tickets/{id}/notes.
In demo mode a toast confirms the command was "queued". In live mode the proxy parses the intent, selects the correct runbook, and forwards it to the appropriate L4 tool (Automate, NinjaRMM, or direct API call).
| Button | Handler | Vendor API (via proxy) |
|---|---|---|
| 🔒 Confirm Isolate | isolateEndpoint() | POST /sentinelone/agents/actions/disconnect SentinelOne v2.1 — body: { filter: { ids: [agentId] } } |
| ▶ Dispatch IR Runbook | dispatchRunbook() | POST /cw/service/tickets/{id}/notes CW Manage — appends COPILOT_DISPATCH:ir-ransomware note |
| ▶ WoL + Restart RPC | wolRestart() | POST /ninja/device/{id}/script NinjaRMM v2 — runs WoL + Restart-Service RpcSs PowerShell |
| ▶ Push Enrollment | dispatchRunbook() | POST /cw/service/tickets/{id}/notes Runbook: mfa-bulk-enroll → L4 Graph API script |
| ⚙ Fix CA Policy | dispatchRunbook() | POST /cw/service/tickets/{id}/notes Runbook: ca-policy-fix → L4 Graph API script |
| ✓ Approve Reboots | approveReboots() | POST /ninja/device/{id}/reboot NinjaRMM v2 — per device in patch job |
| ⛔ Halt Runbook | haltRunbook() | POST /runbook/halt { ticketId } Custom proxy endpoint — cancels active Automate/Ninja job |
| ▶ Purge + Rerun Backup | dispatchRunbook() | POST /cw/service/tickets/{id}/notes Runbook: disk-purge-backup → Automate script |
| Reopen | reopenTicket() | PATCH /cw/service/tickets/{id} Body: [{ op:"replace", path:"/status/name", value:"New" }] |
| View Evidence / Report | viewEvidence() | GET /cw/service/tickets/{id}/notes Returns internal notes with evidence attachments |
| Ping Devices | pingDevices() | GET /ninja/device/{id}/status NinjaRMM v2 — agent online/offline state |
| Defer Patch Window | deferPatch() | PATCH /cw/service/tickets/{id} Updates sub-type to Deferred in CW Manage |
Base URL: https://{cw-host}/v4_6_release/apis/3.0
Auth: Basic — company+publicKey:privateKey (Base64) or OAuth2 bearer token
Docs: developer.connectwise.com/Products/Manage/REST
| Method | Endpoint | Used For |
|---|---|---|
| GET | /service/tickets | Load triage queue, in-flight, and stat counts. Params: conditions, pageSize, orderBy. |
| GET | /service/tickets?conditions=dateClosedSince=[today] | Resolved today count |
| GET | /service/tickets/{id}/notes | View evidence, retrieve Copilot suggestion notes |
| POST | /service/tickets/{id}/notes | Dispatch runbook intent, cancel jobs, Copilot command |
| PATCH | /service/tickets/{id} | Reopen ticket, defer patch window, escalate, update status |
Base URL: https://app.ninjarmm.com/v2
Auth: OAuth2 client_credentials — client_id + client_secret
Docs: app.ninjarmm.com/apidocs
| Method | Endpoint | Used For |
|---|---|---|
| GET | /devices | List managed devices, agent online status |
| GET | /device/{id}/status | Ping device — returns agent connectivity state |
| POST | /device/{id}/script | Dispatch PowerShell (WoL, Restart-Service RpcSs) |
| POST | /device/{id}/reboot | Approve pending reboots in patch deployment |
Base URL: https://{tenant}.sentinelone.net/web/api/v2.1
Auth: Authorization: ApiToken {token} header
Docs: usea1.sentinelone.net/api-doc
| Method | Endpoint | Used For |
|---|---|---|
| POST | /agents/actions/disconnect | Network isolation — removes endpoint from network. Body: { filter: { ids: [agentId] } } |
| GET | /threats | Retrieve active threat context for IR ticket Copilot bar |
Base URL: https://{automate-host}/cwa/api/v1
Auth: Basic or token
Docs: docs.connectwise.com/ConnectWise_Automate
| Method | Endpoint | Used For |
|---|---|---|
| POST | /computers/{id}/commands | Dispatch remote script (backup purge, Mimecast, etc.) |
| GET | /computers/{id} | Computer health, disk status for Copilot context |
COPILOT_DISPATCH:{runbookId}. The proxy watches for these notes via webhook or polling and triggers the actual runbook execution in Automate or NinjaRMM. This is a deliberate architectural choice — CW Manage is the record of intent, not the executor.
POST /service/tickets/{id}/notes is the only CW Manage mechanism for injecting Copilot intent. The note body uses a structured prefix (COPILOT_DISPATCH:) that the proxy parses. This is by design — it ensures all dispatched actions are logged in the ticket audit trail.
POST /agents/actions/disconnect requires the S1 internal agent ID, not the hostname. The proxy must maintain a hostname → agent ID lookup table populated from GET /agents on startup or refreshed on each isolation request. Ticket metadata (client + device name) is used to resolve the correct agent ID.
The proxy URL is set at the top of the dashboard JS in the PROXY object. Change base to your deployed proxy address. The proxy holds all vendor credentials in environment variables — never in the browser.
| Variable | Value |
|---|---|
| CW_HOST | Your CW Manage host (e.g. na.myconnectwise.net) |
| CW_COMPANY | CW company ID |
| CW_PUBLIC_KEY | CW API member public key |
| CW_PRIVATE_KEY | CW API member private key |
| NINJA_CLIENT_ID | NinjaRMM OAuth2 client ID |
| NINJA_CLIENT_SECRET | NinjaRMM OAuth2 client secret |
| S1_HOST | SentinelOne management URL |
| S1_API_TOKEN | SentinelOne API token |
| AUTOMATE_HOST | CW Automate host |
| AUTOMATE_TOKEN | Automate API token |
server.js on localhost:3001 (or your deployed host). Confirm GET http://localhost:3001/health returns { ok: true }.
.env file. Run npm run verify-creds to confirm each vendor connection succeeds.
PROXY.base in the dashboard JS from localhost:3001 to your deployed proxy URL if not running locally.
apiFetch() call block has the real endpoint commented above the mock throw. Remove the await new Promise(...); throw lines and uncomment the real fetch(url, {...}) blocks.
mapCWTickets() function must transform CW response fields (priority.name, status.name, etc.) into the dashboard's internal ticket shape. Confirm field names match your CW Manage API version.
GET /agents (SentinelOne) and cache a hostname → agentId map. This is needed for the Confirm Isolate button to resolve the correct agent.
loadDashboard() successfully, setStatus('live') is called and the yellow demo banner automatically hides. If it stays visible, the proxy is still returning an error.
| Symptom | Cause | Fix |
|---|---|---|
| Demo banner stays visible after proxy is running | CORS not configured on proxy, or proxy URL mismatch | Add Access-Control-Allow-Origin: * header to proxy. Verify PROXY.base matches proxy address exactly. |
| Stat cards stay in loading shimmer | applyStats() not called — proxy returned error |
Check browser console for fetch errors. Confirm /cw/service/tickets route is implemented in proxy. |
| Buttons fire but toast says "[DEMO]" | Expected — proxy is offline. Real is the correct result in demo. | Activate proxy and uncomment real fetch() blocks as described in step 04 of activation checklist. |
| Isolate button fails with 404 | S1 agent ID not resolved | Build hostname → agentId lookup map on proxy startup. Verify SentinelOne tenant URL and API token are correct. |
| Ticket list is empty after live data loads | mapCWTickets() field mapping mismatch |
Log the raw CW API response. Verify priority.name, status.name, and id field paths match your CW version. CW Manage field names vary by version. |
| Clock shows "--:--" | JavaScript not loaded | Check for JS errors in console. Confirm no Content Security Policy blocks inline scripts. |
| Tab switching stops working | event.target.closest('.tab') fails if event context is lost |
Pass the button element explicitly: change onclick to onclick="switchTab('triage', this)" and update the function signature. |