HOME β€Ί DASHBOARDS β€Ί ADMIN INSIGHTS β€” KB
dashboard-admin-insites-tech-workload.html
KNOWLEDGE BASE // DASHBOARDS // SECURITY & WORKLOAD
Admin Insights β€” Security & Workload Console
Complete reference for the two-page MSP operations console: a Security Operations page covering SentinelOne, Huntress, FortiGate, Cisco Umbrella, and ESET threat data; and a Ticket & Workload page covering ConnectWise PSA engineer capacity, ticket aging, and weekly throughput. Includes wiring guide, vendor API audit findings, and all data model shapes.
PROXY WIRED SECURITY PAGE WORKLOAD PAGE 5 SECURITY VENDORS CONNECTWISE PSA
FILE
dashboard-admin-insites-tech-workload.html
PAGES
2 β€” Security Operations Β· Ticket & Workload
PROXY ENDPOINTS
/api/security Β· /api/workload
REFRESH
30 seconds per active page
FALLBACK
mockSecurityData() Β· mockWorkloadData()
01
WHAT THIS TOOL DOES

The Admin Insights console is a two-page single-file HTML dashboard. The Security Operations page gives NOC/SOC engineers a unified threat view across all security platforms β€” active endpoint detections, firewall events, IPS alerts, alert severity breakdown, and a threat timeline. The Ticket & Workload page gives service desk managers visibility into engineer capacity, ticket aging buckets, per-client ticket volume, and weekly throughput.

The two pages are mutually exclusive β€” only one is active at a time. Switching pages clears the auto-refresh interval for the previous page and starts a new 30-second interval for the newly active one. Each page loads independently, so the Security page can be left open for wall display while workload data loads on demand.

WIRED β€” FALLS BACK TO MOCK DATA Both fetchSecurityData() and fetchWorkloadData() now call real proxy endpoints via apiFetch(). If either endpoint is unreachable, times out, or returns a non-200 response, the call silently falls back to the static mock generator. The dashboard always renders.
02
ARCHITECTURE & DATA FLOW

The console is a self-contained single HTML file with no framework or CDN dependencies β€” all rendering is pure DOM manipulation, all charts are hand-drawn SVG. The data engine uses two fetch functions that call your proxy, each with a 6-second timeout and silent fallback.

