ASE Collision Test Prep — Project Brief for Claude

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.


Who you're working with

Communication rules

  1. No technical jargon. Say "the button now shows for all terms," not "removed the hasImage gate from the conditional."
  2. Ask before big changes. Anything touching more than one file or replacing significant content — describe the plan first and wait for "go ahead."
  3. Verify visually. After UI changes, take a screenshot in preview or send a deploy so the user can see it on the live site.
  4. Always deploy. The user goes straight to the live site to check work. After confirmed changes: npx @11ty/eleventy && firebase deploy --only hosting --project ase-portal-5d37c
  5. Auto-deploy is approved. User has granted permission to deploy without asking each time. Build → deploy → report URL.

Project at a glance


Critical commands

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

File structure

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

Firestore data model

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


Caching strategy (IMPORTANT for read costs)

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.


Stripe integration

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:

  1. Client writes to customers/{uid}/checkout_sessions/ with { line_items: [{ price: priceId }], metadata: { module: "BX" } }
  2. Extension creates a Stripe Checkout Session, writes url back
  3. Client redirects to Stripe Checkout
  4. After payment, Extension's webhook writes customers/{uid}/payments/{sessionId}
  5. dashboard.html listens to that subcollection, looks up the priceId in priceToModuleMap, and unlocks only the matching module for exactly the matching duration

LIVE Price IDs (per module — all 5 modules priced identically: $99 / $158 / $207)

B2 — Painting and Refinishing

B3 — Non-Structural Analysis and Damage Repair

B4 — Structural Analysis and Damage Repair

B5 — Mechanical and Electrical Components

B6 — Damage Analysis and Estimating

Where these IDs live in code:

Promo codes: allow_promotion_codes: true is set in all checkout sessions. Promo codes managed in Stripe Dashboard → Coupons.

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

NEVER print API keys, webhook secrets, or any credentials in chat. They live only in Secret Manager.


Open roadmap (as of last session)

Priority order (user's choice)

  1. Stripe live-mode migration — ✅ DONE
  2. In-app upsell ad every 3 questions in simulator (Task #9 — pending) — original top priority
  3. Google Sheet → gear page affiliate links (Task #10 — pending)
  4. Write 12 SEO blog articles (Task #11 — pending) — needed for AdSense reapproval
  5. Submit AdSense for re-review (Task #12 — pending)

Other open tasks (sorted by general impact)

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)

Recently completed (this session)


Things that bit us recently — don't repeat

  1. Firestore reads can explode fast. 19 students = 27K reads in one day before optimization. Always cache static data (questions, terminology, counts) in localStorage with TTL. Use sessionStorage for medium-lived data (leaderboards). Throttle anything called on navigation.
  2. Stripe Coupon duration MUST be "Once" for one-time payment Checkout. "Multiple months" / "Repeating" only works for subscriptions and will silently reject the code with no error.
  3. Stripe 100% off codes don't work in mode: payment. Use 99% off if you need effectively-free.
  4. Adaptive Pricing breaks promo codes. Leave it OFF.
  5. hasImage field in Firestore is unreliable — show the VIEW IMAGE button based on uniqueId presence, not on hasImage.
  6. Don't use --no-verify or skip git hooks unless the user explicitly asks.
  7. Tracking task list: Use TaskCreate/TaskUpdate for multi-step work so the user can see progress.

Working with the user — quick checklist

Before starting work:

While working:

When stuck or unsure:


If something looks weird and not documented here

Check the auto-memory files (live on the current Mac only, may not be present on other machines):

If you're on a fresh machine and those don't exist, this CLAUDE.md is the canonical context.