The ShadowProtect SPX Console is a self-contained single-file HTML dashboard that gives MSP technicians a real-time view of backup health across all clients managed through the StorageCraft partner portal. It surfaces risk, generates ticket notes, and drives consistent daily triage — all from one screen with no install required.
.html file. Open in any modern browser. No server, no npm, no build step.parsePortalTable():/admin-console/admin-accounts-list — client accounts table/admin-console/admin-sessions-list — backup sessions table/admin-console/status-report — raw status HTML/admin-console/manage/eventlog — event log table/admin-console/manage/auditlog — audit log table/auth — Basic Auth validation (returns JSON {ok, reason})/proxy?path=... — all portal traffic routes through localhost:3000
'Account Name' || 'Name' || 'Client' || ...) because the actual column names may vary between portal versions or account types. If StorageCraft changes a column header, parsing silently fails — all clients fall through to ONBOARDING. This is an inherent risk of HTML scraping vs. a real versioned API.probeInteropAPI() function scans 11 candidate paths under /interopAPI/rest/ — accounts, sessions, devices, status, clients, backups, vaults, events, audit, backup-status, storageusage. None of these are confirmed to exist or return useful data. They are discovery probes. The results appear in the detail panel only to help you evaluate what the portal might expose. Do not rely on any of these for production logic.▶ Run Backup Now — no API call, no PSA ticket, no action⚠ Send Alert — no email, no webhook, no PSA ticket⬡ Auto-Resolve — no actionSimilarly,
api.setKey() returns {ok:true} with no body — it is a placeholder method.
main.js) — must be running at localhost:3000. Not included in this file.2. Action button backends — Run Backup / Send Alert / Auto-Resolve need real API endpoints (StorageCraft API, PSA webhook, or email relay).
3. Column name validation — your specific portal's column headers need to be verified against the fallback chains in
parseAccounts() and parseSessions().4. interopAPI probe results — run the probe against your live portal to discover which endpoints actually return JSON data.
The console is entirely browser-side. All portal data flows through a local Node.js reverse proxy that injects Basic Auth session cookies and handles CORS. No data is stored beyond the browser session.
| Layer | Role |
|---|---|
| UI Shell | All HTML/CSS rendering, 5 gauges, panels, 8 themes — in the single .html file |
| parsePortalTable() | Uses DOMParser to extract rows from any portal HTML page's first table. Returns objects keyed by column header. |
| parseAccounts() | Normalises account rows to {id, name, type, status} with multi-name fallback chains |
| parseSessions() | Normalises session rows to {accountId, isOk, isFail, date, size, duration, backupType} |
| buildCheckResult() | Computes per-client outcome, consecutive fails, 7-day fail rate, last success/failure, and snapshots array from filtered sessions |
| api object | 7 methods wrapping portalFetch(): clients, check, queryAll, onboardingClients, refresh, statusReport, eventLog, auditLog |
| Risk Engine | computeRisk() + computeDrift() — 0–100 score from session trend data |
| Credential Manager | Browser PasswordCredential API with sessionStorage fallback. Key: storagecraft-shadowprotect-spx |
| Zone | Description |
|---|---|
| KTC Demo Bar | Fixed 36px top bar. Glowing cyan HOME button. Yellow "DEMO VERSION" notice. Adds padding-top:36px to body. |
| Header | Sticky 40px. Shield logo, app title, Tech Name input, Theme swatches (8), Last Scan badge, Refresh / Scan All / API Key / Zen buttons. |
| Connection Bar | Below header. Host field, Username, Password, Proxy mode selector, Connect / Disconnect buttons, active user display. |
| Gauge Strip | 5 donut-chart gauges in a CSS grid. Clickable filters. Skeleton shimmer until Scan All populates them. |
| NOC Operations Row | Top-risk chips | Vault status chips | NOC filter buttons (All/Critical/Failures/Drift/Healthy) | Auto-scan ▶ 5 min. |
| Main Area | CSS grid 390px 1fr. Left = Client List panel (Clients / ⚠ Queue tabs). Right = Detail panel. |
localStorage['spTechName']. Stamped at the bottom of every generated ticket note. If a client is already open when you save, the note rebuilds immediately via rebuildTicketDisplay().localStorage['spTheme']. Falls back to ice if saved key doesn't match any swatch.fresh class) under 30 minutes.loadClients() — fetches accounts list and onboarding check in parallel. Updates client list without a full session scan.startupScan() — fetches both accounts and full session history via api.queryAll(), scores every client, populates all 5 gauges. Shows loading modal with animated ring progress.Five donut-chart gauges. Each is a clickable filter — clicking one filters the client list to matching clients. The active gauge dims all others to 55% opacity.
| Gauge | Formula | Color logic |
|---|---|---|
| Total Devices | Count of all allClients entries | Always blue — informational |
| Backup Health | okN / (okN + failN) — excludes ONBOARDING | Green ≥90%, Yellow ≥70%, Red below 70% |
| Backed Up <24h | Clients where lastSuccessTime is within 24h. Only real success timestamps — never falls back to lastChecked. | Green ≥90%, Yellow ≥70%, Red below 70% |
| Onboarding | Count with lastConcern === 'ONBOARDING' | Always purple |
| Issues | Count with CONCERNED, CRITICAL, or WATCH (never ONBOARDING) | Green = 0, Yellow = issues but no CRITICAL, Red = any CRITICAL |
lastConcern. Backup Health and Issues gauges show "Run Scan All" until a full scan completes.| Section | What it shows / does |
|---|---|
| ⚠ Risks | Top 4 highest-risk clients by computed score. Each chip clickable — opens that client's detail. Populated after Scan All. |
| Vaults | One chip per vault. In demo mode uses DEMO_VAULTS. In live mode derives vault topology from client-to-vault groupings via buildTopology(). Green = healthy, Yellow = degraded/≥90% full, Red = offline. |
| Filter buttons | All · Critical (score ≥60) · Failures (score ≥30) · Drift (last success ≥24h) · Healthy (score <21, not onboarding) |
| Auto-scan ▶ | 5-minute countdown. Fires api.queryAll() in live mode. In demo mode refreshes gauges from cache only. |
Xh DRIFT or X.Xd DRIFT when last success ≥24h ago and not ONBOARDING.Loads when a client is selected. Calls runCheck() which in demo mode reads from the cached allClients entry directly (avoiding "Failed to fetch"). In live mode calls api.check(client.id).
| Tab | Contents |
|---|---|
| ◈ Overview | Concern banner (pulses on CRITICAL). Backup Statistics accordion (last success, last failure, consecutive fails, 7-day rate). Chart.js bar+line trend chart. Snapshot sparkbar (14 most recent). Action buttons (stubs — see Section 23). Ticket note block. Additional Notes textarea. |
| ⬡ AI Insights | Rule-based (no LLM): predicted failure risk 0–100, estimated hours to next failure, action suggestion text, and tags. Fully deterministic — computed from consecutiveFails and recentFailRatePct. |
| ⊞ Compare | Shift+click clients to add to compare grid. Or use Load All Scanned to auto-populate. Shows name, type, status badge, 7-day fail estimate per card. |
The ⚠ Queue tab in the left panel shows every client with a non-zero risk score, sorted by risk score descending then by most recent failure time. The tab badge shows the count of CRITICAL clients (score ≥60). Clicking any queue row calls runCheck() for that client. OK clients with score <21 and no lastFailure are excluded to keep the queue signal-only.
Derived by deriveConcern(data, fromBulk). Checks for a direct concern field first, then derives from outcome, then from trend data. In bulk scan mode (fromBulk=true) it does not guess ONBOARDING from snapshot timestamps alone.
| Level | Trigger conditions |
|---|---|
| OK | Outcome OK, 7-day fail rate = 0%, no consecutive fails |
| WATCH | 7-day fail rate >0% and <20%, consecutive fails = 0 |
| CONCERNED | Consecutive fails ≥1, OR 7-day fail rate ≥20% |
| CRITICAL | Consecutive fails ≥3, OR 7-day fail rate ≥50%, OR vault offline |
| ONBOARDING | No session activity in last 30 days. Risk score forced to 0. |
| UNKNOWN | Bulk scan with no direct concern field and insufficient data |
computeRisk(client) returns a score 0–100. Drives NOC risk chips, failure queue sort, and filter buttons. ONBOARDING clients always score 0.
| Score | Classification | NOC filter match |
|---|---|---|
| 0–29 | Healthy | Healthy filter |
| 30–59 | Concerned | Failures filter |
| 60–100 | Critical | Critical filter |
computeDrift(client) measures hours since lastSuccessTime. A client can be technically OK (last job succeeded) while drifting if that success was 40h ago.
| Threshold | Level | Badge label | Risk pts |
|---|---|---|---|
| <24h | OK | 14h ago | 0 |
| 24–36h | WARN | 28h DRIFT | +8 |
| 36–72h | FAIL | 1.8d DRIFT | +15 |
| ≥72h | CRITICAL | 4.2d DRIFT | +25 |
| No data | CRITICAL | No backup | +25 |
buildTicketNote(client, data) generates a structured note on every client check. Displayed in a <pre> block and copied to clipboard by Ctrl+Enter or clicking the copy button. Additional Notes are appended on copy if present.
startAutoScan() runs a full api.queryAll() every 5 minutes in live mode, or refreshes gauges and NOC data from cached demo clients in demo mode. Duration is calculated at 300 seconds per cycle.
renderNocRow(), renderFailureQueue(), and updateGauges() from cache — no portal fetch.Credentials use the browser Credential Management API (PasswordCredential) with sessionStorage fallback (key: storagecraft-shadowprotect-spx). Credentials clear on tab close when using the fallback.
backup.securewebportal.net), Username, Password. Click 🔐 Connect & Save. Or click 👁 Demo Mode to skip credentials entirely. DOMContentLoaded calls activateDemoMode() directly — demo loads with no modal.GET /auth fires to the local proxy with headers X-Target-Host, X-Auth-User, X-Auth-Pass. Proxy validates against portal. On success, _apiToken = 'session-active' is set as a sentinel — the session cookie is the real auth mechanism.clearCredentials(), removes sessionStorage entry, calls navigator.credentials.preventSilentAccess().localhost:3000. The browser only ever contacts localhost. The proxy injects credentials as HTTP headers when forwarding to the portal.The console cannot contact the StorageCraft portal directly due to CORS and cookie requirements. The local Node.js proxy (main.js) accepts requests from the browser, injects session cookies captured during Basic Auth, and forwards to the portal.
| Proxy mode | When to use |
|---|---|
| localhost:3000 | Default. Use when opening the HTML file locally (file://) or from a local web server. Proxy must be running before connecting. |
| Direct (same-origin) | Use when the HTML is served from the same host as the portal. Rare in practice. |
Every portalFetch() call has a 30-second AbortController timeout. The proxy port variable _proxyPort is read on every request — switching the dropdown mid-session takes effect on the next call.
Demo mode is the default boot state. DOMContentLoaded calls activateDemoMode() directly — no blocking modal, no credentials needed. The loading modal starts class="hidden".
| Client | Vault | Status | Scenario |
|---|---|---|---|
| ACME Corp — DC01 | Vault-01 | CRITICAL | 3 consecutive fails |
| ACME Corp — SQL01 | Vault-01 | OK | All healthy |
| Globex Industries | Vault-01 | CONCERNED | Intermittent — 50% fail rate |
| Initech — Domain | Vault-02 | OK | All healthy |
| Contoso Web | Vault-02 | WATCH | 1 recent fail |
| Fabrikam Inc | Vault-02 | OK | All healthy |
| Northwind Traders | Vault-02 | ONBOARDING | No sessions |
| Tailspin Toys | Vault-03 | CRITICAL | Vault-03 OFFLINE |
| Alpine Ski House | Vault-03 | CRITICAL | Vault-03 OFFLINE |
| Coho Vineyard | Vault-01 | OK | All healthy |
| Litware Inc — SQL | Vault-02 | OK | All healthy |
| Adventure Works | Vault-01 | WATCH | Last backup 38h ago — drift |
DEMO_MODE === true, runCheck() reads from the cached allClients entry instead of calling api.check(). This prevents "Failed to fetch" errors while still populating the full detail panel.All portal interactions route through the api object. Every method calls portalFetch(path) which prepends the proxy URL, adds X-Target-Host, and sets a 30s abort timeout.
| Method | Endpoint(s) | Returns |
|---|---|---|
| api.clients() | /admin-console/admin-accounts-list | {ok, clients[]} — normalised accounts |
| api.onboardingClients() | Both lists in parallel | Accounts with no sessions in last 30 days |
| api.queryAll() | Both lists in parallel | Full merged client array with outcome, trend, timestamps |
| api.check(id) | /admin-console/admin-sessions-list | Per-client result with snapshots, trend data |
| api.statusReport() | /admin-console/status-report | Raw portal status HTML |
| api.eventLog() | /admin-console/manage/eventlog | Parsed table rows |
| api.auditLog() | /admin-console/manage/auditlog | Parsed table rows |
| api.setKey() | None | STUB Returns {ok:true} — no action |
parsePortalTable(html, tableIndex) uses DOMParser to extract rows from the first (or indexed) table in any portal HTML page. Column names are used as object keys. Two higher-level parsers normalise the raw rows:
| Parser | Column name fallback chain |
|---|---|
| parseAccounts() | Name: Account Name → Name → Client → Account → Company → Organization → first valueID: ID → Account ID → href ?id= param → name-slugified |
| parseSessions() | Account: Account → Account Name → Client → Machine → Computer → NameStatus: Status → Result → State → OutcomeDate: Date → Time → Start Time → Start → Completed → Timestamp |
Every entry in allClients (also exposed as window.allClients) follows this shape. Fields in blue are required by gauge and rendering logic.
| Button | Current state | What would need to be built |
|---|---|---|
| ▶ Run Backup Now | STUB Renders in actions-bar. No onclick handler beyond rendering. | A real StorageCraft API call to trigger an on-demand backup job, or an RMM automation script via your PSA/RMM API. |
| ⚠ Send Alert | STUB Renders in actions-bar. No onclick handler. | A POST to a webhook (PSA, Teams, Slack, PagerDuty) or an email relay endpoint. The ticket note text is already available in lastTicketNote. |
| ⬡ Auto-Resolve | STUB Disabled when client is OK, enabled otherwise — but no action on click. | Depends on what "resolve" means in your workflow — could close a PSA ticket, acknowledge an alert in your monitoring platform, or trigger a remediation script. |
lastTicketNote is always populated. Any button handler you wire can use it directly — e.g. POST it as the body of a PSA ticket creation webhook without any additional formatting work.probeInteropAPI() fires 11 parallel requests to candidate paths under /interopAPI/rest/. Results appear in the detail panel. This is a discovery tool, not a production data source. No current logic in the dashboard reads from these endpoints for rendering or scoring.
| Endpoint | Status shown |
|---|---|
| /interopAPI/rest/accounts | Each returns one of: JSON ✓ (200 + JSON body), HTML 200 (200 but HTML, not JSON), a numeric HTTP status, or ERR (timeout/network error). A 4s timeout applies per request. |
| /interopAPI/rest/sessions | |
| /interopAPI/rest/devices | |
| /interopAPI/rest/status | |
| /interopAPI/rest/clients | |
| /interopAPI/rest/backups | |
| /interopAPI/rest/vaults | |
| /interopAPI/rest/events | |
| /interopAPI/rest/audit | |
| /interopAPI/rest/backup-status | |
| /interopAPI/rest/storageusage |
ice · defaultredmatrixpurpleambercobaltmonoSelection persisted to localStorage['spTheme']. Falls back to ice if saved key doesn't match any swatch in the DOM. To reset: localStorage.removeItem('spTheme') then reload.
node main.js. Verify localhost:3000 is selected in the Proxy dropdown. Check nothing blocks port 3000.parseSessions(). Open DevTools → Network, find the proxy request for admin-sessions-list, and compare actual column headers against the fallback arrays in the code.parseAccounts(). Check the admin-accounts-list response HTML for actual column names and add them to the fallback chains.document.getElementById('loadingModal').classList.add('hidden').localStorage['spTechName']. Private/incognito mode clears localStorage on close. Enter and Save the name at the start of each session in that environment.KB · ShadowProtect SPX Console · stack-shadow-protect-spx.html · v1.0