PAGE LOAD & REFRESH CYCLE
Bootstrap β†’ switchPage('workload') // workload is default on load switchPage(id): clearInterval(secInterval); clearInterval(wlInterval); if id === 'security': loadSecurityPage() secInterval = setInterval(loadSecurityPage, 30000) else: loadWorkloadPage() wlInterval = setInterval(loadWorkloadPage, 30000) loadSecurityPage(): fetchSecurityData() // apiFetch('/api/security', mockSecurityData) β†’ renderSecurityKPIs(d.kpis) β†’ renderThreats(d.threats) β†’ renderFWEvents(d.fwEvents) β†’ renderAlertSummary(d.alertSummary) β†’ renderTopClients(d.topClients) β†’ renderPlatforms(d.platforms) β†’ renderTimeline(d.timeline)
apiFetch() FALLBACK PATTERN
async function apiFetch(path, fallbackFn) { try { const r = await fetch(PROXY_BASE + path, { signal: AbortSignal.timeout(6000) }); if (!r.ok) throw new Error('HTTP ' + r.status); return await r.json(); // ← live data } catch(e) { if (!DEMO_MODE) console.warn(...); return fallbackFn(); // ← mock fallback } }
03
API AUDIT FINDINGS

Every vendor referenced in the dashboard was audited against live API documentation before wiring. The following table documents the findings β€” what the dashboard shows, what real API backs it, and any constraints you need to know before building the proxy layer.

VENDORDASHBOARD USEREAL API STATUSENDPOINTAUTH
SentinelOne Threat detections β€” type, host, severity, time βœ… Confirmed GET /web/api/v2.1/threats Authorization: ApiToken <token>
Huntress Threat detections β€” incident reports, host, severity βœ… Confirmed GET https://api.huntress.io/v1/incident_reports Basic auth β€” Base64(apiKey:apiSecret)
FortiGate Firewall events, IPS alerts, source IPs, counts βœ… Confirmed GET /api/v2/log/disk/ips (IPS)
GET /api/v2/log/disk/traffic (FW)
Authorization: Bearer <token> or URL param
Cisco Umbrella Platform status, event count ("Phishing Link Clicked" event type) ⚠️ Exists β€” OAuth2 required GET https://reports.api.umbrella.com/v2/activity OAuth2 client_credentials β€” requires token endpoint first
ESET Platform status, "Malware Blocked" event type ⚠️ Webhook β€” not pollable ESET PROTECT REST API is device-management only β€” no threat event poll endpoint Events delivered via syslog/webhook only. See C5.
ConnectWise PSA Open tickets, engineer open/capacity, ticket aging, weekly trend βœ… Confirmed GET /v4_6_release/apis/3.0/service/tickets
GET /v4_6_release/apis/3.0/system/members
Basic auth β€” Base64(companyId+publicKey:privateKey) + clientId header
ESET CANNOT BE POLLED β€” ACTION REQUIRED ESET PROTECT's REST API manages devices and policies β€” it does not expose a threat event feed you can poll. To show ESET threat data on the Security page, your proxy must receive ESET webhook/syslog events, store them, and serve them as part of the /api/security response. If your environment does not use ESET, remove it from the platforms array in mockSecurityData() to avoid confusion in the demo view.
NO INVALID DEMO ACTIONS A full audit confirmed the dashboard has zero interactive action buttons anywhere β€” no acknowledge, dismiss, escalate, or close controls in either page. Every panel is read-only. There is nothing to remove or gate.
SECURITY PAGE
04
SECURITY KPIs

Five KPI tiles across the top of the Security Operations page, rendered by renderSecurityKPIs(kpis). Each tile has a label, large value, sub-label, and a color class (danger/warn/ok/info) that sets its border accent.

Active Threats
Count of active endpoint threat detections. Source: kpis.activeThreats. Class: danger if >3, else warn. Live: threats.length from combined SentinelOne + Huntress response.
Firewall Events
Total event count summed across all FortiGate events. Source: kpis.fwEvents. Always warn class. Live: fwEvents.reduce((a,e)=>a+e.count,0).
IPS Alerts
Count of IPS-type events only. Source: kpis.ipsAlerts. Class: danger if >40, else warn. Live: filter FortiGate events where type is IPS and sum counts.
Clients Affected
Count of unique clients with active alerts. Source: kpis.clientsAffected. Always info class. Live: topClients.length.
Tools Online
Count of security platforms with status 'ok'. Source: kpis.toolsOnline. Always ok class. Live: platforms.filter(p=>p.status==='ok').length.
05
THREAT DETECTION LIST

Rendered by renderThreats(threats). Shows a card per threat event with a colored severity dot, threat type, platform source, host name, client name, and time-ago string. Sorted by recency.

type
Human-readable threat description. e.g. "Ransomware Detected", "Lateral Movement". Maps to SentinelOne's threatInfo.threatName or Huntress incident report title.
platform
Originating security tool name β€” "SentinelOne", "Huntress", "ESET", etc. Display-only string.
host
Endpoint hostname. Maps to SentinelOne agentDetectionInfo.agentComputerName or Huntress agent_name.
client
Client/organization name. Maps to SentinelOne account name or Huntress organization name.
severity
'critical', 'high', 'medium', 'low' β€” lowercase exact strings. Used for CSS class on the severity dot.
time
Human-readable time-ago string. e.g. "2m ago". Compute from the threat's createdAt timestamp in your proxy.
06
FIREWALL EVENTS TABLE

Rendered by renderFWEvents(events). A five-column table showing event type, source IP, client, event count, and severity badge. Data comes from FortiGate's log endpoints β€” your proxy aggregates and groups log entries by source IP and event type.

type
"IPS Alert", "Malware Block", "Blocked Connection", "VPN Auth Failure". Map from FortiGate logid or subtype field.
source
Source IP address string from FortiGate srcip field, or "Multiple" if aggregated across many sources.
count
Event occurrence count. Group FortiGate log entries by source + type and sum.
severity
'critical', 'high', 'medium', 'low'. Map from FortiGate level field: emergency/alert/critical β†’ critical, error β†’ high, warning β†’ medium, information/notification β†’ low.
07
ALERT SUMMARY

Rendered by renderAlertSummary(summary). Four horizontal bar gauges showing the proportion of Critical / High / Medium / Low alerts. Each bar fills proportionally to its share of the total. Source: alertSummary: {critical, high, medium, low} β€” integer counts from your proxy.

In production these counts are the aggregate of all cross-platform alerts β€” SentinelOne threats + Huntress incidents + FortiGate events β€” bucketed into the four severity levels. Your proxy normalizes vendor-specific severity naming before returning.

08
TOP AFFECTED CLIENTS

Rendered by renderTopClients(clients). A three-column table: client name, total alert count, and a severity badge for their highest-severity active alert. Shows the top 5 clients by alert count descending.

name
Client/organization name string.
alerts
Total active alert count across all platforms for this client.
severity
Highest-severity active alert for this client. 'critical', 'high', 'medium', or 'low'.
09
SECURITY PLATFORMS

Rendered by renderPlatforms(platforms). A card per security platform showing icon, name, event count, and an Online/Degraded/Offline status pill. The status pill is green for 'ok', amber for 'warn', and red for 'err'.

PLATFORMSTATUS LOGICEVENT COUNT SOURCE
SentinelOneok if API responds within timeout; warn if latency high; err on failureCount of active threats from GET /web/api/v2.1/threats
Huntressok if API responds; err on auth failure or timeoutCount of open incident reports from GET /v1/incident_reports
FortiGateok if log API responds; warn if degraded; err on failureTotal log entry count from IPS + traffic endpoints combined
Cisco Umbrellaok if OAuth token valid and activity API respondsCount of activity events from GET /v2/activity
ESETok if webhook receiver has received events recently; warn if stale; err if no events in threshold windowCount of stored ESET webhook events β€” NOT polled
10
THREAT TIMELINE

Rendered by renderTimeline(timeline). A vertical timeline of recent threat events with severity color coding, HH:MM timestamp, event description, and client name. Items are displayed most-recent first. The timeline wraps in a .timeline CSS class that draws a vertical spine line down the left side.

sev
'critical', 'high', 'medium', 'low' β€” controls the dot color and left border accent on each timeline item.
time
HH:MM format string e.g. "14:02". Compute from event timestamp in your proxy.
text
Event description string. Normalize from vendor-specific message fields β€” SentinelOne threatInfo.threatName + action, Huntress incident summary, FortiGate msg field.
client
Client/organization name string.
WORKLOAD PAGE
11
WORKLOAD KPIs

Five KPI tiles rendered by renderWorkloadKPIs(kpis). All five source from ConnectWise PSA in production.

Open Tickets
Total open ticket count. Source: kpis.openTickets. Live: GET /service/tickets?conditions=status/name!="Closed" β€” use X-Total-Count response header to avoid fetching all records.
Overloaded
Engineers with more open tickets than their capacity. Source: kpis.overloaded. Class: danger if >1, else warn. Live: count of engineers where open > capacity.
Aged 24h+
Open tickets past 24-hour SLA threshold. Source: kpis.aged24. Always danger class. Live: CW tickets where dateEntered < [now - 24h] and status is open.
Closed Today
Tickets closed since midnight local time. Source: kpis.closedToday. Always ok class. Live: CW tickets where dateResolved >= [today 00:00].
Throughput
Closed / opened ratio for the current day as a percentage string. Source: kpis.throughput. e.g. "91%".
12
ENGINEER WORKLOAD

Rendered by renderEngineers(engineers). A four-column table: engineer name, open ticket count, a visual capacity bar, and a status badge. The capacity bar fills to Math.min(pct, 100)% β€” it caps visually at 100% even if overloaded, but the percentage label shows the real value.

name
Engineer display name string. Maps to CW member firstName + ' ' + lastName or identifier.
open
Count of open tickets assigned to this engineer. Live: GET /service/tickets?conditions=owner/id={memberId} AND status/name!="Closed".
capacity
Maximum ticket capacity for this engineer. Hardcoded to 15 in mock. In production, store capacity per engineer in your proxy config or derive from CW member custom fields.
pct
Computed: Math.round(open/capacity*100). Threshold: β‰₯100% β†’ danger (Overload) Β· β‰₯80% β†’ warn (Near cap) Β· below 80% β†’ ok (Available).
13
TICKET AGING DISTRIBUTION

Rendered by renderAging(aging). A four-bucket bar chart showing ticket age distribution. Each bar fills proportionally to its count relative to the max bucket. Colors: 0-2h green, 2-8h yellow, 8-24h orange, 24h+ red.

BUCKETCSS CLASSCOLORCW FILTER
0-2hage-freshGreendateEntered >= [now-2h]
2-8hage-warmYellowdateEntered between [now-8h] and [now-2h]
8-24hage-hotOrangedateEntered between [now-24h] and [now-8h]
24h+age-staleReddateEntered < [now-24h]

The label field must match exactly one of the four values above β€” the aging renderer references label==='24h+' directly when computing the KPI Aged 24h+ value.

14
TICKETS BY CLIENT

Rendered by renderClientWorkload(clients). A five-column table: client name, open count, high-priority count, stale (24h+) count, and a 7-point SVG sparkline of the client's ticket trend over the past 7 days.

open
Total open tickets for this client. Live: CW tickets where company/name = client and status open.
high
High-priority open tickets. Color-coded: >3 red Β· >1 yellow Β· otherwise muted. Live: CW tickets filtered by priority/name="High" or "Critical".
stale
Open tickets aged 24h+. Badge class: >2 β†’ age-stale (red) Β· >0 β†’ age-hot (orange) Β· 0 β†’ age-fresh (green).
trend
Array of 7 integers β€” daily ticket count for Mon–Sun. Used to draw the SVG sparkline. Live: CW weekly report grouped by company and day.
15
TICKET VOLUME TREND

Rendered by renderVolumeTrend(weekTrend). A two-line SVG area chart (hand-drawn, no Chart.js dependency) showing opened vs closed tickets per day over the current week. Blue line for opened, green for closed, with gradient fill areas. Day labels render along the bottom axis.

labels
Array of 7 day label strings: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']. Fixed β€” do not change these.
opened
Array of 7 integers β€” tickets created per day this week. Live: CW tickets grouped by dateEntered day.
closed
Array of 7 integers β€” tickets closed per day this week. Live: CW tickets grouped by dateResolved day.
16
WEEKLY THROUGHPUT

Rendered by renderThroughput(kpis, weekTrend). Shows the weekly closed/opened ratio as a large percentage with color coding (green β‰₯90%, yellow β‰₯70%, red below 70%), plus two horizontal bar gauges for total opened and total closed with their raw counts.

The percentage is computed client-side: Math.round(closed/opened*100) where closed and opened are the sums of weekTrend.closed and weekTrend.opened arrays. The proxy does not need to compute this β€” just provide accurate arrays and the render function handles it.

17
DATA MODEL

Your proxy must return objects matching these shapes exactly. Field names are hardcoded throughout all renderer functions.

GET /api/security RESPONSE SHAPE
{ kpis: { activeThreats: number, fwEvents: number, // total FW event count (sum of all counts) ipsAlerts: number, // IPS event count only clientsAffected: number, toolsOnline: number, }, threats: [{ id: number, type: string, // e.g. "Ransomware Detected" client: string, platform: string, // e.g. "SentinelOne" host: string, // hostname severity: string, // 'critical'|'high'|'medium'|'low' time: string, // e.g. "2m ago" }], fwEvents: [{ type: string, // e.g. "IPS Alert" source: string, // IP address or "Multiple" client: string, count: number, severity: string, // 'critical'|'high'|'medium'|'low' }], alertSummary: { critical: n, high: n, medium: n, low: n }, topClients: [{ name: string, alerts: number, severity: string }], platforms: [{ name: string, // display name icon: string, // emoji status: string, // 'ok'|'warn'|'err' events: number, }], timeline: [{ sev: string, // 'critical'|'high'|'medium'|'low' time: string, // "HH:MM" text: string, // event description client: string, }], }
GET /api/workload RESPONSE SHAPE
{ kpis: { openTickets: number, overloaded: number, aged24: number, closedToday: number, throughput: string, // e.g. "91%" β€” string with percent sign }, engineers: [{ name: string, open: number, capacity: number, // max tickets β€” set per engineer in proxy config pct: number, // Math.round(open/capacity*100) }], aging: [ { label: '0-2h', count: number, cls: 'age-fresh' }, { label: '2-8h', count: number, cls: 'age-warm' }, { label: '8-24h', count: number, cls: 'age-hot' }, { label: '24h+', count: number, cls: 'age-stale' }, ], clients: [{ name: string, open: number, high: number, stale: number, trend: number[], // exactly 7 integers (Mon-Sun) }], weekTrend: { labels: ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'], opened: number[], // 7 integers closed: number[], // 7 integers }, }
18
SEVERITY REFERENCE
VALUECSS VARUSED IN
'critical'--redThreat dots, FW event badge, alert summary bar, timeline dot, top client badge
'high'--accent-orangeSame as above, amber/orange rendering
'medium'--yellowYellow rendering across all severity displays
'low'--greenGreen rendering β€” informational/healthy
'ok'--greenPlatform pill, KPI class, engineer status
'warn'--yellowPlatform pill degraded state
'err'--redPlatform pill offline state
CASE SENSITIVE All severity and status values are lowercase exact-match strings. The CSS class system uses them directly: class="sev ${e.severity}" and class="plat-pill ${p.status}". Any capitalization difference will produce an unstyled element.
CONFIGURATION GUIDE
C1
PREREQUISITES
Azure Function Proxy
Credentials for all vendors must never appear in browser code. Route all calls through your Azure Function proxy with Key Vault credential injection via Managed Identity. The Function App exposes /api/security and /api/workload as aggregation endpoints.
SentinelOne API Token
Generate from SentinelOne console: Settings β†’ Users β†’ Service Users. Create a Service User with IR Team or Site Viewer role. Copy the API token. Store in Key Vault as s1-api-token.
Huntress API Credentials
Generate from Huntress portal: Account Settings β†’ API Credentials. You receive a public key and a secret key. Store both in Key Vault as huntress-api-key and huntress-api-secret.
FortiGate API Token
Create a REST API administrator in FortiGate with read-only permissions. The token is generated at creation. Trusted Host should be set to your proxy's IP. Store as fortigate-token and fortigate-host.
Cisco Umbrella OAuth2
From Umbrella dashboard: Admin β†’ API Keys β†’ Create. You receive a client_id and client_secret. Your proxy must first call the token endpoint to get a bearer token, then call the reporting API. Store credentials as umbrella-client-id and umbrella-client-secret.
ConnectWise PSA API Keys
System β†’ Members β†’ API Members β†’ Create API Member β†’ API Keys tab β†’ Generate. You receive a Public Key and Private Key. Also note your Company ID. Store as cw-company-id, cw-public-key, cw-private-key, and register for a Client ID at developer.connectwise.com.
Engineer Capacity Config
The workload endpoint needs per-engineer capacity values. Since CW PSA does not store a "max ticket capacity" field natively, store these in your proxy configuration (a JSON map of member ID β†’ capacity integer). Default is 15 in the mock.
C2
PROXY CONFIG VARIABLES
// Near top of the <script> block const PROXY_BASE = ''; // Empty string = same origin as this HTML file // Set to full URL if proxy is on a different domain: // e.g. 'https://your-funcapp.azurewebsites.net' const DEMO_MODE = true; // true = fallback errors are silent (safe default) // false = fallback errors logged to browser console
SAME-ORIGIN DEPLOYMENT If this HTML file and the Function App are served from the same SharePoint site or domain, leave PROXY_BASE as an empty string. No CORS configuration needed. Set to the full Function App URL only when the dashboard and proxy are on different origins.
C3
WIRE SECURITY ENDPOINT

Your proxy's GET /api/security handler must fan out to all security vendors, normalize the results to the shapes in Section 17, and return a single JSON object. Build incrementally β€” start with SentinelOne and ConnectWise, let the others fall back to mock until ready.

  • 1
    SentinelOne threatsCall GET https://<instance>.sentinelone.net/web/api/v2.1/threats?resolved=false&limit=20 with Authorization: ApiToken <token>. Map each threat to the threat object shape: threatInfo.threatName β†’ type, agentDetectionInfo.agentComputerName β†’ host, agentRealtimeInfo.accountName β†’ client, threatInfo.confidenceLevel β†’ severity.
  • 2
    Huntress incidentsCall GET https://api.huntress.io/v1/incident_reports?limit=10 with Basic auth (base64 apiKey:apiSecret). Map each incident: summary β†’ type, agent_name β†’ host, organization_name β†’ client. Huntress uses critical/high/low β€” map low to medium in your proxy.
  • 3
    FortiGate eventsCall GET https://<fortigate-ip>/api/v2/log/disk/ips?rows=50 and /api/v2/log/disk/traffic?rows=100 with Authorization: Bearer <token>. Group by srcip and type. Map FortiGate level field: emergency/alert/critical β†’ critical Β· error β†’ high Β· warning β†’ medium Β· information β†’ low.
  • 4
    Cisco UmbrellaFirst call POST https://api.umbrella.com/auth/v2/token with client credentials to get a bearer token. Then call GET https://reports.api.umbrella.com/v2/activity?limit=20. Map activity type to the threat/event objects. OAuth token typically expires in 3600s β€” cache it in your proxy.
  • 5
    ESET (webhook-based)See C5. Configure ESET PROTECT to forward threat events via syslog or webhook to your proxy. Your proxy stores recent events and serves them from an in-memory store when /api/security is called.
  • 6
    Aggregate and normalizeMerge all threat arrays, compute alertSummary counts by severity, build topClients sorted by alert count, compute platforms status based on each vendor's API response health, and build timeline sorted by most recent.
C4
WIRE WORKLOAD ENDPOINT

Your proxy's GET /api/workload handler aggregates from ConnectWise PSA only. All authentication uses Basic auth with Base64 encoded companyId+publicKey:privateKey and a clientId header.

  • 1
    Engineers and open ticketsCall GET /v4_6_release/apis/3.0/system/members?conditions=inactiveFlag=false to get all active members. Then for each member, call GET /service/tickets?conditions=owner/id={id} AND status/name!="Closed"&fields=id using X-Total-Count header to get open ticket count without fetching all records.
  • 2
    Ticket aging bucketsCall GET /service/tickets?conditions=status/name!="Closed"&fields=id,dateEntered with pagination. Compute each ticket's age from dateEntered and bucket into the four age ranges.
  • 3
    Per-client ticket tableCall GET /service/tickets?conditions=status/name!="Closed"&fields=id,company/name,priority/name,dateEntered grouped by company/name. For each client compute open, high (priority "High" or "Critical"), and stale (age > 24h). For the 7-day trend, query tickets opened each day of the current week filtered by company.
  • 4
    Weekly volume trendQuery tickets with dateEntered between [start-of-week] and [now] grouped by day for opened, and dateResolved between [start-of-week] and [now] grouped by day for closed. Return 7 values per array (Mon–Sun), with 0 for future days.
C5
ESET INTEGRATION NOTE

ESET PROTECT's REST API is designed for device and policy management β€” it does not expose a queryable threat event feed. This is not a gap in the dashboard design; it requires a different integration pattern than the other four security vendors.

ESET IS WEBHOOK-BASED β€” NOT POLLABLE The ESET PROTECT REST API endpoints are for managing devices, groups, policies, and tasks. Threat detections are delivered as notifications β€” via ESET PROTECT's built-in notification system, email alerts, or syslog forwarding. There is no GET /threats endpoint you can poll.

To include ESET threat data on the Security page, configure your proxy as follows:

  • 1
    Enable syslog forwarding in ESET PROTECTIn the ESET PROTECT console, go to More β†’ Settings β†’ Advanced Settings. Under Logging, enable Syslog and point it to your proxy server's IP and port. Set format to JSON if available.
  • 2
    Receive and store events in your proxyAdd a UDP/TCP syslog listener to your Function App or a sidecar service. Parse incoming ESET JSON payloads and store the last N threat events in a fast store (Redis, Cosmos DB, or even an in-memory array if freshness tolerance allows).
  • 3
    Serve from /api/securityWhen /api/security is called, include stored ESET events in the threats array and set the ESET platform object's status based on whether new events have arrived within the expected window (e.g. if no events in 5 minutes during business hours, set warn).
  • 4
    If not using ESETRemove ESET from the platforms array in mockSecurityData() and remove any ESET entries from the threats mock array. This prevents ESET showing as "Online" with fake events in the demo view.
C6
VERIFY & TROUBLESHOOT
  • βœ“
    Set DEMO_MODE = false while testing. Proxy failures log to browser console only when DEMO_MODE = false. In its default true state, fallbacks are completely silent β€” you will see demo data with no indication anything failed.
  • βœ“
    Check DevTools Network tab. With DEMO_MODE = false, open DevTools β†’ Network, switch pages, and watch the calls to /api/security and /api/workload. Confirm 200 status and valid JSON response body before checking rendered output.
  • βœ“
    Severity values are lowercase. All severity strings must be 'critical', 'high', 'medium', 'low', 'ok', 'warn', or 'err' β€” exact lowercase. Capitalized versions will produce unstyled elements with no error.
  • βœ“
    Aging label '24h+' must match exactly. The KPI computation uses aging.find(a=>a.label==='24h+'). If your proxy returns '24h' or '24h +' the Aged 24h+ KPI will show undefined and potentially crash the renderer.
  • βœ“
    weekTrend arrays must have exactly 7 items. The SVG chart uses array length to compute x-step spacing. Arrays shorter or longer than 7 will produce misaligned labels.
  • βœ“
    Client trend arrays must have exactly 7 items. The sparkline function also expects 7 points. Fewer produces a compressed sparkline; more produces one that overflows the SVG bounds.
  • βœ“
    API credentials in Key Vault, not in proxy code. Verify Function App Application Settings show Key Vault reference strings, not raw credential values.
SYMPTOMCAUSEFIX
Dashboard shows mock data despite proxy being liveapiFetch() caught an error and fell back silentlySet DEMO_MODE = false, reload, check browser console for the error message.
Platform pills all show "Offline"status field not matching 'ok'/'warn'/'err'Log your proxy's platforms array. Check for capitalization issues or typos.
Aged 24h+ KPI shows NaN or 0aging array missing the '24h+' label bucketConfirm your proxy returns all four aging buckets with exact label strings.
Throughput chart renders but numbers are 0weekTrend.opened or weekTrend.closed is empty or all zerosVerify CW date range query is using correct timezone. CW stores dates in UTC.
Security page loads but threat list is emptyProxy returning empty threats arrayCall SentinelOne /threats directly via curl with your token to verify active threats exist. Check if resolved=false filter is applied.
Auto-refresh stops after switching pagesswitchPage() should clear both intervals β€” check console for errorsAny error in a renderer function can break the subsequent setInterval call. Check console for uncaught errors on page switch.