Offline Demo Portal — demo.astradial.com¶
Built 2026-07-20 for Medicall Chennai (Jul 24–26, stall 3A9). Status: LIVE and offline-verified. This replaces the original plan text with the as-built runbook.
What it is¶
A pixel-identical build of the real editor dashboard with no login, no servers, no Firebase, no SIP — every API answered from bundled fixtures for a fictional high-volume org, Astra Hospital (org id demo). Once loaded with internet, a service worker makes it fully offline: refresh, deep links and navigation all work with zero connectivity. Verified by killing the local server mid-session.
- URL: https://demo.astradial.com (Cloudflare Pages project
astradial-demo, direct-upload — no git integration, no CI, no connection to prod/staging deploys) - Branch:
demo-portalin astradial-platform (LOCAL ONLY — never merged to staging/main; the demo ships as a static upload, not through deploy workflows) - USB fallback:
AstradialDevelopment/Dump/astra-demo-offline.zip— unzip andnpx serve outon any laptop.
Architecture (editor/, all behind NEXT_PUBLIC_DEMO=1)¶
| Piece | File | What it does |
|---|---|---|
| Interceptor | lib/demo/index.ts | Monkey-patches window.fetch (any /api/* → fixture router; non-GET → 400 "Demo mode — changes are disabled") and EventSource (fake refresh events). Serves the live-calls SSE as a streaming Response. Seeds org_access in localStorage → auth bypassed. |
| Fixtures | lib/demo/fixtures.ts | Astra Hospital: ~700 calls/day × 7 days, 12 departments, 40 extensions, 26 tickets, CRM companies/leads, workflows, IVR, bot. All timestamps relative to Date.now() at page load — data always reads as "today / T-1 / T-2" on any demo day. Live calls animate via a 3–6 call generator. |
| Bootstrap | components/DemoInit.tsx | Runs interceptor at module scope (before first fetch); registers the SW; renders the "● Demo · Offline ready ✓" pill with caching progress. |
| Export config | next.config.ts (demoConfig), server layout.tsx wrappers with generateStaticParams for [orgId] (demo), [workflowId], [ivrId], [botId] | output: "export" only when the flag is set; production standalone build unchanged (wrappers return []). |
| Build | scripts/build-demo.sh | Stashes app/api + app/admin (unsupported in export), builds with the flag, generates the SW. |
| Service worker | scripts/gen-demo-sw.mjs → out/demo-sw.js | Precaches all ~382 export files. Pages are cached under CLEAN urls (no .html) — Chrome rejects redirected cache entries for navigations, which broke the first attempt. Cache-first with navigation fallback; stray /api/ audio requests get /demo-audio.mp3. |
| Audio | public/demo-audio.mp3 | TTS: "This is a demo audio. To create your account, visit editor dot astradial dot com." All recording/greeting/MOH play paths demo-guarded to it. |
Page-level demo guards: calls-page recording play/download URLs, departments playPreview(), live-calls Firestore phonebook (skipped; save shows demo toast).
Rebuild & redeploy (≈1 minute)¶
cd astradial-platform/editor # branch: demo-portal
# edit lib/demo/fixtures.ts as needed
./scripts/build-demo.sh # → out/
npx wrangler pages deploy out --project-name astradial-demo --branch main
Wrangler is OAuth'd as hariandprojects@gmail.com. After deploy, the SW updates itself on next page load: it installs in the background (~10 s) and the NEW build appears one reload later — don't panic when the first load still shows old data.
Pre-event ritual (day before, on the actual stall laptop)¶
- Open https://demo.astradial.com with internet; wait for "Offline ready ✓" (bottom-right pill).
- Airplane mode ON → click through every sidebar page, refresh a few, play a recording.
- Bookmark the URL; disable sleep and auto-updates; full charge + charger; pack the USB zip.
- If anything looks wrong at the venue: the zip +
npx serve outon localhost is the fallback.
Gotchas learned while building (for the next demo)¶
- SW + redirects: precache
.htmlfiles under clean URLs, or Chrome shows an error page on offline navigations (redirected cache responses are rejected). - Tickets list filter sends
status=open,in_progress,closed(comma list) — fixture filters must split it. Sidebar/overview count probesstatus=open&limit=1and readstotal; the tickets header shows open+in_progress, so the probe answers with the same sum to keep every surface consistent. - Gateway client strips
/adminwhen proxied → fixture routes must match both/api/gateway/admin/orgs/...and/api/gateway-proxy/orgs/.... <audio>/new Audio()loads bypass the fetch interceptor (browser-native) — handled per-page guards + SW fallback, not the interceptor.- Live-call fixture IDs need a monotonic counter —
Date.now()+randomcollided and caused duplicate React keys. - Overview charts are tall; the series render near the bottom — scroll before concluding "charts are empty".