Skip to content

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-portal in 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 and npx serve out on 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.mjsout/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)

  1. Open https://demo.astradial.com with internet; wait for "Offline ready ✓" (bottom-right pill).
  2. Airplane mode ON → click through every sidebar page, refresh a few, play a recording.
  3. Bookmark the URL; disable sleep and auto-updates; full charge + charger; pack the USB zip.
  4. If anything looks wrong at the venue: the zip + npx serve out on localhost is the fallback.

Gotchas learned while building (for the next demo)

  • SW + redirects: precache .html files 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 probes status=open&limit=1 and reads total; the tickets header shows open+in_progress, so the probe answers with the same sum to keep every surface consistent.
  • Gateway client strips /admin when 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()+random collided and caused duplicate React keys.
  • Overview charts are tall; the series render near the bottom — scroll before concluding "charts are empty".