← 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.1
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.
Firestore Structure
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 |
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 |
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. Also writes wod and wodDate fields to the Firestore club document for use by Cloud Meeting Report. |
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 when a roster name is tapped. |
handleNameInput(el) | Uppercases input, filters roster live as user types. |
renderSpeakerListFiltered(f) | Renders a filtered subset of the roster. |
askDeleteSpeaker(n) | Modal to confirm deletion. Auto-pushes to cloud on confirm. |
clearNameInput() | Clears the name input field and resets roster to full list. Called by the inline ✕ clear button. |
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.