ScalePad
Asset Intelligence Console — Knowledge Base
⚡ Demo Mode ⚠ Proxy Pending Rev 1.0 · 2026
01

What This Tool Is

A real-time hardware lifecycle and asset intelligence console built on ScalePad Lifecycle Manager. Surfaces warranty status, end-of-life risk, fleet health scores, and pipeline opportunities across all managed clients.

Purpose & ScopeAsset Intelligence

The ScalePad Asset Intelligence Console is a single-page HTML operations dashboard that connects to the ScalePad Lifecycle Manager GraphQL API (https://api.scalepad.com/graphql). It is built for MSP account managers, vCIOs, and NOC engineers who need immediate visibility into hardware warranty expiry, end-of-life risk, and refresh opportunity pipelines across their entire managed client fleet.

The dashboard uses a fixed-layout design optimised for wall-board display and quick triage. All data panels are derived from ScalePad's asset graph. In demo mode the dashboard renders with fully synthetic but structurally accurate data matching the ScalePad API schema — switching to live data requires only a proxy configuration change and one flag in the script block.

Who Uses It

vCIO / Account Managers — Use the fleet health scores, EOL risk panel, and pipeline opportunities to drive quarterly business review conversations. The client list panel sorts by health score for immediate at-risk identification.

NOC Engineers — Hardware table provides fast lookup of any device's warranty status with search, filter, and sort. The expiring ≤90 days KPI filters directly to at-risk devices.

Procurement & Operations — Breakdown charts show fleet composition by manufacturer, client tier, and warranty split. Opportunity pipeline surfaces devices due for refresh with estimated value.

Practice Management — Fleet health gauge and average client score provide an aggregate metric for MSP leadership reporting without needing portal access.

02

API Audit Findings

Every data-fetching action, button, and displayed value audited against the ScalePad Lifecycle Manager API documentation. All gaps documented and corrected.

⚠ Critical Finding
The original file had zero API wiring. All data was pure synthetic JavaScript with no fetch calls, no loadDashboard(), no startAutoRefresh(), and no proxy pattern. The KTC home bar style block and div were also present. All of these have been corrected in Directive 1.
Audit Results by Panel
Panel / ValueCorrect APIStatusNotes
KPI — Total Assets, Active/Expiring/Expired GraphQL: assets query ✓ Wired Computed from warrantyEndDate on asset nodes. See §14.
KPI — EOL Now, EOL ≤12mo GraphQL: assets query ✓ Wired Computed from endOfLifeDate field on asset nodes.
KPI — Fleet Health (avg score) Derived from assets ⚠ Derived ScalePad does not return a pre-computed health score. Score is computed client-side from warranty/EOL ratios per client.
KPI — Open Pipeline ($) No ScalePad endpoint ⚠ Gap ScalePad does not expose opportunities via API. Value is demo-only. In production: source from PSA (CW Manage, HaloPSA).
Gauges — Warranty Coverage, Fleet Health, EOL Risk GraphQL: assets query ✓ Wired All three derived from asset data. Animates from real data on proxy activation.
Gauge — Pipeline Win Rate No ScalePad endpoint ⚠ Gap No opportunity win/loss data in ScalePad API. Demo only. Source from PSA in production.
Client List Panel GraphQL: partners query ✓ Wired Partner/client list with health score, asset count, and at-risk counts from partners query.
Hardware Table GraphQL: assets query ✓ Wired Full asset list with warranty, EOL, manufacturer, type per device.
EOL Risk Panel GraphQL: assets query ✓ Wired Filtered subset of assets where endOfLifeDate is within 24 months.
Activity Feed No dedicated endpoint ⚠ Derived No audit-log or activity-stream endpoint in ScalePad API. Activity is derived from expired/EOL asset state at query time. Documented in comments.
Opportunities Pipeline Table No ScalePad endpoint ⚠ Gap ScalePad public API does not expose opportunities. Table is demo-only. In production: source from PSA system's opportunity/proposal module.
Fleet Breakdown (mfr, tier, warranty) GraphQL: assets query ✓ Wired All breakdown bars derived from asset and partner data from live API queries.
Topbar API Key Input N/A — disabled by design ⚠ Design Input is intentionally disabled. Authentication must happen server-side in the proxy. Exposing the API key client-side is a security risk. Input shows users where to configure; actual key lives in the proxy env.
Changes Applied (Directive 1)
ChangeReason
Removed <style id="ktc-home-bar-style">Suite directive: remove KTC home bar style entirely
Removed #ktc-demo-bar div and HOME linkSuite directive: topbar shows only tool name, no home nav
Added var DEMO_MODE = trueSingle flag controls demo vs. live branch in refreshAll()
Added var SCALEPAD_PROXY constantProxy base path — maps to ScalePad GraphQL endpoint
Added async function scalePadFetch(query, variables)Proxy-ready GraphQL fetch wrapper with real fetch commented above mock fallback
Added async function loadDashboard()Suite-standard init: calls scalePadFetch(), normalises data, then refreshAll()
Added function startAutoRefresh(ms)Suite-standard 30-second polling wrapper
Added function refreshAll() with DEMO/live branchAll render functions called here; footer text shows Demo Mode vs Live
Replaced DOMContentLoaded with loadDashboard(); startAutoRefresh(30000);Suite-standard boot sequence
Added API limitations block in code commentsDocuments Opportunities gap, Activity gap, Health Score derivation, and key security note
03

Integration Status

Current state of each integration layer. All live features activate when the proxy endpoint is configured and DEMO_MODE is set to false.

⚠ Proxy Required
The dashboard currently runs in demo mode. All displayed data is synthetic. Live data flows when a valid proxy is wired to scalePadFetch() with a real ScalePad API key injected server-side.
Layer Status
LayerStatusRequired Action
Dashboard HTML/JS ✓ Ready All render functions, wiring pattern, and GraphQL query stubs are implemented.
Proxy / Backend Not configured Needs a reverse proxy or serverless function that injects the ScalePad API key and forwards POST requests to https://api.scalepad.com/graphql.
ScalePad GraphQL API Not reachable ScalePad API access requires an API key generated in ScalePad Portal → Settings → Integrations.
Auth / Token Not configured Bearer token obtained from ScalePad Settings. Does not expire automatically; rotate on a schedule. Must live in the proxy environment — never in browser JS.
Opportunities Pipeline Not available in ScalePad API No public API for opportunities. Requires PSA integration (e.g. CW Manage GET /opportunities) as a separate data source. See §16.
04

Architecture

Single self-contained HTML file. No build step, no dependencies. All logic runs in-browser. The proxy layer is the only external dependency needed for live data.

Data Flow
// Boot sequence (suite-standard): loadDashboard() // → scalePadFetch() → normalise data → refreshAll() startAutoRefresh(30000) // → setInterval(refreshAll, 30000) // scalePadFetch — proxy-ready GraphQL wrapper: async function scalePadFetch(query, variables) { if (!DEMO_MODE) { // REAL FETCH — uncomment when proxy is live: // const r = await fetch(SCALEPAD_PROXY, { // method: 'POST', // headers: { // 'Content-Type': 'application/json', // 'Authorization': 'Bearer ' + SCALEPAD_KEY // }, // body: JSON.stringify({ query, variables }) // }); // return (await r.json()).data; } return null; // demo: callers use synthetic data }
Auth Model

ScalePad uses API key authentication passed as a Bearer token. Keys are generated in the ScalePad Partner Portal and do not expire by default. All authentication must happen server-side in the proxy — never expose the key in client-side JavaScript or HTML.

// Headers required on every request: Authorization: Bearer YOUR_SCALEPAD_API_KEY Content-Type: application/json // Request body (all queries use POST): { "query": "query { assets(pageSize: 1000) { nodes { id ... } } }", "variables": {} }
State Variables
VariableTypePurpose
CLArrayClient/partner list. Populated from partners GraphQL query in live mode.
HWArrayHardware asset list. Populated from assets GraphQL query in live mode.
LCArrayLifecycle-at-risk subset of HW where endOfLifeDate < now + 2 years.
OPPSArrayOpportunities pipeline. Demo-only — no ScalePad API endpoint for this data.
CSObjectClient health scores keyed by client ID. Computed client-side from HW data.
hwPNumberCurrent hardware table page number.
hwPSNumberPage size for hardware table (default: 18).
hwWFStringActive warranty filter: 'all' | 'active' | 'expiring' | 'expired'.
hwSStringHardware table search query string.
hwCFString|nullActive client filter ID (null = show all clients).
hwSortObjectSort state: { col: 'dw', dir: 1 }. Column key and direction (1=asc, -1=desc).
DEMO_MODEBooleanIf true, scalePadFetch() returns null and synthetic data is used. Set false when proxy is live.
05

Topbar

44px fixed header. Contains the ScalePad brand, an API key connection input (disabled by design), and sync/mode status indicators.

Topbar Elements
ElementID/ClassBehavior
Brand logo + name.tb-brandStatic. "SP" logo mark + "ScalePad" in Exo 2 / "LIFECYCLE INSIGHTS" sub.
API key input.tb-api-inIntentionally disabled. Authentication happens server-side in the proxy. The input is visible UI only — it does not send the key to any endpoint.
Connect button.tb-api-btnDisabled. Proxy must be configured server-side before this becomes functional.
Demo badge.demo-badgePulses with CSS animation while in demo mode. Removed from view when DEMO_MODE=false and footer updates to "Live".
Status dot.sdotGreen dot. In live mode this reflects real API connectivity status.
Sync time#syncTimeSet to new Date().toLocaleTimeString() on every refreshAll() call.
06

KPI Row

Eight KPI cards spanning the full dashboard width. The first four are clickable and apply warranty filters to the hardware table. Rendered by buildRow1().

KPI Cards (×8)Row 1, Left 8 Columns
Injected into #row1 via buildRow1(). Each KPI has a label, value, sub-label, and a 2px color accent bar at the top.
API: GraphQL assets query — warrantyEndDate, endOfLifeDate fields
KPI Definitions
KPICalculationClick ActionColor
Total AssetsHW.lengthFilters hardware table to AllBlue
Active Warrantydw ≥ 90 (days to warranty end)Filters hardware table to ActiveGreen
Expiring ≤90d0 ≤ dw < 90Filters hardware table to ExpiringYellow
Expireddw < 0Filters hardware table to ExpiredRed
EOL Nowde < 0 (days to end-of-life)Non-clickableOrange
EOL ≤12mo0 ≤ de < 365Non-clickableYellow
Open PipelineSum of open+pending opportunity valuesNon-clickablePurple
Fleet HealthAverage of all client health scores (0–100)Non-clickableAccent
07

NOC Gauges (×4)

Four SVG donut gauges at the right end of Row 1. Each gauge animates its stroke-dashoffset on render to fill the ring proportionally.

Gauge Definitions
GaugeSVG IDMetricAPI Source
Warranty Coverageg-covactive / HW.length × 100%assets query — warrantyEndDate
Fleet Health Scoreg-hltAverage client health score (0–100)Derived from asset data — not returned by API
EOL Riskg-eol(eolNow + eolSoon) / LC.length × 100%assets query — endOfLifeDate
Pipeline Win Rateg-winwon / (won + lost) × 100%Demo only — no ScalePad API endpoint
⚠ API Limitation
The Pipeline Win Rate gauge has no ScalePad API backing. Opportunity win/loss data is not exposed by the ScalePad GraphQL API. In production this should be sourced from your PSA system (CW Manage, HaloPSA, etc.) or removed from the dashboard.
08

Clients Panel

Left column of Row 2. Scrollable list of all clients sorted by health score (descending). Clicking a client filters the hardware table and lifecycle panel to that client's assets.

Client ListRow 2, Left Column (200px)
Rendered by buildClients(). Each row shows: client name, city/state/vertical, a health score progress bar, the numeric score and letter grade (A–D), and chips for asset count, expired warranty count, EOL count, and open opportunity count.
API: GraphQL partners query — id, name, city, state, vertical, tier fields
Health Score Algorithm

Health score is computed client-side — ScalePad does not return a pre-computed score. Starting from 100, penalties are applied:

score = 100 score -= (expiredWarranty / totalAssets) × 40 score -= (expiringWarranty / totalAssets) × 20 score -= (eolDevices / totalAssets) × 30 score = clamp(score, 20, 100) Grade: A ≥85 · B ≥75 · C ≥60 · D <60
09

Hardware Table

Center panel of Row 2. Sortable, filterable, searchable table of all hardware assets. 18 rows per page. Rendered by buildHW().

Hardware AssetsRow 2, Center (1fr)
Primary data table. Filters stacked: warranty filter (all/active/expiring/expired), client filter (via client panel selection), and text search across name, manufacturer, model, client, serial, and type.
API: GraphQL assets query — full asset node with warrantyEndDate, endOfLifeDate
Table Columns
ColumnFieldSortableNotes
Devicea.name✓ (col: name)Manufacturer + model name
Clienta.cli✓ (col: cli)Owning client name
Makera.mfr✓ (col: mfr)Manufacturer (Dell, HP, Lenovo, etc.)
Typea.type✓ (col: type)Laptop / Desktop / Workstation / Server / Tablet
Purchaseda.pd (purchaseDate)✓ (col: pd)Formatted as DD Mon YYYY
Warranty Endsa.we (warrantyEndDate)✓ (col: we)Date of warranty expiry
Days Lefta.dw (daysToWarrantyEnd)✓ Default sort (col: dw)Green ≥90d, Yellow 0–89d, Red = expired (shows as "Nd ago")
StatuswSt(a.dw)ACTIVE / EXPIRING / EXPIRED badge
10

EOL Risk Panel

Right column top panel. Shows the 25 highest-risk lifecycle items (EOL or EOL soon) sorted by urgency and days to end-of-life. Rendered by buildLC().

EOL RiskRow 2, Right Column Top
Filtered subset of HW where endOfLifeDate < now + 730 days. Sorted: EOL devices first (de<0), then EOL soon (de<365), then remainder by ascending days. Each row shows a lifecycle progress bar (age as % of 5-year lifespan) and an EOL / SOON / OK chip.
API: GraphQL assets query — endOfLifeDate field per asset node
11

Activity Feed

Right column bottom panel. A short chronological list of asset-state change events. Rendered by buildActivity().

⚠ API Limitation — No Audit Log Endpoint
ScalePad does not provide a dedicated activity, audit-log, or event-stream API endpoint. The activity feed is entirely derived from the current asset state at query time: expired warranties, EOL-flagged devices, and new opportunities are each surfaced as synthetic "events". In production, this panel can be enriched with webhooks from your PSA or RMM system for actual timestamped events.
12

Opportunities Pipeline

Row 3 left panel. Table of hardware refresh and renewal opportunities, sorted by value descending. Rendered by buildOpps().

✗ No ScalePad API Endpoint
The ScalePad public GraphQL API does not expose opportunity, proposal, or pipeline data. The opportunities table is demo-only and will always show synthetic data. To activate this panel with live data, wire it to your PSA system's opportunity module:

CW Manage: GET /v4_6_release/apis/3.0/sales/opportunities
HaloPSA: GET /api/Opportunity
Autotask: GET /atservicesrest/v1.0/Opportunities
13

Fleet Breakdown

Row 3 right panel. Two columns of horizontal bar charts: manufacturer distribution, client tier distribution, and warranty split. Rendered by buildBreakdown().

Fleet BreakdownRow 3, Right Panel
Three bar chart groups: By Manufacturer (all manufacturers sorted by count), By Tier (Enterprise/Mid-Market/SMB per client tier applied to asset count), Warranty Split (Active/Expiring/Expired counts). All derived from HW and CL arrays from the assets and partners queries.
API: GraphQL assets + partners queries
14

API Endpoints

ScalePad Lifecycle Manager uses a single GraphQL endpoint. All queries and mutations are POST requests to https://api.scalepad.com/graphql.

GraphQL Endpoint
POST https://api.scalepad.com/graphql Single GraphQL endpoint for all queries. Requires Authorization: Bearer API_KEY header. All data access (assets, partners, lifecycle insights) goes through this endpoint. Proxy Required
Key GraphQL Queries
QueryReturnsUsed ForStatus
partners { id name city state vertical tier } Array of partner/client objects Client list panel, tier breakdown, health score computation ✓ Wired
assets(pageSize: N) { nodes { id systemName manufacturer modelNumber deviceType serialNumber purchaseDate warrantyEndDate endOfLifeDate partner { id name } } } Paginated array of hardware asset nodes Hardware table, KPI cards, EOL risk panel, breakdown charts, gauges ✓ Wired
lifecycleInsights { endOfLifeAlerts { assetId assetName endOfLifeDate } } Pre-computed EOL alert objects Alternative to filtering endOfLifeDate on assets manually Optional
Sample GraphQL Query
// POST https://api.scalepad.com/graphql // Authorization: Bearer YOUR_API_KEY { "query": "query GetAssets { assets(pageSize: 1000) { nodes { id systemName manufacturer modelNumber deviceType serialNumber purchaseDate warrantyEndDate endOfLifeDate partner { id name } } pageInfo { hasNextPage endCursor } } }" } // Response shape: { "data": { "assets": { "nodes": [ { "id": "asset_123", "systemName": "Dell OptiPlex 7090", "warrantyEndDate": "2025-06-15", ... } ] } } }
Endpoints NOT Available in ScalePad APIGaps
FeatureStatusAlternative
Opportunities / Pipeline Not available CW Manage GET /sales/opportunities, HaloPSA GET /api/Opportunity
Activity / Audit Log Not available Derive from asset state changes; enrich with PSA/RMM webhooks
Pre-computed Health Score Derived only Compute client-side from warranty/EOL ratios as implemented
Win Rate / Opportunity Status Not available Source from PSA opportunity win/loss fields
15

Config & Fields

All configurable constants and threshold values in the dashboard script block.

Script Constants
ConstantDefaultEffect
DEMO_MODEtrueSet to false when proxy is live. Controls whether scalePadFetch() fires real requests.
SCALEPAD_PROXY'/api/scalepad'Base path for the proxy. Maps to https://api.scalepad.com/graphql on the server.
hwPS18Hardware table page size. Increase for larger screens.
startAutoRefresh(ms)30000Poll interval in ms. Reduce to 15000 for more frequent updates; increase to 60000 to reduce API load.
LC filter threshold730 daysEOL risk panel shows assets with endOfLifeDate within 2 years (de < 730).
wSt expiring threshold90 daysAssets with dw < 90 && dw ≥ 0 are flagged "expiring". Change inline in wSt().
Health score: expired weight40Penalty multiplier for expired warranty ratio in client health score.
Health score: expiring weight20Penalty multiplier for near-expiry ratio.
Health score: EOL weight30Penalty multiplier for EOL device ratio.
Health score minimum20No client score drops below 20 regardless of fleet state.
Asset Field Mapping (Demo → API)
Demo FieldScalePad API FieldType
a.ididString (UUID)
a.namesystemNameString — display name in ScalePad
a.mfrmanufacturerString
a.modelmodelNumberString
a.typedeviceTypeString enum
a.snserialNumberString
a.cidpartner.idString (UUID)
a.clipartner.nameString
a.pdpurchaseDateISO 8601 date string
a.wewarrantyEndDateISO 8601 date string
a.eolendOfLifeDateISO 8601 date string
a.dwComputedMath.round((new Date(warrantyEndDate) - now) / 86400000)
a.deComputedMath.round((new Date(endOfLifeDate) - now) / 86400000)
16

Documented Limitations

Known constraints of the ScalePad API and the current dashboard implementation, with recommended mitigations.

Limitation Register
#LimitationImpactMitigation
L-01 No Opportunities API Opportunities pipeline shows demo data only. Source from PSA opportunity module (CW Manage, HaloPSA, Autotask). Wire as a separate psaFetch() call in loadDashboard().
L-02 No Activity/Audit Log Endpoint Activity feed derived from static asset state, not real timestamps. Supplement with PSA ticket notes or RMM event webhooks for real timestamped events.
L-03 Health Score is Computed, Not Returned Low — derived value is accurate. Current algorithm is sound. If ScalePad adds a native health score field in future, swap CS[c.id].s to use the API value.
L-04 GraphQL Pagination Required Large fleets (>1000 devices) require cursor-based pagination. The loadDashboard() query stub includes pageInfo { hasNextPage endCursor }. Implement a loop in scalePadFetch() that follows the cursor until hasNextPage = false.
L-05 API Key Must Never Be Client-Side Security risk if key is embedded in HTML. The topbar API key input is disabled by design. All authentication happens in the proxy. The key lives in the proxy environment variable only.
L-06 No Real-Time Push Notifications Low — 30-second polling is sufficient for this use case. The 30-second startAutoRefresh() interval provides near-real-time data. ScalePad does not support WebSocket subscriptions via the public API.
17

Proxy Activation Checklist

Step-by-step guide to move the dashboard from demo mode to live ScalePad data. Complete all steps before setting DEMO_MODE = false.

Pre-Activation Steps
1
Generate a ScalePad API Key
Log in to ScalePad Partner Portal → Settings → Integrations → API Keys. Generate a new key. Copy it immediately — it is only shown once. Store it in your proxy environment variables as SCALEPAD_API_KEY.
2
Deploy a Proxy Endpoint
Deploy a server-side proxy (Nginx, Express, Cloudflare Worker, or similar) that:
• Accepts POST requests at /api/scalepad
• Adds Authorization: Bearer $SCALEPAD_API_KEY to the request headers
• Forwards the request body to https://api.scalepad.com/graphql
• Returns the response JSON to the browser
3
Test the Proxy Manually
Send a test GraphQL query via curl to confirm the proxy returns real ScalePad data before wiring the dashboard:
curl -X POST /api/scalepad -H "Content-Type: application/json" -d '{"query":"query{partners{id name}}"}'
4
Update Dashboard Constants
In the dashboard script block:
• Set var DEMO_MODE = false;
• Confirm var SCALEPAD_PROXY = '/api/scalepad'; matches your proxy route.
• Uncomment the real fetch block inside scalePadFetch().
5
Implement Asset Normaliser
In loadDashboard(), uncomment the scalePadFetch() calls and add a normaliser function that maps the ScalePad API field names (systemName, warrantyEndDate, etc.) to the dashboard's internal field names (name, we, etc.) as documented in §15.
6
Handle Pagination for Large Fleets
If the fleet exceeds ~1000 assets, implement cursor-based pagination in the GraphQL query. Use the pageInfo.hasNextPage and pageInfo.endCursor fields returned by the assets query to fetch all pages before rendering.
7
Wire the Opportunities Panel (Optional)
The Opportunities panel requires a separate PSA integration. Add a psaFetch() function alongside scalePadFetch() and populate OPPS from your PSA's opportunity/proposal endpoint. Update the footer to reflect the PSA source.
8
Verify and Monitor
Open the browser console and confirm no API errors on load. Verify the footer switches from "Demo Mode" to "Live". Confirm client count, asset count, and KPI values match what you see in the ScalePad portal.
Go-Live Checklist
  • ScalePad API key generated and stored in proxy environment variable
  • Proxy endpoint deployed and tested with curl (returns real ScalePad data)
  • DEMO_MODE set to false in dashboard script block
  • Real fetch block uncommented inside scalePadFetch()
  • Asset normaliser function implemented mapping ScalePad fields to internal fields
  • Partners normaliser function implemented
  • Pagination handled if fleet exceeds 1000 assets
  • Browser console shows no errors on load
  • Footer text shows "Live" (not "Demo Mode")
  • KPI counts match ScalePad portal values
  • Opportunities panel wired to PSA (or documented as pending)
18

Troubleshooting

Common issues encountered when activating live data or operating the dashboard.

Dashboard still shows demo data after setting DEMO_MODE = false
Confirm you also uncommented the real fetch block inside scalePadFetch(). The DEMO_MODE flag gates the branch but the function returns null unless the fetch() call inside it is active. Also ensure the browser has no cached version of the old file (hard refresh with Ctrl+Shift+R).
scalePadFetch returns 401 Unauthorized
The proxy is not injecting the Authorization header correctly. Verify your proxy passes Authorization: Bearer YOUR_KEY — not ApiKey or any other scheme. Confirm the key was copied correctly from the ScalePad portal and has not been rotated or revoked.
CORS error in browser console
The dashboard is calling ScalePad directly instead of through the proxy, or your proxy is not setting Access-Control-Allow-Origin headers. Ensure SCALEPAD_PROXY points to your own proxy server, not to https://api.scalepad.com directly. The proxy must add CORS headers to its responses.
Hardware table is empty after switching to live data
The normaliser function has not been implemented. The dashboard expects assets to have fields like a.name, a.mfr, a.we, a.eol. ScalePad returns systemName, manufacturer, warrantyEndDate, endOfLifeDate. Add a normaliser in loadDashboard() to map the API fields to internal names before assigning to the HW array.
Only the first ~100 assets appear — fleet has many more
The GraphQL query is returning the first page only. ScalePad uses cursor-based pagination. Check the response for pageInfo.hasNextPage and pageInfo.endCursor. Implement a loop in scalePadFetch() that continues fetching with assets(after: $cursor, pageSize: 500) until hasNextPage = false, then concatenates all node arrays before rendering.
Opportunities panel always shows demo data
This is expected — ScalePad does not have an opportunities API. The OPPS array is populated with synthetic data and will not be replaced by scalePadFetch(). To show real data, add a separate psaFetch() function that calls your PSA's opportunity endpoint and normalises the response into the OPPS array format expected by buildOpps().
Auto-refresh fires too frequently and causes rate limiting
Increase the interval passed to startAutoRefresh() from 30000ms to 60000ms (1 minute) or 120000ms (2 minutes). ScalePad rate limits vary by account tier. If you hit rate limits, check the proxy response headers for Retry-After or X-RateLimit-* headers and implement exponential backoff in scalePadFetch().