loadDashboard(), and a 30-second auto-refresh cycle are in place. Live data flows once the proxy is stood up and each fetchXxx() function is pointed at real endpoints.The Unified API Command Console is a single-file HTML governance dashboard for MSP API operations. It provides a centralized NOC view across all vendor integrations — surfacing system health, live API activity, security anomalies, key age and rotation status, and compliance posture in one screen. It is designed to be the operational layer for managing the credential and integration health of an MSP's full vendor stack.
fetchSystemsData, fetchAlertsData, fetchKeysData, fetchActivityData), loadDashboard(), and a 30-second startAutoRefresh() cycle are all in place. Each wrapper contains a comment with the exact fetch() call to uncomment. Mock data stays as the fallback until the proxy is live./v4_6/apis/3.0/tickets for ConnectWise, /v2/devices for Ninja, /api/v1/organizations for Meraki, etc. — are the real, documented API paths for those vendors. These are the paths the proxy will forward to.injectDemoBanner() call once the proxy is confirmed working.fetchXxx() body is pointed at real endpoints, the dashboard feeds live data with no other changes needed.fetchSystemsData() — wraps systems[], ready for fetch('/api/systems')fetchAlertsData() — wraps secAlerts[], ready for fetch('/api/security/alerts')fetchKeysData() — wraps apiKeys[], ready for fetch('/api/keys')fetchActivityData() — wraps activityPool[], ready for fetch('/api/activity/recent')loadDashboard() — orchestrates all four fetches in parallel, re-renders all panelsstartAutoRefresh(30000) — 30-second cycle, starts on page loadStatus badge — clickable, calls
loadDashboard() on demand, shows "REFRESHING…" during loadPanel header badge counts — recomputed from live data on every
loadDashboard() callCompliance metrics — recomputed from live key data (rotation, no-owner, admin counts)
createPSATicket() — Create Ticket button calls this; contains commented-out ConnectWise PSA POSTdisableApiKey() — Disable Key button calls this; contains commented-out backend POST pattern
fetchXxx() function has the real fetch() call commented out directly above the mock fallback. Uncomment and delete the mock line for each endpoint as the proxy surfaces it. No other changes required — the render functions are already wired to receive the data.For
createPSATicket(): uncomment the fetch() block inside the function. The ConnectWise PSA endpoint, auth header format, and ticket body shape are all documented in the comment.For
disableApiKey(): uncomment the fetch('/api/keys/disable') block. Keeper record update and vendor-specific revocation notes are in the comment.
POST /v1/record call. Step ⑥ Revoke contains a commented-out vendor invalidation call. Uncomment both when the proxy is ready. The grace period countdown timer is real and will govern the overlap window.activityPool[]. This continues to run as a placeholder until a real log stream (WebSocket, SIEM poll, or API gateway feed) is connected. The row rendering function generateActivityRow() stays unchanged — replace the data source, not the renderer.The dashboard has a full fetch layer in place. The architecture below shows the wired state — mock data fills each slot until the proxy routes the call to the real vendor endpoint.
| Component | Current state | Goes live when |
|---|---|---|
| Systems panel | Mock systems[] via fetchSystemsData() | Proxy exposes /api/systems |
| Activity monitor | Pool of 16 entries, randomized 1.8s | Real log stream connected to activityPool source |
| Security alerts | Mock secAlerts[] via fetchAlertsData() | Proxy exposes /api/security/alerts |
| Key inventory | Mock apiKeys[] via fetchKeysData() | Proxy exposes /api/keys from Keeper/KV |
| Compliance panel | Recomputed from live key data on every load | Already live — driven by key inventory |
| Disable Key | disableApiKey() — mock toast, real fetch commented out | Uncomment the fetch block inside the function |
| Create Ticket | createPSATicket() — mock toast, real fetch commented out | Uncomment the fetch block inside the function |
| Rotation wizard | UI complete — Generate and Revoke steps have commented fetch calls | Uncomment Keeper + vendor calls in steps ③ and ⑥ |
| Auto-refresh | startAutoRefresh(30000) — running on 30s cycle | Already running — no change needed |
The dashboard uses a named CSS grid with 7 areas across 3 columns. The grid is responsive — collapses to 2 columns at 1280px and single column at 768px.
| Zone | Panel class | Fixed height | Description |
|---|---|---|---|
| Top Banner | Sticky header | auto | Brand, stat chips, clickable status badge, system clock. No home bar — removed. |
| Ticker | .ticker-wrap | auto | Scrolling vendor status feed, 40s animation loop |
| Systems | .panel-systems | auto | CSS auto-fill grid of sys-cards, min 148px each |
| Stats bar | .statsbar | auto | 6 traffic-chip KPI cards |
| Activity | .panel-activity | 600px | Live API log table with internal scroll |
| Security | .panel-security | 600px | Alert list with internal scroll |
| Keys | .panel-keys | flex fill | Key inventory table (shares panel with Activity via tab switcher) |
| Actions | .panel-actions | flex fill | 6 action buttons + action log + pending items + on-call engineer |
| Compliance | .panel-compliance | flex fill | 2×2 metric grid + 8 policy rows |
#stat-active — updated by loadDashboard() from live system count), WARNINGS (7, yellow blink), CRITICAL ALERTS (3, red pulse), API CALLS/HR (#stat-calls, cosmetic counter).#clock — real toTimeString(), updates every 1 second. #dateline — toDateString().toUpperCase().loadDashboard() on click for on-demand refresh. Shows "● REFRESHING…" during a load cycle, reverts to "● ALL SYSTEMS NOMINAL" on completion, "● LOAD ERROR" on failure.callCount starts at 148,302, drifts ±10–40 every 3 seconds. For live use replace this interval with a real poll from your API gateway metrics.A horizontally scrolling ticker below the banner. Runs on pure CSS animation (animation: ticker 40s linear infinite). Content is duplicated in HTML for a seamless loop — 10 items appear twice, giving 20 total spans so the second half fills in as the first scrolls out.
.ticker-inner innerHTML with a JavaScript function that reads from the same data source as the systems panel. The IP address 198.51.100.42 is from RFC 5737 documentation range — intentionally not a real IP.Rendered by renderSystems() from the systems[] array into #systems-grid. Each card shows vendor name, status dot, calls/hr, and last sync time. Clicking a card opens the system detail modal.
| Status | CSS class | Left border | Dot behavior |
|---|---|---|---|
| HEALTHY | .sys-card.healthy | Green with glow | Solid green dot |
| WARNING | .sys-card.warning | Yellow with glow | Yellow dot, blink 1.4s |
| CRITICAL | .sys-card.critical | Red with glow | Red dot, blink 0.8s |
Current demo distribution: 29 healthy, 3 warning (KnowBe4, Auvik, LastPass), 2 critical (Meraki Console, Mimecast). The panel header badge counts (healthy/warning/critical/total) are now recomputed from live data on every loadDashboard() call — they will reflect real vendor status once the proxy is live.
api.{name}.com from a simple string replace — it is cosmetic. For live use, add a real endpoint field to each entry in systems[] so the modal shows the actual vendor base URL.Six traffic-chip KPI cards showing call volume by traffic source. All values are static HTML — not computed from the activity pool. Each chip has a 2px colored top accent, a large value, a percentage, and a sub-label.
| Chip | Color | Value | Pct | Sub-label |
|---|---|---|---|---|
| Corporate Network | green | 76,917 | 52% | internal · corp LAN |
| VPN Gateway | cyan | 41,525 | 28% | tunneled · remote users |
| Cloud Infrastructure | blue | 20,762 | 14% | AWS · Azure · GCP |
| Remote Agents | orange | 5,932 | 4% | endpoint · scheduled |
| Automation / Scripts | purple | 2,200 | 1.5% | cron · pipelines |
| Unknown Sources | red | 2,966 | 2% | unclassified · review |
The left panel, 600px fixed height with internal scroll. Initialized by initActivityTable() with 20 random rows from activityPool. Every 1.8 seconds a new row is prepended and the oldest row (beyond 24) is removed, simulating a live log feed.
.row-crit (red left border) for HTTP 4xx/5xx. .row-warn (yellow left border) for HTTP 429 (rate limit). Normal rows have no highlight.10. or 172. render in normal text. 198.51.100.42 renders in red — it is the RFC 5737 documentation IP used as the "unknown/suspicious" source in demo data.switchTab(tab) shows/hides #view-activity and #view-keys divs and updates badge text. Both views share the same panel-keys/panel-activity layout area.Right-column panel, 600px fixed height. Rendered by renderAlerts() from secAlerts[]. Each alert row shows a colored severity bar, type, detail text, severity tag, API key name, and timestamp. Clicking an alert opens the alert detail modal.
| Alert type | Severity | Key | What it demonstrates |
|---|---|---|---|
| Unknown IP Detected | HIGH | MERAKI-API-01 | External IP not in allowlist (Amsterdam, NL) |
| Multiple Auth Failures | HIGH | MIME-SEC-01 | 14 consecutive 401s from same external IP |
| Burst API Traffic | HIGH | AUVIK-NET-KEY | Rate limit breach — 2,400 calls/60s (normal: 180/min) |
| Key Used Outside Region | MED | KB4-PHISH-KEY | EU-WEST-1 origin for normally US-EAST key |
| Rotation Overdue | MED | CW-PSA-LEGACY | 104-day-old key past 90-day policy |
| Privilege Escalation | MED | ITG-READ-ONLY | Read-only key attempted POST — denied |
| Orphaned Key Activity | MED | LEGACY-RMM-API | No-owner key active after 47-day dormancy |
Rendered by renderKeys() from apiKeys[] (22 entries). Row highlighting: red background for critical status, yellow background for rotation status. Clicking any row opens the key detail modal.
| Status | Tag displayed | Row color | Meaning |
|---|---|---|---|
healthy | HEALTHY | Normal | In policy, used recently, owner assigned |
rotation | ROTATION REQ | Yellow row | Age >90d or policy-flagged |
critical | CRITICAL | Red row | Active security incident (auth failures, unknown IP) |
highpriv | HIGH PRIVILEGE | Normal | Admin-level permissions — requires dual approval to rotate |
unused | UNUSED | Normal | Never used or dormant (LASTPASS-INT: 142d old, never used) |
Age coloring on the age column: red if >90 days, yellow if >60 days, normal otherwise — computed inline in renderKeys(). Owner names starting with ( (e.g. (unassigned), (orphaned)) render in red.
disableApiKey(keyName, system). Contains commented-out fetch('/api/keys/disable') POST with notes on Keeper record update and vendor-specific revocation. Demo fallback fires a toast. Uncomment the fetch block when the proxy is ready.createPSATicket(). Contains the full commented-out ConnectWise PSA POST /v4_6/apis/3.0/service/tickets call with correct auth header format (Basic companyId+publicKey:privateKey) and ticket body shape. Demo fallback generates a random ticket ID. Uncomment when proxy is ready.| Button | Current behavior | Goes live when |
|---|---|---|
| Disable API Key | disableApiKey() — mock toast, real fetch commented out | Uncomment the fetch block in disableApiKey() |
| Rotate Key | 7-step wizard with mock key generation — Generate and Revoke steps have commented fetch calls | Uncomment Keeper + vendor calls in wizard steps ③ and ⑥ |
| View Logs | Modal with 5 hardcoded log lines → export toast | Replace modal body with fetch from SIEM or vendor audit endpoint |
| Export Activity | Format selector modal → toast | Backend export endpoint querying real log database |
| Create Ticket | createPSATicket() — mock toast, real fetch commented out | Uncomment the fetch block in createPSATicket() |
| Security Scan | Confirmation modal → toast | Backend job to validate keys, check allowlists, detect violations |
The panel also contains three sub-sections below the buttons: Last Actions Log (#action-log, rendered by renderActionLog(), updates when actions fire), Pending Action Items (5 hardcoded urgent/review items), and On-Call Engineer (L. Chen, static — Page/Slack/Escalate buttons fire toasts only).
Bottom-right panel. Four metric cards (2×2 grid) followed by 8 policy rows. All values are static HTML — not computed from the key inventory array.
showModal(title, bodyHTML, actions) populates and activates the single modal overlay. closeModal(e) closes on backdrop click. closeModalDirect() closes programmatically. The modal has a fade+scale-in animation (modalIn keyframe). Width is 480px default, expanded to 600px for the rotation wizard via #modal-box-inner and a dynamic style injection.
| Modal trigger | Function | Width |
|---|---|---|
| System card click | showSystemDetail(name, status, calls, sync) | 480px |
| Alert item click | showAlertDetail(type, key, sev) | 480px |
| Key row click | showKeyDetail(name) | 480px |
| Action buttons | showAction(action) → configs[action] | 480px |
| Rotate Key button | Patched via window.showAction override → startRotationWizard() | 600px |
A 7-step guided workflow for key rotation. State is held in rotState object. Each step re-renders the modal body. A visual progress bar shows completed (green ✓), active (cyan glow), and pending (dim) steps.
| # | Step | What happens | Real action needed |
|---|---|---|---|
| ① | Select Key | Shows rotation candidates (status: rotation/critical/highpriv). User selects one. | Filter live key inventory |
| ② | Review & Confirm | Shows key details, notification method selector (Email/Slack/PSA Ticket), grace period selector (30s demo / 5m / 15m / 1hr). Admin keys show dual-approval block. | Send notification via chosen channel |
| ③ | Generate New Key | Animated 5-step sequence. generateFakeKey() returns vault://keeper/IT-API-Keys/XXXXXXXX-XXXX. Key preview revealed with copy button. | Call vendor API to issue real new key; store in Keeper/KV |
| ④ | Notify Owner | Shows notification preview with owner name and chosen channel. Simulate Send button fires toast. | Real webhook/email/Slack API call |
| ⑤ | Grace Period | SVG countdown ring. Old and new keys shown as "both active." When timer hits zero, revoke button activates. | Both keys must remain valid during real grace period |
| ⑥ | Revoke Old Key | 5-step animated revocation sequence. Fires toast on completion. | Call vendor API to invalidate old key; update credential store |
| ⑦ | Complete | Green success display. Updates ACTION_LOG_ENTRIES. Done button closes modal. | Mark rotation complete in audit log |
rotState._approved = true and shows a fake approver name. For live use, this needs a real approval flow — a PSA ticket, a Slack approval bot, or a second-factor confirmation from a named engineer.showToast(msg, type) appends a toast to #toast-container (fixed bottom-right). Auto-dismisses after 3.5 seconds with a fade-out animation. Types: green (●), red (⚠), yellow (⚡), cyan (◉). Two toasts fire automatically on load: green "Console initialized" at 800ms, red "Unknown IP detected on MERAKI-API-01" at 3000ms.
| Vendor | Status | Demo calls/hr | Category |
|---|---|---|---|
| ConnectWise PSA | healthy | 12,441 | PSA |
| CW PSA Console | healthy | 5,302 | PSA |
| CW Automate | healthy | 9,102 | RMM |
| Ninja RMM | healthy | 9,208 | RMM |
| PSA Manage | healthy | 4,880 | PSA |
| Meraki Console | critical | 8,201 | Network |
| ScreenConnect | healthy | 7,440 | Remote |
| Mimecast | critical | 5,640 | Email Security |
| Huntress | healthy | 3,940 | EDR/MDR |
| IT Glue | healthy | 3,201 | Documentation |
| Datto | healthy | 4,440 | Backup |
| Acronis | healthy | 2,880 | Backup |
| Cisco Umbrella | healthy | 2,880 | DNS Security |
| Auvik | warning | 6,340 | Network Mon |
| SentinelOne | healthy | 4,102 | EDR |
| Sophos | healthy | 3,240 | AV/EDR |
| Dropsuite | healthy | 2,104 | Email Backup |
| Proofpoint | healthy | 2,104 | Email Security |
| Liongard | healthy | 2,080 | Automation |
| DUO Security | healthy | 1,883 | MFA |
| Malwarebytes | healthy | 1,440 | AV |
| KnowBe4 | warning | 1,540 | Security Training |
| Axcient | healthy | 1,204 | Backup |
| RocketCyber | healthy | 1,302 | SOC/MDR |
| N-Able Cove | healthy | 3,110 | Backup |
| Axcient/eFolder | healthy | 880 | Backup |
| Ruckus | warning | 880 | Wireless |
| Keeper | healthy | 702 | Password Mgmt |
| LastPass | warning | 612 | Password Mgmt |
| BrightGauge | healthy | 520 | Reporting |
| ScalePad | healthy | 640 | Asset Mgmt |
| 8x8 / MS Dialpad | healthy | 441 / 340 | VoIP |
The activityPool[] entries use real, documented vendor API endpoint paths. These are accurate reference material for the live integration phase.
| System | Key name | Endpoint | Vendor docs section |
|---|---|---|---|
| ConnectWise PSA | CW-PSA-PROD-01 | /v4_6/apis/3.0/tickets | Manage REST API v4.6 |
| Ninja RMM | NINJA-MAIN-KEY | /v2/devices | NinjaOne API v2 |
| Meraki Console | MERAKI-API-01 | /api/v1/organizations | Meraki Dashboard API v1 |
| Huntress | HUNT-PROD-KEY | /v1/agents | Huntress Partner API v1 |
| SentinelOne | S1-PROD-MAIN | /web/api/v2.1/threats | SentinelOne Management API v2.1 |
| Mimecast | MIME-SEC-01 | /api/email/logs | Mimecast Email Security API |
| IT Glue | ITG-READ-ONLY | /v1/organizations | IT Glue API v1 |
| DUO Security | DUO-AUTH-KEY | /admin/v1/users | Duo Admin API v1 |
| Datto | DATTO-BKP-01 | /bcdr/device | Datto BCDR API |
| Auvik | AUVIK-NET-KEY | /v1/inventory/network | Auvik API v1 |
| Acronis | ACR-BACKUP-KEY | /api/2/tenants | Acronis Cyber Cloud API v2 |
| Sophos | SOPH-CENTRAL | /whoami/v1 | Sophos Central API v1 |
| Cisco Umbrella | UMB-NET-SEC | /v2/domains | Umbrella Management API v2 |
| ScreenConnect | SC-REMOTE-KEY | /Services/PageService | ConnectWise Control REST API |
| KnowBe4 | KB4-PHISH-KEY | /v1/phishing/campaigns | KnowBe4 API v1 |
| RocketCyber | RC-SOC-KEY | /api/v3/incidents | RocketCyber API v3 |
Each entry in apiKeys[] follows this shape. The live version should pull from Keeper, Azure Key Vault, or your credential store and normalise to this schema before rendering.
The wiring is done. These are the steps to activate live data once the proxy is running. Each fetch wrapper already contains the exact commented-out call — nothing needs to be architected, just uncommented and pointed at the right endpoint.
node main.js from the proxy directory. Confirm it is listening on localhost:3000. All fetch wrappers route through this proxy.fetchSystemsData(), uncomment the return fetch('/api/systems').then(r => r.json()) line and delete the mock fallback lines. The proxy needs to expose /api/systems returning an array matching the {name, status, calls, sync} shape. Badge counts recompute automatically on the first load.return fetch('/api/security/alerts').then(r => r.json()) in fetchAlertsData(). The proxy endpoint should return an array of alert objects matching the secAlerts[] shape — see Section 10 for the field list.return fetch('/api/keys').then(r => r.json()) in fetchKeysData(). The proxy should pull from Keeper, Azure Key Vault, or your credential store and normalize to the schema in Section 19. Compliance metrics recompute automatically from this data.createPSATicket(), uncomment the full fetch() block. The ConnectWise PSA endpoint URL, auth header format (Basic companyId+publicKey:privateKey), and ticket body shape are all documented in the comment. Delete the demo toast fallback below it.disableApiKey(), uncomment the fetch('/api/keys/disable') POST block. Notes on Keeper record update and vendor-specific revocation endpoints are in the comment.renderRotationStep(), step index 2 (Generate) contains a commented-out Keeper POST /v1/record call. Step index 5 (Revoke) contains a commented-out vendor invalidation call. Uncomment both when the Keeper integration and vendor key revocation endpoints are confirmed through the proxy.injectDemoBanner() call near the top of the script block once live data is confirmed flowing. The auto-refresh is already running — no other boot changes needed.loadDashboard() on every cycle. They reflect the actual distribution of whatever is in systems[] — no manual update needed when entries change.startRotationWizard() is called with that key's data pre-loaded. When triggered from the quick action button directly, it defaults to CW-PSA-LEGACY. Expected behavior — the wizard step 0 selector allows changing the selection.rotState.graceTimer keeps running if the modal is closed during step ⑤. Add clearInterval(rotState.graceTimer) inside closeModalDirect() to clean this up when the wizard is abandoned.if (rows.length > 24) rows[rows.length - 1].remove().loadDashboard() fires once on boot, then startAutoRefresh(30000) starts the 30-second cycle. If a load is already in progress when the interval fires, _dashboardLoading blocks the second call so cycles don't stack.animation: ticker 40s linear infinite. If you update the ticker innerHTML dynamically, force a restart: el.style.animation = 'none'; el.offsetHeight; el.style.animation = ''.KB · Unified API Command Console · dashboard-unified-api-console.html · v1.1 — Wired & Proxy-Ready