| Layer | Technology |
|---|---|
| Frontend | Semantic HTML5 + plain CSS3 + JavaScript ES6 (vanilla, no framework, no bundler) |
| Backend | Node.js + Express (static file serving, legacy fallback API) |
| Realtime persistence | Supabase (Postgres + Realtime), called directly from the browser |
| Client persistence | localStorage (scenes, patch, design, theme) |
| Hosting | Vercel (static output + serverless functions in api/) |
| Graphics | Native 2D Canvas (Stage View, Catalyst preview) |
Browser (public/) Supabase (Postgres + Realtime)
┌───────────────────────┐ ┌───────────────────────────┐
│ console.html │ │ table: cues │
│ ├── app.js (core) │──REST───▶ │ ├── insert/select/delete │
│ ├── module-*.js (x9) │◀─realtime── │ └── Row Level Security policy │
│ └── style.css │ └───────────────────────────┘
└───────────────────────┘
│ Vercel (api/)
▼ ┌───────────────────────┐
localStorage │ /api/config │
(scenes, patch, │ → exposes Supabase URL │
design, theme) │ + anon key to frontend │
└───────────────────────┘
Every module-*.js is an independent script (IIFE) that reads/writes shared state on window.GMA3 and injects its own panel into the DOM. Cue data lives in Supabase and is shared/synced in real time across every open session; everything else lives in the browser.
grandma3-project/
├── server.js Express: static serving + legacy in-memory cue API
├── api/ Vercel serverless functions (config, health)
├── vercel.json Static output config, framework auto-detect disabled
├── package.json
├── public/
│ ├── index.html Landing page ("Enter Simulation")
│ ├── console.html Console shell: screen tabs + physical deck
│ ├── css/style.css Green-terminal theme, console/deck skin
│ ├── docs/ Servable copies of the HTML docs
│ └── js/
│ ├── app.js Core: faders, cues (Supabase), scenes, patch,
│ │ design, command line, encoders, Stage View
│ └── module-*.js Self-registering panels (9 modules)
├── supabase/schema.sql Table + RLS policies + realtime publication
├── samples/default-scenes.json Ready-to-import demo scene pack
└── docs/ Documentation (Markdown + HTML)
pointer event → percentage calculation → state.faders[i] updated → DOM + Stage View repaint
form submit → supabase.from('cues').insert(...) → Realtime event fired
↓
every open session: loadCues() → re-render grid
Store Scene → localStorage.setItem → render list GO → localStorage.getItem → state.faders = scene → repaint
setInterval(N seconds) → next scene → apply faders → sync Catalyst layer
| Method | Route / call | Purpose |
|---|---|---|
| GET | /api/health | Server health check |
| GET | /api/config | Exposes Supabase URL/anon key to the frontend |
| - | supabase.from('cues').insert() | Create a cue (primary path) |
| - | supabase.from('cues').select() | List cues (primary path) |
| - | supabase.from('cues').delete() | Delete a cue (primary path) |
| POST/GET/DELETE | /api/cue/* | Legacy in-memory fallback, used only if Supabase isn't configured |
Full detail with request/response examples: docs/API.md.
| Data | Where it lives | Survives a restart? | Shared across users? |
|---|---|---|---|
| Cues | Supabase (Postgres) | Yes | Yes, in real time (Realtime subscription) |
| Scenes | Browser localStorage | Yes (per browser) | No |
| Fixture patch | localStorage | Yes (per browser) | No |
| Design | localStorage | Yes (per browser) | No |
| Theme | localStorage | Yes (per browser) | No |
Path to a production-ready setup (see also docs/TECHNICAL.md):
localStorage, so a setup is shared across stations.localStorage, ES6 and WebSocket support