← Back to Manual

Technical Reference

Meeting Tools v3.2.4  ·  Developer contract for index.html  ·  Southern Dutchess Toastmasters

Sections Architecture Firebase LocalStorage Keys Global State Screens Functions CSS Data Schemas Rules for Safe Changes Deployment

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

FunctionDirectionTrigger
autoSyncDown()Cloud → LocalApp open, club switch
autoSyncUp()Local → CloudSpeaker add/delete, filler change, settings save
syncUp(clubId)Local → CloudManual Push button
syncDown(clubId)Cloud → LocalManual Pull button
writeSession(data)Local → CloudCommit & 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)

ConstantFull Key (prod)Stores
KEY_CLUBSprod_clubsJSON array of club objects
KEY_ACTIVE_CLUBprod_active_clubActive club ID string
KEY_MDprod_md_toggleBoolean string for markdown export
KEY_AI_KEYprod_openai_keyOpenAI API key string
KEY_DEVICE_NAMEprod_device_nameFull name of this device's user for session audit trail
KEY_LAST_SESSIONprod_last_session_tsAlias — actual storage is club-scoped (see below)

Club-Scoped Keys

Pattern: prefix + 'club_' + clubId + '_' + type

Type SuffixExample Full KeyStores
speakersprod_club_c123_speakersJSON array of speaker name strings
fillersprod_club_c123_fillersJSON array of filler word strings
logsprod_club_c123_logsJSON array of session log strings
wod_fullprod_club_c123_wod_fullObject {word, definition, example}
custom_limitsprod_club_c123_custom_limitsObject {g, cy, r} in minutes
transcriptsprod_club_c123_transcriptsJSON array of transcript objects
last_session_tsprod_club_c123_last_session_tsUnix 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:

KeyPurpose
prod_sdtm_club_nameV2 club name — triggers migration if present
prod_sdtm_speakersV2 roster
prod_sdtm_logsV2 logs
prod_sdtm_fillersV2 filler list
prod_sdtm_wod_fullV2 WOD
prod_sdtm_transcriptsV2 transcripts

Global State Variables

VariableTypePurpose
clubsarrayAll club objects for this device
activeClubIdstringID of the currently active club
currentModestring'timer' or 'counter'
currentSpeakerstringActive speaker's name (uppercased)
currentTallyobject{fillerWord: count} for current session
ivintervalTimer interval handle
startnumberTimestamp when timer last started/resumed
elapsednumberSeconds elapsed in current timer session
limsobject{g, y, r} thresholds in seconds
selectedTypeLabelstringSelected role label e.g. "SPEECH"
fillerListarrayActive club's filler word list
allSpeakersarrayActive club's roster
activeWodDataobject{word, definition, example} from current WOD
recordingEnabledbooleanWhether Record Speech toggle is on
mediaRecorderMediaRecorderActive recorder instance (null when idle)
recordingStreamMediaStreamActive microphone stream (null when idle)
audioChunksarrayCollected audio data blobs
transcriptResultstringPlain text from last Whisper transcription
transcriptCountsobject{fillerWord: count} auto-detected from transcript
recordingStartTimenumberTimestamp when recording started
speechDurationnumberSeconds of recorded speech
activeTranscriptIndexnumberIndex of transcript currently open in viewer
sessionFromTranscriptbooleanFlag set true by mergeTranscriptToCounter() to prevent saveCounterSession() from writing a duplicate session
counterStartTimenumberTimestamp (ms) recorded when initTally() runs — used to compute manual session duration for filler scoring
activeReportTabstring'local' or 'cloud' — tracks which tab is active on the report screen

PACE State Variables v3.2.4

VariableTypePurpose
paceRolesarrayCurrent club PACE roles — [{name, points}] — loaded from Firestore or PACE_ROLES default
paceSelectedYearstringCurrently displayed TM year e.g. "2025-26"
paceCurrentMemberstringMember name open in entry or history screen
paceEntryDateStrstringSelected date in entry screen — "YYYY-MM-DD"
paceEntryCacheobjectKeyed by member name — cached Firestore entries for selected date
paceMemberSelectedRolesSetRoles toggled on in current member entry screen
paceHistoryEntriesarrayEntries loaded for member history drill-down
paceEntryReturnFnfunction|nullNavigation callback — set when entering from history, null from entry screen
paceSpinnerTimertimer|nullModule-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 IDPurposeOn Show
welcomeFirst-run onboarding — Join, Create, or Explore
menuMain hub with Timer / Ah-Counter tab toggle
wodWord of the Day — search, generate, select
setupSpeaker name input + roster + record toggle + role gridrenderSetupGrid(), refreshRoster()
counterDisplayLive tally grid during counter session
timerDisplayLive countdown/countup timer
reportTwo-tab report: This Device (local log) and Cloud (all devices today via Firestore)switchReportTab('local'), loadReport()
settingsAPI key, markdown toggle, backup/restore, clear
clubManagerManage clubs, sync codes, push/pullrenderClubManager()
fillerManagerAdd/edit/delete filler words for active clubrenderFillerManager()
progressSpeaker improvement overview from FirestoreloadProgress()
speakerProgressIndividual speaker session historyopenSpeakerProgress(name)
transcriptionWhisper recording, transcript, and filler analysisinitTranscription()
transcriptListList of all saved transcripts this meetingloadTranscriptList()
transcriptViewSingle transcript with summary and shareopenTranscript(i)

PACE Screens v3.2.4

Screen IDPurpose
pacePACE leaderboard — ranked list, year selector, Roles and Embed buttons
paceEntryEntry roster — date picker, member list with entry status
paceMemberEntryRole selection for one member — checkbox grid, live tally, Save/Remove
paceMemberHistoryMember drill-down — summary card, chronological entry list
paceRoleManagerAdd/edit/delete roles and point values — synced to cloud

Functions

Init & Club Management

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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)

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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

FunctionWhat 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)

VariableValueUsage
--bg#F2F2F7Page background
--card#FFFFFFCard / surface background
--accent#007AFFPrimary blue — buttons, names, tabs
--text#000000Primary text
--subtext#3A3A3CSecondary text
--red#FF3B30Timer max / delete actions
--green#34C759Timer min / save actions
--yellow#FFCC00Timer target
--gold#BF941AWOD accent color
--border#D1D1D6Card and input borders

Key Classes to Preserve

Class / IDPurpose
.screen / .screen.activeShow/hide screen system — do not rename
.tab / .tab.activeMode switcher tabs
.main-menu-cardMenu item cards
.role-card / .role-card.selectedSetup grid role cards
.tally-card / .tally-card.wod-activeCounter tally cards
.tally-del / .tally-minusDelete/decrement on tally cards
.wod-card-big, .wod-word, .wod-def, .wod-exampleWOD display layout
.footer-btn / .secondary-btnAll action buttons
.button-rowButton container at screen bottom
.credit-footerBottom branding area on menu
#customModalModal overlay — sole modal system
.highlight-fillerRed highlight on filler words in transcript
.highlight-wodGold highlight on Word of the Day in transcript
.sync-code-boxSync code display in club manager
.progress-speaker-rowClickable speaker row in progress list
.session-rowIndividual session entry in speaker history
.spinnerLoading 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