A single-file SPA (index.html) for managing call-center QA evaluations, backed by Netlify Functions and a Postgres/Supabase data layer. Includes role-based access control, an Auto QA pipeline that scores Yellow Messenger transcripts (rule-based and LLM), and an NPS dashboard.
Requirements: Node 20+, npm, and the Netlify CLI for local functions/dev server.
npm installThe frontend is a static file, so any static server works for UI-only work:
npx http-server . -p 8934To exercise the Netlify Functions (auth, data sync, Auto QA) locally, use Netlify Dev instead, which also proxies /api/* for you:
netlify devSet these in Netlify (Site configuration → Environment variables) for production, or in a local .env for netlify dev. Every integration degrades gracefully when its variables are unset — the app runs client-side-only without them.
| Variable | Used for |
|---|---|
SUPABASE_URL, SUPABASE_ANON_KEY |
Supabase mirror of app data (netlify/functions/data.ts, reconcile.ts) |
QMS_SESSION_SECRET |
Signs/verifies session tokens issued by netlify/functions/auth.ts |
AUTOQA_AI_API_KEY, AUTOQA_AI_MODEL |
LLM-based Auto QA scoring (Khmer/English transcripts) |
ANTHROPIC_API_KEY, ANTHROPIC_MODEL |
Anthropic-specific Auto QA scoring path |
netlify/functions/auto-qa-daily.ts also runs as a Netlify Scheduled Function (0 2 * * *) once deployed — no separate cron setup needed.
npm run typecheck # tsc --noEmit over netlify/functions, lib, db
npm run check-index-syntax # parse-checks index.html's inline <script>
npm run test:dbsync-merge # regression test for the offline-sync merge logic
npm run ci # all of the above — same as CI runs on push/PR to main- Offline sync is merge-based, not replace-based.
_DbSync(inindex.html) diffs each bucket against the last-confirmed server state and pushes only what actually changed (op:'merge'with explicit upserts/deletes) via the server'sqams_merge_bucketfunction — never a blind full-array replace. A stale device's sync can never silently overwrite data it doesn't know about. Seescripts/test_dbsync_merge_logic.mjsfor the regression coverage. /api/data's GET is intentionally unauthenticated (health-check/initial load), but never returns password hashes —data.tsstrips thepasswordfield from theusersbucket before responding. Session-staleness detection (_sigingetCurrentUser()) is keyed on apwVersioncounter, not the password hash itself.
Deploys to Netlify (netlify.toml configures the publish directory and security headers). Push to main to trigger a deploy once the site is linked (netlify link / netlify init).
MIT — see LICENSE.