The ShadowProtect SPX Console is a self-contained HTML file that gives MSP technicians a real-time unified view of backup status across every client managed through the StorageCraft partner portal. It replaces manual portal browsing with a purpose-built NOC dashboard that surfaces risk, generates ticket notes, and drives consistent daily triage.
.html file. Open it in any modern browser. No server, no npm, no build step required./admin-console/admin-accounts-list and /admin-console/admin-sessions-list — via a local Node.js reverse proxy. No official JSON API required.node main.js) for live datasessionStorage fallback when browser Credential API unavailableThe console is entirely browser-side. It fetches data from the StorageCraft portal through a local Node.js reverse proxy (main.js) that injects Basic Auth session cookies and strips CORS. No data is stored beyond the session.
| Layer | Role | Location |
|---|---|---|
| UI Shell | All HTML/CSS rendering, panels, gauges, 8 themes | stack-shadow-protect-spx.html |
| HTML Parser | parsePortalTable() extracts rows from portal HTML. parseAccounts() and parseSessions() normalise column names | Inline JS |
| api object | Wraps all portal fetches: clients(), queryAll(), check(id), statusReport(), eventLog(), auditLog() | Inline JS |
| Risk Engine | computeRisk() and computeDrift() produce 0–100 scores from session trends | Inline JS |
| Credential Manager | Uses browser PasswordCredential API with sessionStorage fallback. Key: storagecraft-shadowprotect-spx | Browser storage |
| Proxy | Reverse-proxies requests, injects cookies, handles CORS. Auth validated via GET /auth | main.js (Node.js) |
The console is a single-page app with a fixed chrome layer at the top and a two-column main area. From top to bottom and left to right:
| Zone | Description |
|---|---|
| KTC Demo Bar | Fixed top bar (36px, #07111f background). Glowing cyan HOME button links back to the command hub. Yellow "DEMO VERSION" notice centered. Adds padding-top:36px to body. |
| Header | Sticky, 40px. Shield logo, app title, Tech Name input + Save, Theme swatches (8 options), Last Scan badge, Refresh / Scan All / API Key / Zen buttons. |
| Gauge Strip | 5 donut-chart gauges in a CSS grid. Each is a clickable filter. Populated by Scan All. Skeleton shimmer until data loads. |
| Gauge Refresh Row | Active filter chip (clears on click) and Refresh Gauges button. Recalculates from cached data without an API call. |
| NOC Operations Row | ⚠ Top-risk chips | Vaults status chips | NOC filter buttons (All/Critical/Failures/Drift/Healthy) | Auto-scan (▶ 5 min). |
| Main Area | CSS grid: 390px 1fr. Left = Client List panel (with Clients / ⚠ Queue tabs). Right = Detail panel. Sidebar collapse button between them. |
| Footer | Dim monospace text: "StorageCraft · ShadowProtect SPX Console". |
localStorage under spTechName. Stamped at the bottom of every generated ticket note. If a ticket is already open when you save, it rebuilds immediately with the new name.spTheme. Falls back to ice if the saved key doesn't match any swatch in the DOM.fresh class) when under 30 minutes. Font: JetBrains Mono, 9px, 1px letter-spacing.loadClients() — fetches accounts list and onboarding check in parallel. Updates client list without a full session scan. Use at session start or after client changes.startupScan() — fetches both accounts and full session history simultaneously via api.queryAll(), scores every client, populates all 5 gauges. Shows the loading modal with animated ring progress.grid-template-columns.Five donut-chart gauges run across the top. Each is a clickable filter — clicking a gauge filters the client list to only matching clients. The active gauge dims all others to 55% opacity. Click again to clear.
| Gauge | Formula | Color logic |
|---|---|---|
| Total Devices | Count of all allClients entries. | Always blue — informational only. |
| Backup Health | okN / (okN + failN) — excludes ONBOARDING clients. Requires Scan All to populate. | Green ≥90%, Yellow ≥70%, Red below 70%. |
| Backed Up <24h | Count of non-onboarding clients where lastSuccessTime is within 24 hours. Only real success timestamps count — never falls back to lastChecked. | Green ≥90%, Yellow ≥70%, Red below 70%. |
| Onboarding | Count with lastConcern === 'ONBOARDING'. | Always purple — no alarm threshold. |
| Issues | Count with CONCERNED, CRITICAL, or WATCH status (never ONBOARDING). Requires Scan All. | Green = 0, Yellow = issues but no CRITICAL, Red = any CRITICAL. |
lastConcern. Backup Health and Issues need session history data from Scan All or an individual client check before they show real numbers.| Section | What it shows / does |
|---|---|
| ⚠ Risks | Top 4 highest-risk clients by computed score. Each chip is clickable — selecting one opens that client's detail panel. Populated after Scan All via renderNocRow(). |
| Vaults | One chip per vault. Green = healthy, Yellow = degraded replication or capacity ≥90%, Red = offline. In demo mode uses DEMO_VAULTS — Vault-03 is forced offline. In live mode derives topology from client-to-vault groupings via buildTopology(). |
| Filter | 5 quick-filter buttons overriding the gauge filter: All, Critical (score ≥60), Failures (score ≥30), Drift (last success ≥24h), Healthy (score <21, not onboarding). |
| Auto-Scan | Starts a 5-minute countdown. Fires triggerAutoScan() on expiry, which calls api.queryAll() in live mode or refreshes gauges from cache in demo mode. Countdown displays as M:SS and resets to 5:00 after each fire. |
The left panel shows all clients as rows with colored left borders indicating status. Clicking a row runs a live check (or reads from demo cache) and opens the detail panel. Tabs at the top toggle between the client list and the Failure Queue.
CONCERN_ORDER map), Last Checked, Risk Score (highest first).Xh DRIFT or X.Xd DRIFT when last success ≥24h ago and client is not ONBOARDING.loading). Removed by updateGauges() on first render.The right panel loads when a client is selected. It calls runCheck() which in live mode calls api.check(), and in demo mode reads directly from the cached allClients entry to avoid a "Failed to fetch" error. Three tabs render different views.
| Tab | Contents |
|---|---|
| ◈ Overview | Concern banner (color-coded, pulses on CRITICAL). Backup Statistics accordion (last success, last failure, consecutive fails, 7-day rate, overall rate). Snapshot sparkbar (14 most recent, color-coded). Backup trend chart (Chart.js, 30-day bar + success-rate line). Action buttons. Ticket note block. Additional Notes textarea. |
| ⬡ AI Insights | Rule-based analysis: predicted failure risk score 0–100, estimated hours to next failure, suggested action text, and tags (Consecutive Fails, High Fail Rate, Stale Backup, Healthy, No Activity). Fully deterministic — no LLM call. |
| ⊞ Compare | Shift+click clients to add to compare grid. Or use Load All Scanned to auto-populate with all scanned clients. Shows name, type, status badge, 7-day fail estimate per card. |
critical-pulse keyframe animation.The ⚠ Queue tab in the left panel surfaces every client with a non-zero risk score, sorted by risk score descending then by most recent failure time. It is a prioritized work queue for the NOC session.
runCheck() for that client — identical to clicking in the client list.lastFailure are also excluded to keep the queue signal-only.The 10-step guided tour (WT array) highlights key UI elements with a spotlight overlay and a floating card. It is triggered by first-time detection via localStorage.getItem('spTourDone'). Steps can be navigated with Next/Back, and the tour can be skipped at any step.
| # | Title | Target element |
|---|---|---|
| 01 | Welcome to ShadowProtect Console | None (centered card) |
| 02 | Step 1 — Set Your API Key | #btnSetKey |
| 03 | Step 2 — Enter Your Tech Name | #techBar |
| 04 | Step 3 — Refresh Client List | #btnRefresh |
| 05 | Step 4 — Check a Client in Real Time | #list |
| 06 | Step 5 — Review Status & Ticket Note | #detailBody |
| 07 | Step 6 — Add Your Own Notes | #btnCopyTicket |
| 08 | Step 7 — Scan All Clients at Once | #btnQueryAll |
| 09 | Step 8 — Filter by Gauge | #gaugeStrip |
| 10 | Step 9 — Refresh Clients Anytime | #btnRefresh |
Concern levels are derived by deriveConcern(data, fromBulk). The function 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 — those aren't available from the sessions list alone.
| Level | Trigger conditions | Priority |
|---|---|---|
| OK | Outcome is OK, 7-day fail rate = 0%, no consecutive fails | Lowest |
| WATCH | 7-day fail rate >0% and <20%, consecutive fails = 0 | Low |
| CONCERNED | Consecutive fails ≥1, OR 7-day fail rate ≥20%; outcome is NOT OK | Medium |
| CRITICAL | Consecutive fails ≥3, OR 7-day fail rate ≥50%, OR vault offline | Highest |
| ONBOARDING | No session activity in the last 30 days. Overrides all scoring — risk score forced to 0. | Separate |
| UNKNOWN | Bulk scan with no direct concern field and insufficient data to classify | N/A |
computeRisk(client) runs on every client and returns a score 0–100. It drives the NOC risk chips, the failure queue sort, and the filter buttons. ONBOARDING clients are always forced to score 0 and their tags are cleared.
| Score | Classification | Badge color | NOC filter match |
|---|---|---|---|
| 0–29 | Healthy | OK | Healthy filter |
| 30–59 | Watch / Concerned | CONCERNED | Failures filter |
| 60–100 | Critical | CRITICAL | Critical filter |
computeDrift(client) measures elapsed time since the last successful backup using lastSuccessTime. A client can be technically OK (last job succeeded) while drifting if that success was 40h ago. Drift contributes to risk score independently of the concern level.
| 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. It is displayed in a <pre> block and copied to clipboard by btnCopyTicket or Ctrl+Enter. If Additional Notes are present they are appended as a separate section.
rebuildTicketDisplay() — the note updates immediately without re-running the check.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. The countdown timer updates every second.
_autoScanInterval and _autoScanCountdownTimer. Resets button text and clears countdown display.Credentials are managed through a first-run modal. The console uses the browser Credential Management API (PasswordCredential) where available, falling back to sessionStorage with key storagecraft-shadowprotect-spx. Credentials clear on tab close when using the fallback.
backup.securewebportal.net), Username, and Password. Click 🔐 Connect & Save. Or click 👁 Demo Mode to skip credentials entirely.GET /auth fires to the local proxy with headers X-Target-Host, X-Auth-User, X-Auth-Pass. The proxy validates against the portal. On success, _apiToken = 'session-active' is set as a sentinel and the loading modal appears for startup scan.DOMContentLoaded handler now calls activateDemoMode() directly — demo data loads immediately with no modal. To use live credentials, click ⬡ API Key and connect. If you previously saved credentials the modal pre-fills them and shows the "Credentials found" badge.clearCredentials(), removes sessionStorage entry, and calls navigator.credentials.preventSilentAccess() to require re-authentication next time.localhost:3000. The browser only ever contacts localhost. The proxy injects credentials as HTTP headers when forwarding to the portal. Nothing is sent to any remote server.The console cannot contact the StorageCraft portal directly from a browser 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 file is served from the same host as the portal. All fetches route without the proxy prefix. Rare in practice. |
The _proxyPort variable (default '3000') is read on every portalFetch() call. Switching the dropdown mid-session takes effect on the next request. Every portalFetch() has a 30-second AbortController timeout.
Demo mode loads instantly with no credentials or proxy. It is the default boot state — DOMContentLoaded calls activateDemoMode() directly, bypassing initCredentials() entirely. The loading modal is hidden at page start (class="hidden") so there is no blocking splash screen.
| Client | Vault | Status | Scenario |
|---|---|---|---|
| ACME Corp — DC01 | Vault-01 | CRITICAL | 3 consecutive fails — score 90 |
| 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, low rate |
| Fabrikam Inc | Vault-02 | OK | All healthy |
| Northwind Traders | Vault-02 | ONBOARDING | No sessions — new client |
| Tailspin Toys | Vault-03 | CRITICAL | Vault-03 OFFLINE — score 100 |
| Alpine Ski House | Vault-03 | CRITICAL | Vault-03 OFFLINE — score 100 |
| 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 |
offline:true in DEMO_VAULTS. Both show CRITICAL regardless of session history. The vault chip in the NOC row shows ⊘ OFFLINE in red.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 with realistic data.DEMO_MODE = false, removes body.demo-mode class, clears all client data, and re-opens the credential modal.All portal interactions are routed through the api object. Each method calls portalFetch(path), which prepends the proxy URL, adds X-Target-Host, and sets a 30-second 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 — flagged ONBOARDING |
| api.queryAll() | Both lists in parallel | Full merged client array with outcome, trend, last success/failure, checked timestamp |
| api.check(id) | /admin-console/admin-sessions-list | Per-client result with snapshots array, trend data — used by runCheck() |
| 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 |
parsePortalTable(html, tableIndex) uses DOMParser to extract every row from a portal page's first (or indexed) <table>. Column names are used as object keys. Rows with all-empty cells are filtered out. 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 |
parseAccounts() and parseSessions().Every entry in allClients (also exposed as window.allClients) follows this shape. Fields in blue are required by the gauge and rendering logic.
ice · defaultredmatrixpurpleambercobaltmonotesla-black (CSS-only, no swatch)Selection is stored in localStorage under spTheme and restored on load. tesla-black has CSS theme variables defined but no swatch in the header — it can be set manually: document.body.setAttribute('data-theme','tesla-black'). To reset to default: localStorage.removeItem('spTheme') then reload.
node main.js. Verify localhost:3000 is selected in the Proxy dropdown in the connection bar. Check that nothing blocks port 3000.admin-accounts-list, and check the actual column headers in the HTML response against the fallback chain in parseAccounts().parseSessions() against the actual sessions HTML. If no sessions match for any client, every client falls through to ONBOARDING.setTimeout(...classList.add('hidden'), 10000). If it persists beyond that, check DevTools console for uncaught exceptions. Manual override: document.getElementById('loadingModal').classList.add('hidden').DEMO_MODE check exists at the top of runCheck() — it must short-circuit to the cached data path before reaching api.check(). If the check is missing, the fix is to add the if (DEMO_MODE) { ... } else { data = await api.check(...) } branch.localStorage under spTechName. Private/incognito mode clears localStorage on close. Enter and Save the name at the start of each session in that environment.portalFetch() has a 30-second AbortController timeout. Slow portal responses or high network latency to backup.securewebportal.net can trigger this. Increase the setTimeout value on the timer inside portalFetch() if persistent.KB · ShadowProtect SPX Console · stack-shadow-protect-spx.html · v1.0