/users/{uid} — Data DictionaryGround truth for every field synced to Firestore, traced directly from the site’s source (not from Sheets output — the Sheets output is downstream of this and has had bugs). Two separate data sources feed the “Users (Auth + Firestore)” dashboard:
fetchAllAuthUsers().w_* fields — the actual Firestore document at /users/{uid}. Written client-side by assets/js/cloud-sync.js, logicgate.js, post.html, course-lesson.html, course-lab.html, course-quiz-block.html, courses/index.html.[!note] Local vs. cloud Every
w_*key is gated behind sign-in — nothing is written to Firestore for a signed-out visitor. Sign-in pulls the cloud copy down and overwrites local storage; sign-out wipes local storage. There is never a merge step.
| Field | Type | Description | Notes |
|---|---|---|---|
uid |
string | Unique ID for this user, assigned by Firebase Auth | doubles as the doc ID in /users/{uid} |
email |
string | The email address they signed in with | |
providers |
string (joined) | Which sign-in method(s) they’ve used | comma-joined list of providerIds (e.g. google.com) |
createdAt |
date | When the account was first created | from createdAt (ms since epoch) |
lastLoginAt |
date | Most recent sign-in | from lastLoginAt (ms since epoch) |
disabled |
boolean | Whether an admin has blocked this account from signing in |
SYNCED_KEYS (arrays of strings)Defined in cloud-sync.js:22. Each is mirrored to localStorage while signed in, and pushed to Firestore via a PATCH with updateMask.fieldPaths scoped to that one key — so writes to one key never touch the others. On sign-in, the whole array is pulled and overwrites local.
| Field | Description | Element type | Written by | Mutation | Order semantics |
|---|---|---|---|---|---|
w_favorites |
Blog posts the user has bookmarked/saved to read later | post URL (string) | _layouts/post.html (save/unsave button) |
push to add, splice to remove — can shrink | append order; index 0 = oldest still-saved favorite (UI reverses it to show newest-first, see reading-list.js:51) |
w_read |
Every blog post the user has opened, logged automatically | post URL (string) | _layouts/post.html (fires passively on every page view, signed-in only) |
push only, de-duped (indexOf check first) — never removed |
append order; index 0 = first ever post read, not most recent |
w_completed |
Blog posts the user explicitly marked as finished reading | post URL (string) | _layouts/post.html (“Mark as complete” button) |
push only, de-duped — never removed | append order; index 0 = first post marked complete |
w_course_progress |
Every course lesson/lab/quiz the user has completed | lesson/lab/quiz URL (string) | _layouts/course-lesson.html, _layouts/course-lab.html, _includes/course-quiz-block.html |
push only, de-duped — never removed | append order = completion order across the whole curriculum, not grouped by course |
w_course_celebrated |
Which courses’ completion animation has already played, so it doesn’t replay on the next visit | course id (string) | courses/index.html |
push to add; explicitly removeItem‘d entirely in one dev/reset path (courses/index.html:376) |
append order |
[!warning] Not currently in the dashboard
w_course_celebratedexists in Firestore and is actively synced, but the Apps Script dashboard doesn’t read it yet — it’s UI bookkeeping (has the celebration animation played?) rather than a metric, but flagging it here in case you want it added.
[!warning] Order correction An earlier version of the dashboard script/notes assumed
w_read’s order was most-recent-first. Source confirms it’spush()— oldest-first.order: 0in the Content Engagement sheet is the first thing read/completed/favorited, not the latest.
MEMORY_JSON_KEYS (single JSON-stringified objects, LogicGate only)Defined in cloud-sync.js:31. Unlike the arrays above, these never touch localStorage while signed in — they live only in memory and in Firestore, so a signed-in player’s scoring leaves nothing on a shared device. Each is written as one Firestore stringValue containing JSON.stringify(obj), not mapped field-by-field. Source: assets/js/logicgate.js.
w_logicgate_state — today’s game| Key | Description | Type | Notes |
|---|---|---|---|
day |
Which daily puzzle this game belongs to | integer | puzzle day index (dayIndex()); state resets to a fresh game whenever day no longer matches today’s index |
guesses |
The words the user has guessed so far today, with the letter-by-letter result for each | array of { word: string, result: string[] } |
one entry per guess submitted; result is per-letter ("correct" \| "present" \| "absent") |
status |
Whether today’s game is still in progress or finished | string | "playing" \| "won" \| "lost" |
finishRecorded |
Whether this finished game has already been counted into the all-time stats | boolean | guards against double-counting a finished game into w_logicgate_stats |
hintUsed |
Whether the user used a hint on today’s puzzle | boolean |
w_logicgate_stats — all-time stats| Key | Description | Type | Notes |
|---|---|---|---|
played |
Total games played, ever | integer | |
wins |
Total games won, ever | integer | |
maxStreak |
Longest daily-win streak ever reached | integer | |
distribution |
How many wins took 1 guess, 2 guesses, etc. | integer[6] | count of wins by guess-count, index 0 = won in 1 guess … index 5 = won in 6 guesses |
w_logicgate_streak — daily win streak| Key | Description | Type | Notes |
|---|---|---|---|
date |
The date of the user’s last counted win | string (YYYY-MM-DD) |
date of the last counted win |
count |
Current consecutive-day win streak | integer | consecutive daily wins; same-day replay doesn’t change it, a gap of >1 day resets to 1 |
[!warning]
w_mldle_*fields do not exist in this codebase Earlier sample data pasted into this conversation includedw_mldle_state,w_mldle_stats,w_mldle_streak. A repo-wide search (grep -ri mldle) finds zero matches anywhere in the site’s HTML/JS. These fields aren’t written by any current code path — likely leftover/test data in Firestore from an earlier prototype, not a live feature. The dashboard script no longer reads them (removed per request), but if real user docs still contain this data in Firestore, it’s orphaned, not wrong.
MEMORY_INT_KEYS (single integer, LogicGate only)Defined in cloud-sync.js:37 (added 2026-07-05). Same “never touches localStorage while signed in” treatment as MEMORY_JSON_KEYS above, but written as a plain Firestore integerValue — no JSON wrapping, since there’s only one number.
| Field | Description | Type | Notes |
|---|---|---|---|
w_logicgate_rank |
The player’s permanent LogicGate rank: their position, in first-ever-win order across every player, the first time they ever won a puzzle | integer | claimed once via CloudSync.claimRank(), never recalculated afterwards — absent/null means the player hasn’t won a puzzle yet (shown as “Unranked” in the UI) |
[!note] Backed by a counter doc outside
/users/{uid}The next rank to hand out is tracked on a single shared document,meta/logicgate_rank_counter(seefirestore.rules) — not part of any user’s doc, so it won’t appear in a per-user Firestore export and doesn’t belong in a per-user dashboard sheet. Only the result of claiming a rank (the integer above) is stored per-user.
localStorage only — never in FirestoreDefined in cloud-sync.js:36 (LOCAL_ONLY_CLEAR_KEYS). Wiped on sign-out for shared-device privacy, same as the synced keys, but not present in the Firestore doc at all — don’t expect these in any Firestore export.
| Field | Description | Notes |
|---|---|---|
w_streak |
The user’s consecutive-day reading streak (separate from w_logicgate_streak) |
considered fine to be per-device |
w_streak_celebrated |
Whether the reading-streak celebration animation has already played | UI flag |
| Sheet | Source fields |
|---|---|
| Users | all auth fields + counts of w_completed/w_course_progress/w_read + all of w_logicgate_state/stats/streak |
| Content Engagement | w_completed, w_read (long format, type + url + order) |
| Favorites | w_favorites (long format, own sheet — kept separate from Content Engagement) |
| Course Progress | w_course_progress, url split into course/section by path segment |
| Game Distributions | w_logicgate_stats.distribution (long format) |
| (not yet mapped) | w_course_celebrated |
[!note] Dashboard script now saved in-repo A copy of the actual Apps Script (
syncFirestoreToSheetand friends) is saved atnotes/firestore-dashboard-sync.gs— bound to the Google Sheet itself (script.google.com), not deployed from this repo, so it must still be pasted in manually after edits here.w_logicgate_rankwas added to its Users sheet (columnlogicgate_rank) on 2026-07-05.