← Back to Manual
Technical Reference
Meeting Tools v3.2.4 · Developer contract for index.html · Southern Dutchess Toastmasters
Architecture Overview
Meeting Tools is a single-file PWA (index.html) with no build system, no bundler, and no external CSS files. All styles, markup, and JavaScript live in one file. Firebase SDKs are loaded via CDN at runtime.
Multi-Club Data Model (v3.x)
As of v3.0.0, all data is scoped to a club. Each club has a unique local ID. The active club ID is stored in localStorage. Data keys are prefixed with both the environment prefix and the club ID:
prefix + 'club_' + clubId + '_' + dataType
// e.g. prod_club_c1234567890_speakers
Environment Detection
const isTest = window.location.pathname.toLowerCase().includes('/test/');
const prefix = isTest ? 'test_' : 'prod_';
This means the same file deployed at /test/index.html and /index.html maintains fully isolated data in the same browser with zero code changes.
Club Object Schema
{
id: string, // 'c' + Date.now() — unique local ID
name: string, // Display name
syncKey: string, // Firestore document ID (empty string if no sync)
practiceOnly: bool // If true: no cloud sync, no session writes
}
Firebase Integration v3.3
Configuration
const firebaseConfig = {
apiKey: "AIzaSyC2QfJzXrVg9TmUasmtfLbegSlUGmg5JR4",
authDomain: "sdtm-tm-app.firebaseapp.com",
projectId: "sdtm-tm-app",
storageBucket: "sdtm-tm-app.firebasestorage.app",
messagingSenderId: "429843202069",
appId: "1:429843202069:web:eec66b741bc3fc90934e1c"
};
Uses Firebase compat API v9.23.0 loaded via CDN. No bundler required. Three services initialized:
const db = firebase.firestore(); // Firestore database
const auth = firebase.auth(); // Google Authentication (v3.3.0+)
Firebase Auth (v3.3.0+)
Google Sign-In via signInWithPopup. Auth state persists across sessions via Firebase's default local persistence.
const provider = new firebase.auth.GoogleAuthProvider();
await auth.signInWithPopup(provider); // triggers Google popup
auth.onAuthStateChanged(async (user) => { ... }); // fires on page load + sign-in/out
auth.getRedirectResult(); // called on load to handle redirect flows
Whitelist check: on every sign-in, the app reads superAdmins/{user.email}. If the doc exists with authorized: true, currentAdminUser is set and the admin panel renders. Otherwise the user is signed out immediately.
Firestore Structure
superAdmins/ ← Root-level collection (v3.3.0+)
[email]/ ← Document ID = Gmail address
authorized: boolean ← Must be true
clubs/
[syncKey]/ ← 8-char code e.g. "ABCD-EF3G"
name: string
speakers: string[]
fillers: string[]
apiKey: string
createdAt: timestamp
lastUpdated: timestamp
wod: object ← {word, definition, example} — written by selectWod()
wodDate: string ← toDateString() of date WOD was selected
paceRoles: object[] ← [{name, points}] — club's PACE role list, synced
sessions/
[auto-id]/
speaker: string
role: string
date: string
type: 'timer' | 'counter'
createdAt: timestamp
// Timer sessions also include:
result: string ← e.g. "05:23"
elapsedSeconds: number
// Counter sessions also include:
fillerDetail: object ← {fillerWord: count}
totalFillers: number
// Transcription and scored manual sessions (≥15s) also include:
totalWords: number
fillerPct: string ← e.g. "4.2"
fillerScore: string ← "EXCELLENT" | "GOOD" | "NEEDS WORK"
fillersPerMin: string
durationMins: string
Sync Functions
| Function | Direction | Trigger |
autoSyncDown() | Cloud → Local | App open, club switch |
autoSyncUp() | Local → Cloud | Speaker add/delete, filler change, settings save |
syncUp(clubId) | Local → Cloud | Manual Push button |
syncDown(clubId) | Cloud → Local | Manual Pull button |
writeSession(data) | Local → Cloud | Commit & Exit, Save Results, Merge to Counter |
All sync functions are fire-and-forget with silent catch — a failed sync never interrupts the user. practiceOnly clubs are always skipped.
Roster Merge Strategy
Pull operations use a union merge — the merged roster is the combination of local and cloud speakers, sorted. This means additions always propagate but deletions currently do not sync automatically. The manual Push button pushes the exact local state.
Sync Code Format
8 characters, split with a hyphen after position 4. Character set excludes confusable characters (no 0, O, 1, I): ABCDEFGHJKLMNPQRSTUVWXYZ23456789. The sync code is the Firestore document ID.
Security
Current Firestore rules: allow read, write: if true (open access). The sync code acts as the shared secret — knowing the code is required to access a club's data. Rules should be tightened before wide public distribution.
LocalStorage Keys
Global Keys (not club-scoped)
| Constant | Full Key (prod) | Stores |
KEY_CLUBS | prod_clubs | JSON array of club objects |
KEY_ACTIVE_CLUB | prod_active_club | Active club ID string |
KEY_MD | prod_md_toggle | Boolean string for markdown export |
KEY_AI_KEY | prod_openai_key | OpenAI API key string |
KEY_DEVICE_NAME | prod_device_name | Full name of this device's user for session audit trail |
KEY_LAST_SESSION | prod_last_session_ts | Alias — actual storage is club-scoped (see below) |
Club-Scoped Keys
Pattern: prefix + 'club_' + clubId + '_' + type
| Type Suffix | Example Full Key | Stores |
speakers | prod_club_c123_speakers | JSON array of speaker name strings |
fillers | prod_club_c123_fillers | JSON array of filler word strings |
logs | prod_club_c123_logs | JSON array of session log strings |
wod_full | prod_club_c123_wod_full | Object {word, definition, example} |
custom_limits | prod_club_c123_custom_limits | Object {g, cy, r} in minutes |
transcripts | prod_club_c123_transcripts | JSON array of transcript objects |
last_session_ts | prod_club_c123_last_session_ts | Unix timestamp (ms) of last committed session — used for stale data banner |
Legacy v2 Keys (migration source only)
The following keys are read once during migration from v2 and never written again:
| Key | Purpose |
prod_sdtm_club_name | V2 club name — triggers migration if present |
prod_sdtm_speakers | V2 roster |
prod_sdtm_logs | V2 logs |
prod_sdtm_fillers | V2 filler list |
prod_sdtm_wod_full | V2 WOD |
prod_sdtm_transcripts | V2 transcripts |
Global State Variables
| Variable | Type | Purpose |
clubs | array | All club objects for this device |
activeClubId | string | ID of the currently active club |
currentMode | string | 'timer' or 'counter' |
currentSpeaker | string | Active speaker's name (uppercased) |
currentTally | object | {fillerWord: count} for current session |
iv | interval | Timer interval handle |
start | number | Timestamp when timer last started/resumed |
elapsed | number | Seconds elapsed in current timer session |
lims | object | {g, y, r} thresholds in seconds |
selectedTypeLabel | string | Selected role label e.g. "SPEECH" |
fillerList | array | Active club's filler word list |
allSpeakers | array | Active club's roster |
activeWodData | object | {word, definition, example} from current WOD |
recordingEnabled | boolean | Whether Record Speech toggle is on |
mediaRecorder | MediaRecorder | Active recorder instance (null when idle) |
recordingStream | MediaStream | Active microphone stream (null when idle) |
audioChunks | array | Collected audio data blobs |
transcriptResult | string | Plain text from last Whisper transcription |
transcriptCounts | object | {fillerWord: count} auto-detected from transcript |
recordingStartTime | number | Timestamp when recording started |
speechDuration | number | Seconds of recorded speech |
activeTranscriptIndex | number | Index of transcript currently open in viewer |
sessionFromTranscript | boolean | Flag set true by mergeTranscriptToCounter() to prevent saveCounterSession() from writing a duplicate session |
counterStartTime | number | Timestamp (ms) recorded when initTally() runs — used to compute manual session duration for filler scoring |
activeReportTab | string | 'local' or 'cloud' — tracks which tab is active on the report screen |
PACE State Variables v3.2.4
| Variable | Type | Purpose |
paceRoles | array | Current club PACE roles — [{name, points}] — loaded from Firestore or PACE_ROLES default |
paceSelectedYear | string | Currently displayed TM year e.g. "2025-26" |
paceCurrentMember | string | Member name open in entry or history screen |
paceEntryDateStr | string | Selected date in entry screen — "YYYY-MM-DD" |
paceEntryCache | object | Keyed by member name — cached Firestore entries for selected date |
paceMemberSelectedRoles | Set | Roles toggled on in current member entry screen |
paceHistoryEntries | array | Entries loaded for member history drill-down |
paceEntryReturnFn | function|null | Navigation callback — set when entering from history, null from entry screen |
paceSpinnerTimer | timer|null | Module-level timeout for leaderboard spinner — prevents stale retry loop |
Admin & Meeting Planner State Variables v3.3.0
| Variable | Type | Purpose |
currentAdminUser | FirebaseUser|null | Signed-in admin user object. Null when not authenticated. |
adminClubCache | array | Fetched club records for admin panel — [{id, name, lastUpdated}]. Client-side filtered without re-fetching. |
selectedTheme | string | Title of the theme selected in Meeting Planner. Used in table topics prompt and copy/share output. |
selectedThemeReason | string | One-sentence relevance description of the selected theme. Included in copy/share output. |
rosterSelectedName | string | Full name locked when user taps a roster entry on setup screen. Guards against partial-backspace bug — cleared only when typed input diverges from this value. |
Screens
All screens use showScreen(id) — hides all .screen divs, shows target. Additional setup runs for certain screens (see function table).
| Screen ID | Purpose | On Show |
welcome | First-run onboarding — Join, Create, or Explore | — |
menu | Main hub with Timer / Ah-Counter tab toggle | — |
wod | Word of the Day — search, generate, select | — |
setup | Speaker name input + roster + record toggle + role grid | renderSetupGrid(), refreshRoster() |
counterDisplay | Live tally grid during counter session | — |
timerDisplay | Live countdown/countup timer | — |
report | Two-tab report: This Device (local log) and Cloud (all devices today via Firestore) | switchReportTab('local'), loadReport() |
settings | API key, markdown toggle, backup/restore, clear | — |
clubManager | Manage clubs, sync codes, push/pull | renderClubManager() |
fillerManager | Add/edit/delete filler words for active club | renderFillerManager() |
progress | Speaker improvement overview from Firestore | loadProgress() |
speakerProgress | Individual speaker session history | openSpeakerProgress(name) |
transcription | Whisper recording, transcript, and filler analysis | initTranscription() |
transcriptList | List of all saved transcripts this meeting | loadTranscriptList() |
transcriptView | Single transcript with summary and share | openTranscript(i) |
PACE Screens v3.2.4
| Screen ID | Purpose |
pace | PACE leaderboard — ranked list, year selector, Roles and Embed buttons |
paceEntry | Entry roster — date picker, member list with entry status |
paceMemberEntry | Role selection for one member — checkbox grid, live tally, Save/Remove |
paceMemberHistory | Member drill-down — summary card, chronological entry list |
paceRoleManager | Add/edit/delete roles and point values — synced to cloud |
Meeting Planner & Admin Screens v3.3.0
| Screen ID | Purpose | On Show |
meetingPlanner | Meeting prep hub — WOD card, Theme Suggester, Table Topics Generator | initMeetingPlanner() — updates WOD status label |
wod | Word of the Day — search/random/select. Back returns to meetingPlanner. | Clears input field |
Functions
Init & Club Management
| Function | What it does |
initApp() | Body onload — runs migration, checks for fresh install, loads club data, sets UI state, calls autoSyncDown(). Shows welcome screen if no clubs exist. |
migrateIfNeeded() | Detects v2 data and migrates to v3 club structure. No-ops on fresh installs. |
loadClubData() | Reads clubs array and active club's speakers/fillers from localStorage into globals. |
updateClubUI() | Updates subtitle, footer, and settings labels to reflect active club name + sync status. |
getActiveClub() | Returns the active club object from the clubs array. |
clubKey(suffix) | Returns scoped localStorage key for active club: prefix+'club_'+activeClubId+'_'+suffix |
showClubSwitcher() | Shows the bottom sheet club switcher overlay. |
closeClubSwitcher(event) | Closes the club switcher. Closes on backdrop tap. |
switchClub(id) | Sets activeClubId, reloads club data, updates UI, triggers autoSyncDown(). |
renderClubManager() | Builds club manager list with sync controls for each club. |
askAddClub() | Modal to create a new club with name and Practice Only toggle. |
askEditClub(id) | Modal to rename a club. |
askDeleteClub(id) | Modal to delete a club and all its local data. Blocks deletion of the currently active club with an error modal. |
Firebase Sync
| Function | What it does |
generateSyncCode() | Returns an 8-char sync code (format XXXX-XXXX) using unambiguous characters. |
enableSync(clubId) | Generates code, creates Firestore document with roster/fillers/apiKey, saves code to club object. |
askJoinByCode() | Modal prompts for sync code, fetches club from Firestore, creates local club, pulls all data. |
autoSyncDown() | Silent pull on app open/club switch. Merges roster (union), cloud wins on fillers and apiKey. |
autoSyncUp() | Silent push on any roster or filler change. Skips if no syncKey or practiceOnly. |
syncUp(clubId) | Manual push — sends speakers, fillers, apiKey to Firestore. Updates status label. |
syncDown(clubId) | Manual pull — fetches and merges speakers, fillers, apiKey. Updates status label. |
setSyncStatus(clubId, msg) | Updates sync status text in club manager UI. |
writeSession(data) | Fire-and-forget write to sessions subcollection. Skips if no syncKey or practiceOnly. |
Welcome Screen
| Function | What it does |
showWelcomeJoin() | Modal for sync code entry. Joins club, pulls all data, proceeds to menu. |
showWelcomeCreate() | Modal for new club name + Practice Only toggle. Creates club, proceeds to menu. |
showWelcomePractice() | Sets active club to TEST CLUB and proceeds directly to menu. |
Filler Manager
| Function | What it does |
renderFillerManager() | Builds list of current club's filler words with edit/delete buttons. |
askAddFiller() | Modal to add a new filler word. Pushes to cloud on confirm. |
askEditFiller(oldWord) | Modal to rename a filler word. Pushes to cloud on confirm. |
askDeleteFiller(word) | Modal to remove a filler word. Pushes to cloud on confirm. |
Progress
| Function | What it does |
loadProgress() | Fetches last 500 sessions from Firestore, groups by speaker, filters to current roster members only, renders list with rates and trends. |
openSpeakerProgress(encodedName) | Fetches all sessions for a speaker, renders summary stats and session-by-session history. |
Word of the Day
| Function | What it does |
nextWod() | Picks random word from WOD_LIBRARY, calls fetchWordDetails(). |
searchWod() | Reads manual input, calls fetchWordDetails(). |
fetchWordDetails(word) | Calls GPT-4o-mini for definition + example JSON. Renders to WOD card. |
selectWod() | Pins active WOD to fillerList, saves to wod_full in localStorage. Writes wod and wodDate to Firestore club doc. Returns to meetingPlanner screen. |
updateWodBanner() | Reads wod_full from localStorage. If set, shows word/definition/example in the Verbal tab gold banner. Hides banner if no word is active. Called when switching to Verbal tab. |
Meeting Planner v3.3.0
| Function | What it does |
initMeetingPlanner() | On screen show — updates WOD status label to show active word or "Tap to search or generate". |
suggestThemes() | Reads meeting date input. Calls GPT-4o-mini with a prompt requesting 5 themed suggestions tied to that date. Parses JSON response, calls renderThemes(). |
renderThemes(themes) | Renders 5 selectable theme cards. Each card onclick calls selectTheme(). |
selectTheme(index, title, reason) | Highlights selected card, sets selectedTheme and selectedThemeReason, reveals Table Topics section, clears previous questions and action buttons. |
generateTableTopics() | Calls GPT-4o-mini with selected theme. Returns 8 numbered questions. Shows Copy/Share buttons on success. |
buildTableTopicsText() | Assembles copy/share output: theme title, reason, divider, numbered questions. Applies markdown formatting if KEY_MD toggle is on. |
copyTableTopics() | Calls buildTableTopicsText(), writes to clipboard via navigator.clipboard. |
shareTableTopics() | Calls navigator.share() if available (mobile). Falls back to copyTableTopics() on desktop. |
Roster
| Function | What it does |
refreshRoster() | Renders full speaker list in speakerArea. |
saveSpeaker() | Validates name, warns if single word, adds to roster, auto-pushes to cloud. |
doAddSpeaker(n) | Performs the actual add after single-name warning is accepted. |
selectSpeaker(n) | Fills name input and sets rosterSelectedName when a roster name is tapped. |
handleNameInput(el) | Uppercases input, filters roster live. Clears rosterSelectedName only if typed value diverges from the locked name — backspacing alone preserves the lock. |
renderSpeakerListFiltered(f) | Renders a filtered subset of the roster. |
askDeleteSpeaker(n) | Modal to confirm deletion. Auto-pushes to cloud on confirm. |
clearNameInput() | Clears name input, resets roster to full list, clears rosterSelectedName. |
Super Admin v3.3.0
| Function | What it does |
handleAdminSignIn() | Opens Google Sign-In popup via GoogleAuthProvider. onAuthStateChanged handles result. |
handleAdminSignOut() | Calls auth.signOut(). onAuthStateChanged fires and calls renderAdminSignedOut(). |
renderAdminSignedIn() | Hides login card, shows signed-in card with email, calls loadAdminClubs(). |
renderAdminSignedOut() | Shows login card, hides signed-in card and admin panel. |
loadAdminClubs() | Fetches all docs from root clubs collection. Populates adminClubCache with id, name, lastUpdated. Calls renderAdminClubList(). |
renderAdminClubList(clubs) | Renders club rows with name, sync key, last updated date, and Delete button. Shows count footer. |
filterAdminClubs(query) | Client-side filter on adminClubCache by name or ID. Re-renders list without Firestore call. |
askDeleteAdminClub(id, name) | Confirmation modal showing club name. On confirm, deletes Firestore doc and removes from adminClubCache. |
Navigation & Setup
| Function | What it does |
showScreen(id) | Hides all screens, shows target. Triggers setup functions for relevant screens. |
renderSetupGrid() | Builds role card grid — timer presets or counter roles depending on currentMode. |
selectRole(el, g, y, r, label) | Sets lims and selectedTypeLabel, highlights selected card. |
askCustomTime(el) | Modal with threshold inputs (timer) or custom role name (counter). |
switchMode(m) | Switches between timer and counter tabs, updates UI accordingly. |
handleStart() | Validates name + role. Routes to transcription, timerDisplay, or counterDisplay. |
Tally (Counter)
| Function | What it does |
initTally() | Resets currentTally to zero for all fillers, records counterStartTime = Date.now() for manual scoring, renders grid. |
renderTally() | Builds tally cards, sorts WOD to top. |
incTally(f) | Increments a filler count. |
decTally(e, f) | Decrements a filler count (min 0). |
delFiller(e, f) | Removes filler from current session via modal confirm. |
saveCounterSession() | Saves/merges tally to club logs. For sessions ≥15 seconds, estimates word count at 130 wpm and computes fillerPct, fillerScore, fillersPerMin, durationMins — same fields as transcription sessions. Shows score card in modal before dismissing. Calls writeSession() with full stats. |
parseTallyString(s) | Deserializes log string back to object. |
serializeTally(o) | Serializes tally object to log string. |
addFillerOnTheFly() | Opens modal to add a new filler during a live session. Starts count at 1. Toggle saves permanently to club list (default on). |
Timer
| Function | What it does |
startTimer() | Starts interval, updates display, changes body background on thresholds. |
togglePause() | Pauses/resumes the timer interval. Button label toggles PAUSE ↔ RESUME. Resume uses inline interval to preserve exact elapsed time. |
resetTimer() | Stops interval, resets elapsed to 0, resets display and background, restarts clock. |
commitAndExit() | Saves result to logs, calls writeSession(), resets background, returns to menu. |
Report
| Function | What it does |
loadReport() | Builds formatted report string from club logs + WOD, renders to reportLog (This Device tab). |
switchReportTab(tab) | Switches between 'local' and 'cloud' report panels. Sets activeReportTab. Calls loadCloudReport() on first cloud switch. |
loadCloudReport() | Async. Queries Firestore for today's sessions across all devices. Fetches club doc for WOD. Renders Timing Report and Counter Report sections separately with full breakdown. Updates status line with count and timestamp. |
generateReportText() | Returns plain or markdown report string. If activeReportTab is 'cloud', returns the rendered cloud panel text directly. |
copyReport() | Copies active tab's report to clipboard. |
shareReport() | Shares active tab's report via Web Share API. |
Transcription
| Function | What it does |
initTranscription() | Resets all transcription state, sets up record button. |
startTranscriptionRecording() | Requests microphone, starts MediaRecorder, updates UI. |
stopTranscriptionRecording() | Stops MediaRecorder and stream, triggers Whisper. |
sendToWhisper() | POSTs audio blob to OpenAI Whisper API, calls analyzeTranscript(). |
analyzeTranscript(text) | Detects fillers in transcript text, computes score and stats. |
showTranscriptResults(text) | Renders highlighted HTML, score card, and filler breakdown. |
showTranscriptionError(msg) | Renders error state with retry button. |
mergeTranscriptToCounter() | Saves transcript to localStorage, calls writeSession(), navigates to counterDisplay. |
cancelTranscription() | Stops stream if active, returns to menu. |
Transcript List & View
| Function | What it does |
loadTranscriptList() | Renders list of saved transcripts from club localStorage. |
openTranscript(i) | Loads transcript at index i into the viewer screen. |
shareTranscript() | Shares active transcript via Web Share API or clipboard. |
Settings & Data
| Function | What it does |
saveSettings() | Saves markdown toggle and API key. Calls autoSyncUp(). |
exportData() | Downloads JSON backup of active club's speakers, fillers, name, and API key. |
handleImportFile(i) | Reads JSON backup, restores speakers/fillers/apiKey, reloads page. |
askClearLogs() | Modal to confirm reset of club logs, WOD, transcripts, and last_session_ts. |
checkStaleBanner() | Reads last_session_ts — shows yellow stale data banner if data is 3+ hours old and logs exist. Called on menu show and initApp. |
toggleSyncCodeVisibility(clubId, syncKey) | Toggles sync code display between masked (••••-••••) and revealed in Club Manager. |
openModal(t, b, c, cancelLabel, confirmLabel) | Core modal. cancelLabel and confirmLabel are optional — default to "Cancel" / "OK". |
CSS
CSS Variables (:root)
| Variable | Value | Usage |
--bg | #F2F2F7 | Page background |
--card | #FFFFFF | Card / surface background |
--accent | #007AFF | Primary blue — buttons, names, tabs |
--text | #000000 | Primary text |
--subtext | #3A3A3C | Secondary text |
--red | #FF3B30 | Timer max / delete actions |
--green | #34C759 | Timer min / save actions |
--yellow | #FFCC00 | Timer target |
--gold | #BF941A | WOD accent color |
--border | #D1D1D6 | Card and input borders |
Key Classes to Preserve
| Class / ID | Purpose |
.screen / .screen.active | Show/hide screen system — do not rename |
.tab / .tab.active | Mode switcher tabs |
.main-menu-card | Menu item cards |
.role-card / .role-card.selected | Setup grid role cards |
.tally-card / .tally-card.wod-active | Counter tally cards |
.tally-del / .tally-minus | Delete/decrement on tally cards |
.wod-card-big, .wod-word, .wod-def, .wod-example | WOD display layout |
.footer-btn / .secondary-btn | All action buttons |
.button-row | Button container at screen bottom |
.credit-footer | Bottom branding area on menu |
#customModal | Modal overlay — sole modal system |
.highlight-filler | Red highlight on filler words in transcript |
.highlight-wod | Gold highlight on Word of the Day in transcript |
.sync-code-box | Sync code display in club manager |
.progress-speaker-row | Clickable speaker row in progress list |
.session-row | Individual session entry in speaker history |
.spinner | Loading animation for async operations |
Data Schemas
Transcript Object (localStorage)
{
speaker: string,
role: string,
date: string, // toLocaleString()
highlightedHTML: string, // rendered HTML with highlight spans
fillerSummary: string, // e.g. "UM: 3, AH: 2"
plainText: string, // raw Whisper transcript
fillerPct: string, // e.g. "4.2"
fillerScore: string, // "EXCELLENT" | "GOOD" | "NEEDS WORK"
totalWords: number,
totalFillers: number,
fillersPerMin: string,
durationMins: string
}
Log String Format (localStorage)
"SPEAKER NAME | ROLE LABEL :: RESULT"
// Timer example: "KEVIN SHAN | SPEECH :: 05:42"
// Counter example: "KEVIN SHAN | AH COUNTER :: UM: 3, AH: 2"
// No fillers: "KEVIN SHAN | GRAMMARIAN :: 0 Fillers"
Default Filler List
["AH", "UH", "UM", "ER", "SO", "LIKE", "YOU KNOW", "WORD OF DAY"]
WORD OF DAY is a placeholder replaced by the active WOD word's uppercase form when a WOD is selected.
Rules for Safe Feature Addition
1. Every existing function name must remain identical — callers in HTML use string names.
2. Every localStorage key constant name must remain identical.
3. Every screen ID must remain identical — showScreen() references them by string.
4. No existing CSS class may be renamed or removed without updating all references.
5. New screens are added as <div id="newscreen" class="screen"> blocks — showScreen() handles them automatically.
6. New localStorage keys must follow the prefix + 'club_' + clubId + '_' pattern for club-scoped data, or pk(NEW_CONSTANT) for global keys.
7. New global variables must not shadow any existing variable name.
8. The body background must always return to var(--bg) when leaving the timer screen.
9. openModal(t, b, c) is the only modal system — never use alert() or confirm().
10. Transcripts are never included in backup export JSON — they are meeting-specific session data.
11. writeSession() must always be a fire-and-forget call — never await it in a user-blocking context.
12. The practiceOnly flag must be checked before any Firestore write. Use autoSyncUp() and writeSession() which check it automatically.
13. Never prompt for localStorage access in incognito — the app must work fully offline and in private browsing.
Deployment Notes
- The
prefix variable auto-detects /test/ in the URL and uses test_ for all keys. No code changes needed when promoting from test to production.
- Copying
/test/index.html to /index.html is the full deployment process.
- Test and production data are fully isolated in localStorage on the same device.
- Test and production Firestore data are also isolated — test clubs created at
/test/ write to Firestore with test_-prefixed sync codes. (Note: sync codes are generated independently, not prefixed — but test users only share codes with other test users.)
- The version label is rendered dynamically by
initApp() into #versionLabel.
- Service worker (
sw.js) uses network-first strategy — new deployments are served immediately without cache busting required.
- Firebase Firestore security rules are currently open (
if true). Tighten before wide public distribution.
- The Cloud Meeting Report uses a date-range query on
createdAt with orderBy. This requires a composite index on the sessions subcollection. Firebase will log a direct link to create it on first use if it doesn't exist.