Read this whole file at session start. It is the source of truth for how to work on this codebase. The user is non-technical and works visually — keep responses plain-English, prefer screenshots over code excerpts, and confirm scope before big multi-file changes.
admin_uploader.html toolhasImage gate from the conditional."npx @11ty/eleventy && firebase deploy --only hosting --project ase-portal-5d37case-portal-5d37c (project number: 319996537465)G-NX6HH6GKCG) + Microsoft Clarity (wwplh93pi6)All run from project root: /Users/mariohernandez/Library/CloudStorage/GoogleDrive-mm.hdz@icloud.com/My Drive/Sheridan Technical/ASE_website/ASE_Portal
| What | Command |
|---|---|
| Build the site | npx @11ty/eleventy |
| Build + deploy hosting (most common) | npx @11ty/eleventy && firebase deploy --only hosting --project ase-portal-5d37c |
| Deploy Cloud Functions | firebase deploy --only functions --project ase-portal-5d37c |
| Deploy Firestore Stripe Extension config | firebase deploy --only extensions --project ase-portal-5d37c |
| Local dev preview | npx @11ty/eleventy --serve (port 8080) |
| View hosted site | https://asecollisiontestprep.com |
ASE_Portal/
├── _includes/
│ ├── layout.html # Base template — has site-wide head, schema.org, header, footer
│ ├── header.html # Top nav
│ ├── footer.html # Bottom footer
│ └── article.html # Blog post layout (has share buttons at bottom)
├── b2.html ... b6.html # Per-module landing pages with Stripe checkout buttons
├── simulator.html # THE SIMULATOR — single page serves all 5 modules via ?module=B2 URL param. Huge file (~340KB)
├── dashboard.html # Student dashboard: module unlocks, leaderboard, pass probability gauges
├── dashboard2.html # Admin dashboard: user roster, telemetry, support tickets, analytics
├── login.html / register.html / forgot-password.html / verify.html
├── why-us.html, blog.html, gear.html, faq.html, contact.html, etc.
├── functions/
│ ├── index.js # Cloud Functions (Stripe webhook backup, analytics sync, etc.)
│ └── scripts/ # One-time migration scripts (e.g. create-stripe-live-products.js)
├── extensions/
│ └── firestore-stripe-payments.env # Stripe Extension manifest
├── _site/ # Eleventy build output — DO NOT EDIT, regenerated on every build
├── firebase.json # Hosting + functions + extensions config
├── firestore.rules # Firestore security rules
└── CLAUDE.md # This file
| Collection / Doc | What it holds |
|---|---|
users/{uid} |
Profile: fullName, email, address, role, isAdmin, unlockedModules[], accessExpires, redeemedCode |
customers/{uid} |
Stripe customer record (managed by Firebase Stripe Extension) |
customers/{uid}/checkout_sessions/ |
Pending Stripe sessions — Extension writes back url |
customers/{uid}/payments/{sessionId} |
Succeeded payments — webhook writes here, dashboard reads to unlock modules |
customers/{uid}/subscriptions/ |
Stripe subscriptions (currently unused — all sales are one-time) |
leaderboards/{uid} |
Per-user stats: { name, role, loc, stats: { B2: {correct, wrong, total, time}, B3: {...}, ... } } |
bundled_questions_b2 … _b6 |
Question banks per module (one doc per category, with questions array) |
bundled_data/master_terminology_b2 … _b6 |
Key-term flashcard data per module |
bundled_data/master_terminology |
LEGACY single-doc terminology (still readable as fallback) |
support_tickets/{ticketId} |
Student error reports + general support messages |
analytics_daily/{YYYY-MM-DD} |
Daily GA4 + Clarity aggregate, written by dailyAnalyticsSync Cloud Function |
Firebase Storage folders: term_images/, question_images/, feedback_images/. Term image filename pattern: {uniqueId}.jpg|.gif|.mp4. The imageExt field on each term doc drives the button label (VIEW IMAGE / VIEW GIF / VIEW VIDEO).
| Where | Key | TTL | What it caches |
|---|---|---|---|
simulator.html |
q_cache_v8_{mod} |
24h | Questions per module (localStorage) |
simulator.html |
t_cache_v8_{mod} |
24h | Terminology per module (localStorage) |
dashboard.html |
rc_counts_v1 |
24h | Per-module question + term counts (localStorage) |
dashboard.html |
lb_cache_v1 |
5 min | Full leaderboards collection (sessionStorage) |
simulator.html |
window.__userDocCache |
session | users/{uid} doc deduped across access check + profile load |
simulator.html |
window.__lastDashRefresh |
30s throttle | refreshDashboardStats() rate limit |
If you add a new Firestore read, ASK: does it need to be live? Can it cache? Without caching, 19 students = 1,400 reads each = quota burn.
Live mode is ACTIVE. Real money is being charged.
Architecture: Firebase Stripe Extension (publisher invertase, instance firestore-stripe-payments v0.3.12, region us-east1).
Flow:
customers/{uid}/checkout_sessions/ with { line_items: [{ price: priceId }], metadata: { module: "BX" } }url backcustomers/{uid}/payments/{sessionId}priceId in priceToModuleMap, and unlocks only the matching module for exactly the matching durationB2 — Painting and Refinishing
price_1TbUc0IwFVQrPexDJ3lvUi6Lprice_1TbUc1IwFVQrPexDOKfF5GMpprice_1TbUc2IwFVQrPexDMlwZTb61B3 — Non-Structural Analysis and Damage Repair
price_1TbUc3IwFVQrPexDYFrV5aPK · 60d: price_1TbUc5IwFVQrPexDNkKei1mH · 90d: price_1TbUc6IwFVQrPexDAT3rLIW1B4 — Structural Analysis and Damage Repair
price_1TbUc7IwFVQrPexDlohesIUl · 60d: price_1TbUc8IwFVQrPexDM96Rzn26 · 90d: price_1TbUc9IwFVQrPexD09bx5vDwB5 — Mechanical and Electrical Components
price_1TbUcAIwFVQrPexDtTWjdqsU · 60d: price_1TbUcBIwFVQrPexDtD8tCy8F · 90d: price_1TbUcCIwFVQrPexDkBb0Dr8iB6 — Damage Analysis and Estimating
price_1TbUcDIwFVQrPexDYpH8QMvw · 60d: price_1TbUcEIwFVQrPexDIVdxXFyL · 90d: price_1TbUcFIwFVQrPexDJfQVZ6ZZWhere these IDs live in code:
b2.html–b6.html: const price1Month/2Month/3Month near top of script sectiondashboard.html: priceToModuleMap (~line 1007) and MODULE_PRICES (~line 1404)functions/index.js: priceCatalog (custom webhook backup)Promo codes: allow_promotion_codes: true is set in all checkout sessions. Promo codes managed in Stripe Dashboard → Coupons.
TECHSAVE10 — 10% off, duration: once — WORKSFREETESTPREP2026 — 100% off — REJECTED BY STRIPE (Checkout in mode: payment cannot apply discounts that result in $0 totals). User has chosen to skip this; will create a different code.Adaptive Pricing: DISABLED at https://dashboard.stripe.com/settings/adaptive-pricing — left off because it interferes with promo codes.
Secrets (in Google Cloud Secret Manager under project ase-portal-5d37c):
firestore-stripe-payments-STRIPE_API_KEY — live rk_live_... restricted keyext-firestore-stripe-payments-STRIPE_WEBHOOK_SECRET — live whsec_...STRIPE_SECRET_KEY / STRIPE_WEBHOOK_SECRET — not yet set (only needed if reviving the custom webhook in functions/index.js)NEVER print API keys, webhook secrets, or any credentials in chat. They live only in Secret Manager.
| ID | Task | Notes |
|---|---|---|
| #14 | 65-question cert-style exam per category in simulator | All 5 ASE Q types, timed, no inline check, exam review at end |
| #16 | Simulator how-to-use tutorial (mobile + desktop) | First-visit modal walkthrough |
| #20 | Donation flow on dashboard | Decision pending: Stripe Payment Link vs Ko-fi vs custom |
| #21 | Admin: smart question spell-checker | Needs LLM API + Cloud Function |
| #23 | Fix GA4 active users showing 0 on dashboard2 | Needs Realtime API + function redeploy. Currently uses runReport (daily aggregate); should use runRealtimeReport |
| #24 | Support ticket: resolution notifications + student-facing history | Add "My Tickets" section to dashboard.html, email when resolved |
| #25 | dashboard2 telemetry roster: modules in single row + per-user stats | Layout change |
| #26 | Global Learning Insights: fix wrong "most active" + add "Revoke All modules" security action | Aggregation query bug + new admin button |
| #30 | Automate blog article posting with images | Workflow: Google Doc → Eleventy markdown OR admin form → Firestore + 11ty |
| #31 | Blog comments | Recommend Firestore-backed comments with dashboard2 moderation (auth already exists) |
mode: payment. Use 99% off if you need effectively-free.hasImage field in Firestore is unreliable — show the VIEW IMAGE button based on uniqueId presence, not on hasImage.--no-verify or skip git hooks unless the user explicitly asks.Before starting work:
While working:
npx @11ty/eleventy to verify build is cleanfirebase deploy --only hosting --project ase-portal-5d37cWhen stuck or unsure:
Check the auto-memory files (live on the current Mac only, may not be present on other machines):
~/.claude/projects/-Users-mariohernandez-Library-CloudStorage-GoogleDrive-mm-hdz-icloud-com-My-Drive-Sheridan-Technical-ASE-website-ASE-Portal/memory/MEMORY.md (and siblings)If you're on a fresh machine and those don't exist, this CLAUDE.md is the canonical context.