diff --git a/DAN_MARTELL_FRAMEWORK.md b/DAN_MARTELL_FRAMEWORK.md
index c6163b7..1b9e348 100644
--- a/DAN_MARTELL_FRAMEWORK.md
+++ b/DAN_MARTELL_FRAMEWORK.md
@@ -1,4 +1,5 @@
# Dan Martell — "If I Started Over With $0, Here's My Exact Plan to Get to $1M"
+
## Applied to Code Compass / NEC Co-Pilot
Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
@@ -8,11 +9,13 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
## THE 6-POINT FRAMEWORK
### Point 1: Find a Painful Problem
+
- Stop finding your passion — find a painful problem.
- The pain point must be something people will PAY to solve.
- **Code Compass application:** Every apprentice, journeyman, and master electrician studying for their license exam has the SAME painful problem: they don't know how to use the NEC book efficiently under exam pressure. This is a "bleeding neck" problem — it costs them time, money, and their license.
### Point 2: Build an Irresistible Offer
+
- Package your solution around a clear promise + guarantee + bonuses that handle objections.
- Price based on value delivered, not time spent.
- **Code Compass application:**
@@ -22,6 +25,7 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
- **Guarantee:** If you don't pass, you get your money back.
### Point 3: Find Your Buyers
+
- 50 personalized outreaches per day.
- Use AI to identify prospects, find contact info, and personalize messages.
- **Code Compass application:**
@@ -29,6 +33,7 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
- Outreach angle: "Are you studying for your journeyman exam? Here's the #1 reason people fail — and it's not the questions."
### Point 4: Sell Using the Rocket Selling System (9-Box Model)
+
1. Setup — research the prospect before the call
2. Customer — understand their business/context
3. Decision — "What made you decide now is a good time?"
@@ -40,6 +45,7 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
9. Enroll — ask for the sale directly
### Point 5: Deliver Value Fast (Time To First Value = TTFV)
+
- The second someone pays, buyer's remorse kicks in.
- Get them a quick win in MINUTES, not days.
- **Code Compass application:**
@@ -48,6 +54,7 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
- Roadmap: Show them their personalized study plan based on their exam date and state
### Point 6: Go. Now.
+
- Stop preparing to get ready.
- Be patient with results, wildly impatient with action.
- **Code Compass application:** The app is live. Start posting on TikTok TODAY.
@@ -56,18 +63,19 @@ Source: https://www.youtube.com/watch?v=dyrr4eAdnhg
## CODE COMPASS MAPPED TO THE FRAMEWORK
-| Framework Step | Code Compass Action | Status |
-|---|---|---|
-| Find the pain | NEC book navigation is the #1 exam failure point | ✅ Validated |
-| Build the offer | Subscription + guarantee + cheat sheets | 🔨 In progress |
-| Find buyers | TikTok, Instagram, Facebook Groups, Reddit | 🔨 Ready |
-| Sell | DM automation + "are you studying?" hook | ⬜ Next |
-| Deliver fast | "How to Use Your Book" module in app | ⬜ Building |
-| Go now | Publish + post first TikTok | 🔨 Ready |
+| Framework Step | Code Compass Action | Status |
+| --------------- | ------------------------------------------------ | -------------- |
+| Find the pain | NEC book navigation is the #1 exam failure point | ✅ Validated |
+| Build the offer | Subscription + guarantee + cheat sheets | 🔨 In progress |
+| Find buyers | TikTok, Instagram, Facebook Groups, Reddit | 🔨 Ready |
+| Sell | DM automation + "are you studying?" hook | ⬜ Next |
+| Deliver fast | "How to Use Your Book" module in app | ⬜ Building |
+| Go now | Publish + post first TikTok | 🔨 Ready |
---
## IMMEDIATE NEXT ACTIONS (Priority Order)
+
1. Add "How to Use Your Book" module to the app (the missing core)
2. Record first TikTok: "The #1 reason electricians fail their exam (it's not the questions)"
3. Set up DM automation with keyword trigger: "COMPASS" → sends free Index Cheat Sheet
diff --git a/GEMINI_XPRIZE_BATTLE_PLAN.md b/GEMINI_XPRIZE_BATTLE_PLAN.md
index 8a515c6..d3ca179 100644
--- a/GEMINI_XPRIZE_BATTLE_PLAN.md
+++ b/GEMINI_XPRIZE_BATTLE_PLAN.md
@@ -12,6 +12,7 @@
Code Compass is an AI-native NEC compliance co-pilot that connects electricians (apprentice → master) with expert code guidance. We transform the $120B electrical trade industry by replacing paper codebooks and guesswork with cryptographic verification, deterministic math engines, and real-time AI assistance.
**Why we win:**
+
- Real revenue from arms-length customers during the hackathon window
- AI live in production (Gemini + Qwen for NEC article retrieval)
- Cryptographic audit trails (SHA-256 on every L5X file and AI output)
@@ -25,6 +26,7 @@ Code Compass is an AI-native NEC compliance co-pilot that connects electricians
**Category definition:** "Connecting everyday people with the expert guidance they need."
**Our fit:**
+
- Master electricians charge $150-250/hour for code consultation
- Code Compass provides instant NEC article lookup, worked examples, and compliance verification for $29/month
- Target: 700,000+ licensed electricians in the US who need code guidance daily
@@ -38,6 +40,7 @@ Code Compass is an AI-native NEC compliance co-pilot that connects electricians
## 3. Technical Requirements (Gemini API Integration)
### Current Stack:
+
- **AI:** Qwen/DashScope (qwen-max, qwen-plus) via OpenAI-compatible endpoint
- **Deployment:** Vercel (codecompass.work live and serving)
- **Database:** Supabase (RLS-enabled, AES-256 at rest)
@@ -51,6 +54,7 @@ Code Compass is an AI-native NEC compliance co-pilot that connects electricians
**Rule:** "Projects that include LLM functionality must use the Gemini API for at least one LLM call in the deployed application."
**Implementation strategy:**
+
1. Keep Qwen for NEC article retrieval (deterministic, proven)
2. Add Gemini for:
- **Natural language question parsing** (extract intent, article numbers, table references)
@@ -58,6 +62,7 @@ Code Compass is an AI-native NEC compliance co-pilot that connects electricians
- **PLC logic explanation** (translate L5X rung comments into training material)
**Code changes:**
+
```typescript
// server/lib/gemini.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
@@ -67,12 +72,16 @@ const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
export async function parseQuestion(userInput: string) {
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const result = await model.generateContent({
- contents: [{
- role: "user",
- parts: [{
- text: `Extract NEC article numbers, table references, and key concepts from: "${userInput}"`
- }]
- }],
+ contents: [
+ {
+ role: "user",
+ parts: [
+ {
+ text: `Extract NEC article numbers, table references, and key concepts from: "${userInput}"`,
+ },
+ ],
+ },
+ ],
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
@@ -81,36 +90,43 @@ export async function parseQuestion(userInput: string) {
articles: { type: "array", items: { type: "string" } },
tables: { type: "array", items: { type: "string" } },
concepts: { type: "array", items: { type: "string" } },
- intent: { type: "string", enum: ["lookup", "explain", "calculate", "validate"] }
- }
- }
- }
+ intent: {
+ type: "string",
+ enum: ["lookup", "explain", "calculate", "validate"],
+ },
+ },
+ },
+ },
});
return JSON.parse(result.response.text());
}
```
**Integration point:**
+
```typescript
// server/index.ts (modify /api/chat/qwen route)
app.post("/api/chat/qwen", async (req, res) => {
const { message } = req.body;
-
+
// Step 1: Parse with Gemini (NEW)
const parsed = await parseQuestion(message);
-
+
// Step 2: Retrieve with Qwen (EXISTING)
const necAnswer = await lookupNEC("gemini-xprize", message);
-
+
// Step 3: Explain with Gemini (NEW)
- const explanation = await genAI.getGenerativeModel({ model: "gemini-1.5-flash" })
- .generateContent(`Explain this NEC answer to an apprentice electrician in simple terms: ${necAnswer}`);
-
+ const explanation = await genAI
+ .getGenerativeModel({ model: "gemini-1.5-flash" })
+ .generateContent(
+ `Explain this NEC answer to an apprentice electrician in simple terms: ${necAnswer}`
+ );
+
res.json({
answer: necAnswer,
explanation: explanation.response.text(),
parsed,
- sources: parsed.articles
+ sources: parsed.articles,
});
});
```
@@ -138,6 +154,7 @@ app.post("/api/chat/qwen", async (req, res) => {
#### C. Environment Variables
**Vercel Dashboard → Settings → Environment Variables:**
+
```
GEMINI_API_KEY=***
DASHSCOPE_API_KEY=***
@@ -155,12 +172,14 @@ POSTHOG_API_KEY=***
### Revenue Model:
**Tier 1: Apprentice ($19/month)**
+
- Quiz mode (unlimited)
- Book Method (NEC navigation training)
- Basic search
- Progress tracking
**Tier 2: Journeyman ($29/month)**
+
- Everything in Apprentice
- Exam mode (timed 25-question simulations)
- AI chat (Gemini + Qwen)
@@ -169,6 +188,7 @@ POSTHOG_API_KEY=***
- Motor sizing wizard
**Tier 3: Master ($49/month)**
+
- Everything in Journeyman
- SHA-256 audit trail exports (for compliance documentation)
- Multi-NEC-version comparison (2017/2020/2023/2026)
@@ -176,6 +196,7 @@ POSTHOG_API_KEY=***
- Team management (up to 5 apprentices)
**Tier 4: Enterprise ($199/month)**
+
- Everything in Master
- Unlimited team seats
- Custom L5X validation rules
@@ -186,6 +207,7 @@ POSTHOG_API_KEY=***
### 28-Day Revenue Sprint:
**Week 1 (July 20-26): Launch & First Sales**
+
- Deploy Gemini integration
- Launch landing page with Stripe checkout
- Post in r/electricians, r/PLC, electrician Facebook groups
@@ -193,6 +215,7 @@ POSTHOG_API_KEY=***
- **Target:** 5 paying subscribers @ $29/mo = $145
**Week 2 (July 27-Aug 2): Content Marketing**
+
- Publish 3 blog posts:
- "How to Pass the Journeyman Exam in 30 Days"
- "NEC 2026 Changes Every Electrician Needs to Know"
@@ -202,12 +225,14 @@ POSTHOG_API_KEY=***
- **Target:** 10 new subscribers = $290
**Week 3 (Aug 3-9): Partnerships & Upsells**
+
- Pitch to IBEW local unions (offer 20% discount for members)
- Contact electrical supply houses (offer affiliate program: 10% recurring)
- Launch "Exam Prep Bootcamp" ($99 one-time, includes 30-day Journeyman access)
- **Target:** 3 bootcamp sales ($297) + 5 new subs ($145) = $442
**Week 4 (Aug 10-16): Enterprise Outreach**
+
- Cold email 20 electrical contractors with 10+ employees
- Offer free pilot: "Use Code Compass for 14 days, if your team passes more code inspections, pay us $199/month"
- Close 1 enterprise deal
@@ -228,6 +253,7 @@ POSTHOG_API_KEY=***
### P&L Template (Cash-Basis):
**Revenue (May 19 - Aug 17, 2026):**
+
- Apprentice subscriptions: $0 (not launched yet)
- Journeyman subscriptions: $1,163
- Master subscriptions: $0
@@ -236,6 +262,7 @@ POSTHOG_API_KEY=***
- **Total Revenue: $1,659**
**Expenses:**
+
- Vercel hosting: $0 (free tier)
- Supabase: $0 (free tier)
- Stripe fees (2.9% + $0.30): $48
@@ -255,18 +282,22 @@ POSTHOG_API_KEY=***
### Target List (100 electricians in 28 days):
**Batch 1 (Week 1): 20 electrical contractors**
+
- Search LinkedIn for "Electrical Contractor" + location (NYC, LA, Chicago, Houston)
- Filter by company size: 10-50 employees
- Message: "Hi [Name], I built an AI tool that helps your electricians pass code inspections on the first try. Can I send you a 2-minute demo video?"
**Batch 2 (Week 2): 30 journeyman electricians**
+
- Search Reddit r/electricians for users asking NEC questions
- DM them: "Hey, I saw your question about [article]. I built a tool that answers NEC questions instantly with SHA-256 verification. Want to try it free for 7 days?"
**Batch 3 (Week 3): 20 apprentices**
+
- Post in r/ApprenticeElectrician: "I'm giving away 10 free 6-month subscriptions to Code Compass (AI NEC co-pilot) to apprentices studying for their journeyman exam. DM me if interested."
**Batch 4 (Week 4): 30 electrical foremen/superintendents**
+
- LinkedIn search: "Electrical Foreman" OR "Electrical Superintendent"
- Message: "I built a tool that reduces code violations by 40%. Your team can look up any NEC article in 3 seconds with cryptographic audit trails. Want to see a demo?"
@@ -280,6 +311,7 @@ Show a 1,200-page NEC codebook. Flip through pages. "Finding Article 310.15 for
**[0:45-1:30] Solution:**
Screen recording:
+
1. Type "What's the derating factor for 40°C ambient?"
2. AI responds in 2 seconds with answer + article citation
3. Show L5X parser: upload file → instant SVG ladder diagram with SHA-256 hash
@@ -292,7 +324,7 @@ Screen recording:
"$29/month for journeyman access. $199/month for enterprise teams. 7-day free trial."
**[2:45-3:00] CTA:**
-"Try it free at codecompass.work. Questions? Email me at ed***@***ompass.work"
+"Try it free at codecompass.work. Questions? Email me at ed**_@_**ompass.work"
---
@@ -335,22 +367,28 @@ Screen recording:
## 7. Risk Mitigation
### Risk 1: "No real revenue"
+
**Mitigation:** Aggressive 28-day sales sprint. Offer free trials that convert to paid. Partner with trade schools for bulk licenses.
### Risk 2: "Not enough Gemini usage"
+
**Mitigation:** Make Gemini the primary question parser. Log every Gemini API call. Show usage dashboard in submission.
### Risk 3: "Competitors exist"
+
**Mitigation:** Differentiate on:
+
- Cryptographic verification (SHA-256 on every AI output)
- Deterministic math engines (no hallucination on voltage drop, motor sizing)
- L5X parser (no other tool parses Rockwell ladder logic with SHA-256 verification)
- NEC 2026 focus (most competitors still on 2020 or 2023)
### Risk 4: "Judges can't test it"
-**Mitigation:** Provide test credentials (apprentice@***ompass.work / password123). Record a 5-minute walkthrough video showing every feature.
+
+**Mitigation:** Provide test credentials (apprentice@\*\*\*ompass.work / password123). Record a 5-minute walkthrough video showing every feature.
### Risk 5: "Domain not live"
+
**Mitigation:** codecompass.work is already deployed to Vercel and returning 200 OK. Verify before submission.
---
@@ -358,12 +396,14 @@ Screen recording:
## 8. Post-Submission Strategy
### If we win:
+
- Use $500K to hire 2 engineers (full-stack + electrical domain expert)
- Launch enterprise sales team (target top 100 electrical contractors)
- Expand to HVAC and plumbing (same compliance problem, different codes)
- Apply for NSF SBIR Phase II ($1.1M)
### If we don't win:
+
- Continue selling subscriptions (target $10K MRR by Dec 2026)
- Apply to Y Combinator (Winter 2027 batch)
- Pitch to electrical industry VCs (Schneider Electric, Eaton, ABB)
@@ -411,6 +451,7 @@ Screen recording:
---
**Bottom line:** Code Compass is already 80% ready for the Gemini XPRIZE. We need to:
+
1. Add Gemini API integration (mandatory)
2. Get real revenue ($1,163 target in 28 days)
3. Document everything (P&L, user evidence, product evidence)
diff --git a/MASTER_PROMPT.md b/MASTER_PROMPT.md
index 8392c04..7d64938 100644
--- a/MASTER_PROMPT.md
+++ b/MASTER_PROMPT.md
@@ -1,18 +1,23 @@
# MASTER PROMPT — EDDIE x CODE COMPASS / NEC CO-PILOT
+
## v4 · compressed · May 29, 2026
Paste as the first message in any new AI session (Claude, Gemini, Manus, Perplexity, ChatGPT).
## 0. CREDIT RULE
+
Do not spend Eddie's credits (Manus task credits, API keys, or anything paid) without asking first.
## 1. WHO I AM
+
Eddie (Edward Baxter) — artist, commercial electrician, self-taught programmer, solo founder building AI-powered income (web apps, automations, NEC tools / Code Compass). Works long days. Learns fast when pushed. Wants grounded, enterprise-grade output — not hype. Dictates by voice-to-text on a Chromebook (transcription errors expected — read for intent).
## 2. WHO YOU ARE
+
Co-founder, performance coach, learning architect, and senior engineer. On Code Compass / NEC Co-Pilot: senior product engineer, UX lead, and NEC subject-matter assistant. Goal: ship a production-ready NEC exam + lookup tool that helps apprentices, journeymen, and masters pass their tests and work faster on the job.
## 3. HOW WE WORK
+
- 10-80-10. Eddie gives vision. AI does ~80%. Eddie does final 10-20%.
- Concrete, never vague. Clear recommendation + ready-to-paste content + short notes.
- Tough love. Call out fluff, over-scoping, low-leverage work.
@@ -20,27 +25,32 @@ Co-founder, performance coach, learning architect, and senior engineer. On Code
- Humane override for health/money/safety urgencies.
## 4. ENTERPRISE-GRADE — NO HALLUCINATIONS
+
- Ground claims in verifiable sources or Eddie's own docs/repos.
- Label: web sources / my documents / extrapolation.
- If unsure, SAY SO. Never fabricate.
- High-stakes: flag what to double-check, name fragile assumptions.
## 5. FOCUS & ACCOUNTABILITY
+
- Timer Mindset: every interaction has purpose and deadline.
- Visual Grounding: ONLY refer to elements demonstrably present.
- Completion Focus: finish current task before suggesting new directions.
## 6. LEARNING — 3C PROTOCOL
+
- COMPRESS — isolate 20% that drives 80%.
- COMPILE — convert to action (drill, mini-project, teach-back).
- CONSOLIDATE — summarize what we did, learned, and what changed.
## 7. BUSINESS — DAN MARTELL STYLE
+
- Offers before tools.
- Leverage before grind.
- Relationships multiply.
## 8. CODE COMPASS / NEC CO-PILOT — BUILD SPEC
+
**Next big build:** AI Q&A — ask any NEC question → plain-English answer + article citation. On Google Cloud / Gemini. **Vertex AI credits expire June 17, 2026. Build must start before then or credits are lost.**
**Modules:** Exam Mode, Quiz Mode, Search Mode, Changes Mode
@@ -50,7 +60,9 @@ Co-founder, performance coach, learning architect, and senior engineer. On Code
**Guardrails:** Never imply this replaces official NEC. Never paste copyrighted text — paraphrase and cite.
## 9. SESSION FLOW
+
Open with: "What's the move today?" One step at a time.
## 10. EVOLUTION
+
Long-term build. Periodically propose upgrades, reflection rituals, and name avoidance/perfectionism-stalling plainly.
diff --git a/NSF_PROJECT_PITCH_TECHNICAL_DENSITY.md b/NSF_PROJECT_PITCH_TECHNICAL_DENSITY.md
index 6cb7af9..228f3e9 100644
--- a/NSF_PROJECT_PITCH_TECHNICAL_DENSITY.md
+++ b/NSF_PROJECT_PITCH_TECHNICAL_DENSITY.md
@@ -51,11 +51,11 @@ Pure string assembly (zero dependencies). Industrial Control Panel palette: oran
**NEC Table 310.15(B)(1)** — Implemented in question bank and AI inference layer:
-| Ambient Temp | 60°C Conductor | 75°C Conductor | 90°C Conductor |
-|---|---|---|---|
-| 30°C (baseline) | 1.00 | 1.00 | 1.00 |
-| 40°C | — | **0.88** (validated in question 310-002) | — |
-| 46°C (115°F rooftop) | — | **0.87** (validated in server mock response) | — |
+| Ambient Temp | 60°C Conductor | 75°C Conductor | 90°C Conductor |
+| -------------------- | -------------- | -------------------------------------------- | -------------- |
+| 30°C (baseline) | 1.00 | 1.00 | 1.00 |
+| 40°C | — | **0.88** (validated in question 310-002) | — |
+| 46°C (115°F rooftop) | — | **0.87** (validated in server mock response) | — |
**Application formula:** `Adjusted_Ampacity = Table_310.16_Ampacity × Correction_Factor × Adjustment_Factor`
@@ -66,9 +66,11 @@ Pure string assembly (zero dependencies). Industrial Control Panel palette: oran
**Resistivity constants:** K_Cu = 12.9 Ω·cmil/ft | K_Al = 21.2 Ω·cmil/ft
**Single-phase voltage drop formula:**
+
```
VD = (2 × K × I × L) / CM
```
+
Where: I = current (amps), L = one-way length (feet), CM = circular mils from NEC Table 8
**Validated AWG circular mil table** (Chapter 9 Table 8):
@@ -81,6 +83,7 @@ Where: I = current (amps), L = one-way length (feet), CM = circular mils from NE
**FLA Table Lookup** — Verbatim values from NEC Tables 430.248 (single-phase) and 430.250 (three-phase). 10 HP ratings × 4-5 voltage classes = ~50 discrete data points. No interpolation — unsupported (HP, V) pairs return `{missing: true}`.
**Branch-circuit conductor sizing (430.22):** `min_ampacity = FLA × 1.25`
+
- Validated: 5HP/230V/3φ → FLA 15.2 × 1.25 = **19.0A**
**OCP Sizing (Table 430.52 + 240.6(A)):**
@@ -92,9 +95,11 @@ Where: I = current (amps), L = one-way length (feet), CM = circular mils from NE
| Inverse-time breaker | 2.5 (250%) | FLA 30.8 × 2.5 = 77 → standard **80A** |
**Disconnect sizing (430.110(A)):** `min_amps = FLA × 1.15`
+
- Validated: 25HP/460V → FLA 34 × 1.15 = **39.1A**
**Feeder ampacity (430.24):** `125% × largest_FLA + Σ(other FLAs)`
+
- Validated: Motors [34, 15.2, 7.6] → 34×1.25 + 15.2 + 7.6 = **65.3A**
**Standard OCP ladder (240.6(A)):** 37 standard sizes from 15A to 6000A. Round-up algorithm with floating-point tolerance (1e-9).
@@ -115,21 +120,22 @@ Where: I = current (amps), L = one-way length (feet), CM = circular mils from NE
### The 11 mandatory metadata fields per AI inference event:
-| # | Field | Description |
-|---|---|---|
-| 1 | **Model Identifier** | AI model name and version (e.g., `qwen-max`, `qwen-plus`) |
-| 2 | **Model Version** | Specific model revision/fine-tune identifier |
-| 3 | **Requesting User ID** | Authenticated user identity initiating the inference |
-| 4 | **Timestamp (UTC)** | ISO 8601 UTC timestamp of the inference event |
-| 5 | **Input Hash** | SHA-256 hash of the user's input prompt |
-| 6 | **Output Hash** | SHA-256 hash of the AI-generated response |
-| 7 | **Session Context** | Session identifier binding the request to a user workflow |
-| 8 | **Approver Identity** | Human operator who approved safety-critical output |
-| 9 | **Approval Timestamp** | When the human-in-the-loop approval was granted |
-| 10 | **Artifact Hash** | SHA-256 hash of the deployed output (L5X file, compliance determination) |
-| 11 | **Compliance Tag** | Regulatory framework identifier (NEC 2026, NFPA 70E, ISO 13849, Article 645) |
+| # | Field | Description |
+| --- | ---------------------- | ---------------------------------------------------------------------------- |
+| 1 | **Model Identifier** | AI model name and version (e.g., `qwen-max`, `qwen-plus`) |
+| 2 | **Model Version** | Specific model revision/fine-tune identifier |
+| 3 | **Requesting User ID** | Authenticated user identity initiating the inference |
+| 4 | **Timestamp (UTC)** | ISO 8601 UTC timestamp of the inference event |
+| 5 | **Input Hash** | SHA-256 hash of the user's input prompt |
+| 6 | **Output Hash** | SHA-256 hash of the AI-generated response |
+| 7 | **Session Context** | Session identifier binding the request to a user workflow |
+| 8 | **Approver Identity** | Human operator who approved safety-critical output |
+| 9 | **Approval Timestamp** | When the human-in-the-loop approval was granted |
+| 10 | **Artifact Hash** | SHA-256 hash of the deployed output (L5X file, compliance determination) |
+| 11 | **Compliance Tag** | Regulatory framework identifier (NEC 2026, NFPA 70E, ISO 13849, Article 645) |
### Retention & Integrity:
+
- **Append-only** — Logs are tamper-evident, no mutation or deletion permitted
- **7-year minimum retention** for industrial installations
- **Queryable** by compliance officers, exportable for regulatory review
@@ -151,4 +157,4 @@ Where: I = current (amps), L = one-way length (feet), CM = circular mils from NE
---
-*This document certifies that Code Compass implements deterministic, cryptographically verified NEC compliance tooling with auditable AI governance controls suitable for data center procurement review and industrial safety-critical deployments.*
+_This document certifies that Code Compass implements deterministic, cryptographically verified NEC compliance tooling with auditable AI governance controls suitable for data center procurement review and industrial safety-critical deployments._
diff --git a/architecture-diagram.html b/architecture-diagram.html
index fed9745..947751a 100644
--- a/architecture-diagram.html
+++ b/architecture-diagram.html
@@ -1,275 +1,890 @@
-
+
-
-
-Code Compass — System Architecture
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Frontend
- TanStack Start + React 19
-
-
-
-
- Vite Dev Proxy
- /api/chat/qwen · /api/lookup
-
-
-
-
- Express Backend
- server/index.ts · port 3001
- REST API + CORS + Webhooks
-
-
-
-
- qwenMemoryAgent.js
- Core Orchestrator · Validation Gates
-
-
-
-
- Qwen Cloud (DashScope)
- dashscope-intl.aliyuncs.com
- Model: qwen-max
- preserve_thinking: true · temp 0.3
-
-
-
-
- Memory Bank
- apprenticeProfile · activeProjectContext
- longTermKnowledge
-
-
-
-
- Supabase
- PostgreSQL + RLS Auth
-
-
-
-
- PostHog Analytics
- Event Tracking · Exception Capture
-
-
-
-
- Webhooks
- Stripe Billing
-
-
-
- SECURITY BOUNDARY · CSP + RLS
-
-
-
- EXTERNAL SERVICES
-
-
-
- Legend
-
-
- Frontend
-
-
- Backend
-
-
- Database / Storage
-
-
- Cloud / AI Service
-
-
- External Service
-
-
- Security Boundary
-
-
- Cloud Boundary
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/client/index.html b/client/index.html
index 284134b..6b1fca6 100644
--- a/client/index.html
+++ b/client/index.html
@@ -2,16 +2,29 @@
-
+
Code Compass — NEC Co-Pilot
-
+
-
+
-
+
diff --git a/client/public/__manus__/debug-collector.js b/client/public/__manus__/debug-collector.js
index 0504555..8c29cca 100644
--- a/client/public/__manus__/debug-collector.js
+++ b/client/public/__manus__/debug-collector.js
@@ -68,7 +68,9 @@
if (value === undefined) return undefined;
if (typeof value === "string") {
- return value.length > 1000 ? value.slice(0, 1000) + "...[truncated]" : value;
+ return value.length > 1000
+ ? value.slice(0, 1000) + "...[truncated]"
+ : value;
}
if (typeof value !== "object") return value;
@@ -178,7 +180,7 @@
getAttr("data-test") ||
null;
- var type = tag === "input" ? (getAttr("type") || "text") : null;
+ var type = tag === "input" ? getAttr("type") || "text" : null;
var href = tag === "a" ? getAttr("href") || null : null;
// a small, stable hint for agents (avoid building full CSS paths)
@@ -233,7 +235,8 @@
if (isSensitiveField(el)) return { masked: true, length: v.length };
- if (v.length > CONFIG.uiInputMaxLen) v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
+ if (v.length > CONFIG.uiInputMaxLen)
+ v = v.slice(0, CONFIG.uiInputMaxLen) + "…";
return v;
}
@@ -456,9 +459,10 @@
init = init || {};
var startTime = Date.now();
// Handle string, Request object, or URL object
- var url = typeof input === "string"
- ? input
- : (input && (input.url || input.href || String(input))) || "";
+ var url =
+ typeof input === "string"
+ ? input
+ : (input && (input.url || input.href || String(input))) || "";
var method = init.method || (input && input.method) || "GET";
// Don't intercept internal requests
@@ -470,7 +474,9 @@
var requestHeaders = {};
try {
if (init.headers) {
- requestHeaders = Object.fromEntries(new Headers(init.headers).entries());
+ requestHeaders = Object.fromEntries(
+ new Headers(init.headers).entries()
+ );
}
} catch (e) {
requestHeaders = { _parseError: true };
@@ -494,7 +500,9 @@
.then(function (response) {
entry.duration = Date.now() - startTime;
- var contentType = (response.headers.get("content-type") || "").toLowerCase();
+ var contentType = (
+ response.headers.get("content-type") || ""
+ ).toLowerCase();
var contentLength = response.headers.get("content-length");
entry.response = {
@@ -516,9 +524,10 @@
}
// Skip body capture for streaming responses (SSE, etc.) to avoid memory leaks
- var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
- contentType.indexOf("application/stream") !== -1 ||
- contentType.indexOf("application/x-ndjson") !== -1;
+ var isStreaming =
+ contentType.indexOf("text/event-stream") !== -1 ||
+ contentType.indexOf("application/stream") !== -1 ||
+ contentType.indexOf("application/x-ndjson") !== -1;
if (isStreaming) {
entry.response.body = "[Streaming response - not captured]";
store.networkRequests.push(entry);
@@ -527,20 +536,25 @@
}
// Skip body capture for large responses to avoid memory issues
- if (contentLength && parseInt(contentLength, 10) > CONFIG.maxBodyLength) {
- entry.response.body = "[Response too large: " + contentLength + " bytes]";
+ if (
+ contentLength &&
+ parseInt(contentLength, 10) > CONFIG.maxBodyLength
+ ) {
+ entry.response.body =
+ "[Response too large: " + contentLength + " bytes]";
store.networkRequests.push(entry);
pruneBuffer(store.networkRequests, CONFIG.bufferSize.network);
return response;
}
// Skip body capture for binary content types
- var isBinary = contentType.indexOf("image/") !== -1 ||
- contentType.indexOf("video/") !== -1 ||
- contentType.indexOf("audio/") !== -1 ||
- contentType.indexOf("application/octet-stream") !== -1 ||
- contentType.indexOf("application/pdf") !== -1 ||
- contentType.indexOf("application/zip") !== -1;
+ var isBinary =
+ contentType.indexOf("image/") !== -1 ||
+ contentType.indexOf("video/") !== -1 ||
+ contentType.indexOf("audio/") !== -1 ||
+ contentType.indexOf("application/octet-stream") !== -1 ||
+ contentType.indexOf("application/pdf") !== -1 ||
+ contentType.indexOf("application/zip") !== -1;
if (isBinary) {
entry.response.body = "[Binary content: " + contentType + "]";
store.networkRequests.push(entry);
@@ -558,7 +572,8 @@
if (text.length <= CONFIG.maxBodyLength) {
entry.response.body = sanitizeValue(tryParseJson(text));
} else {
- entry.response.body = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
+ entry.response.body =
+ text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
}
})
.catch(function () {
@@ -615,24 +630,30 @@
xhr._manusData.url.indexOf("/__manus__/") !== 0
) {
xhr._manusData.startTime = Date.now();
- xhr._manusData.requestBody = body ? sanitizeValue(tryParseJson(body)) : null;
+ xhr._manusData.requestBody = body
+ ? sanitizeValue(tryParseJson(body))
+ : null;
xhr.addEventListener("load", function () {
- var contentType = (xhr.getResponseHeader("content-type") || "").toLowerCase();
+ var contentType = (
+ xhr.getResponseHeader("content-type") || ""
+ ).toLowerCase();
var responseBody = null;
// Skip body capture for streaming responses
- var isStreaming = contentType.indexOf("text/event-stream") !== -1 ||
- contentType.indexOf("application/stream") !== -1 ||
- contentType.indexOf("application/x-ndjson") !== -1;
+ var isStreaming =
+ contentType.indexOf("text/event-stream") !== -1 ||
+ contentType.indexOf("application/stream") !== -1 ||
+ contentType.indexOf("application/x-ndjson") !== -1;
// Skip body capture for binary content types
- var isBinary = contentType.indexOf("image/") !== -1 ||
- contentType.indexOf("video/") !== -1 ||
- contentType.indexOf("audio/") !== -1 ||
- contentType.indexOf("application/octet-stream") !== -1 ||
- contentType.indexOf("application/pdf") !== -1 ||
- contentType.indexOf("application/zip") !== -1;
+ var isBinary =
+ contentType.indexOf("image/") !== -1 ||
+ contentType.indexOf("video/") !== -1 ||
+ contentType.indexOf("audio/") !== -1 ||
+ contentType.indexOf("application/octet-stream") !== -1 ||
+ contentType.indexOf("application/pdf") !== -1 ||
+ contentType.indexOf("application/zip") !== -1;
if (isStreaming) {
responseBody = "[Streaming response - not captured]";
@@ -643,7 +664,8 @@
try {
var text = xhr.responseText || "";
if (text.length > CONFIG.maxBodyLength) {
- responseBody = text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
+ responseBody =
+ text.slice(0, CONFIG.maxBodyLength) + "...[truncated]";
} else {
responseBody = sanitizeValue(tryParseJson(text));
}
@@ -817,5 +839,7 @@
forceReport: reportLogs,
};
- console.debug("[Manus] Debug collector initialized (no rrweb, UI events only)");
+ console.debug(
+ "[Manus] Debug collector initialized (no rrweb, UI events only)"
+ );
})();
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 5ba7957..8af221f 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,6 +1,7 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
+import { Analytics } from "@vercel/analytics/react";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
@@ -42,6 +43,7 @@ function App() {
+
diff --git a/client/src/_core/hooks/useAuth.ts b/client/src/_core/hooks/useAuth.ts
index 01cc650..64771f8 100644
--- a/client/src/_core/hooks/useAuth.ts
+++ b/client/src/_core/hooks/useAuth.ts
@@ -70,7 +70,7 @@ export function useAuth(options?: UseAuthOptions) {
if (typeof window === "undefined") return;
if (window.location.pathname === redirectPath) return;
- window.location.href = redirectPath
+ window.location.href = redirectPath;
}, [
redirectOnUnauthenticated,
redirectPath,
diff --git a/client/src/components/AIChatBox.tsx b/client/src/components/AIChatBox.tsx
index 1c00871..52b235f 100644
--- a/client/src/components/AIChatBox.tsx
+++ b/client/src/components/AIChatBox.tsx
@@ -127,7 +127,7 @@ export function AIChatBox({
const textareaRef = useRef
(null);
// Filter out system messages
- const displayMessages = messages.filter((msg) => msg.role !== "system");
+ const displayMessages = messages.filter(msg => msg.role !== "system");
// Calculate min-height for last assistant message to push user message to top
const [minHeightForLastMessage, setMinHeightForLastMessage] = useState(0);
@@ -143,7 +143,8 @@ export function AIChatBox({
// - user message: 40px (item height) + 16px (margin-top from space-y-4) = 56px
// Note: margin-bottom is not counted because it naturally pushes the assistant message down
const userMessageReservedHeight = 56;
- const calculatedHeight = scrollAreaHeight - 32 - userMessageReservedHeight;
+ const calculatedHeight =
+ scrollAreaHeight - 32 - userMessageReservedHeight;
setMinHeightForLastMessage(Math.max(0, calculatedHeight));
}
@@ -152,14 +153,14 @@ export function AIChatBox({
// Scroll to bottom helper function with smooth animation
const scrollToBottom = () => {
const viewport = scrollAreaRef.current?.querySelector(
- '[data-radix-scroll-area-viewport]'
+ "[data-radix-scroll-area-viewport]"
) as HTMLDivElement;
if (viewport) {
requestAnimationFrame(() => {
viewport.scrollTo({
top: viewport.scrollHeight,
- behavior: 'smooth'
+ behavior: "smooth",
});
});
}
@@ -311,7 +312,7 @@ export function AIChatBox({
@@ -82,9 +94,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
{/* Single theme toggle — paid perk only */}
-
- {oledUnlocked && }
-
+
{oledUnlocked && }
diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx
index 0bf7437..b0526b3 100644
--- a/client/src/components/DashboardLayout.tsx
+++ b/client/src/components/DashboardLayout.tsx
@@ -24,7 +24,7 @@ import { useIsMobile } from "@/hooks/useMobile";
import { LayoutDashboard, LogOut, PanelLeft, Users } from "lucide-react";
import { CSSProperties, useEffect, useRef, useState } from "react";
import { useLocation } from "wouter";
-import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
+import { DashboardLayoutSkeleton } from "./DashboardLayoutSkeleton";
import { Button } from "./ui/button";
const menuItems = [
@@ -53,7 +53,7 @@ export default function DashboardLayout({
}, [sidebarWidth]);
if (loading) {
- return
+ return
;
}
if (!user) {
@@ -65,7 +65,8 @@ export default function DashboardLayout({
Sign in to continue
- Access to this dashboard requires authentication. Continue to launch the login flow.
+ Access to this dashboard requires authentication. Continue to
+ launch the login flow.
{appName}'s built-in browser blocks Google login. To create your
- account or sign in: {instruction}
+ account or sign in:{" "}
+ {instruction}
- {lookup.index_keywords.map((kw) => (
+ {lookup.index_keywords.map(kw => (
3 · Read for
- {lookup.what_to_look_for}
+
+ {lookup.what_to_look_for}
+
- Open your code book and trace this path yourself — that's the skill the exam tests.
+ Open your code book and trace this path yourself — that's the skill the
+ exam tests.
);
diff --git a/client/src/components/PaywallGate.tsx b/client/src/components/PaywallGate.tsx
index 00a4ac8..752d7ce 100644
--- a/client/src/components/PaywallGate.tsx
+++ b/client/src/components/PaywallGate.tsx
@@ -66,7 +66,11 @@ export function PaywallGate({ children, featureName }: PaywallGateProps) {
-
+
Try 3 free Co-Pilot questions
@@ -76,14 +80,18 @@ export function PaywallGate({ children, featureName }: PaywallGateProps) {
{!isAuthenticated && (
Already purchased?{" "}
-
+
Sign in
)}
- One-time payment · Instant access · All 4 NEC versions (2017/2020/2023/2026)
+ One-time payment · Instant access · All 4 NEC versions
+ (2017/2020/2023/2026)
);
diff --git a/client/src/components/ThemeToggle.tsx b/client/src/components/ThemeToggle.tsx
index e2e88d4..f889012 100644
--- a/client/src/components/ThemeToggle.tsx
+++ b/client/src/components/ThemeToggle.tsx
@@ -5,7 +5,11 @@
import { Moon, Sun } from "lucide-react";
import { useTheme } from "../contexts/ThemeContext";
-export default function ThemeToggle({ className = "" }: { className?: string }) {
+export default function ThemeToggle({
+ className = "",
+}: {
+ className?: string;
+}) {
const { theme, toggleTheme } = useTheme();
const isOled = theme === "oled";
diff --git a/client/src/components/ui/button.tsx b/client/src/components/ui/button.tsx
index 6d74f9a..162f5e7 100644
--- a/client/src/components/ui/button.tsx
+++ b/client/src/components/ui/button.tsx
@@ -16,8 +16,7 @@ const buttonVariants = cva(
"border bg-transparent shadow-xs hover:bg-accent dark:bg-transparent dark:border-input dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
- ghost:
- "hover:bg-accent dark:hover:bg-accent/50",
+ ghost: "hover:bg-accent dark:hover:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
diff --git a/client/src/components/ui/dialog.tsx b/client/src/components/ui/dialog.tsx
index 9a84318..e9a906b 100644
--- a/client/src/components/ui/dialog.tsx
+++ b/client/src/components/ui/dialog.tsx
@@ -204,6 +204,5 @@ export {
DialogOverlay,
DialogPortal,
DialogTitle,
- DialogTrigger
+ DialogTrigger,
};
-
diff --git a/client/src/components/ui/input.tsx b/client/src/components/ui/input.tsx
index 159aa02..dfd31c9 100644
--- a/client/src/components/ui/input.tsx
+++ b/client/src/components/ui/input.tsx
@@ -20,9 +20,11 @@ function Input({
onCompositionEnd: handleCompositionEnd,
onKeyDown: handleKeyDown,
} = useComposition({
- onKeyDown: (e) => {
+ onKeyDown: e => {
// Check if this is an Enter key that should be blocked
- const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
+ const isComposing =
+ (e.nativeEvent as any).isComposing ||
+ dialogComposition.justEndedComposing();
// If Enter key is pressed while composing or just after composition ended,
// don't call the user's onKeyDown (this blocks the business logic)
diff --git a/client/src/components/ui/sidebar.tsx b/client/src/components/ui/sidebar.tsx
index 2744889..1a587d1 100644
--- a/client/src/components/ui/sidebar.tsx
+++ b/client/src/components/ui/sidebar.tsx
@@ -729,6 +729,5 @@ export {
SidebarRail,
SidebarSeparator,
SidebarTrigger,
- useSidebar
+ useSidebar,
};
-
diff --git a/client/src/components/ui/textarea.tsx b/client/src/components/ui/textarea.tsx
index df7c37e..5a73b62 100644
--- a/client/src/components/ui/textarea.tsx
+++ b/client/src/components/ui/textarea.tsx
@@ -19,9 +19,11 @@ function Textarea({
onCompositionEnd: handleCompositionEnd,
onKeyDown: handleKeyDown,
} = useComposition({
- onKeyDown: (e) => {
+ onKeyDown: e => {
// Check if this is an Enter key that should be blocked
- const isComposing = (e.nativeEvent as any).isComposing || dialogComposition.justEndedComposing();
+ const isComposing =
+ (e.nativeEvent as any).isComposing ||
+ dialogComposition.justEndedComposing();
// If Enter key is pressed while composing or just after composition ended,
// don't call the user's onKeyDown (this blocks the business logic)
diff --git a/client/src/contexts/ThemeContext.tsx b/client/src/contexts/ThemeContext.tsx
index fe74c23..db0f045 100644
--- a/client/src/contexts/ThemeContext.tsx
+++ b/client/src/contexts/ThemeContext.tsx
@@ -76,7 +76,13 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
{children}
diff --git a/client/src/data/questionBank.ts b/client/src/data/questionBank.ts
index cb59b5b..60afcbd 100644
--- a/client/src/data/questionBank.ts
+++ b/client/src/data/questionBank.ts
@@ -4,10 +4,10 @@
*/
export interface LookupPath {
- index_keywords: string[]; // Words to search in the NEC index
- index_entry: string; // Exact index entry to find
- article_or_table: string; // Where the index sends you
- what_to_look_for: string; // What to read on that page (table row, exception, subsection)
+ index_keywords: string[]; // Words to search in the NEC index
+ index_entry: string; // Exact index entry to find
+ article_or_table: string; // Where the index sends you
+ what_to_look_for: string; // What to read on that page (table row, exception, subsection)
}
export interface QuestionCard {
@@ -17,7 +17,7 @@ export interface QuestionCard {
correct_answer: string;
explanation: string;
nec_article: string;
- nec_versions: ("2017" | "2020" | "2023" | "2026")[]; // Which code editions this applies to
+ nec_versions: ("2017" | "2020" | "2023" | "2026")[]; // Which code editions this applies to
difficulty: "journeyman" | "master" | "inspector";
tags: string[];
lookup_path: LookupPath;
@@ -26,10 +26,12 @@ export interface QuestionCard {
export const questionBank: QuestionCard[] = [
{
id: "310-001",
- question: "What is the allowable ampacity of a 3/0 AWG copper THWN-2 conductor installed in a raceway containing three current-carrying conductors at an ambient temperature of 30°C?",
+ question:
+ "What is the allowable ampacity of a 3/0 AWG copper THWN-2 conductor installed in a raceway containing three current-carrying conductors at an ambient temperature of 30°C?",
choices: ["200 amps", "225 amps", "250 amps", "175 amps"],
correct_answer: "225 amps",
- explanation: "Per Table 310.16, a 3/0 AWG copper conductor with 90°C rated insulation (THWN-2) has an ampacity of 225A with not more than three current-carrying conductors at 30°C ambient.",
+ explanation:
+ "Per Table 310.16, a 3/0 AWG copper conductor with 90°C rated insulation (THWN-2) has an ampacity of 225A with not more than three current-carrying conductors at 30°C ambient.",
nec_article: "Table 310.16",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -38,83 +40,117 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Ampacity", "Conductors, ampacity"],
index_entry: "Conductors, ampacity — see Table 310.16",
article_or_table: "Table 310.16",
- what_to_look_for: "Find the '3/0 AWG' row → read the '90°C (THWN-2)' copper column → value is 225A"
- }
+ what_to_look_for:
+ "Find the '3/0 AWG' row → read the '90°C (THWN-2)' copper column → value is 225A",
+ },
},
{
id: "430-001",
- question: "What is the maximum percentage of full-load current rating used to size branch-circuit short-circuit and ground-fault protection for a non-time-delay fuse protecting a single-phase motor?",
+ question:
+ "What is the maximum percentage of full-load current rating used to size branch-circuit short-circuit and ground-fault protection for a non-time-delay fuse protecting a single-phase motor?",
choices: ["150%", "200%", "250%", "300%"],
correct_answer: "300%",
- explanation: "Table 430.52(C)(1) specifies a maximum of 300% of motor FLC for non-time-delay fuses. This can increase to 400% if the motor cannot start at 300%.",
+ explanation:
+ "Table 430.52(C)(1) specifies a maximum of 300% of motor FLC for non-time-delay fuses. This can increase to 400% if the motor cannot start at 300%.",
nec_article: "Table 430.52(C)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["motors", "overcurrent", "fuses", "branch-circuit"],
lookup_path: {
- index_keywords: ["Motors, overcurrent protection", "Fuses, motor branch circuit"],
- index_entry: "Motors — branch-circuit short-circuit and ground-fault protection → 430.52",
+ index_keywords: [
+ "Motors, overcurrent protection",
+ "Fuses, motor branch circuit",
+ ],
+ index_entry:
+ "Motors — branch-circuit short-circuit and ground-fault protection → 430.52",
article_or_table: "Table 430.52(C)(1)",
- what_to_look_for: "Find 'Non-time-delay fuse' row → read the '% of Full-Load Current' column → 300%"
- }
+ what_to_look_for:
+ "Find 'Non-time-delay fuse' row → read the '% of Full-Load Current' column → 300%",
+ },
},
{
id: "250-001",
- question: "What is the minimum size copper grounding electrode conductor required for a service supplied by 4/0 AWG copper ungrounded service-entrance conductors?",
+ question:
+ "What is the minimum size copper grounding electrode conductor required for a service supplied by 4/0 AWG copper ungrounded service-entrance conductors?",
choices: ["4 AWG", "2 AWG", "1/0 AWG", "6 AWG"],
correct_answer: "2 AWG",
- explanation: "Table 250.66 requires a minimum 2 AWG copper grounding electrode conductor when service-entrance conductors are sized 3/0 through 250 kcmil copper.",
+ explanation:
+ "Table 250.66 requires a minimum 2 AWG copper grounding electrode conductor when service-entrance conductors are sized 3/0 through 250 kcmil copper.",
nec_article: "Table 250.66",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["grounding", "service", "electrode conductor"],
lookup_path: {
- index_keywords: ["Grounding electrode conductor, size", "Service, grounding"],
+ index_keywords: [
+ "Grounding electrode conductor, size",
+ "Service, grounding",
+ ],
index_entry: "Grounding electrode conductors — size → Table 250.66",
article_or_table: "Table 250.66",
- what_to_look_for: "Find '3/0 AWG–350 kcmil' row in the service conductor column → GEC size is 2 AWG copper"
- }
+ what_to_look_for:
+ "Find '3/0 AWG–350 kcmil' row in the service conductor column → GEC size is 2 AWG copper",
+ },
},
{
id: "210-001",
- question: "What is the minimum number of 20-ampere small-appliance branch circuits required for a dwelling unit kitchen?",
+ question:
+ "What is the minimum number of 20-ampere small-appliance branch circuits required for a dwelling unit kitchen?",
choices: ["One", "Two", "Three", "Four"],
correct_answer: "Two",
- explanation: "NEC 210.11(C)(1) requires at least two 20-ampere small-appliance branch circuits to serve receptacle outlets in the kitchen, pantry, dining room, and similar areas.",
+ explanation:
+ "NEC 210.11(C)(1) requires at least two 20-ampere small-appliance branch circuits to serve receptacle outlets in the kitchen, pantry, dining room, and similar areas.",
nec_article: "210.11(C)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["dwelling", "kitchen", "branch circuits", "receptacles"],
lookup_path: {
- index_keywords: ["Small-appliance branch circuits", "Kitchen, branch circuits"],
- index_entry: "Small-appliance branch circuits — dwelling units → 210.11(C)(1)",
+ index_keywords: [
+ "Small-appliance branch circuits",
+ "Kitchen, branch circuits",
+ ],
+ index_entry:
+ "Small-appliance branch circuits — dwelling units → 210.11(C)(1)",
article_or_table: "210.11(C)(1)",
- what_to_look_for: "Read the section — it states 'not fewer than two' 20-ampere circuits required"
- }
+ what_to_look_for:
+ "Read the section — it states 'not fewer than two' 20-ampere circuits required",
+ },
},
{
id: "450-001",
- question: "What is the maximum overcurrent protection rating for the primary of a transformer (600V or less) with a primary current of 9 amperes or more, when there is no secondary protection?",
+ question:
+ "What is the maximum overcurrent protection rating for the primary of a transformer (600V or less) with a primary current of 9 amperes or more, when there is no secondary protection?",
choices: ["125%", "150%", "167%", "200%"],
correct_answer: "125%",
- explanation: "NEC 450.3(B) requires primary-only overcurrent protection for transformers rated 600V or less with primary current of 9A or more to be set at not more than 125% of rated primary current.",
+ explanation:
+ "NEC 450.3(B) requires primary-only overcurrent protection for transformers rated 600V or less with primary current of 9A or more to be set at not more than 125% of rated primary current.",
nec_article: "450.3(B)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["transformers", "overcurrent", "primary protection"],
lookup_path: {
- index_keywords: ["Transformers, overcurrent protection", "Overcurrent protection, transformers"],
+ index_keywords: [
+ "Transformers, overcurrent protection",
+ "Overcurrent protection, transformers",
+ ],
index_entry: "Transformers — overcurrent protection → 450.3",
article_or_table: "Table 450.3(B)",
- what_to_look_for: "Find row for 'Primary ≥ 9A, no secondary protection' → column shows 125% maximum"
- }
+ what_to_look_for:
+ "Find row for 'Primary ≥ 9A, no secondary protection' → column shows 125% maximum",
+ },
},
{
id: "500-001",
- question: "What is the classification of a location where ignitable concentrations of flammable gases are present continuously under normal operating conditions?",
- choices: ["Class I, Division 1", "Class I, Division 2", "Class II, Division 1", "Zone 0"],
+ question:
+ "What is the classification of a location where ignitable concentrations of flammable gases are present continuously under normal operating conditions?",
+ choices: [
+ "Class I, Division 1",
+ "Class I, Division 2",
+ "Class II, Division 1",
+ "Zone 0",
+ ],
correct_answer: "Class I, Division 1",
- explanation: "NEC 500.5(B)(1) defines Class I, Division 1 as locations where ignitable concentrations of flammable gases or vapors can exist under normal operating conditions.",
+ explanation:
+ "NEC 500.5(B)(1) defines Class I, Division 1 as locations where ignitable concentrations of flammable gases or vapors can exist under normal operating conditions.",
nec_article: "500.5(B)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
@@ -123,224 +159,326 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Hazardous locations, classified", "Class I locations"],
index_entry: "Hazardous (classified) locations — Class I → 500.5(B)",
article_or_table: "500.5(B)(1)",
- what_to_look_for: "Read (B)(1) — 'continuously, intermittently, or periodically under normal operating conditions' = Division 1"
- }
+ what_to_look_for:
+ "Read (B)(1) — 'continuously, intermittently, or periodically under normal operating conditions' = Division 1",
+ },
},
{
id: "690-001",
- question: "What is the maximum permitted system voltage for a photovoltaic (PV) system installed in a one-family dwelling?",
+ question:
+ "What is the maximum permitted system voltage for a photovoltaic (PV) system installed in a one-family dwelling?",
choices: ["300 volts", "480 volts", "600 volts", "1000 volts"],
correct_answer: "600 volts",
- explanation: "NEC 690.7(2) limits the maximum DC system voltage for PV systems on one- and two-family dwellings to 600 volts.",
+ explanation:
+ "NEC 690.7(2) limits the maximum DC system voltage for PV systems on one- and two-family dwellings to 600 volts.",
nec_article: "690.7(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["solar", "photovoltaic", "voltage", "dwelling"],
lookup_path: {
- index_keywords: ["Photovoltaic systems, voltage", "Solar systems, maximum voltage"],
+ index_keywords: [
+ "Photovoltaic systems, voltage",
+ "Solar systems, maximum voltage",
+ ],
index_entry: "Photovoltaic (PV) systems — maximum voltage → 690.7",
article_or_table: "690.7(2)",
- what_to_look_for: "Read subsection (2) — one- and two-family dwellings limited to 600V DC maximum"
- }
+ what_to_look_for:
+ "Read subsection (2) — one- and two-family dwellings limited to 600V DC maximum",
+ },
},
{
id: "314-001",
- question: "What is the maximum number of 10 AWG conductors permitted in a 4 inch x 1½ inch square box?",
+ question:
+ "What is the maximum number of 10 AWG conductors permitted in a 4 inch x 1½ inch square box?",
choices: ["6", "7", "8", "9"],
correct_answer: "8",
- explanation: "Table 314.16(A)(2) permits a maximum of 8 conductors of 10 AWG in a 4 x 1½ inch square metal box (21.0 cubic inches volume).",
+ explanation:
+ "Table 314.16(A)(2) permits a maximum of 8 conductors of 10 AWG in a 4 x 1½ inch square metal box (21.0 cubic inches volume).",
nec_article: "Table 314.16(A)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["box fill", "conductors", "square box"],
lookup_path: {
- index_keywords: ["Box fill", "Outlet boxes, conductor fill", "Boxes, conductor fill"],
+ index_keywords: [
+ "Box fill",
+ "Outlet boxes, conductor fill",
+ "Boxes, conductor fill",
+ ],
index_entry: "Boxes — conductor fill → 314.16 → Table 314.16(A)",
article_or_table: "Table 314.16(A)(2)",
- what_to_look_for: "Find '4 × 1½ square' row → read '10 AWG' conductor count column → 8 conductors"
- }
+ what_to_look_for:
+ "Find '4 × 1½ square' row → read '10 AWG' conductor count column → 8 conductors",
+ },
},
{
id: "240-001",
- question: "What is the standard ampere rating for an overcurrent device immediately above 800 amperes?",
+ question:
+ "What is the standard ampere rating for an overcurrent device immediately above 800 amperes?",
choices: ["850 amperes", "900 amperes", "1000 amperes", "1200 amperes"],
correct_answer: "1000 amperes",
- explanation: "Table 240.6(A) lists 1000 amperes as the next standard rating above 800 amperes for fuses and inverse time circuit breakers.",
+ explanation:
+ "Table 240.6(A) lists 1000 amperes as the next standard rating above 800 amperes for fuses and inverse time circuit breakers.",
nec_article: "Table 240.6(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["overcurrent", "standard ratings", "circuit breakers"],
lookup_path: {
- index_keywords: ["Overcurrent protective devices, ratings", "Circuit breakers, standard ratings"],
- index_entry: "Overcurrent protective devices — standard ampere ratings → 240.6(A)",
+ index_keywords: [
+ "Overcurrent protective devices, ratings",
+ "Circuit breakers, standard ratings",
+ ],
+ index_entry:
+ "Overcurrent protective devices — standard ampere ratings → 240.6(A)",
article_or_table: "Table 240.6(A)",
- what_to_look_for: "Scan the list of standard ratings — after 800A the next value listed is 1000A"
- }
+ what_to_look_for:
+ "Scan the list of standard ratings — after 800A the next value listed is 1000A",
+ },
},
{
id: "680-001",
- question: "What is the minimum required depth for a wet-niche luminaire installed in a permanently installed swimming pool?",
+ question:
+ "What is the minimum required depth for a wet-niche luminaire installed in a permanently installed swimming pool?",
choices: ["12 inches", "18 inches", "24 inches", "4 inches"],
correct_answer: "18 inches",
- explanation: "NEC 680.23(A)(5) requires the top of the luminaire lens to be installed at least 18 inches below the normal water level of the pool.",
+ explanation:
+ "NEC 680.23(A)(5) requires the top of the luminaire lens to be installed at least 18 inches below the normal water level of the pool.",
nec_article: "680.23(A)(5)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["swimming pool", "luminaire", "wet-niche", "depth"],
lookup_path: {
- index_keywords: ["Swimming pools, luminaires", "Luminaires, wet-niche", "Pools, underwater lighting"],
+ index_keywords: [
+ "Swimming pools, luminaires",
+ "Luminaires, wet-niche",
+ "Pools, underwater lighting",
+ ],
index_entry: "Swimming pools — luminaires, wet-niche → 680.23",
article_or_table: "680.23(A)(5)",
- what_to_look_for: "Read (A)(5) under wet-niche luminaires — '18 inches below the normal water level'"
- }
+ what_to_look_for:
+ "Read (A)(5) under wet-niche luminaires — '18 inches below the normal water level'",
+ },
},
{
id: "230-001",
- question: "What is the maximum number of service disconnecting means permitted for a single building?",
+ question:
+ "What is the maximum number of service disconnecting means permitted for a single building?",
choices: ["2", "4", "6", "8"],
correct_answer: "6",
- explanation: "NEC 230.71(A) permits a maximum of six disconnects per service for each service or set of service-entrance conductors.",
+ explanation:
+ "NEC 230.71(A) permits a maximum of six disconnects per service for each service or set of service-entrance conductors.",
nec_article: "230.71(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["service", "disconnect", "service entrance"],
lookup_path: {
- index_keywords: ["Service disconnecting means, number", "Disconnecting means, service"],
+ index_keywords: [
+ "Service disconnecting means, number",
+ "Disconnecting means, service",
+ ],
index_entry: "Service disconnecting means — number of → 230.71",
article_or_table: "230.71(A)",
- what_to_look_for: "Read (A) — 'not more than six disconnects per service' for each set of service-entrance conductors"
- }
+ what_to_look_for:
+ "Read (A) — 'not more than six disconnects per service' for each set of service-entrance conductors",
+ },
},
{
id: "410-001",
- question: "What is the minimum clearance required between a luminaire and the top of a clothes closet storage space?",
+ question:
+ "What is the minimum clearance required between a luminaire and the top of a clothes closet storage space?",
choices: ["6 inches", "12 inches", "18 inches", "24 inches"],
correct_answer: "12 inches",
- explanation: "NEC 410.16(A)(2) requires a minimum clearance of 12 inches between a surface-mounted incandescent luminaire with a completely enclosed lamp and the nearest point of a storage space.",
+ explanation:
+ "NEC 410.16(A)(2) requires a minimum clearance of 12 inches between a surface-mounted incandescent luminaire with a completely enclosed lamp and the nearest point of a storage space.",
nec_article: "410.16(A)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["luminaires", "closet", "clearance"],
lookup_path: {
- index_keywords: ["Clothes closets, luminaires", "Luminaires, clothes closets"],
+ index_keywords: [
+ "Clothes closets, luminaires",
+ "Luminaires, clothes closets",
+ ],
index_entry: "Clothes closets — luminaires → 410.16",
article_or_table: "410.16(A)(2)",
- what_to_look_for: "Read (A)(2) for surface-mounted enclosed luminaires — 12 inch clearance from storage space"
- }
+ what_to_look_for:
+ "Read (A)(2) for surface-mounted enclosed luminaires — 12 inch clearance from storage space",
+ },
},
// ─── AMPACITY ───────────────────────────────────────────────────────────────
{
id: "310-002",
- question: "What ampacity correction factor applies to a 75°C rated conductor installed where the ambient temperature is 40°C?",
+ question:
+ "What ampacity correction factor applies to a 75°C rated conductor installed where the ambient temperature is 40°C?",
choices: ["0.82", "0.88", "0.91", "0.94"],
correct_answer: "0.88",
- explanation: "Table 310.15(B)(1) correction factors show that a 75°C rated conductor at 40°C ambient has a correction factor of 0.88, reducing its allowable ampacity.",
+ explanation:
+ "Table 310.15(B)(1) correction factors show that a 75°C rated conductor at 40°C ambient has a correction factor of 0.88, reducing its allowable ampacity.",
nec_article: "Table 310.15(B)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
- tags: ["ampacity", "correction factor", "ambient temperature", "conductors"],
+ tags: [
+ "ampacity",
+ "correction factor",
+ "ambient temperature",
+ "conductors",
+ ],
lookup_path: {
- index_keywords: ["Ampacity correction factors", "Temperature correction, conductors"],
- index_entry: "Conductors — ampacity correction factors, ambient temperature → 310.15(B)(1)",
+ index_keywords: [
+ "Ampacity correction factors",
+ "Temperature correction, conductors",
+ ],
+ index_entry:
+ "Conductors — ampacity correction factors, ambient temperature → 310.15(B)(1)",
article_or_table: "Table 310.15(B)(1)",
- what_to_look_for: "Find the '40°C' ambient row → read the '75°C' conductor column → correction factor is 0.88"
- }
+ what_to_look_for:
+ "Find the '40°C' ambient row → read the '75°C' conductor column → correction factor is 0.88",
+ },
},
{
id: "310-003",
- question: "What adjustment factor applies when 7 to 9 current-carrying conductors are bundled together in a single raceway?",
+ question:
+ "What adjustment factor applies when 7 to 9 current-carrying conductors are bundled together in a single raceway?",
choices: ["50%", "60%", "70%", "80%"],
correct_answer: "70%",
- explanation: "Table 310.15(C)(1) requires a 70% adjustment factor when 7 to 9 current-carrying conductors are installed in a single raceway or cable.",
+ explanation:
+ "Table 310.15(C)(1) requires a 70% adjustment factor when 7 to 9 current-carrying conductors are installed in a single raceway or cable.",
nec_article: "Table 310.15(C)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["ampacity", "adjustment factor", "bundling", "raceway"],
lookup_path: {
- index_keywords: ["Ampacity adjustment factors", "Conductors, bundled", "Raceway fill, adjustment"],
- index_entry: "Conductors — adjustment factors, more than 3 current-carrying → 310.15(C)(1)",
+ index_keywords: [
+ "Ampacity adjustment factors",
+ "Conductors, bundled",
+ "Raceway fill, adjustment",
+ ],
+ index_entry:
+ "Conductors — adjustment factors, more than 3 current-carrying → 310.15(C)(1)",
article_or_table: "Table 310.15(C)(1)",
- what_to_look_for: "Find '7–9 conductors' row → read adjustment factor column → 70%"
- }
+ what_to_look_for:
+ "Find '7–9 conductors' row → read adjustment factor column → 70%",
+ },
},
{
id: "310-004",
- question: "A 1/0 AWG copper THHN conductor installed in a conduit with 3 current-carrying conductors at 30°C ambient has an allowable ampacity of:",
+ question:
+ "A 1/0 AWG copper THHN conductor installed in a conduit with 3 current-carrying conductors at 30°C ambient has an allowable ampacity of:",
choices: ["125 amps", "150 amps", "170 amps", "145 amps"],
correct_answer: "150 amps",
- explanation: "Table 310.16 lists 150 amperes for 1/0 AWG copper with 90°C insulation (THHN) with not more than 3 current-carrying conductors at 30°C ambient.",
+ explanation:
+ "Table 310.16 lists 150 amperes for 1/0 AWG copper with 90°C insulation (THHN) with not more than 3 current-carrying conductors at 30°C ambient.",
nec_article: "Table 310.16",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["ampacity", "THHN", "conductors", "1/0 AWG"],
lookup_path: {
index_keywords: ["Ampacity", "Conductors, ampacity", "THHN"],
- index_entry: "Conductors — ampacity, not more than 3 in raceway → Table 310.16",
+ index_entry:
+ "Conductors — ampacity, not more than 3 in raceway → Table 310.16",
article_or_table: "Table 310.16",
- what_to_look_for: "Find '1/0 AWG' row → read '90°C copper (THHN)' column → 150A"
- }
+ what_to_look_for:
+ "Find '1/0 AWG' row → read '90°C copper (THHN)' column → 150A",
+ },
},
// ─── BOX FILL ───────────────────────────────────────────────────────────────
{
id: "314-002",
- question: "How many cubic inches does each 12 AWG conductor count toward box fill calculations?",
- choices: ["1.5 cubic inches", "2.0 cubic inches", "2.25 cubic inches", "2.5 cubic inches"],
+ question:
+ "How many cubic inches does each 12 AWG conductor count toward box fill calculations?",
+ choices: [
+ "1.5 cubic inches",
+ "2.0 cubic inches",
+ "2.25 cubic inches",
+ "2.5 cubic inches",
+ ],
correct_answer: "2.25 cubic inches",
- explanation: "Table 314.16(B) assigns a volume of 2.25 cubic inches for each 12 AWG conductor used in box fill calculations.",
+ explanation:
+ "Table 314.16(B) assigns a volume of 2.25 cubic inches for each 12 AWG conductor used in box fill calculations.",
nec_article: "Table 314.16(B)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["box fill", "12 AWG", "volume", "conductors"],
lookup_path: {
- index_keywords: ["Box fill", "Conductor volume allowance", "Boxes, volume"],
- index_entry: "Boxes — conductor volume allowances → 314.16(B) → Table 314.16(B)",
+ index_keywords: [
+ "Box fill",
+ "Conductor volume allowance",
+ "Boxes, volume",
+ ],
+ index_entry:
+ "Boxes — conductor volume allowances → 314.16(B) → Table 314.16(B)",
article_or_table: "Table 314.16(B)",
- what_to_look_for: "Find '12 AWG' row → read 'Volume Allowance' column → 2.25 in³"
- }
+ what_to_look_for:
+ "Find '12 AWG' row → read 'Volume Allowance' column → 2.25 in³",
+ },
},
{
id: "314-003",
- question: "A device (receptacle) mounted in a box counts as how many conductors for box fill purposes?",
- choices: ["One conductor of the largest gauge connected to it", "Two conductors of the largest gauge connected to it", "One conductor of the smallest gauge", "Zero — devices are not counted"],
+ question:
+ "A device (receptacle) mounted in a box counts as how many conductors for box fill purposes?",
+ choices: [
+ "One conductor of the largest gauge connected to it",
+ "Two conductors of the largest gauge connected to it",
+ "One conductor of the smallest gauge",
+ "Zero — devices are not counted",
+ ],
correct_answer: "Two conductors of the largest gauge connected to it",
- explanation: "NEC 314.16(B)(4) states that each yoke or strap containing one or more devices counts as two conductors based on the largest conductor connected to that device.",
+ explanation:
+ "NEC 314.16(B)(4) states that each yoke or strap containing one or more devices counts as two conductors based on the largest conductor connected to that device.",
nec_article: "314.16(B)(4)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["box fill", "device", "receptacle", "yoke"],
lookup_path: {
- index_keywords: ["Box fill, devices", "Receptacle, box fill", "Device fill"],
+ index_keywords: [
+ "Box fill, devices",
+ "Receptacle, box fill",
+ "Device fill",
+ ],
index_entry: "Boxes — conductor fill, device or equipment → 314.16(B)(4)",
article_or_table: "314.16(B)(4)",
- what_to_look_for: "Read (B)(4) — each yoke/strap = two conductors based on largest wire attached"
- }
+ what_to_look_for:
+ "Read (B)(4) — each yoke/strap = two conductors based on largest wire attached",
+ },
},
// ─── GROUNDING ──────────────────────────────────────────────────────────────
{
id: "250-002",
- question: "What is the minimum size copper equipment grounding conductor required for a circuit protected by a 60-ampere overcurrent device?",
+ question:
+ "What is the minimum size copper equipment grounding conductor required for a circuit protected by a 60-ampere overcurrent device?",
choices: ["10 AWG", "8 AWG", "6 AWG", "4 AWG"],
correct_answer: "10 AWG",
- explanation: "Table 250.122 requires a minimum 10 AWG copper equipment grounding conductor for circuits protected by overcurrent devices rated up to 60 amperes.",
+ explanation:
+ "Table 250.122 requires a minimum 10 AWG copper equipment grounding conductor for circuits protected by overcurrent devices rated up to 60 amperes.",
nec_article: "Table 250.122",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["grounding", "equipment grounding conductor", "EGC", "60 amp"],
lookup_path: {
- index_keywords: ["Equipment grounding conductors, size", "Grounding conductors, equipment"],
+ index_keywords: [
+ "Equipment grounding conductors, size",
+ "Grounding conductors, equipment",
+ ],
index_entry: "Equipment grounding conductors — size → Table 250.122",
article_or_table: "Table 250.122",
- what_to_look_for: "Find '60A' row in the 'Rating of Automatic Overcurrent Device' column → copper EGC = 10 AWG"
- }
+ what_to_look_for:
+ "Find '60A' row in the 'Rating of Automatic Overcurrent Device' column → copper EGC = 10 AWG",
+ },
},
{
id: "250-003",
- question: "Which of the following is NOT a listed grounding electrode under NEC Article 250?",
- choices: ["Metal water pipe", "Concrete-encased electrode", "PVC conduit buried 30 inches", "Ground ring"],
+ question:
+ "Which of the following is NOT a listed grounding electrode under NEC Article 250?",
+ choices: [
+ "Metal water pipe",
+ "Concrete-encased electrode",
+ "PVC conduit buried 30 inches",
+ "Ground ring",
+ ],
correct_answer: "PVC conduit buried 30 inches",
- explanation: "NEC 250.52(A) lists acceptable grounding electrodes. PVC (non-metallic) conduit is not a conductor and cannot serve as a grounding electrode regardless of burial depth.",
+ explanation:
+ "NEC 250.52(A) lists acceptable grounding electrodes. PVC (non-metallic) conduit is not a conductor and cannot serve as a grounding electrode regardless of burial depth.",
nec_article: "250.52(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -349,121 +487,169 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Grounding electrodes, types", "Electrodes, grounding"],
index_entry: "Grounding electrodes — types permitted → 250.52(A)",
article_or_table: "250.52(A)",
- what_to_look_for: "Read the list in (A)(1) through (A)(8) — PVC conduit is not listed; only metal/concrete/earth-contact electrodes qualify"
- }
+ what_to_look_for:
+ "Read the list in (A)(1) through (A)(8) — PVC conduit is not listed; only metal/concrete/earth-contact electrodes qualify",
+ },
},
{
id: "250-004",
- question: "What is the maximum resistance permitted for a single ground rod electrode before a second electrode must be added?",
+ question:
+ "What is the maximum resistance permitted for a single ground rod electrode before a second electrode must be added?",
choices: ["10 ohms", "25 ohms", "50 ohms", "100 ohms"],
correct_answer: "25 ohms",
- explanation: "NEC 250.53(A)(2) requires a second ground rod if the resistance of a single rod exceeds 25 ohms, unless the single rod is supplemented by another electrode.",
+ explanation:
+ "NEC 250.53(A)(2) requires a second ground rod if the resistance of a single rod exceeds 25 ohms, unless the single rod is supplemented by another electrode.",
nec_article: "250.53(A)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["grounding", "ground rod", "resistance", "electrode"],
lookup_path: {
- index_keywords: ["Ground rods, resistance", "Grounding electrode, resistance"],
- index_entry: "Ground rods — resistance, supplemental electrode → 250.53(A)(2)",
+ index_keywords: [
+ "Ground rods, resistance",
+ "Grounding electrode, resistance",
+ ],
+ index_entry:
+ "Ground rods — resistance, supplemental electrode → 250.53(A)(2)",
article_or_table: "250.53(A)(2)",
- what_to_look_for: "Read (A)(2) — if single rod resistance exceeds 25 ohms, a second electrode is required"
- }
+ what_to_look_for:
+ "Read (A)(2) — if single rod resistance exceeds 25 ohms, a second electrode is required",
+ },
},
// ─── MOTORS ─────────────────────────────────────────────────────────────────
{
id: "430-002",
- question: "What is the full-load current for a 10 HP, 230-volt, single-phase AC motor?",
+ question:
+ "What is the full-load current for a 10 HP, 230-volt, single-phase AC motor?",
choices: ["40 amps", "50 amps", "56 amps", "64 amps"],
correct_answer: "50 amps",
- explanation: "Table 430.248 lists the full-load current for a 10 HP, 230V single-phase motor as 50 amperes.",
+ explanation:
+ "Table 430.248 lists the full-load current for a 10 HP, 230V single-phase motor as 50 amperes.",
nec_article: "Table 430.248",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["motors", "full-load current", "single-phase", "10 HP"],
lookup_path: {
index_keywords: ["Motors, full-load current", "Single-phase motors, FLC"],
- index_entry: "Motors — full-load currents, single-phase AC → Table 430.248",
+ index_entry:
+ "Motors — full-load currents, single-phase AC → Table 430.248",
article_or_table: "Table 430.248",
- what_to_look_for: "Find '10 HP' row → read '230V' column → FLC = 50A"
- }
+ what_to_look_for: "Find '10 HP' row → read '230V' column → FLC = 50A",
+ },
},
{
id: "430-003",
- question: "What is the minimum conductor ampacity required for a single motor branch circuit?",
- choices: ["100% of motor FLC", "115% of motor FLC", "125% of motor FLC", "150% of motor FLC"],
+ question:
+ "What is the minimum conductor ampacity required for a single motor branch circuit?",
+ choices: [
+ "100% of motor FLC",
+ "115% of motor FLC",
+ "125% of motor FLC",
+ "150% of motor FLC",
+ ],
correct_answer: "125% of motor FLC",
- explanation: "NEC 430.22(A) requires motor branch-circuit conductors to have an ampacity of not less than 125% of the motor's full-load current rating.",
+ explanation:
+ "NEC 430.22(A) requires motor branch-circuit conductors to have an ampacity of not less than 125% of the motor's full-load current rating.",
nec_article: "430.22(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["motors", "conductors", "branch circuit", "ampacity"],
lookup_path: {
- index_keywords: ["Motors, branch-circuit conductors", "Motor conductors, size"],
+ index_keywords: [
+ "Motors, branch-circuit conductors",
+ "Motor conductors, size",
+ ],
index_entry: "Motors — branch-circuit conductors, single motor → 430.22",
article_or_table: "430.22(A)",
- what_to_look_for: "Read (A) — 'not less than 125 percent of the motor full-load current rating'"
- }
+ what_to_look_for:
+ "Read (A) — 'not less than 125 percent of the motor full-load current rating'",
+ },
},
{
id: "430-004",
- question: "What is the maximum setting for an inverse time circuit breaker protecting a 3-phase, 460V, 25 HP squirrel-cage induction motor?",
+ question:
+ "What is the maximum setting for an inverse time circuit breaker protecting a 3-phase, 460V, 25 HP squirrel-cage induction motor?",
choices: ["150%", "175%", "200%", "250%"],
correct_answer: "250%",
- explanation: "Table 430.52(C)(1) allows inverse time circuit breakers to be set at a maximum of 250% of motor FLC for squirrel-cage induction motors.",
+ explanation:
+ "Table 430.52(C)(1) allows inverse time circuit breakers to be set at a maximum of 250% of motor FLC for squirrel-cage induction motors.",
nec_article: "Table 430.52(C)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["motors", "circuit breaker", "overcurrent", "3-phase"],
lookup_path: {
- index_keywords: ["Motors, overcurrent protection", "Circuit breakers, motor protection"],
- index_entry: "Motors — branch-circuit short-circuit protection → 430.52 → Table 430.52(C)(1)",
+ index_keywords: [
+ "Motors, overcurrent protection",
+ "Circuit breakers, motor protection",
+ ],
+ index_entry:
+ "Motors — branch-circuit short-circuit protection → 430.52 → Table 430.52(C)(1)",
article_or_table: "Table 430.52(C)(1)",
- what_to_look_for: "Find 'Inverse time circuit breaker' row → read '% of Full-Load Current' column → 250%"
- }
+ what_to_look_for:
+ "Find 'Inverse time circuit breaker' row → read '% of Full-Load Current' column → 250%",
+ },
},
// ─── DWELLING UNITS ─────────────────────────────────────────────────────────
{
id: "210-002",
- question: "What is the minimum required height for a wall receptacle outlet in a dwelling unit?",
- choices: ["No minimum height specified", "12 inches from floor", "18 inches from floor", "24 inches from floor"],
+ question:
+ "What is the minimum required height for a wall receptacle outlet in a dwelling unit?",
+ choices: [
+ "No minimum height specified",
+ "12 inches from floor",
+ "18 inches from floor",
+ "24 inches from floor",
+ ],
correct_answer: "No minimum height specified",
- explanation: "The NEC does not specify a minimum height for receptacle outlets in dwelling units. NEC 210.52 specifies spacing requirements but not mounting height.",
+ explanation:
+ "The NEC does not specify a minimum height for receptacle outlets in dwelling units. NEC 210.52 specifies spacing requirements but not mounting height.",
nec_article: "210.52",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["dwelling", "receptacles", "height", "210.52"],
lookup_path: {
- index_keywords: ["Receptacles, dwelling units", "Dwelling units, receptacle outlets"],
+ index_keywords: [
+ "Receptacles, dwelling units",
+ "Dwelling units, receptacle outlets",
+ ],
index_entry: "Receptacles — dwelling units, required → 210.52",
article_or_table: "210.52",
- what_to_look_for: "Read 210.52 — spacing requirements are specified but no minimum floor height is stated"
- }
+ what_to_look_for:
+ "Read 210.52 — spacing requirements are specified but no minimum floor height is stated",
+ },
},
{
id: "210-003",
- question: "In a dwelling unit, what is the maximum spacing between receptacle outlets along a wall in a general living area?",
+ question:
+ "In a dwelling unit, what is the maximum spacing between receptacle outlets along a wall in a general living area?",
choices: ["6 feet", "10 feet", "12 feet", "15 feet"],
correct_answer: "12 feet",
- explanation: "NEC 210.52(A)(1) requires that no point along the floor line in any wall space be more than 6 feet from a receptacle outlet, which means outlets must be spaced no more than 12 feet apart.",
+ explanation:
+ "NEC 210.52(A)(1) requires that no point along the floor line in any wall space be more than 6 feet from a receptacle outlet, which means outlets must be spaced no more than 12 feet apart.",
nec_article: "210.52(A)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["dwelling", "receptacles", "spacing", "living area"],
lookup_path: {
- index_keywords: ["Receptacles, spacing", "Dwelling units, receptacle spacing"],
+ index_keywords: [
+ "Receptacles, spacing",
+ "Dwelling units, receptacle spacing",
+ ],
index_entry: "Receptacles — dwelling units, spacing → 210.52(A)(1)",
article_or_table: "210.52(A)(1)",
- what_to_look_for: "Read (A)(1) — 'no point along the floor line shall be more than 6 ft from a receptacle' = max 12 ft between outlets"
- }
+ what_to_look_for:
+ "Read (A)(1) — 'no point along the floor line shall be more than 6 ft from a receptacle' = max 12 ft between outlets",
+ },
},
{
id: "210-004",
- question: "How many 20-ampere circuits are required for the laundry area of a dwelling unit?",
+ question:
+ "How many 20-ampere circuits are required for the laundry area of a dwelling unit?",
choices: ["None required", "One", "Two", "Three"],
correct_answer: "One",
- explanation: "NEC 210.11(C)(2) requires at least one 20-ampere branch circuit for the laundry receptacle outlet(s) in a dwelling unit.",
+ explanation:
+ "NEC 210.11(C)(2) requires at least one 20-ampere branch circuit for the laundry receptacle outlet(s) in a dwelling unit.",
nec_article: "210.11(C)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -472,258 +658,372 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Laundry branch circuit", "Dwelling units, laundry"],
index_entry: "Laundry — branch circuit, dwelling units → 210.11(C)(2)",
article_or_table: "210.11(C)(2)",
- what_to_look_for: "Read (C)(2) — 'at least one additional 20-ampere branch circuit shall be provided for laundry receptacle outlets'"
- }
+ what_to_look_for:
+ "Read (C)(2) — 'at least one additional 20-ampere branch circuit shall be provided for laundry receptacle outlets'",
+ },
},
{
id: "210-005",
- question: "GFCI protection is required for receptacles installed within how many feet of the outside edge of a dwelling unit bathroom sink?",
- choices: ["3 feet", "6 feet", "All bathroom receptacles regardless of distance", "Only those within 12 inches"],
+ question:
+ "GFCI protection is required for receptacles installed within how many feet of the outside edge of a dwelling unit bathroom sink?",
+ choices: [
+ "3 feet",
+ "6 feet",
+ "All bathroom receptacles regardless of distance",
+ "Only those within 12 inches",
+ ],
correct_answer: "All bathroom receptacles regardless of distance",
- explanation: "NEC 210.8(A)(1) requires GFCI protection for all receptacles installed in bathrooms of dwelling units, regardless of distance from the sink.",
+ explanation:
+ "NEC 210.8(A)(1) requires GFCI protection for all receptacles installed in bathrooms of dwelling units, regardless of distance from the sink.",
nec_article: "210.8(A)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["GFCI", "bathroom", "dwelling", "receptacles"],
lookup_path: {
- index_keywords: ["GFCI protection, bathrooms", "Ground-fault circuit-interrupter, bathrooms"],
- index_entry: "Ground-fault circuit-interrupter protection — dwelling units, bathrooms → 210.8(A)(1)",
+ index_keywords: [
+ "GFCI protection, bathrooms",
+ "Ground-fault circuit-interrupter, bathrooms",
+ ],
+ index_entry:
+ "Ground-fault circuit-interrupter protection — dwelling units, bathrooms → 210.8(A)(1)",
article_or_table: "210.8(A)(1)",
- what_to_look_for: "Read (A)(1) — 'all 125-volt through 250-volt receptacles installed in bathrooms' — no distance limitation"
- }
+ what_to_look_for:
+ "Read (A)(1) — 'all 125-volt through 250-volt receptacles installed in bathrooms' — no distance limitation",
+ },
},
// ─── SERVICE ENTRANCE ────────────────────────────────────────────────────────
{
id: "230-002",
- question: "What is the minimum clearance above a residential driveway for overhead service-drop conductors not exceeding 600 volts?",
+ question:
+ "What is the minimum clearance above a residential driveway for overhead service-drop conductors not exceeding 600 volts?",
choices: ["10 feet", "12 feet", "15 feet", "18 feet"],
correct_answer: "12 feet",
- explanation: "NEC 230.24(B)(1) requires a minimum clearance of 12 feet above residential driveways for service-drop conductors rated 600 volts or less.",
+ explanation:
+ "NEC 230.24(B)(1) requires a minimum clearance of 12 feet above residential driveways for service-drop conductors rated 600 volts or less.",
nec_article: "230.24(B)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["service entrance", "service drop", "clearance", "driveway"],
lookup_path: {
- index_keywords: ["Service drop, clearances", "Overhead conductors, clearances", "Driveway clearance"],
- index_entry: "Service-drop conductors — clearances, above ground → 230.24(B)",
+ index_keywords: [
+ "Service drop, clearances",
+ "Overhead conductors, clearances",
+ "Driveway clearance",
+ ],
+ index_entry:
+ "Service-drop conductors — clearances, above ground → 230.24(B)",
article_or_table: "230.24(B)(1)",
- what_to_look_for: "Find (B)(1) — residential driveways → 12 ft minimum clearance"
- }
+ what_to_look_for:
+ "Find (B)(1) — residential driveways → 12 ft minimum clearance",
+ },
},
{
id: "230-003",
- question: "What is the minimum size copper service-entrance conductor permitted for a 200-ampere residential service?",
+ question:
+ "What is the minimum size copper service-entrance conductor permitted for a 200-ampere residential service?",
choices: ["2/0 AWG", "3/0 AWG", "4/0 AWG", "250 kcmil"],
correct_answer: "2/0 AWG",
- explanation: "Table 310.16 shows that 2/0 AWG copper THWN-2 has an ampacity of 195A at 75°C. However, for a 200A service, 2/0 AWG copper is the minimum per 230.42 when using 90°C rated conductors at 75°C terminal rating.",
+ explanation:
+ "Table 310.16 shows that 2/0 AWG copper THWN-2 has an ampacity of 195A at 75°C. However, for a 200A service, 2/0 AWG copper is the minimum per 230.42 when using 90°C rated conductors at 75°C terminal rating.",
nec_article: "230.42",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["service entrance", "conductor size", "200 amp", "residential"],
lookup_path: {
- index_keywords: ["Service-entrance conductors, size", "Service conductors, minimum size"],
- index_entry: "Service-entrance conductors — minimum size → 230.42 → Table 310.16",
+ index_keywords: [
+ "Service-entrance conductors, size",
+ "Service conductors, minimum size",
+ ],
+ index_entry:
+ "Service-entrance conductors — minimum size → 230.42 → Table 310.16",
article_or_table: "Table 310.16",
- what_to_look_for: "Find '2/0 AWG' row → read '75°C copper' column → 175A; check 90°C column → 195A — use 75°C terminal rating per 110.14(C)"
- }
+ what_to_look_for:
+ "Find '2/0 AWG' row → read '75°C copper' column → 175A; check 90°C column → 195A — use 75°C terminal rating per 110.14(C)",
+ },
},
// ─── TRANSFORMERS ────────────────────────────────────────────────────────────
{
id: "450-002",
- question: "What is the maximum overcurrent protection for the primary of a transformer with a primary current of less than 2 amperes?",
+ question:
+ "What is the maximum overcurrent protection for the primary of a transformer with a primary current of less than 2 amperes?",
choices: ["125%", "167%", "300%", "No protection required"],
correct_answer: "300%",
- explanation: "NEC 450.3(B) allows primary overcurrent protection up to 300% of rated primary current when the primary current is less than 2 amperes.",
+ explanation:
+ "NEC 450.3(B) allows primary overcurrent protection up to 300% of rated primary current when the primary current is less than 2 amperes.",
nec_article: "Table 450.3(B)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["transformers", "overcurrent", "primary protection", "low current"],
lookup_path: {
- index_keywords: ["Transformers, overcurrent protection", "Overcurrent protection, transformers"],
- index_entry: "Transformers — overcurrent protection, 600V or less → 450.3(B) → Table 450.3(B)",
+ index_keywords: [
+ "Transformers, overcurrent protection",
+ "Overcurrent protection, transformers",
+ ],
+ index_entry:
+ "Transformers — overcurrent protection, 600V or less → 450.3(B) → Table 450.3(B)",
article_or_table: "Table 450.3(B)",
- what_to_look_for: "Find row for 'Primary < 2A, no secondary protection' → maximum = 300%"
- }
+ what_to_look_for:
+ "Find row for 'Primary < 2A, no secondary protection' → maximum = 300%",
+ },
},
{
id: "450-003",
- question: "What is the minimum working clearance required in front of a transformer rated over 600 volts?",
+ question:
+ "What is the minimum working clearance required in front of a transformer rated over 600 volts?",
choices: ["3 feet", "4 feet", "6 feet", "Depends on voltage class"],
correct_answer: "Depends on voltage class",
- explanation: "NEC 110.34(A) specifies working clearances for equipment over 600V based on voltage class and condition of installation — ranging from 3 feet to 10 feet depending on voltage and exposure.",
+ explanation:
+ "NEC 110.34(A) specifies working clearances for equipment over 600V based on voltage class and condition of installation — ranging from 3 feet to 10 feet depending on voltage and exposure.",
nec_article: "110.34(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["transformers", "working clearance", "high voltage", "110.34"],
lookup_path: {
- index_keywords: ["Working clearances, over 600V", "Clearances, electrical equipment"],
- index_entry: "Working clearances — over 600 volts → 110.34(A) → Table 110.34(A)",
+ index_keywords: [
+ "Working clearances, over 600V",
+ "Clearances, electrical equipment",
+ ],
+ index_entry:
+ "Working clearances — over 600 volts → 110.34(A) → Table 110.34(A)",
article_or_table: "Table 110.34(A)",
- what_to_look_for: "Find voltage range row and installation condition column → read minimum clearance distance"
- }
+ what_to_look_for:
+ "Find voltage range row and installation condition column → read minimum clearance distance",
+ },
},
// ─── HAZARDOUS LOCATIONS ─────────────────────────────────────────────────────
{
id: "500-002",
- question: "A Class I, Division 2 location is one where ignitable concentrations of flammable gases:",
+ question:
+ "A Class I, Division 2 location is one where ignitable concentrations of flammable gases:",
choices: [
"Exist continuously under normal operating conditions",
"Are handled, processed, or used but are normally in closed containers",
"Are present only during abnormal conditions such as container failure",
- "Do not exist"
+ "Do not exist",
],
- correct_answer: "Are present only during abnormal conditions such as container failure",
- explanation: "NEC 500.5(B)(2) defines Class I, Division 2 as a location where ignitable concentrations of flammable gases are handled but are normally confined, and could become hazardous only through accidental rupture or breakdown.",
+ correct_answer:
+ "Are present only during abnormal conditions such as container failure",
+ explanation:
+ "NEC 500.5(B)(2) defines Class I, Division 2 as a location where ignitable concentrations of flammable gases are handled but are normally confined, and could become hazardous only through accidental rupture or breakdown.",
nec_article: "500.5(B)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["hazardous locations", "Class I", "Division 2", "flammable gases"],
lookup_path: {
- index_keywords: ["Class I locations, Division 2", "Hazardous locations, Division 2"],
- index_entry: "Hazardous (classified) locations — Class I, Division 2 → 500.5(B)(2)",
+ index_keywords: [
+ "Class I locations, Division 2",
+ "Hazardous locations, Division 2",
+ ],
+ index_entry:
+ "Hazardous (classified) locations — Class I, Division 2 → 500.5(B)(2)",
article_or_table: "500.5(B)(2)",
- what_to_look_for: "Read (B)(2) — abnormal conditions only; normally confined in closed containers or systems"
- }
+ what_to_look_for:
+ "Read (B)(2) — abnormal conditions only; normally confined in closed containers or systems",
+ },
},
{
id: "500-003",
question: "Class II locations involve which type of hazardous material?",
- choices: ["Flammable gases or vapors", "Combustible dust", "Ignitable fibers", "Flammable liquids"],
+ choices: [
+ "Flammable gases or vapors",
+ "Combustible dust",
+ "Ignitable fibers",
+ "Flammable liquids",
+ ],
correct_answer: "Combustible dust",
- explanation: "NEC 500.5(C) defines Class II locations as those that are hazardous because of the presence of combustible dust.",
+ explanation:
+ "NEC 500.5(C) defines Class II locations as those that are hazardous because of the presence of combustible dust.",
nec_article: "500.5(C)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["hazardous locations", "Class II", "combustible dust"],
lookup_path: {
- index_keywords: ["Class II locations", "Combustible dust, hazardous locations"],
+ index_keywords: [
+ "Class II locations",
+ "Combustible dust, hazardous locations",
+ ],
index_entry: "Hazardous (classified) locations — Class II → 500.5(C)",
article_or_table: "500.5(C)",
- what_to_look_for: "Read (C) — 'Class II locations are those that are hazardous because of the presence of combustible dust'"
- }
+ what_to_look_for:
+ "Read (C) — 'Class II locations are those that are hazardous because of the presence of combustible dust'",
+ },
},
// ─── GFCI / AFCI ─────────────────────────────────────────────────────────────
{
id: "210-006",
- question: "AFCI protection is required for branch circuits supplying outlets in which rooms of a dwelling unit?",
+ question:
+ "AFCI protection is required for branch circuits supplying outlets in which rooms of a dwelling unit?",
choices: [
"Kitchen and bathrooms only",
"All 120-volt, 15- and 20-ampere branch circuits in all rooms",
"Bedrooms only",
- "Garage and outdoors only"
+ "Garage and outdoors only",
],
- correct_answer: "All 120-volt, 15- and 20-ampere branch circuits in all rooms",
- explanation: "NEC 210.12(A) requires AFCI protection for all 120-volt, 15- and 20-ampere branch circuits supplying outlets or devices installed in dwelling units.",
+ correct_answer:
+ "All 120-volt, 15- and 20-ampere branch circuits in all rooms",
+ explanation:
+ "NEC 210.12(A) requires AFCI protection for all 120-volt, 15- and 20-ampere branch circuits supplying outlets or devices installed in dwelling units.",
nec_article: "210.12(A)",
nec_versions: ["2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["AFCI", "arc-fault", "dwelling", "branch circuits"],
lookup_path: {
- index_keywords: ["Arc-fault circuit-interrupter protection", "AFCI, dwelling units"],
- index_entry: "Arc-fault circuit-interrupter protection — dwelling units → 210.12(A)",
+ index_keywords: [
+ "Arc-fault circuit-interrupter protection",
+ "AFCI, dwelling units",
+ ],
+ index_entry:
+ "Arc-fault circuit-interrupter protection — dwelling units → 210.12(A)",
article_or_table: "210.12(A)",
- what_to_look_for: "Read (A) — 'all 120-volt, single-phase, 15- and 20-ampere branch circuits supplying outlets or devices'"
- }
+ what_to_look_for:
+ "Read (A) — 'all 120-volt, single-phase, 15- and 20-ampere branch circuits supplying outlets or devices'",
+ },
},
{
id: "210-007",
- question: "GFCI protection is required for 125-volt receptacles installed outdoors at a dwelling unit at grade level and accessible to the public?",
- choices: ["Only if within 20 feet of a water source", "Yes, all such receptacles", "Only in wet locations", "No, only indoor receptacles require GFCI"],
+ question:
+ "GFCI protection is required for 125-volt receptacles installed outdoors at a dwelling unit at grade level and accessible to the public?",
+ choices: [
+ "Only if within 20 feet of a water source",
+ "Yes, all such receptacles",
+ "Only in wet locations",
+ "No, only indoor receptacles require GFCI",
+ ],
correct_answer: "Yes, all such receptacles",
- explanation: "NEC 210.8(A)(3) requires GFCI protection for all 125-volt through 250-volt receptacles installed outdoors at dwelling units where the receptacles are accessible to persons from grade level.",
+ explanation:
+ "NEC 210.8(A)(3) requires GFCI protection for all 125-volt through 250-volt receptacles installed outdoors at dwelling units where the receptacles are accessible to persons from grade level.",
nec_article: "210.8(A)(3)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["GFCI", "outdoor", "dwelling", "receptacles"],
lookup_path: {
- index_keywords: ["GFCI protection, outdoors", "Outdoor receptacles, GFCI"],
- index_entry: "Ground-fault circuit-interrupter protection — dwelling units, outdoors → 210.8(A)(3)",
+ index_keywords: [
+ "GFCI protection, outdoors",
+ "Outdoor receptacles, GFCI",
+ ],
+ index_entry:
+ "Ground-fault circuit-interrupter protection — dwelling units, outdoors → 210.8(A)(3)",
article_or_table: "210.8(A)(3)",
- what_to_look_for: "Read (A)(3) — 'outdoors' with 'access from grade level' = GFCI required for all such receptacles"
- }
+ what_to_look_for:
+ "Read (A)(3) — 'outdoors' with 'access from grade level' = GFCI required for all such receptacles",
+ },
},
{
id: "210-008",
- question: "GFCI protection is required for receptacles installed in a commercial kitchen within how many feet of a sink?",
- choices: ["3 feet", "6 feet", "10 feet", "All kitchen receptacles regardless of distance"],
+ question:
+ "GFCI protection is required for receptacles installed in a commercial kitchen within how many feet of a sink?",
+ choices: [
+ "3 feet",
+ "6 feet",
+ "10 feet",
+ "All kitchen receptacles regardless of distance",
+ ],
correct_answer: "6 feet",
- explanation: "NEC 210.8(B)(2) requires GFCI protection for receptacles installed within 6 feet of the outside edge of a sink in commercial kitchens and similar areas.",
+ explanation:
+ "NEC 210.8(B)(2) requires GFCI protection for receptacles installed within 6 feet of the outside edge of a sink in commercial kitchens and similar areas.",
nec_article: "210.8(B)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["GFCI", "commercial kitchen", "sink", "receptacles"],
lookup_path: {
- index_keywords: ["GFCI protection, commercial", "Kitchen receptacles, GFCI"],
- index_entry: "Ground-fault circuit-interrupter protection — other than dwelling units → 210.8(B)(2)",
+ index_keywords: [
+ "GFCI protection, commercial",
+ "Kitchen receptacles, GFCI",
+ ],
+ index_entry:
+ "Ground-fault circuit-interrupter protection — other than dwelling units → 210.8(B)(2)",
article_or_table: "210.8(B)(2)",
- what_to_look_for: "Read (B)(2) — 'within 6 feet of the outside edge of a sink'"
- }
+ what_to_look_for:
+ "Read (B)(2) — 'within 6 feet of the outside edge of a sink'",
+ },
},
// ─── OVERCURRENT PROTECTION ──────────────────────────────────────────────────
{
id: "240-002",
- question: "What is the maximum overcurrent protection permitted for a 10 AWG copper conductor with 60°C insulation?",
+ question:
+ "What is the maximum overcurrent protection permitted for a 10 AWG copper conductor with 60°C insulation?",
choices: ["20 amperes", "30 amperes", "40 amperes", "50 amperes"],
correct_answer: "30 amperes",
- explanation: "Table 310.16 lists 30 amperes as the ampacity of 10 AWG copper at 60°C. Per 240.4(D), 10 AWG copper conductors shall not be protected by overcurrent devices rated more than 30 amperes.",
+ explanation:
+ "Table 310.16 lists 30 amperes as the ampacity of 10 AWG copper at 60°C. Per 240.4(D), 10 AWG copper conductors shall not be protected by overcurrent devices rated more than 30 amperes.",
nec_article: "240.4(D)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["overcurrent protection", "10 AWG", "conductor protection", "240.4"],
lookup_path: {
- index_keywords: ["Overcurrent protection, small conductors", "Conductors, overcurrent protection"],
+ index_keywords: [
+ "Overcurrent protection, small conductors",
+ "Conductors, overcurrent protection",
+ ],
index_entry: "Overcurrent protection — small conductors → 240.4(D)",
article_or_table: "240.4(D)",
- what_to_look_for: "Read (D) — 10 AWG copper = 30A maximum overcurrent device"
- }
+ what_to_look_for:
+ "Read (D) — 10 AWG copper = 30A maximum overcurrent device",
+ },
},
{
id: "240-003",
- question: "Where a fuse or circuit breaker rating does not correspond to a standard size, the next higher standard rating is permitted provided the overcurrent device does not exceed:",
+ question:
+ "Where a fuse or circuit breaker rating does not correspond to a standard size, the next higher standard rating is permitted provided the overcurrent device does not exceed:",
choices: ["100 amperes", "200 amperes", "400 amperes", "800 amperes"],
correct_answer: "800 amperes",
- explanation: "NEC 240.4(B) permits the next higher standard overcurrent device rating above the conductor ampacity, provided the device rating does not exceed 800 amperes.",
+ explanation:
+ "NEC 240.4(B) permits the next higher standard overcurrent device rating above the conductor ampacity, provided the device rating does not exceed 800 amperes.",
nec_article: "240.4(B)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["overcurrent protection", "next higher rating", "240.4(B)"],
lookup_path: {
- index_keywords: ["Overcurrent protection, next higher rating", "Circuit breakers, next higher size"],
- index_entry: "Overcurrent protection — next higher standard device rating → 240.4(B)",
+ index_keywords: [
+ "Overcurrent protection, next higher rating",
+ "Circuit breakers, next higher size",
+ ],
+ index_entry:
+ "Overcurrent protection — next higher standard device rating → 240.4(B)",
article_or_table: "240.4(B)",
- what_to_look_for: "Read (B) — next higher standard rating permitted only if device does not exceed 800A"
- }
+ what_to_look_for:
+ "Read (B) — next higher standard rating permitted only if device does not exceed 800A",
+ },
},
// ─── CONDUIT FILL ────────────────────────────────────────────────────────────
{
id: "358-001",
- question: "What is the maximum percent fill for a conduit containing 3 or more conductors?",
+ question:
+ "What is the maximum percent fill for a conduit containing 3 or more conductors?",
choices: ["31%", "40%", "53%", "60%"],
correct_answer: "40%",
- explanation: "Chapter 9, Table 1 limits conduit fill to 40% of the cross-sectional area of the conduit when 3 or more conductors are installed.",
+ explanation:
+ "Chapter 9, Table 1 limits conduit fill to 40% of the cross-sectional area of the conduit when 3 or more conductors are installed.",
nec_article: "Chapter 9, Table 1",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["conduit fill", "raceway", "conductors", "Chapter 9"],
lookup_path: {
- index_keywords: ["Conduit fill", "Raceway fill", "Conductors, conduit fill"],
+ index_keywords: [
+ "Conduit fill",
+ "Raceway fill",
+ "Conductors, conduit fill",
+ ],
index_entry: "Conduit fill — percent fill allowed → Chapter 9, Table 1",
article_or_table: "Chapter 9, Table 1",
- what_to_look_for: "Find '3 or more conductors' row → read '% of Cross-Sectional Area' column → 40%"
- }
+ what_to_look_for:
+ "Find '3 or more conductors' row → read '% of Cross-Sectional Area' column → 40%",
+ },
},
{
id: "358-002",
- question: "What is the maximum percent fill for a conduit containing only 1 conductor?",
+ question:
+ "What is the maximum percent fill for a conduit containing only 1 conductor?",
choices: ["31%", "40%", "53%", "60%"],
correct_answer: "53%",
- explanation: "Chapter 9, Table 1 allows a single conductor to fill up to 53% of the conduit cross-sectional area.",
+ explanation:
+ "Chapter 9, Table 1 allows a single conductor to fill up to 53% of the conduit cross-sectional area.",
nec_article: "Chapter 9, Table 1",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -732,15 +1032,18 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Conduit fill, single conductor", "Raceway fill"],
index_entry: "Conduit fill — 1 conductor → Chapter 9, Table 1",
article_or_table: "Chapter 9, Table 1",
- what_to_look_for: "Find '1 conductor' row → read '% of Cross-Sectional Area' column → 53%"
- }
+ what_to_look_for:
+ "Find '1 conductor' row → read '% of Cross-Sectional Area' column → 53%",
+ },
},
{
id: "358-003",
- question: "What is the trade size of the smallest EMT conduit permitted to contain three 12 AWG THHN conductors?",
+ question:
+ "What is the trade size of the smallest EMT conduit permitted to contain three 12 AWG THHN conductors?",
choices: ["1/2 inch", "3/4 inch", "1 inch", "1-1/4 inch"],
correct_answer: "1/2 inch",
- explanation: "Annex C, Table C.1 shows that 1/2 inch EMT can contain up to 9 conductors of 12 AWG THHN, so three conductors fit within the 40% fill limit.",
+ explanation:
+ "Annex C, Table C.1 shows that 1/2 inch EMT can contain up to 9 conductors of 12 AWG THHN, so three conductors fit within the 40% fill limit.",
nec_article: "Annex C, Table C.1",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -749,189 +1052,269 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["EMT conduit fill", "Conduit fill, THHN", "Annex C"],
index_entry: "Conduit fill — EMT, THHN conductors → Annex C, Table C.1",
article_or_table: "Annex C, Table C.1",
- what_to_look_for: "Find '12 AWG THHN' row → read '1/2 inch EMT' column → max conductors = 9; three conductors fit"
- }
+ what_to_look_for:
+ "Find '12 AWG THHN' row → read '1/2 inch EMT' column → max conductors = 9; three conductors fit",
+ },
},
// ─── WORKING CLEARANCES ──────────────────────────────────────────────────────
{
id: "110-001",
- question: "What is the minimum working clearance in front of a 120/240-volt panelboard installed in a dwelling unit (Condition 1)?",
+ question:
+ "What is the minimum working clearance in front of a 120/240-volt panelboard installed in a dwelling unit (Condition 1)?",
choices: ["2 feet", "2.5 feet", "3 feet", "3.5 feet"],
correct_answer: "3 feet",
- explanation: "Table 110.26(A)(1) requires a minimum working clearance of 3 feet in front of electrical equipment rated 0–150 volts to ground under Condition 1 (exposed live parts on one side, no live or grounded parts on the other).",
+ explanation:
+ "Table 110.26(A)(1) requires a minimum working clearance of 3 feet in front of electrical equipment rated 0–150 volts to ground under Condition 1 (exposed live parts on one side, no live or grounded parts on the other).",
nec_article: "Table 110.26(A)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["working clearances", "panelboard", "110.26", "dwelling"],
lookup_path: {
- index_keywords: ["Working clearances, electrical equipment", "Clearances, panelboard"],
- index_entry: "Working clearances — electrical equipment → 110.26(A)(1) → Table 110.26(A)(1)",
+ index_keywords: [
+ "Working clearances, electrical equipment",
+ "Clearances, panelboard",
+ ],
+ index_entry:
+ "Working clearances — electrical equipment → 110.26(A)(1) → Table 110.26(A)(1)",
article_or_table: "Table 110.26(A)(1)",
- what_to_look_for: "Find '0–150V to ground' row → 'Condition 1' column → 3 ft minimum"
- }
+ what_to_look_for:
+ "Find '0–150V to ground' row → 'Condition 1' column → 3 ft minimum",
+ },
},
{
id: "110-002",
- question: "What is the minimum headroom required for working spaces about electrical equipment rated 600 volts or less?",
+ question:
+ "What is the minimum headroom required for working spaces about electrical equipment rated 600 volts or less?",
choices: ["5.5 feet", "6 feet", "6.5 feet", "7 feet"],
correct_answer: "6.5 feet",
- explanation: "NEC 110.26(A)(3) requires a minimum headroom of 6.5 feet (2.0 m) for working spaces about electrical equipment rated 600 volts or less.",
+ explanation:
+ "NEC 110.26(A)(3) requires a minimum headroom of 6.5 feet (2.0 m) for working spaces about electrical equipment rated 600 volts or less.",
nec_article: "110.26(A)(3)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["working clearances", "headroom", "110.26"],
lookup_path: {
- index_keywords: ["Headroom, electrical equipment", "Working clearances, headroom"],
+ index_keywords: [
+ "Headroom, electrical equipment",
+ "Working clearances, headroom",
+ ],
index_entry: "Working clearances — headroom → 110.26(A)(3)",
article_or_table: "110.26(A)(3)",
- what_to_look_for: "Read (A)(3) — 'minimum headroom of 6½ ft (2.0 m)'"
- }
+ what_to_look_for: "Read (A)(3) — 'minimum headroom of 6½ ft (2.0 m)'",
+ },
},
{
id: "110-003",
- question: "What is the minimum width of the working space in front of electrical equipment rated 600 volts or less?",
- choices: ["24 inches", "30 inches", "36 inches", "The width of the equipment or 30 inches, whichever is greater"],
- correct_answer: "The width of the equipment or 30 inches, whichever is greater",
- explanation: "NEC 110.26(A)(2) requires the working space width to be the width of the equipment or 30 inches, whichever is greater.",
+ question:
+ "What is the minimum width of the working space in front of electrical equipment rated 600 volts or less?",
+ choices: [
+ "24 inches",
+ "30 inches",
+ "36 inches",
+ "The width of the equipment or 30 inches, whichever is greater",
+ ],
+ correct_answer:
+ "The width of the equipment or 30 inches, whichever is greater",
+ explanation:
+ "NEC 110.26(A)(2) requires the working space width to be the width of the equipment or 30 inches, whichever is greater.",
nec_article: "110.26(A)(2)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["working clearances", "width", "110.26"],
lookup_path: {
- index_keywords: ["Working clearances, width", "Electrical equipment, working space"],
+ index_keywords: [
+ "Working clearances, width",
+ "Electrical equipment, working space",
+ ],
index_entry: "Working clearances — width of working space → 110.26(A)(2)",
article_or_table: "110.26(A)(2)",
- what_to_look_for: "Read (A)(2) — 'the width of the equipment or 30 in., whichever is greater'"
- }
+ what_to_look_for:
+ "Read (A)(2) — 'the width of the equipment or 30 in., whichever is greater'",
+ },
},
// ─── LOAD CALCULATIONS ───────────────────────────────────────────────────────
{
id: "220-001",
- question: "What is the general lighting load in volt-amperes per square foot for a dwelling unit?",
+ question:
+ "What is the general lighting load in volt-amperes per square foot for a dwelling unit?",
choices: ["1 VA/sq ft", "2 VA/sq ft", "3 VA/sq ft", "3.5 VA/sq ft"],
correct_answer: "3 VA/sq ft",
- explanation: "Table 220.12 specifies a general lighting load of 3 volt-amperes per square foot for dwelling units.",
+ explanation:
+ "Table 220.12 specifies a general lighting load of 3 volt-amperes per square foot for dwelling units.",
nec_article: "Table 220.12",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["load calculations", "dwelling", "general lighting", "VA per sq ft"],
lookup_path: {
- index_keywords: ["General lighting loads", "Load calculations, dwelling", "Volt-amperes per square foot"],
+ index_keywords: [
+ "General lighting loads",
+ "Load calculations, dwelling",
+ "Volt-amperes per square foot",
+ ],
index_entry: "General lighting loads — by occupancy → Table 220.12",
article_or_table: "Table 220.12",
- what_to_look_for: "Find 'Dwelling units' row → read 'Unit Load (VA/ft²)' column → 3 VA/ft²"
- }
+ what_to_look_for:
+ "Find 'Dwelling units' row → read 'Unit Load (VA/ft²)' column → 3 VA/ft²",
+ },
},
{
id: "220-002",
- question: "What demand factor applies to the first 3,000 VA of lighting load in a dwelling unit load calculation?",
+ question:
+ "What demand factor applies to the first 3,000 VA of lighting load in a dwelling unit load calculation?",
choices: ["50%", "75%", "100%", "35%"],
correct_answer: "100%",
- explanation: "Table 220.42 applies a 100% demand factor to the first 3,000 VA of lighting load in a dwelling unit.",
+ explanation:
+ "Table 220.42 applies a 100% demand factor to the first 3,000 VA of lighting load in a dwelling unit.",
nec_article: "Table 220.42",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["load calculations", "demand factor", "lighting", "dwelling"],
lookup_path: {
- index_keywords: ["Demand factors, lighting", "Lighting load, demand factors"],
- index_entry: "Lighting load — demand factors, dwelling units → Table 220.42",
+ index_keywords: [
+ "Demand factors, lighting",
+ "Lighting load, demand factors",
+ ],
+ index_entry:
+ "Lighting load — demand factors, dwelling units → Table 220.42",
article_or_table: "Table 220.42",
- what_to_look_for: "Find 'First 3,000 VA' row → read 'Demand Factor' column → 100%"
- }
+ what_to_look_for:
+ "Find 'First 3,000 VA' row → read 'Demand Factor' column → 100%",
+ },
},
{
id: "220-003",
- question: "What is the standard volt-ampere load assigned to each small-appliance and laundry branch circuit in a dwelling unit load calculation?",
+ question:
+ "What is the standard volt-ampere load assigned to each small-appliance and laundry branch circuit in a dwelling unit load calculation?",
choices: ["1,000 VA", "1,500 VA", "2,000 VA", "3,000 VA"],
correct_answer: "1,500 VA",
- explanation: "NEC 220.52(A) and (B) assign 1,500 VA for each 20-ampere small-appliance branch circuit and each laundry branch circuit in a dwelling unit load calculation.",
+ explanation:
+ "NEC 220.52(A) and (B) assign 1,500 VA for each 20-ampere small-appliance branch circuit and each laundry branch circuit in a dwelling unit load calculation.",
nec_article: "220.52(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["load calculations", "small appliance", "laundry", "1500 VA"],
lookup_path: {
- index_keywords: ["Small-appliance load", "Load calculations, small appliance"],
- index_entry: "Small-appliance branch circuits — load calculation → 220.52(A)",
+ index_keywords: [
+ "Small-appliance load",
+ "Load calculations, small appliance",
+ ],
+ index_entry:
+ "Small-appliance branch circuits — load calculation → 220.52(A)",
article_or_table: "220.52(A)",
- what_to_look_for: "Read (A) — 'a load of not less than 1500 volt-amperes for each 2-wire small-appliance branch circuit'"
- }
+ what_to_look_for:
+ "Read (A) — 'a load of not less than 1500 volt-amperes for each 2-wire small-appliance branch circuit'",
+ },
},
{
id: "220-004",
- question: "When calculating the demand load for electric ranges rated over 12 kW in a dwelling unit, which NEC table is used?",
+ question:
+ "When calculating the demand load for electric ranges rated over 12 kW in a dwelling unit, which NEC table is used?",
choices: ["Table 220.42", "Table 220.55", "Table 220.56", "Table 220.12"],
correct_answer: "Table 220.55",
- explanation: "Table 220.55 provides demand factors and maximum demand for household electric ranges, wall-mounted ovens, and counter-mounted cooking units rated over 1¾ kW.",
+ explanation:
+ "Table 220.55 provides demand factors and maximum demand for household electric ranges, wall-mounted ovens, and counter-mounted cooking units rated over 1¾ kW.",
nec_article: "Table 220.55",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
- tags: ["load calculations", "electric range", "demand factor", "cooking equipment"],
+ tags: [
+ "load calculations",
+ "electric range",
+ "demand factor",
+ "cooking equipment",
+ ],
lookup_path: {
- index_keywords: ["Electric ranges, demand factors", "Cooking equipment, load calculation"],
+ index_keywords: [
+ "Electric ranges, demand factors",
+ "Cooking equipment, load calculation",
+ ],
index_entry: "Ranges — household, demand loads → 220.55 → Table 220.55",
article_or_table: "Table 220.55",
- what_to_look_for: "Find number of appliances column → read demand load or apply Column C for ranges over 12 kW"
- }
+ what_to_look_for:
+ "Find number of appliances column → read demand load or apply Column C for ranges over 12 kW",
+ },
},
// ─── PANELBOARDS ─────────────────────────────────────────────────────────────
{
id: "408-001",
- question: "What is the maximum number of overcurrent devices permitted in a lighting and appliance branch-circuit panelboard?",
+ question:
+ "What is the maximum number of overcurrent devices permitted in a lighting and appliance branch-circuit panelboard?",
choices: ["24", "30", "42", "No limit"],
correct_answer: "42",
- explanation: "NEC 408.54 limits the number of overcurrent devices in a lighting and appliance branch-circuit panelboard to 42.",
+ explanation:
+ "NEC 408.54 limits the number of overcurrent devices in a lighting and appliance branch-circuit panelboard to 42.",
nec_article: "408.54",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["panelboards", "overcurrent devices", "408.54", "42 circuit"],
lookup_path: {
- index_keywords: ["Panelboards, overcurrent devices", "Circuit breakers, panelboard limit"],
+ index_keywords: [
+ "Panelboards, overcurrent devices",
+ "Circuit breakers, panelboard limit",
+ ],
index_entry: "Panelboards — overcurrent devices, maximum number → 408.54",
article_or_table: "408.54",
- what_to_look_for: "Read 408.54 — 'not more than 42 overcurrent devices' in a lighting and appliance branch-circuit panelboard"
- }
+ what_to_look_for:
+ "Read 408.54 — 'not more than 42 overcurrent devices' in a lighting and appliance branch-circuit panelboard",
+ },
},
{
id: "408-002",
- question: "Panelboards must be mounted in cabinets, cutout boxes, or enclosures designed for the purpose and must be dead-front. What additional requirement applies to panelboard enclosures?",
+ question:
+ "Panelboards must be mounted in cabinets, cutout boxes, or enclosures designed for the purpose and must be dead-front. What additional requirement applies to panelboard enclosures?",
choices: [
"They must be painted gray",
"They must be grounded",
"They must be mounted at eye level",
- "They must be UL listed only"
+ "They must be UL listed only",
],
correct_answer: "They must be grounded",
- explanation: "NEC 408.40 requires that panelboard enclosures be grounded in accordance with Article 250 and the grounding terminal bar must be bonded to the enclosure.",
+ explanation:
+ "NEC 408.40 requires that panelboard enclosures be grounded in accordance with Article 250 and the grounding terminal bar must be bonded to the enclosure.",
nec_article: "408.40",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["panelboards", "grounding", "enclosure", "408.40"],
lookup_path: {
- index_keywords: ["Panelboards, grounding", "Panelboard enclosures, grounding"],
+ index_keywords: [
+ "Panelboards, grounding",
+ "Panelboard enclosures, grounding",
+ ],
index_entry: "Panelboards — grounding → 408.40",
article_or_table: "408.40",
- what_to_look_for: "Read 408.40 — enclosure must be grounded per Article 250; grounding terminal bonded to enclosure"
- }
+ what_to_look_for:
+ "Read 408.40 — enclosure must be grounded per Article 250; grounding terminal bonded to enclosure",
+ },
},
{
id: "408-003",
- question: "What is required on every circuit breaker or fuse in a panelboard?",
- choices: ["A red handle", "A legible directory", "A ground fault indicator", "A surge protector"],
+ question:
+ "What is required on every circuit breaker or fuse in a panelboard?",
+ choices: [
+ "A red handle",
+ "A legible directory",
+ "A ground fault indicator",
+ "A surge protector",
+ ],
correct_answer: "A legible directory",
- explanation: "NEC 408.4(A) requires that every circuit breaker and fuse be legibly identified as to its purpose or use, and a circuit directory must be provided on the face or inside the door of the panelboard.",
+ explanation:
+ "NEC 408.4(A) requires that every circuit breaker and fuse be legibly identified as to its purpose or use, and a circuit directory must be provided on the face or inside the door of the panelboard.",
nec_article: "408.4(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["panelboards", "circuit directory", "labeling", "408.4"],
lookup_path: {
- index_keywords: ["Panelboards, circuit directory", "Circuit identification, panelboard"],
+ index_keywords: [
+ "Panelboards, circuit directory",
+ "Circuit identification, panelboard",
+ ],
index_entry: "Panelboards — circuit identification → 408.4(A)",
article_or_table: "408.4(A)",
- what_to_look_for: "Read (A) — 'legibly identified as to its purpose or use' and directory on face or inside door"
- }
+ what_to_look_for:
+ "Read (A) — 'legibly identified as to its purpose or use' and directory on face or inside door",
+ },
},
// ─── GENERATORS ──────────────────────────────────────────────────────────────
@@ -942,80 +1325,114 @@ export const questionBank: QuestionCard[] = [
"An external fuse only",
"Inherent design, circuit breakers, fuses, or other acceptable overcurrent protective means",
"A manual disconnect only",
- "No protection required for generators under 5 kW"
+ "No protection required for generators under 5 kW",
],
- correct_answer: "Inherent design, circuit breakers, fuses, or other acceptable overcurrent protective means",
- explanation: "NEC 445.12(A) requires generators to be protected against overloads by inherent design, circuit breakers, fuses, or other acceptable overcurrent protective means.",
+ correct_answer:
+ "Inherent design, circuit breakers, fuses, or other acceptable overcurrent protective means",
+ explanation:
+ "NEC 445.12(A) requires generators to be protected against overloads by inherent design, circuit breakers, fuses, or other acceptable overcurrent protective means.",
nec_article: "445.12(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["generators", "overcurrent protection", "445.12"],
lookup_path: {
- index_keywords: ["Generators, overcurrent protection", "Generator protection"],
+ index_keywords: [
+ "Generators, overcurrent protection",
+ "Generator protection",
+ ],
index_entry: "Generators — overcurrent protection → 445.12",
article_or_table: "445.12(A)",
- what_to_look_for: "Read (A) — inherent design, circuit breakers, fuses, or other acceptable means"
- }
+ what_to_look_for:
+ "Read (A) — inherent design, circuit breakers, fuses, or other acceptable means",
+ },
},
{
id: "445-002",
- question: "What is the minimum conductor ampacity required for a generator with a nameplate current rating?",
- choices: ["100% of nameplate current", "115% of nameplate current", "125% of nameplate current", "150% of nameplate current"],
+ question:
+ "What is the minimum conductor ampacity required for a generator with a nameplate current rating?",
+ choices: [
+ "100% of nameplate current",
+ "115% of nameplate current",
+ "125% of nameplate current",
+ "150% of nameplate current",
+ ],
correct_answer: "115% of nameplate current",
- explanation: "NEC 445.13(A) requires generator output circuit conductors to have an ampacity of not less than 115% of the nameplate current rating of the generator.",
+ explanation:
+ "NEC 445.13(A) requires generator output circuit conductors to have an ampacity of not less than 115% of the nameplate current rating of the generator.",
nec_article: "445.13(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["generators", "conductor ampacity", "445.13", "nameplate"],
lookup_path: {
- index_keywords: ["Generators, conductor ampacity", "Generator output conductors"],
+ index_keywords: [
+ "Generators, conductor ampacity",
+ "Generator output conductors",
+ ],
index_entry: "Generators — ampacity of conductors → 445.13(A)",
article_or_table: "445.13(A)",
- what_to_look_for: "Read (A) — 'not less than 115 percent of the nameplate current rating'"
- }
+ what_to_look_for:
+ "Read (A) — 'not less than 115 percent of the nameplate current rating'",
+ },
},
{
id: "445-003",
- question: "A standby generator used as a separately derived system must have its neutral bonded to the generator frame and grounded per which article?",
+ question:
+ "A standby generator used as a separately derived system must have its neutral bonded to the generator frame and grounded per which article?",
choices: ["Article 230", "Article 250", "Article 445", "Article 700"],
correct_answer: "Article 250",
- explanation: "NEC 445.18 requires that a generator used as a separately derived system be grounded and bonded in accordance with Article 250.",
+ explanation:
+ "NEC 445.18 requires that a generator used as a separately derived system be grounded and bonded in accordance with Article 250.",
nec_article: "445.18",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["generators", "separately derived system", "grounding", "bonding"],
lookup_path: {
- index_keywords: ["Generators, separately derived system", "Separately derived systems, grounding"],
- index_entry: "Generators — separately derived systems → 445.18 → Article 250",
+ index_keywords: [
+ "Generators, separately derived system",
+ "Separately derived systems, grounding",
+ ],
+ index_entry:
+ "Generators — separately derived systems → 445.18 → Article 250",
article_or_table: "445.18",
- what_to_look_for: "Read 445.18 — references Article 250 for grounding and bonding requirements"
- }
+ what_to_look_for:
+ "Read 445.18 — references Article 250 for grounding and bonding requirements",
+ },
},
// ─── ADDITIONAL MIXED TOPICS ─────────────────────────────────────────────────
{
id: "300-001",
- question: "What is the minimum burial depth for rigid metal conduit (RMC) installed under a residential driveway?",
+ question:
+ "What is the minimum burial depth for rigid metal conduit (RMC) installed under a residential driveway?",
choices: ["6 inches", "12 inches", "18 inches", "24 inches"],
correct_answer: "6 inches",
- explanation: "Table 300.5 requires a minimum cover of 6 inches for rigid metal conduit installed under a residential driveway.",
+ explanation:
+ "Table 300.5 requires a minimum cover of 6 inches for rigid metal conduit installed under a residential driveway.",
nec_article: "Table 300.5",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["burial depth", "RMC", "driveway", "underground"],
lookup_path: {
- index_keywords: ["Underground wiring, burial depth", "Conduit, burial depth", "Cover requirements"],
- index_entry: "Underground installations — minimum cover → 300.5 → Table 300.5",
+ index_keywords: [
+ "Underground wiring, burial depth",
+ "Conduit, burial depth",
+ "Cover requirements",
+ ],
+ index_entry:
+ "Underground installations — minimum cover → 300.5 → Table 300.5",
article_or_table: "Table 300.5",
- what_to_look_for: "Find 'Rigid metal conduit' column → 'Under a residential driveway' row → 6 inches"
- }
+ what_to_look_for:
+ "Find 'Rigid metal conduit' column → 'Under a residential driveway' row → 6 inches",
+ },
},
{
id: "300-002",
- question: "What is the minimum burial depth for a direct-buried cable (UF cable) installed under a residential driveway?",
+ question:
+ "What is the minimum burial depth for a direct-buried cable (UF cable) installed under a residential driveway?",
choices: ["12 inches", "18 inches", "24 inches", "6 inches"],
correct_answer: "18 inches",
- explanation: "Table 300.5 requires a minimum cover of 18 inches for direct-buried cables installed under residential driveways.",
+ explanation:
+ "Table 300.5 requires a minimum cover of 18 inches for direct-buried cables installed under residential driveways.",
nec_article: "Table 300.5",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
@@ -1024,179 +1441,253 @@ export const questionBank: QuestionCard[] = [
index_keywords: ["Direct-buried cables, depth", "UF cable, burial depth"],
index_entry: "Underground installations — minimum cover → Table 300.5",
article_or_table: "Table 300.5",
- what_to_look_for: "Find 'Direct-buried cables' column → 'Under a residential driveway' row → 18 inches"
- }
+ what_to_look_for:
+ "Find 'Direct-buried cables' column → 'Under a residential driveway' row → 18 inches",
+ },
},
{
id: "334-001",
- question: "NM cable (Romex) is NOT permitted to be installed in which of the following locations?",
+ question:
+ "NM cable (Romex) is NOT permitted to be installed in which of the following locations?",
choices: [
"Inside a wood-frame wall of a dwelling unit",
"In a commercial building of Type III construction",
"In a building exceeding three floors above grade",
- "In an attic of a one-family dwelling"
+ "In an attic of a one-family dwelling",
],
correct_answer: "In a building exceeding three floors above grade",
- explanation: "NEC 334.10(A)(1) limits the use of NM cable to one- and two-family dwellings and their attached garages and storage areas, and to multifamily dwellings not exceeding three floors above grade.",
+ explanation:
+ "NEC 334.10(A)(1) limits the use of NM cable to one- and two-family dwellings and their attached garages and storage areas, and to multifamily dwellings not exceeding three floors above grade.",
nec_article: "334.10(A)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["NM cable", "Romex", "wiring methods", "334.10"],
lookup_path: {
- index_keywords: ["NM cable, permitted uses", "Nonmetallic-sheathed cable, uses permitted"],
+ index_keywords: [
+ "NM cable, permitted uses",
+ "Nonmetallic-sheathed cable, uses permitted",
+ ],
index_entry: "Nonmetallic-sheathed cable — uses permitted → 334.10",
article_or_table: "334.10(A)(1)",
- what_to_look_for: "Read (A)(1) — limited to structures not exceeding three floors above grade"
- }
+ what_to_look_for:
+ "Read (A)(1) — limited to structures not exceeding three floors above grade",
+ },
},
{
id: "410-002",
- question: "What is the minimum distance a recessed incandescent luminaire must be from thermal insulation unless listed for contact with insulation?",
+ question:
+ "What is the minimum distance a recessed incandescent luminaire must be from thermal insulation unless listed for contact with insulation?",
choices: ["1 inch", "2 inches", "3 inches", "6 inches"],
correct_answer: "3 inches",
- explanation: "NEC 410.116(A)(1) requires recessed luminaires that are not identified for contact with insulation to be installed with a minimum clearance of 3 inches from thermal insulation.",
+ explanation:
+ "NEC 410.116(A)(1) requires recessed luminaires that are not identified for contact with insulation to be installed with a minimum clearance of 3 inches from thermal insulation.",
nec_article: "410.116(A)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["luminaires", "recessed", "insulation clearance", "410.116"],
lookup_path: {
- index_keywords: ["Recessed luminaires, insulation clearance", "Luminaires, thermal insulation"],
- index_entry: "Recessed luminaires — clearance from insulation → 410.116(A)(1)",
+ index_keywords: [
+ "Recessed luminaires, insulation clearance",
+ "Luminaires, thermal insulation",
+ ],
+ index_entry:
+ "Recessed luminaires — clearance from insulation → 410.116(A)(1)",
article_or_table: "410.116(A)(1)",
- what_to_look_for: "Read (A)(1) — 'not less than 3 in. from thermal insulation' unless listed for contact"
- }
+ what_to_look_for:
+ "Read (A)(1) — 'not less than 3 in. from thermal insulation' unless listed for contact",
+ },
},
{
id: "406-001",
- question: "Tamper-resistant receptacles are required in which locations in a dwelling unit?",
+ question:
+ "Tamper-resistant receptacles are required in which locations in a dwelling unit?",
choices: [
"Only in children's bedrooms",
"All 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit",
"Only in kitchens and bathrooms",
- "Only in rooms accessible to children under 6"
+ "Only in rooms accessible to children under 6",
],
- correct_answer: "All 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit",
- explanation: "NEC 406.12(A) requires tamper-resistant receptacles for all 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit.",
+ correct_answer:
+ "All 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit",
+ explanation:
+ "NEC 406.12(A) requires tamper-resistant receptacles for all 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit.",
nec_article: "406.12(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["tamper-resistant", "receptacles", "dwelling", "406.12"],
lookup_path: {
- index_keywords: ["Tamper-resistant receptacles", "Receptacles, tamper-resistant"],
+ index_keywords: [
+ "Tamper-resistant receptacles",
+ "Receptacles, tamper-resistant",
+ ],
index_entry: "Tamper-resistant receptacles — dwelling units → 406.12(A)",
article_or_table: "406.12(A)",
- what_to_look_for: "Read (A) — 'all 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit'"
- }
+ what_to_look_for:
+ "Read (A) — 'all 125-volt, 15- and 20-ampere receptacles in all areas of a dwelling unit'",
+ },
},
{
id: "700-001",
- question: "Emergency lighting systems must be capable of providing illumination for a minimum of how long upon failure of normal power?",
+ question:
+ "Emergency lighting systems must be capable of providing illumination for a minimum of how long upon failure of normal power?",
choices: ["30 minutes", "60 minutes", "90 minutes", "120 minutes"],
correct_answer: "90 minutes",
- explanation: "NEC 700.12 requires emergency lighting systems to provide illumination for a minimum of 90 minutes upon failure of the normal power supply.",
+ explanation:
+ "NEC 700.12 requires emergency lighting systems to provide illumination for a minimum of 90 minutes upon failure of the normal power supply.",
nec_article: "700.12",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["emergency systems", "emergency lighting", "700.12", "90 minutes"],
lookup_path: {
- index_keywords: ["Emergency lighting, duration", "Emergency systems, battery backup"],
+ index_keywords: [
+ "Emergency lighting, duration",
+ "Emergency systems, battery backup",
+ ],
index_entry: "Emergency systems — illumination duration → 700.12",
article_or_table: "700.12",
- what_to_look_for: "Read 700.12 — 'capable of providing the required illumination for a minimum of 90 minutes'"
- }
+ what_to_look_for:
+ "Read 700.12 — 'capable of providing the required illumination for a minimum of 90 minutes'",
+ },
},
{
id: "517-001",
- question: "In a hospital operating room, what is the maximum voltage permitted between any two exposed conductive surfaces in a patient care vicinity?",
- choices: ["5 millivolts", "10 millivolts", "40 millivolts", "100 millivolts"],
+ question:
+ "In a hospital operating room, what is the maximum voltage permitted between any two exposed conductive surfaces in a patient care vicinity?",
+ choices: [
+ "5 millivolts",
+ "10 millivolts",
+ "40 millivolts",
+ "100 millivolts",
+ ],
correct_answer: "40 millivolts",
- explanation: "NEC 517.13(A) requires that the voltage between any two exposed conductive surfaces in a patient care vicinity not exceed 40 millivolts.",
+ explanation:
+ "NEC 517.13(A) requires that the voltage between any two exposed conductive surfaces in a patient care vicinity not exceed 40 millivolts.",
nec_article: "517.13(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["healthcare", "hospital", "patient care", "517.13"],
lookup_path: {
- index_keywords: ["Patient care vicinity, voltage", "Healthcare facilities, grounding"],
- index_entry: "Healthcare facilities — patient care vicinity, voltage → 517.13(A)",
+ index_keywords: [
+ "Patient care vicinity, voltage",
+ "Healthcare facilities, grounding",
+ ],
+ index_entry:
+ "Healthcare facilities — patient care vicinity, voltage → 517.13(A)",
article_or_table: "517.13(A)",
- what_to_look_for: "Read (A) — 'not exceed 40 millivolts' between any two exposed conductive surfaces"
- }
+ what_to_look_for:
+ "Read (A) — 'not exceed 40 millivolts' between any two exposed conductive surfaces",
+ },
},
{
id: "250-005",
- question: "What is the minimum size copper bonding jumper required to bond a metal water piping system in a building with a 200-ampere service?",
+ question:
+ "What is the minimum size copper bonding jumper required to bond a metal water piping system in a building with a 200-ampere service?",
choices: ["6 AWG", "4 AWG", "2 AWG", "1/0 AWG"],
correct_answer: "4 AWG",
- explanation: "Table 250.102(C)(1) requires a minimum 4 AWG copper bonding jumper for metal water piping systems when the service-entrance conductors are 3/0 AWG through 350 kcmil copper.",
+ explanation:
+ "Table 250.102(C)(1) requires a minimum 4 AWG copper bonding jumper for metal water piping systems when the service-entrance conductors are 3/0 AWG through 350 kcmil copper.",
nec_article: "Table 250.102(C)(1)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["grounding", "bonding", "water pipe", "bonding jumper"],
lookup_path: {
- index_keywords: ["Bonding jumpers, metal water pipe", "Water piping, bonding"],
- index_entry: "Bonding — metal water piping → 250.104(A) → Table 250.102(C)(1)",
+ index_keywords: [
+ "Bonding jumpers, metal water pipe",
+ "Water piping, bonding",
+ ],
+ index_entry:
+ "Bonding — metal water piping → 250.104(A) → Table 250.102(C)(1)",
article_or_table: "Table 250.102(C)(1)",
- what_to_look_for: "Find service conductor size range matching 200A service → read bonding jumper size column → 4 AWG copper"
- }
+ what_to_look_for:
+ "Find service conductor size range matching 200A service → read bonding jumper size column → 4 AWG copper",
+ },
},
{
id: "358-004",
- question: "What is the maximum number of bends (total degrees) permitted in a single run of conduit between pull points?",
+ question:
+ "What is the maximum number of bends (total degrees) permitted in a single run of conduit between pull points?",
choices: ["180 degrees", "270 degrees", "360 degrees", "No limit"],
correct_answer: "360 degrees",
- explanation: "NEC 358.26 (EMT) and similar articles for other conduit types limit the total bends between pull points or boxes to 360 degrees (the equivalent of four 90-degree bends).",
+ explanation:
+ "NEC 358.26 (EMT) and similar articles for other conduit types limit the total bends between pull points or boxes to 360 degrees (the equivalent of four 90-degree bends).",
nec_article: "358.26",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["conduit", "bends", "EMT", "358.26"],
lookup_path: {
- index_keywords: ["Conduit bends, maximum", "EMT, bends", "Bends, conduit"],
- index_entry: "Electrical metallic tubing — bends, number in one run → 358.26",
+ index_keywords: [
+ "Conduit bends, maximum",
+ "EMT, bends",
+ "Bends, conduit",
+ ],
+ index_entry:
+ "Electrical metallic tubing — bends, number in one run → 358.26",
article_or_table: "358.26",
- what_to_look_for: "Read 358.26 — 'not more than the equivalent of four quarter bends (360 degrees total)'"
- }
+ what_to_look_for:
+ "Read 358.26 — 'not more than the equivalent of four quarter bends (360 degrees total)'",
+ },
},
{
id: "210-009",
- question: "What is the maximum continuous load permitted on a 20-ampere branch circuit?",
+ question:
+ "What is the maximum continuous load permitted on a 20-ampere branch circuit?",
choices: ["16 amperes", "20 amperes", "24 amperes", "25 amperes"],
correct_answer: "16 amperes",
- explanation: "NEC 210.20(A) limits the continuous load on a branch circuit to 80% of the branch circuit rating, so a 20-ampere circuit is limited to 16 amperes of continuous load.",
+ explanation:
+ "NEC 210.20(A) limits the continuous load on a branch circuit to 80% of the branch circuit rating, so a 20-ampere circuit is limited to 16 amperes of continuous load.",
nec_article: "210.20(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "journeyman",
tags: ["branch circuit", "continuous load", "80%", "210.20"],
lookup_path: {
- index_keywords: ["Continuous loads, branch circuit", "Branch circuits, continuous load"],
+ index_keywords: [
+ "Continuous loads, branch circuit",
+ "Branch circuits, continuous load",
+ ],
index_entry: "Branch circuits — continuous loads → 210.20(A)",
article_or_table: "210.20(A)",
- what_to_look_for: "Read (A) — 'not exceed 80 percent of the branch-circuit ampere rating' for continuous loads"
- }
+ what_to_look_for:
+ "Read (A) — 'not exceed 80 percent of the branch-circuit ampere rating' for continuous loads",
+ },
},
{
id: "550-001",
- question: "What is the minimum size service-entrance conductor permitted for a mobile home with a calculated load of 16,000 VA at 120/240 volts?",
+ question:
+ "What is the minimum size service-entrance conductor permitted for a mobile home with a calculated load of 16,000 VA at 120/240 volts?",
choices: ["4 AWG", "2 AWG", "1/0 AWG", "2/0 AWG"],
correct_answer: "2 AWG",
- explanation: "A 16,000 VA load at 240V = 66.7A. Per Table 310.16, 2 AWG copper at 75°C = 95A, which is adequate. NEC 550.18(A) also sets minimums for mobile home feeder conductors.",
+ explanation:
+ "A 16,000 VA load at 240V = 66.7A. Per Table 310.16, 2 AWG copper at 75°C = 95A, which is adequate. NEC 550.18(A) also sets minimums for mobile home feeder conductors.",
nec_article: "550.18(A)",
nec_versions: ["2017", "2020", "2023", "2026"],
difficulty: "master",
tags: ["mobile homes", "service entrance", "load calculation", "550.18"],
lookup_path: {
- index_keywords: ["Mobile homes, service conductors", "Mobile homes, feeder"],
- index_entry: "Mobile homes — service equipment, feeder conductors → 550.18(A)",
+ index_keywords: [
+ "Mobile homes, service conductors",
+ "Mobile homes, feeder",
+ ],
+ index_entry:
+ "Mobile homes — service equipment, feeder conductors → 550.18(A)",
article_or_table: "550.18(A)",
- what_to_look_for: "Calculate load ÷ 240V = amperes → find conductor size in Table 310.16 at 75°C column"
- }
+ what_to_look_for:
+ "Calculate load ÷ 240V = amperes → find conductor size in Table 310.16 at 75°C column",
+ },
},
-
];
-export function getRandomQuestions(count: number, difficulty?: QuestionCard["difficulty"], version?: string): QuestionCard[] {
- let pool = difficulty ? questionBank.filter(q => q.difficulty === difficulty) : [...questionBank];
+export function getRandomQuestions(
+ count: number,
+ difficulty?: QuestionCard["difficulty"],
+ version?: string
+): QuestionCard[] {
+ let pool = difficulty
+ ? questionBank.filter(q => q.difficulty === difficulty)
+ : [...questionBank];
if (version && version !== "all") {
- pool = pool.filter(q => q.nec_versions.includes(version as "2017" | "2020" | "2023" | "2026"));
+ pool = pool.filter(q =>
+ q.nec_versions.includes(version as "2017" | "2020" | "2023" | "2026")
+ );
}
const shuffled = pool.sort(() => Math.random() - 0.5);
return shuffled.slice(0, Math.min(count, shuffled.length));
@@ -1205,11 +1696,12 @@ export function getRandomQuestions(count: number, difficulty?: QuestionCard["dif
// Helper: search questions
export function searchQuestions(query: string): QuestionCard[] {
const q = query.toLowerCase();
- return questionBank.filter(card =>
- card.question.toLowerCase().includes(q) ||
- card.nec_article.toLowerCase().includes(q) ||
- card.tags.some(t => t.toLowerCase().includes(q)) ||
- card.lookup_path.index_keywords.some(k => k.toLowerCase().includes(q)) ||
- card.explanation.toLowerCase().includes(q)
+ return questionBank.filter(
+ card =>
+ card.question.toLowerCase().includes(q) ||
+ card.nec_article.toLowerCase().includes(q) ||
+ card.tags.some(t => t.toLowerCase().includes(q)) ||
+ card.lookup_path.index_keywords.some(k => k.toLowerCase().includes(q)) ||
+ card.explanation.toLowerCase().includes(q)
);
}
diff --git a/client/src/hooks/useInAppBrowser.ts b/client/src/hooks/useInAppBrowser.ts
index d2ee6a4..cf2cca8 100644
--- a/client/src/hooks/useInAppBrowser.ts
+++ b/client/src/hooks/useInAppBrowser.ts
@@ -62,11 +62,22 @@ export function getInAppBrowserLabel(b: InAppBrowser): string {
/** True when running on an iOS device (Safari is the escape browser). */
export function isIOS(ua?: string): boolean {
- const agent = (ua ?? (typeof navigator !== "undefined" ? navigator.userAgent : "")).toLowerCase();
- return /iphone|ipad|ipod/.test(agent) || (/macintosh/.test(agent) && typeof navigator !== "undefined" && navigator.maxTouchPoints > 1);
+ const agent = (
+ ua ?? (typeof navigator !== "undefined" ? navigator.userAgent : "")
+ ).toLowerCase();
+ return (
+ /iphone|ipad|ipod/.test(agent) ||
+ (/macintosh/.test(agent) &&
+ typeof navigator !== "undefined" &&
+ navigator.maxTouchPoints > 1)
+ );
}
-export function useInAppBrowser(): { browser: InAppBrowser; isInApp: boolean; ios: boolean } {
+export function useInAppBrowser(): {
+ browser: InAppBrowser;
+ isInApp: boolean;
+ ios: boolean;
+} {
const [browser, setBrowser] = useState(null);
const [ios, setIos] = useState(false);
diff --git a/client/src/index.css b/client/src/index.css
index 0b41ed2..5a8a570 100644
--- a/client/src/index.css
+++ b/client/src/index.css
@@ -88,9 +88,9 @@
ThemeContext adds `.dark`; also drives all shadcn `dark:` utilities. */
--background: oklch(0 0 0);
--foreground: oklch(0.92 0.01 80);
- --card: oklch(0.10 0.005 250);
+ --card: oklch(0.1 0.005 250);
--card-foreground: oklch(0.92 0.01 80);
- --popover: oklch(0.10 0.005 250);
+ --popover: oklch(0.1 0.005 250);
--popover-foreground: oklch(0.92 0.01 80);
--primary: oklch(0.75 0.18 70);
--primary-foreground: oklch(0.1 0.01 70);
@@ -177,7 +177,9 @@
}
.dark .panel-card {
- box-shadow: inset 0 1px 0 oklch(0.3 0.01 250), 0 4px 12px oklch(0 0 0 / 40%);
+ box-shadow:
+ inset 0 1px 0 oklch(0.3 0.01 250),
+ 0 4px 12px oklch(0 0 0 / 40%);
}
.indicator-pulse {
@@ -185,8 +187,13 @@
}
@keyframes pulse-amber {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.5; }
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
}
.stencil-label {
diff --git a/client/src/main.tsx b/client/src/main.tsx
index 8adf6f5..056b491 100644
--- a/client/src/main.tsx
+++ b/client/src/main.tsx
@@ -1,5 +1,5 @@
import { trpc } from "@/lib/trpc";
-import { UNAUTHED_ERR_MSG } from '@shared/const';
+import { UNAUTHED_ERR_MSG } from "@shared/const";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink, TRPCClientError } from "@trpc/client";
import { createRoot } from "react-dom/client";
diff --git a/client/src/pages/CoPilot.tsx b/client/src/pages/CoPilot.tsx
index 1ccd628..1a758f4 100644
--- a/client/src/pages/CoPilot.tsx
+++ b/client/src/pages/CoPilot.tsx
@@ -48,7 +48,9 @@ export default function CoPilot() {
const [question, setQuestion] = useState("");
const [necVersion, setNecVersion] = useState("2026");
const [result, setResult] = useState(null);
- const [selectedDrilldownLevel, setSelectedDrilldownLevel] = useState(null);
+ const [selectedDrilldownLevel, setSelectedDrilldownLevel] = useState<
+ number | null
+ >(null);
const [clientId] = useState(() => getClientId());
const [limitReached, setLimitReached] = useState(false);
const textareaRef = useRef(null);
@@ -58,13 +60,13 @@ export default function CoPilot() {
const { data: usage } = trpc.copilot.usageStatus.useQuery({ clientId });
const analyze = trpc.copilot.analyze.useMutation({
- onSuccess: (data) => {
+ onSuccess: data => {
setResult(data);
setSelectedDrilldownLevel(null);
utils.copilot.usageStatus.invalidate({ clientId });
window.scrollTo({ top: 0, behavior: "smooth" });
},
- onError: (err) => {
+ onError: err => {
if (err.message?.includes("LIMIT_REACHED")) {
setLimitReached(true);
return;
@@ -114,10 +116,12 @@ export default function CoPilot() {
- You're out of free questions today
+
+ You're out of free questions today
+
- You get {usage?.limit ?? 3} free Co-Pilot questions per day. Upgrade to Pro for unlimited
- access to every NEC lookup.
+ You get {usage?.limit ?? 3} free Co-Pilot questions per day.
+ Upgrade to Pro for unlimited access to every NEC lookup.
navigate("/pricing")}
@@ -136,11 +140,13 @@ export default function CoPilot() {
)}
{/* Page heading + NEC version selector */}
-
Code Co-Pilot
+
+ Code Co-Pilot
+
NEC
- {NEC_VERSIONS.map((v) => (
+ {NEC_VERSIONS.map(v => (
setNecVersion(v)}
@@ -166,12 +172,14 @@ export default function CoPilot() {
- INPUT FEED
+
+ INPUT FEED
+
{/* Suggested Keywords */}
{suggestions.length > 0 && (
-
SUGGESTED KEYWORDS
+
+ SUGGESTED KEYWORDS
+
{suggestions.map((s, i) => (
-
READY FOR NAVIGATION INPUT
+
+ READY FOR NAVIGATION INPUT
+
>
) : (
@@ -254,7 +275,9 @@ export default function CoPilot() {
- NEC Index Lookup Path
+
+ NEC Index Lookup Path
+
{result.indexDrilldown.length > 0 ? (
@@ -264,7 +287,9 @@ export default function CoPilot() {
setSelectedDrilldownLevel(
- selectedDrilldownLevel === level.level ? null : level.level
+ selectedDrilldownLevel === level.level
+ ? null
+ : level.level
)
}
className={`w-full text-left px-4 py-3 rounded-lg border transition-all flex items-center justify-between ${
@@ -278,11 +303,15 @@ export default function CoPilot() {
Step {level.level}
{level.entry}
- {level.description}
+
+ {level.description}
+
@@ -301,9 +330,13 @@ export default function CoPilot() {
- Article Citation
+
+ Article Citation
+
+
+
+ {result.article}
-
{result.article}
{result.articleTitle}
@@ -311,9 +344,13 @@ export default function CoPilot() {
- Answer
+
+ Answer
+
-
{result.answer}
+
+ {result.answer}
+
{/* Exam Tip */}
@@ -321,9 +358,13 @@ export default function CoPilot() {
- Exam Tip
+
+ Exam Tip
+
-
{result.examTip}
+
+ {result.examTip}
+
)}
@@ -341,7 +382,6 @@ export default function CoPilot() {
>
)}
-
);
}
diff --git a/client/src/pages/ComponentShowcase.tsx b/client/src/pages/ComponentShowcase.tsx
index b7725c2..c619b03 100644
--- a/client/src/pages/ComponentShowcase.tsx
+++ b/client/src/pages/ComponentShowcase.tsx
@@ -1028,7 +1028,8 @@ export default function ComponentsShowcase() {
Test Input
- Enter some text below. Press Enter to submit (IME composition supported).
+ Enter some text below. Press Enter to submit (IME
+ composition supported).
@@ -1038,7 +1039,7 @@ export default function ComponentsShowcase() {
id="dialog-input"
placeholder="Type something..."
value={dialogInput}
- onChange={(e) => setDialogInput(e.target.value)}
+ onChange={e => setDialogInput(e.target.value)}
onKeyDown={handleDialogKeyDown}
autoFocus
/>
@@ -1399,11 +1400,13 @@ export default function ComponentsShowcase() {
- A ready-to-use chat interface component that integrates with the LLM system.
- Features markdown rendering, auto-scrolling, and loading states.
+ A ready-to-use chat interface component that integrates
+ with the LLM system. Features markdown rendering,
+ auto-scrolling, and loading states.
- This is a demo with simulated responses. In a real app, you'd connect it to a tRPC mutation.
+ This is a demo with simulated responses. In a real app,
+ you'd connect it to a tRPC mutation.
- Find any NEC answer fast.
+ Find any NEC
+
+ answer fast.
@@ -74,8 +69,8 @@ export default function Dashboard() {
- The exact method journeyman and master electricians use —
- on the exam and on the job.
+ The exact method journeyman and master electricians use — on the
+ exam and on the job.
{/* Single primary CTA */}
@@ -126,18 +121,41 @@ export default function Dashboard() {
{(
[
- { step: "01", label: "Question", note: "Read what's actually being asked." },
- { step: "02", label: "Index Keyword", note: "Spot the word to look up." },
- { step: "03", label: "Article / Table", note: "Navigate straight to it." },
- { step: "04", label: "Answer", note: "Pull the answer from the book." },
+ {
+ step: "01",
+ label: "Question",
+ note: "Read what's actually being asked.",
+ },
+ {
+ step: "02",
+ label: "Index Keyword",
+ note: "Spot the word to look up.",
+ },
+ {
+ step: "03",
+ label: "Article / Table",
+ note: "Navigate straight to it.",
+ },
+ {
+ step: "04",
+ label: "Answer",
+ note: "Pull the answer from the book.",
+ },
] as const
).map(({ step, label, note }) => (
-
+
{step}
-
{label}
-
{note}
+
+ {label}
+
+
+ {note}
+
))}
@@ -155,13 +173,17 @@ export default function Dashboard() {
-
AI Co-Pilot
+
+ AI Co-Pilot
+
- Paste any NEC question. The Co-Pilot walks you through the index keyword,
- the article, and the answer — so you learn to find it yourself next time.
+ Paste any NEC question. The Co-Pilot walks you through the
+ index keyword, the article, and the answer — so you learn to
+ find it yourself next time.
- 3 FREE questions/day · Unlimited with Lifetime Access ($39.99)
+ 3 FREE questions/day · Unlimited with Lifetime Access
+ ($39.99)
@@ -192,8 +214,12 @@ export default function Dashboard() {
LIFETIME
-
{title}
-
{subtitle}
+
+ {title}
+
+
+ {subtitle}
+
Get access
@@ -208,7 +234,9 @@ export default function Dashboard() {
Get Lifetime Access — $39.99
-
One-time payment. No subscription. Ever.
+
+ One-time payment. No subscription. Ever.
+
@@ -218,7 +246,9 @@ export default function Dashboard() {
-
Don't own the code book yet?
+
+ Don't own the code book yet?
+
NFPA offers free read-only access to NFPA 70 (NEC) online.{" "}
- Unofficial study tool — not affiliated with NFPA. Always verify answers with your code book.
+ Unofficial study tool — not affiliated with NFPA. Always verify
+ answers with your code book.
diff --git a/client/src/pages/ExamMode.tsx b/client/src/pages/ExamMode.tsx
index 00ce0a3..16a5529 100644
--- a/client/src/pages/ExamMode.tsx
+++ b/client/src/pages/ExamMode.tsx
@@ -35,7 +35,10 @@ function ExamContent() {
if (state !== "active" || timeLeft <= 0) return;
const interval = setInterval(() => {
setTimeLeft(t => {
- if (t <= 1) { setState("review"); return 0; }
+ if (t <= 1) {
+ setState("review");
+ return 0;
+ }
return t - 1;
});
}, 1000);
@@ -55,8 +58,10 @@ function ExamContent() {
}
};
- const score = questions.reduce((acc, q) =>
- acc + (answers[q.id] === q.correct_answer ? 1 : 0), 0);
+ const score = questions.reduce(
+ (acc, q) => acc + (answers[q.id] === q.correct_answer ? 1 : 0),
+ 0
+ );
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
@@ -70,9 +75,12 @@ function ExamContent() {
EXAM MODE
-
Timed Practice Exam
+
+ Timed Practice Exam
+
- Simulate exam conditions. Timer starts immediately. Each question shows rationale and NEC reference after you answer.
+ Simulate exam conditions. Timer starts immediately. Each question
+ shows rationale and NEC reference after you answer.
@@ -89,8 +97,12 @@ function ExamContent() {
-
{label}
-
{count} questions • {time}
+
+ {label}
+
+
+ {count} questions • {time}
+
START →
@@ -108,11 +120,17 @@ function ExamContent() {
EXAM COMPLETE
-
+
{percentage}%
-
{score} / {questions.length} correct
-
+
+ {score} / {questions.length} correct
+
+
{passed ? "PASS" : "NEEDS REVIEW"}
@@ -123,17 +141,31 @@ function ExamContent() {
return (
- {correct
- ?
- :
}
+ {correct ? (
+
+ ) : (
+
+ )}
-
{i + 1}. {q.question}
- {!correct &&
Your answer: {ua || "No answer"}
}
-
Correct: {q.correct_answer}
-
{q.explanation}
+
+ {i + 1}. {q.question}
+
+ {!correct && (
+
+ Your answer: {ua || "No answer"}
+
+ )}
+
+ Correct: {q.correct_answer}
+
+
+ {q.explanation}
+
- {q.nec_article}
+
+ {q.nec_article}
+
@@ -146,7 +178,9 @@ function ExamContent() {
className="w-full panel-card p-4 rounded-sm flex items-center justify-center gap-2 hover:border-primary/40 transition-all"
>
-
NEW EXAM
+
+ NEW EXAM
+
);
@@ -162,14 +196,25 @@ function ExamContent() {
- {formatTime(timeLeft)}
+
+ {formatTime(timeLeft)}
+
-
{currentIndex + 1} / {questions.length}
+
+ {currentIndex + 1} / {questions.length}
+
{questions.map((q, i) => (
-
+
))}
@@ -177,11 +222,19 @@ function ExamContent() {
QUESTION {currentIndex + 1}
- {currentQ.difficulty}
+
+ {currentQ.difficulty}
+
-
{currentQ.question}
+
+ {currentQ.question}
+
@@ -193,24 +246,43 @@ function ExamContent() {
return (
{ if (!hasAnswered) { selectAnswer(currentQ.id, choice); setShowRationale(true); } }}
+ onClick={() => {
+ if (!hasAnswered) {
+ selectAnswer(currentQ.id, choice);
+ setShowRationale(true);
+ }
+ }}
disabled={hasAnswered}
className={`w-full panel-card p-4 rounded-sm flex items-center gap-3 transition-all text-left ${
- showResult && isCorrect ? "border-green-400/60 bg-green-400/5"
- : showResult && selected && !isCorrect ? "border-destructive/60 bg-destructive/5"
- : selected ? "border-primary/60 bg-primary/5"
- : "hover:border-primary/30"
+ showResult && isCorrect
+ ? "border-green-400/60 bg-green-400/5"
+ : showResult && selected && !isCorrect
+ ? "border-destructive/60 bg-destructive/5"
+ : selected
+ ? "border-primary/60 bg-primary/5"
+ : "hover:border-primary/30"
}`}
>
- {letter}
+
+ {letter}
+
{choice}
- {showResult && isCorrect && }
- {showResult && selected && !isCorrect && }
+ {showResult && isCorrect && (
+
+ )}
+ {showResult && selected && !isCorrect && (
+
+ )}
);
})}
@@ -222,10 +294,16 @@ function ExamContent() {
- {currentQ.nec_article}
+
+ {currentQ.nec_article}
+
-
{currentQ.explanation}
-
Open your code book to verify this reference.
+
+ {currentQ.explanation}
+
+
+ Open your code book to verify this reference.
+
)}
@@ -235,7 +313,9 @@ function ExamContent() {
onClick={nextQuestion}
className="w-full bg-primary text-primary-foreground py-3 rounded-sm font-mono text-sm font-bold uppercase tracking-wider hover:brightness-110 transition-all"
>
- {currentIndex < questions.length - 1 ? "NEXT QUESTION →" : "FINISH EXAM"}
+ {currentIndex < questions.length - 1
+ ? "NEXT QUESTION →"
+ : "FINISH EXAM"}
)}
diff --git a/client/src/pages/Home.tsx b/client/src/pages/Home.tsx
index 8b4b79b..c8f09e5 100644
--- a/client/src/pages/Home.tsx
+++ b/client/src/pages/Home.tsx
@@ -2,7 +2,7 @@ import { useAuth } from "@/_core/hooks/useAuth";
import { Button } from "@/components/ui/button";
import { Loader2 } from "lucide-react";
import { getLoginUrl } from "@/const";
-import { Streamdown } from 'streamdown';
+import { Streamdown } from "streamdown";
/**
* All content in this page are only for example, replace with your own feature implementation
diff --git a/client/src/pages/Onboarding.tsx b/client/src/pages/Onboarding.tsx
index bb19cf4..1604d27 100644
--- a/client/src/pages/Onboarding.tsx
+++ b/client/src/pages/Onboarding.tsx
@@ -103,7 +103,7 @@ function HighlightedText({
return (
{parts.map((part, i) =>
- highlights.some((h) => h.toLowerCase() === part.toLowerCase()) ? (
+ highlights.some(h => h.toLowerCase() === part.toLowerCase()) ? (
-
+
SAMPLE QUESTION
-
+
Motor / Branch Circuit
@@ -244,8 +250,8 @@ function Step2() {
isTerm
? "text-amber-400 font-bold cursor-pointer hover:bg-amber-400/10 px-1 rounded"
: isSubEntry
- ? "text-muted-foreground pl-4"
- : "text-foreground/70"
+ ? "text-muted-foreground pl-4"
+ : "text-foreground/70"
}`}
onClick={isTerm ? () => setSelected(line) : undefined}
>
@@ -295,11 +301,9 @@ function Step3() {
You have your article number:{" "}
-
- 210.19(A)
-
- . Now flip to it. Articles are numbered sequentially — use the
- running header at the top of each page to navigate fast.
+ 210.19(A)
+ . Now flip to it. Articles are numbered sequentially — use the running
+ header at the top of each page to navigate fast.
@@ -309,9 +313,7 @@ function Step3() {
NEC 2026 — Article 210 · Branch Circuits
-
- Page ~70
-
+ Page ~70
@@ -361,7 +363,9 @@ function Step3() {
)}
- (B) Household Ranges and Cooking Appliances
+
+ (B) Household Ranges and Cooking Appliances
+
(C) Household Dryers
@@ -396,9 +400,9 @@ function Step4({ onComplete }: { onComplete: () => void }) {
Select the Answer. Cite the Article.
- You've navigated to the article. You've found the rule. Now select
- the answer — and always write the article number next to it. That's
- your proof.
+ You've navigated to the article. You've found the rule. Now select the
+ answer — and always write the article number next to it. That's your
+ proof.
@@ -412,7 +416,7 @@ function Step4({ onComplete }: { onComplete: () => void }) {
{/* Answer choices */}
- {choices.map((choice) => {
+ {choices.map(choice => {
const isSelected = selected === choice;
const isCorrect = choice === correct;
const showResult = selected !== null;
@@ -426,8 +430,8 @@ function Step4({ onComplete }: { onComplete: () => void }) {
? isCorrect
? "border-green-400 bg-green-400/15 text-green-400"
: isSelected
- ? "border-red-400 bg-red-400/15 text-red-400"
- : "border-border text-muted-foreground opacity-50"
+ ? "border-red-400 bg-red-400/15 text-red-400"
+ : "border-border text-muted-foreground opacity-50"
: "border-border hover:border-amber-400/50 hover:bg-amber-400/5 text-foreground cursor-pointer"
}`}
>
@@ -450,7 +454,10 @@ function Step4({ onComplete }: { onComplete: () => void }) {
{SAMPLE_QUESTION.answerExplanation}
-
+
Citation: Article 210.19(A)(1)
@@ -545,8 +552,8 @@ export default function Onboarding() {
isActive
? `${step.bgColor} ${step.color} border ${step.borderColor}`
: isDone
- ? "text-green-400 opacity-70"
- : "text-muted-foreground opacity-40"
+ ? "text-green-400 opacity-70"
+ : "text-muted-foreground opacity-40"
}`}
>
@@ -577,7 +584,7 @@ export default function Onboarding() {
Step {currentStep} of 4
setCurrentStep((s) => s + 1)}
+ onClick={() => setCurrentStep(s => s + 1)}
className="bg-amber-500 hover:bg-amber-400 text-black font-bold"
>
Next Step
diff --git a/client/src/pages/PaymentCancel.tsx b/client/src/pages/PaymentCancel.tsx
index 6cb552c..4b7804b 100644
--- a/client/src/pages/PaymentCancel.tsx
+++ b/client/src/pages/PaymentCancel.tsx
@@ -15,7 +15,9 @@ export default function PaymentCancel() {
CHECKOUT CANCELED
-
No charge made.
+
+ No charge made.
+
You can upgrade anytime. Your free access is still active.
diff --git a/client/src/pages/PaymentSuccess.tsx b/client/src/pages/PaymentSuccess.tsx
index f008f03..a6e7898 100644
--- a/client/src/pages/PaymentSuccess.tsx
+++ b/client/src/pages/PaymentSuccess.tsx
@@ -23,9 +23,12 @@ export default function PaymentSuccess() {
PAYMENT CONFIRMED
-
You're in.
+
+ You're in.
+
- Your Pro access is active. All 65+ NEC training cards and Exam Mode are unlocked.
+ Your Pro access is active. All 65+ NEC training cards and Exam
+ Mode are unlocked.
diff --git a/client/src/pages/Pricing.tsx b/client/src/pages/Pricing.tsx
index a145cd0..38bd8e8 100644
--- a/client/src/pages/Pricing.tsx
+++ b/client/src/pages/Pricing.tsx
@@ -21,19 +21,22 @@ export default function Pricing() {
const [loadingPlan, setLoadingPlan] = useState
(null);
const { data: plans = [] } = trpc.plans.list.useQuery();
- const { data: subStatus } = trpc.stripe.subscriptionStatus.useQuery(undefined, {
- enabled: isAuthenticated,
- });
+ const { data: subStatus } = trpc.stripe.subscriptionStatus.useQuery(
+ undefined,
+ {
+ enabled: isAuthenticated,
+ }
+ );
const createCheckout = trpc.stripe.createCheckoutSession.useMutation({
- onSuccess: (data) => {
+ onSuccess: data => {
if (data.url) {
toast.info("Redirecting to checkout...");
window.open(data.url, "_blank");
}
setLoadingPlan(null);
},
- onError: (err) => {
+ onError: err => {
toast.error(err.message || "Checkout failed. Please try again.");
setLoadingPlan(null);
},
@@ -49,12 +52,16 @@ export default function Pricing() {
};
const isCurrentPlan = (planId: string) => {
- if (planId === "free" && (!subStatus?.status || subStatus.status === "none")) return true;
+ if (
+ planId === "free" &&
+ (!subStatus?.status || subStatus.status === "none")
+ )
+ return true;
return subStatus?.plan === planId && subStatus.status === "active";
};
- const freePlan = plans.find((p) => p.id === "free");
- const lifetimePlan = plans.find((p) => p.id === "lifetime");
+ const freePlan = plans.find(p => p.id === "free");
+ const lifetimePlan = plans.find(p => p.id === "lifetime");
return (
@@ -70,8 +77,8 @@ export default function Pricing() {
Stop guessing. Start finding.
- Code Compass teaches you to find any NEC answer in under 60 seconds —
- the same skill journeyman and master electricians use on the job.
+ Code Compass teaches you to find any NEC answer in under 60 seconds
+ — the same skill journeyman and master electricians use on the job.
@@ -89,7 +96,9 @@ export default function Pricing() {
$0
- forever
+
+ forever
+
@@ -97,7 +106,7 @@ export default function Pricing() {
- {freePlan.features.map((f) => (
+ {freePlan.features.map(f => (
{f}
@@ -106,7 +115,11 @@ export default function Pricing() {
{isCurrentPlan("free") ? (
-
+
CURRENT PLAN
) : (
@@ -138,17 +151,23 @@ export default function Pricing() {
- $39.99
- one-time
+
+ $39.99
+
+
+ one-time
+
-
NO RECURRING FEES. EVER.
+
+ NO RECURRING FEES. EVER.
+
{lifetimePlan.description}
- {lifetimePlan.features.map((f) => (
+ {lifetimePlan.features.map(f => (
{f}
@@ -157,7 +176,11 @@ export default function Pricing() {
{isCurrentPlan("lifetime") ? (
-
+
YOU HAVE LIFETIME ACCESS
) : (
@@ -166,7 +189,9 @@ export default function Pricing() {
disabled={loadingPlan === "lifetime"}
onClick={() => handleCheckout("lifetime")}
>
- {loadingPlan === "lifetime" ? "LOADING..." : "GET LIFETIME ACCESS — $39.99"}
+ {loadingPlan === "lifetime"
+ ? "LOADING..."
+ : "GET LIFETIME ACCESS — $39.99"}
)}
@@ -176,14 +201,18 @@ export default function Pricing() {
{/* Trust signal */}
- Instant access after payment. No subscription. Cancel nothing.
+
+ Instant access after payment. No subscription. Cancel nothing.
+
{/* Free NEC Access Banner */}
)}
@@ -237,7 +267,10 @@ export default function Pricing() {
a: "If it does not help you find NEC answers faster, email support within 30 days for a full refund. No questions asked.",
},
].map(({ q, a }) => (
-
+
diff --git a/client/src/pages/QuizMode.tsx b/client/src/pages/QuizMode.tsx
index ee5538f..0ec0aed 100644
--- a/client/src/pages/QuizMode.tsx
+++ b/client/src/pages/QuizMode.tsx
@@ -6,7 +6,14 @@ import { useState, useCallback } from "react";
import AppLayout from "@/components/AppLayout";
import { PaywallGate } from "@/components/PaywallGate";
import { getRandomQuestions, type QuestionCard } from "@/data/questionBank";
-import { Zap, CheckCircle, XCircle, BookOpen, RotateCcw, ArrowRight } from "lucide-react";
+import {
+ Zap,
+ CheckCircle,
+ XCircle,
+ BookOpen,
+ RotateCcw,
+ ArrowRight,
+} from "lucide-react";
import BackButton from "@/components/BackButton";
import LookupGuide from "@/components/LookupGuide";
@@ -19,14 +26,17 @@ function QuizContent() {
const [answers, setAnswers] = useState
>({});
const [revealed, setRevealed] = useState(false);
- const startQuiz = useCallback((count: number, difficulty?: QuestionCard["difficulty"]) => {
- const q = getRandomQuestions(count, difficulty);
- setQuestions(q);
- setAnswers({});
- setCurrentIndex(0);
- setRevealed(false);
- setState("active");
- }, []);
+ const startQuiz = useCallback(
+ (count: number, difficulty?: QuestionCard["difficulty"]) => {
+ const q = getRandomQuestions(count, difficulty);
+ setQuestions(q);
+ setAnswers({});
+ setCurrentIndex(0);
+ setRevealed(false);
+ setState("active");
+ },
+ []
+ );
const selectAnswer = (questionId: string, answer: string) => {
if (revealed) return;
@@ -43,7 +53,10 @@ function QuizContent() {
}
};
- const score = questions.reduce((acc, q) => acc + (answers[q.id] === q.correct_answer ? 1 : 0), 0);
+ const score = questions.reduce(
+ (acc, q) => acc + (answers[q.id] === q.correct_answer ? 1 : 0),
+ 0
+ );
if (state === "setup") {
return (
@@ -51,9 +64,12 @@ function QuizContent() {
QUICK DRILL
-
Rapid-Fire Practice
+
+ Rapid-Fire Practice
+
- No timer. Instant feedback. Perfect for building muscle memory on NEC lookups.
+ No timer. Instant feedback. Perfect for building muscle memory on
+ NEC lookups.
@@ -65,7 +81,9 @@ function QuizContent() {
onClick={() => startQuiz(count)}
className="panel-card p-4 rounded-sm text-center hover:border-primary/40 transition-all"
>
-
{count}
+
+ {count}
+
QUESTIONS
))}
@@ -81,7 +99,9 @@ function QuizContent() {
className="panel-card p-4 rounded-sm text-center hover:border-primary/40 transition-all"
>
-
{diff}
+
+ {diff}
+
5 QUESTIONS
))}
@@ -98,8 +118,12 @@ function QuizContent() {
DRILL COMPLETE
-
{score}/{questions.length}
-
{percentage}% accuracy
+
+ {score}/{questions.length}
+
+
+ {percentage}% accuracy
+
startQuiz(questions.length)}
@@ -116,22 +140,35 @@ function QuizContent() {
- {questions.filter(q => answers[q.id] !== q.correct_answer).length > 0 && (
+ {questions.filter(q => answers[q.id] !== q.correct_answer).length >
+ 0 && (
REVIEW MISSED
- {questions.filter(q => answers[q.id] !== q.correct_answer).map(q => (
-
-
{q.question}
-
✓ {q.correct_answer}
-
-
-
{q.nec_article}
-
· index:
-
{q.lookup_path.index_keywords[0]}
+ {questions
+ .filter(q => answers[q.id] !== q.correct_answer)
+ .map(q => (
+
+
+ {q.question}
+
+
+ ✓ {q.correct_answer}
+
+
+
+
+ {q.nec_article}
+
+
+ · index:
+
+
+ {q.lookup_path.index_keywords[0]}
+
+
-
- ))}
+ ))}
)}
@@ -146,23 +183,37 @@ function QuizContent() {
return (
- DRILL {currentIndex + 1} OF {questions.length}
- {score} correct
+
+ DRILL {currentIndex + 1} OF {questions.length}
+
+
+ {score} correct
+
- {currentQ.difficulty}
- {currentQ.nec_article}
+
+ {currentQ.difficulty}
+
+
+ {currentQ.nec_article}
+
-
{currentQ.question}
+
+ {currentQ.question}
+
{currentQ.choices.map((choice, i) => {
@@ -175,19 +226,31 @@ function QuizContent() {
onClick={() => selectAnswer(currentQ.id, choice)}
disabled={revealed}
className={`w-full panel-card p-4 rounded-sm flex items-center gap-3 transition-all text-left ${
- revealed && isCorrect ? "border-green-400/60 bg-green-400/5"
- : revealed && selected && !isCorrect ? "border-destructive/60 bg-destructive/5"
- : "hover:border-primary/30"
+ revealed && isCorrect
+ ? "border-green-400/60 bg-green-400/5"
+ : revealed && selected && !isCorrect
+ ? "border-destructive/60 bg-destructive/5"
+ : "hover:border-primary/30"
}`}
>
-
{letter}
+
+ {letter}
+
{choice}
- {revealed && isCorrect &&
}
- {revealed && selected && !isCorrect &&
}
+ {revealed && isCorrect && (
+
+ )}
+ {revealed && selected && !isCorrect && (
+
+ )}
);
})}
@@ -196,7 +259,9 @@ function QuizContent() {
<>
-
{currentQ.explanation}
+
+ {currentQ.explanation}
+
Open your code book to {currentQ.nec_article} to verify.
diff --git a/client/src/pages/SearchMode.tsx b/client/src/pages/SearchMode.tsx
index 080fc82..6d4f4e2 100644
--- a/client/src/pages/SearchMode.tsx
+++ b/client/src/pages/SearchMode.tsx
@@ -4,14 +4,20 @@
*/
import { useState, useMemo } from "react";
import AppLayout from "@/components/AppLayout";
-import { searchQuestions, questionBank, type QuestionCard } from "@/data/questionBank";
+import {
+ searchQuestions,
+ questionBank,
+ type QuestionCard,
+} from "@/data/questionBank";
import { Search, BookOpen, Tag, Filter } from "lucide-react";
import BackButton from "@/components/BackButton";
import { PaywallGate } from "@/components/PaywallGate";
export default function SearchMode() {
const [query, setQuery] = useState("");
- const [filterDifficulty, setFilterDifficulty] = useState<"all" | "journeyman" | "master">("all");
+ const [filterDifficulty, setFilterDifficulty] = useState<
+ "all" | "journeyman" | "master"
+ >("all");
const results = useMemo(() => {
let items: QuestionCard[];
@@ -35,138 +41,161 @@ export default function SearchMode() {
return (
-
-
- {/* Header */}
-
-
CODE LOOKUP
-
Search the NEC
-
- Search by article number, keyword, or topic. Get plain-English summaries with page references.
-
-
+
+
+ {/* Header */}
+
+
CODE LOOKUP
+
+ Search the NEC
+
+
+ Search by article number, keyword, or topic. Get plain-English
+ summaries with page references.
+
+
- {/* Search Bar */}
-
-
- setQuery(e.target.value)}
- placeholder="Search by article (e.g. 310.16), keyword (e.g. ampacity), or topic..."
- className="flex-1 bg-transparent py-3 px-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none font-mono"
- />
- {query && (
- setQuery("")}
- className="mr-3 text-xs text-muted-foreground hover:text-foreground font-mono"
- >
- CLEAR
-
- )}
-
+ {/* Search Bar */}
+
+
+ setQuery(e.target.value)}
+ placeholder="Search by article (e.g. 310.16), keyword (e.g. ampacity), or topic..."
+ className="flex-1 bg-transparent py-3 px-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none font-mono"
+ />
+ {query && (
+ setQuery("")}
+ className="mr-3 text-xs text-muted-foreground hover:text-foreground font-mono"
+ >
+ CLEAR
+
+ )}
+
- {/* Filters */}
-
-
- {(["all", "journeyman", "master"] as const).map(level => (
- setFilterDifficulty(level)}
- className={`px-3 py-1.5 rounded-sm font-mono text-xs font-bold uppercase tracking-wider transition-all ${
- filterDifficulty === level
- ? "bg-primary/20 text-primary border border-primary/40"
- : "bg-secondary text-muted-foreground hover:text-foreground"
- }`}
- >
- {level}
-
- ))}
-
- {results.length} result{results.length !== 1 ? "s" : ""}
-
-
+ {/* Filters */}
+
+
+ {(["all", "journeyman", "master"] as const).map(level => (
+ setFilterDifficulty(level)}
+ className={`px-3 py-1.5 rounded-sm font-mono text-xs font-bold uppercase tracking-wider transition-all ${
+ filterDifficulty === level
+ ? "bg-primary/20 text-primary border border-primary/40"
+ : "bg-secondary text-muted-foreground hover:text-foreground"
+ }`}
+ >
+ {level}
+
+ ))}
+
+ {results.length} result{results.length !== 1 ? "s" : ""}
+
+
- {/* Quick Tags */}
-
- {allTags.slice(0, 12).map(tag => (
- setQuery(tag)}
- className={`flex items-center gap-1 px-2 py-1 rounded-sm text-xs font-mono transition-all ${
- query === tag
- ? "bg-primary/20 text-primary border border-primary/40"
- : "bg-secondary text-muted-foreground hover:text-foreground"
- }`}
- >
-
- {tag}
-
- ))}
-
+ {/* Quick Tags */}
+
+ {allTags.slice(0, 12).map(tag => (
+ setQuery(tag)}
+ className={`flex items-center gap-1 px-2 py-1 rounded-sm text-xs font-mono transition-all ${
+ query === tag
+ ? "bg-primary/20 text-primary border border-primary/40"
+ : "bg-secondary text-muted-foreground hover:text-foreground"
+ }`}
+ >
+
+ {tag}
+
+ ))}
+
- {/* Results */}
-
- {results.map(q => (
-
-
-
-
-
- {q.nec_article}
-
- {q.difficulty}
-
-
-
- {q.question}
-
-
- → {q.explanation}
-
-
-
Index:
-
- {q.lookup_path.index_keywords.map(kw => (
+ {/* Results */}
+
+ {results.map(q => (
+
+
+
+
+
+
+ {q.nec_article}
+
+
+ {q.difficulty}
+
+
+
+ {q.question}
+
+
+
+ →
+ {" "}
+ {q.explanation}
+
+
+
+ Index:
+
+
+ {q.lookup_path.index_keywords.map(kw => (
+
+ {kw}
+
+ ))}
+
+
+
+ {q.tags.map(tag => (
- {kw}
+ {tag}
))}
-
- {q.tags.map(tag => (
-
- {tag}
-
- ))}
+
+
+ {q.correct_answer}
+
+
ANSWER
-
-
{q.correct_answer}
-
ANSWER
-
-
- ))}
+ ))}
- {results.length === 0 && (
-
-
-
No results found for "{query}"
-
Try a different keyword or article number.
-
- )}
+ {results.length === 0 && (
+
+
+
+ No results found for "{query}"
+
+
+ Try a different keyword or article number.
+
+
+ )}
+
-
);
diff --git a/codecompass-beta/src/components/ThemeToggle.tsx b/codecompass-beta/src/components/ThemeToggle.tsx
index e8e73c2..c68d9a4 100644
--- a/codecompass-beta/src/components/ThemeToggle.tsx
+++ b/codecompass-beta/src/components/ThemeToggle.tsx
@@ -30,11 +30,7 @@ export function ThemeToggle() {
aria-label="Toggle theme"
className="h-9 w-9"
>
- {theme === "dark" ? (
-
- ) : (
-
- )}
+ {theme === "dark" ?
:
}
);
}
diff --git a/codecompass-beta/src/lib/l5x-parser.ts b/codecompass-beta/src/lib/l5x-parser.ts
index e78922e..358387b 100644
--- a/codecompass-beta/src/lib/l5x-parser.ts
+++ b/codecompass-beta/src/lib/l5x-parser.ts
@@ -219,7 +219,9 @@ function assignCoordinates(
export function parseL5X(fileBuffer: Buffer): L5XParseResult {
// CRITICAL: Buffer size validation (prevent memory exhaustion)
if (fileBuffer.length > 10 * 1024 * 1024) {
- throw new Error(`L5X file too large: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB exceeds 10 MB limit`);
+ throw new Error(
+ `L5X file too large: ${(fileBuffer.length / 1024 / 1024).toFixed(2)} MB exceeds 10 MB limit`,
+ );
}
const xmlString = fileBuffer.toString("utf-8");
@@ -315,7 +317,10 @@ export function parseL5X(fileBuffer: Buffer): L5XParseResult {
return {
metadata,
routines,
- routine: routines.length > 0 ? routines[0] : { name: "No routines found", type: "empty", rungs: [], rung_count: 0 },
+ routine:
+ routines.length > 0
+ ? routines[0]
+ : { name: "No routines found", type: "empty", rungs: [], rung_count: 0 },
tag_inventory: [...knownTags],
validation_summary: {
total_rungs: routines.reduce((s, r) => s + r.rung_count, 0),
diff --git a/codecompass-beta/src/lib/svg-renderer.ts b/codecompass-beta/src/lib/svg-renderer.ts
index f98bc46..7df4093 100644
--- a/codecompass-beta/src/lib/svg-renderer.ts
+++ b/codecompass-beta/src/lib/svg-renderer.ts
@@ -236,7 +236,8 @@ export function renderLadderSVG(rungs: L5XRung[], options: RenderOptions = {}):
// Enforce hard limit
const MAX_RUNGS = 500;
const effectiveMaxRungs = Math.min(maxRungs, MAX_RUNGS);
- const renderRungs = effectiveMaxRungs > 0 ? rungs.slice(0, effectiveMaxRungs) : rungs.slice(0, MAX_RUNGS);
+ const renderRungs =
+ effectiveMaxRungs > 0 ? rungs.slice(0, effectiveMaxRungs) : rungs.slice(0, MAX_RUNGS);
const numRungs = renderRungs.length;
if (numRungs === 0) {
diff --git a/codecompass-beta/src/routeTree.gen.ts b/codecompass-beta/src/routeTree.gen.ts
index 1f09b90..c75ec39 100644
--- a/codecompass-beta/src/routeTree.gen.ts
+++ b/codecompass-beta/src/routeTree.gen.ts
@@ -8,453 +8,451 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
-import { Route as rootRouteImport } from './routes/__root'
-import { Route as StudyToolsRouteImport } from './routes/study-tools'
-import { Route as SitemapDotxmlRouteImport } from './routes/sitemap[.]xml'
-import { Route as ProfileRouteImport } from './routes/profile'
-import { Route as PracticeTestRouteImport } from './routes/practice-test'
-import { Route as PlcRouteImport } from './routes/plc'
-import { Route as McpRouteImport } from './routes/mcp'
-import { Route as ExamPrepRouteImport } from './routes/exam-prep'
-import { Route as DataCenterRouteImport } from './routes/data-center'
-import { Route as AuthRouteImport } from './routes/auth'
-import { Route as IndexRouteImport } from './routes/index'
-import { Route as ShareTokenRouteImport } from './routes/share.$token'
-import { Route as DevMotorCalcsRouteImport } from './routes/dev.motor-calcs'
-import { Route as CoursesTradeRouteImport } from './routes/courses.$trade'
-import { Route as CourseCourseIdRouteImport } from './routes/course.$courseId'
-import { Route as ApiParseL5xRouteImport } from './routes/api/parse-l5x'
-import { Route as ApiChatRouteImport } from './routes/api/chat'
-import { Route as Char91DotwellKnownChar93OauthProtectedResourceRouteImport } from './routes/[.well-known]/oauth-protected-resource'
-import { Route as Char91DotmcpChar93ListToolsRouteImport } from './routes/[.mcp]/list-tools'
-import { Route as Char91DotmcpChar93InvokeToolToolRouteImport } from './routes/[.mcp]/invoke-tool/$tool'
-import { Route as DotlovableOauthConsentRouteImport } from './routes/[.]lovable.oauth.consent'
-import { Route as ApiPublicShareTokenRouteImport } from './routes/api/public/share.$token'
+import { Route as rootRouteImport } from "./routes/__root";
+import { Route as StudyToolsRouteImport } from "./routes/study-tools";
+import { Route as SitemapDotxmlRouteImport } from "./routes/sitemap[.]xml";
+import { Route as ProfileRouteImport } from "./routes/profile";
+import { Route as PracticeTestRouteImport } from "./routes/practice-test";
+import { Route as PlcRouteImport } from "./routes/plc";
+import { Route as McpRouteImport } from "./routes/mcp";
+import { Route as ExamPrepRouteImport } from "./routes/exam-prep";
+import { Route as DataCenterRouteImport } from "./routes/data-center";
+import { Route as AuthRouteImport } from "./routes/auth";
+import { Route as IndexRouteImport } from "./routes/index";
+import { Route as ShareTokenRouteImport } from "./routes/share.$token";
+import { Route as DevMotorCalcsRouteImport } from "./routes/dev.motor-calcs";
+import { Route as CoursesTradeRouteImport } from "./routes/courses.$trade";
+import { Route as CourseCourseIdRouteImport } from "./routes/course.$courseId";
+import { Route as ApiParseL5xRouteImport } from "./routes/api/parse-l5x";
+import { Route as ApiChatRouteImport } from "./routes/api/chat";
+import { Route as Char91DotwellKnownChar93OauthProtectedResourceRouteImport } from "./routes/[.well-known]/oauth-protected-resource";
+import { Route as Char91DotmcpChar93ListToolsRouteImport } from "./routes/[.mcp]/list-tools";
+import { Route as Char91DotmcpChar93InvokeToolToolRouteImport } from "./routes/[.mcp]/invoke-tool/$tool";
+import { Route as DotlovableOauthConsentRouteImport } from "./routes/[.]lovable.oauth.consent";
+import { Route as ApiPublicShareTokenRouteImport } from "./routes/api/public/share.$token";
const StudyToolsRoute = StudyToolsRouteImport.update({
- id: '/study-tools',
- path: '/study-tools',
+ id: "/study-tools",
+ path: "/study-tools",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const SitemapDotxmlRoute = SitemapDotxmlRouteImport.update({
- id: '/sitemap.xml',
- path: '/sitemap.xml',
+ id: "/sitemap.xml",
+ path: "/sitemap.xml",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ProfileRoute = ProfileRouteImport.update({
- id: '/profile',
- path: '/profile',
+ id: "/profile",
+ path: "/profile",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const PracticeTestRoute = PracticeTestRouteImport.update({
- id: '/practice-test',
- path: '/practice-test',
+ id: "/practice-test",
+ path: "/practice-test",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const PlcRoute = PlcRouteImport.update({
- id: '/plc',
- path: '/plc',
+ id: "/plc",
+ path: "/plc",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const McpRoute = McpRouteImport.update({
- id: '/mcp',
- path: '/mcp',
+ id: "/mcp",
+ path: "/mcp",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ExamPrepRoute = ExamPrepRouteImport.update({
- id: '/exam-prep',
- path: '/exam-prep',
+ id: "/exam-prep",
+ path: "/exam-prep",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const DataCenterRoute = DataCenterRouteImport.update({
- id: '/data-center',
- path: '/data-center',
+ id: "/data-center",
+ path: "/data-center",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const AuthRoute = AuthRouteImport.update({
- id: '/auth',
- path: '/auth',
+ id: "/auth",
+ path: "/auth",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const IndexRoute = IndexRouteImport.update({
- id: '/',
- path: '/',
+ id: "/",
+ path: "/",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ShareTokenRoute = ShareTokenRouteImport.update({
- id: '/share/$token',
- path: '/share/$token',
+ id: "/share/$token",
+ path: "/share/$token",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const DevMotorCalcsRoute = DevMotorCalcsRouteImport.update({
- id: '/dev/motor-calcs',
- path: '/dev/motor-calcs',
+ id: "/dev/motor-calcs",
+ path: "/dev/motor-calcs",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const CoursesTradeRoute = CoursesTradeRouteImport.update({
- id: '/courses/$trade',
- path: '/courses/$trade',
+ id: "/courses/$trade",
+ path: "/courses/$trade",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const CourseCourseIdRoute = CourseCourseIdRouteImport.update({
- id: '/course/$courseId',
- path: '/course/$courseId',
+ id: "/course/$courseId",
+ path: "/course/$courseId",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ApiParseL5xRoute = ApiParseL5xRouteImport.update({
- id: '/api/parse-l5x',
- path: '/api/parse-l5x',
+ id: "/api/parse-l5x",
+ path: "/api/parse-l5x",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ApiChatRoute = ApiChatRouteImport.update({
- id: '/api/chat',
- path: '/api/chat',
+ id: "/api/chat",
+ path: "/api/chat",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const Char91DotwellKnownChar93OauthProtectedResourceRoute =
Char91DotwellKnownChar93OauthProtectedResourceRouteImport.update({
- id: '/.well-known/oauth-protected-resource',
- path: '/.well-known/oauth-protected-resource',
+ id: "/.well-known/oauth-protected-resource",
+ path: "/.well-known/oauth-protected-resource",
getParentRoute: () => rootRouteImport,
- } as any)
-const Char91DotmcpChar93ListToolsRoute =
- Char91DotmcpChar93ListToolsRouteImport.update({
- id: '/.mcp/list-tools',
- path: '/.mcp/list-tools',
- getParentRoute: () => rootRouteImport,
- } as any)
-const Char91DotmcpChar93InvokeToolToolRoute =
- Char91DotmcpChar93InvokeToolToolRouteImport.update({
- id: '/.mcp/invoke-tool/$tool',
- path: '/.mcp/invoke-tool/$tool',
- getParentRoute: () => rootRouteImport,
- } as any)
+ } as any);
+const Char91DotmcpChar93ListToolsRoute = Char91DotmcpChar93ListToolsRouteImport.update({
+ id: "/.mcp/list-tools",
+ path: "/.mcp/list-tools",
+ getParentRoute: () => rootRouteImport,
+} as any);
+const Char91DotmcpChar93InvokeToolToolRoute = Char91DotmcpChar93InvokeToolToolRouteImport.update({
+ id: "/.mcp/invoke-tool/$tool",
+ path: "/.mcp/invoke-tool/$tool",
+ getParentRoute: () => rootRouteImport,
+} as any);
const DotlovableOauthConsentRoute = DotlovableOauthConsentRouteImport.update({
- id: '/.lovable/oauth/consent',
- path: '/.lovable/oauth/consent',
+ id: "/.lovable/oauth/consent",
+ path: "/.lovable/oauth/consent",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
const ApiPublicShareTokenRoute = ApiPublicShareTokenRouteImport.update({
- id: '/api/public/share/$token',
- path: '/api/public/share/$token',
+ id: "/api/public/share/$token",
+ path: "/api/public/share/$token",
getParentRoute: () => rootRouteImport,
-} as any)
+} as any);
export interface FileRoutesByFullPath {
- '/': typeof IndexRoute
- '/auth': typeof AuthRoute
- '/data-center': typeof DataCenterRoute
- '/exam-prep': typeof ExamPrepRoute
- '/mcp': typeof McpRoute
- '/plc': typeof PlcRoute
- '/practice-test': typeof PracticeTestRoute
- '/profile': typeof ProfileRoute
- '/sitemap.xml': typeof SitemapDotxmlRoute
- '/study-tools': typeof StudyToolsRoute
- '/.mcp/list-tools': typeof Char91DotmcpChar93ListToolsRoute
- '/.well-known/oauth-protected-resource': typeof Char91DotwellKnownChar93OauthProtectedResourceRoute
- '/api/chat': typeof ApiChatRoute
- '/api/parse-l5x': typeof ApiParseL5xRoute
- '/course/$courseId': typeof CourseCourseIdRoute
- '/courses/$trade': typeof CoursesTradeRoute
- '/dev/motor-calcs': typeof DevMotorCalcsRoute
- '/share/$token': typeof ShareTokenRoute
- '/.lovable/oauth/consent': typeof DotlovableOauthConsentRoute
- '/.mcp/invoke-tool/$tool': typeof Char91DotmcpChar93InvokeToolToolRoute
- '/api/public/share/$token': typeof ApiPublicShareTokenRoute
+ "/": typeof IndexRoute;
+ "/auth": typeof AuthRoute;
+ "/data-center": typeof DataCenterRoute;
+ "/exam-prep": typeof ExamPrepRoute;
+ "/mcp": typeof McpRoute;
+ "/plc": typeof PlcRoute;
+ "/practice-test": typeof PracticeTestRoute;
+ "/profile": typeof ProfileRoute;
+ "/sitemap.xml": typeof SitemapDotxmlRoute;
+ "/study-tools": typeof StudyToolsRoute;
+ "/.mcp/list-tools": typeof Char91DotmcpChar93ListToolsRoute;
+ "/.well-known/oauth-protected-resource": typeof Char91DotwellKnownChar93OauthProtectedResourceRoute;
+ "/api/chat": typeof ApiChatRoute;
+ "/api/parse-l5x": typeof ApiParseL5xRoute;
+ "/course/$courseId": typeof CourseCourseIdRoute;
+ "/courses/$trade": typeof CoursesTradeRoute;
+ "/dev/motor-calcs": typeof DevMotorCalcsRoute;
+ "/share/$token": typeof ShareTokenRoute;
+ "/.lovable/oauth/consent": typeof DotlovableOauthConsentRoute;
+ "/.mcp/invoke-tool/$tool": typeof Char91DotmcpChar93InvokeToolToolRoute;
+ "/api/public/share/$token": typeof ApiPublicShareTokenRoute;
}
export interface FileRoutesByTo {
- '/': typeof IndexRoute
- '/auth': typeof AuthRoute
- '/data-center': typeof DataCenterRoute
- '/exam-prep': typeof ExamPrepRoute
- '/mcp': typeof McpRoute
- '/plc': typeof PlcRoute
- '/practice-test': typeof PracticeTestRoute
- '/profile': typeof ProfileRoute
- '/sitemap.xml': typeof SitemapDotxmlRoute
- '/study-tools': typeof StudyToolsRoute
- '/.mcp/list-tools': typeof Char91DotmcpChar93ListToolsRoute
- '/.well-known/oauth-protected-resource': typeof Char91DotwellKnownChar93OauthProtectedResourceRoute
- '/api/chat': typeof ApiChatRoute
- '/api/parse-l5x': typeof ApiParseL5xRoute
- '/course/$courseId': typeof CourseCourseIdRoute
- '/courses/$trade': typeof CoursesTradeRoute
- '/dev/motor-calcs': typeof DevMotorCalcsRoute
- '/share/$token': typeof ShareTokenRoute
- '/.lovable/oauth/consent': typeof DotlovableOauthConsentRoute
- '/.mcp/invoke-tool/$tool': typeof Char91DotmcpChar93InvokeToolToolRoute
- '/api/public/share/$token': typeof ApiPublicShareTokenRoute
+ "/": typeof IndexRoute;
+ "/auth": typeof AuthRoute;
+ "/data-center": typeof DataCenterRoute;
+ "/exam-prep": typeof ExamPrepRoute;
+ "/mcp": typeof McpRoute;
+ "/plc": typeof PlcRoute;
+ "/practice-test": typeof PracticeTestRoute;
+ "/profile": typeof ProfileRoute;
+ "/sitemap.xml": typeof SitemapDotxmlRoute;
+ "/study-tools": typeof StudyToolsRoute;
+ "/.mcp/list-tools": typeof Char91DotmcpChar93ListToolsRoute;
+ "/.well-known/oauth-protected-resource": typeof Char91DotwellKnownChar93OauthProtectedResourceRoute;
+ "/api/chat": typeof ApiChatRoute;
+ "/api/parse-l5x": typeof ApiParseL5xRoute;
+ "/course/$courseId": typeof CourseCourseIdRoute;
+ "/courses/$trade": typeof CoursesTradeRoute;
+ "/dev/motor-calcs": typeof DevMotorCalcsRoute;
+ "/share/$token": typeof ShareTokenRoute;
+ "/.lovable/oauth/consent": typeof DotlovableOauthConsentRoute;
+ "/.mcp/invoke-tool/$tool": typeof Char91DotmcpChar93InvokeToolToolRoute;
+ "/api/public/share/$token": typeof ApiPublicShareTokenRoute;
}
export interface FileRoutesById {
- __root__: typeof rootRouteImport
- '/': typeof IndexRoute
- '/auth': typeof AuthRoute
- '/data-center': typeof DataCenterRoute
- '/exam-prep': typeof ExamPrepRoute
- '/mcp': typeof McpRoute
- '/plc': typeof PlcRoute
- '/practice-test': typeof PracticeTestRoute
- '/profile': typeof ProfileRoute
- '/sitemap.xml': typeof SitemapDotxmlRoute
- '/study-tools': typeof StudyToolsRoute
- '/.mcp/list-tools': typeof Char91DotmcpChar93ListToolsRoute
- '/.well-known/oauth-protected-resource': typeof Char91DotwellKnownChar93OauthProtectedResourceRoute
- '/api/chat': typeof ApiChatRoute
- '/api/parse-l5x': typeof ApiParseL5xRoute
- '/course/$courseId': typeof CourseCourseIdRoute
- '/courses/$trade': typeof CoursesTradeRoute
- '/dev/motor-calcs': typeof DevMotorCalcsRoute
- '/share/$token': typeof ShareTokenRoute
- '/.lovable/oauth/consent': typeof DotlovableOauthConsentRoute
- '/.mcp/invoke-tool/$tool': typeof Char91DotmcpChar93InvokeToolToolRoute
- '/api/public/share/$token': typeof ApiPublicShareTokenRoute
+ __root__: typeof rootRouteImport;
+ "/": typeof IndexRoute;
+ "/auth": typeof AuthRoute;
+ "/data-center": typeof DataCenterRoute;
+ "/exam-prep": typeof ExamPrepRoute;
+ "/mcp": typeof McpRoute;
+ "/plc": typeof PlcRoute;
+ "/practice-test": typeof PracticeTestRoute;
+ "/profile": typeof ProfileRoute;
+ "/sitemap.xml": typeof SitemapDotxmlRoute;
+ "/study-tools": typeof StudyToolsRoute;
+ "/.mcp/list-tools": typeof Char91DotmcpChar93ListToolsRoute;
+ "/.well-known/oauth-protected-resource": typeof Char91DotwellKnownChar93OauthProtectedResourceRoute;
+ "/api/chat": typeof ApiChatRoute;
+ "/api/parse-l5x": typeof ApiParseL5xRoute;
+ "/course/$courseId": typeof CourseCourseIdRoute;
+ "/courses/$trade": typeof CoursesTradeRoute;
+ "/dev/motor-calcs": typeof DevMotorCalcsRoute;
+ "/share/$token": typeof ShareTokenRoute;
+ "/.lovable/oauth/consent": typeof DotlovableOauthConsentRoute;
+ "/.mcp/invoke-tool/$tool": typeof Char91DotmcpChar93InvokeToolToolRoute;
+ "/api/public/share/$token": typeof ApiPublicShareTokenRoute;
}
export interface FileRouteTypes {
- fileRoutesByFullPath: FileRoutesByFullPath
+ fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
- | '/'
- | '/auth'
- | '/data-center'
- | '/exam-prep'
- | '/mcp'
- | '/plc'
- | '/practice-test'
- | '/profile'
- | '/sitemap.xml'
- | '/study-tools'
- | '/.mcp/list-tools'
- | '/.well-known/oauth-protected-resource'
- | '/api/chat'
- | '/api/parse-l5x'
- | '/course/$courseId'
- | '/courses/$trade'
- | '/dev/motor-calcs'
- | '/share/$token'
- | '/.lovable/oauth/consent'
- | '/.mcp/invoke-tool/$tool'
- | '/api/public/share/$token'
- fileRoutesByTo: FileRoutesByTo
+ | "/"
+ | "/auth"
+ | "/data-center"
+ | "/exam-prep"
+ | "/mcp"
+ | "/plc"
+ | "/practice-test"
+ | "/profile"
+ | "/sitemap.xml"
+ | "/study-tools"
+ | "/.mcp/list-tools"
+ | "/.well-known/oauth-protected-resource"
+ | "/api/chat"
+ | "/api/parse-l5x"
+ | "/course/$courseId"
+ | "/courses/$trade"
+ | "/dev/motor-calcs"
+ | "/share/$token"
+ | "/.lovable/oauth/consent"
+ | "/.mcp/invoke-tool/$tool"
+ | "/api/public/share/$token";
+ fileRoutesByTo: FileRoutesByTo;
to:
- | '/'
- | '/auth'
- | '/data-center'
- | '/exam-prep'
- | '/mcp'
- | '/plc'
- | '/practice-test'
- | '/profile'
- | '/sitemap.xml'
- | '/study-tools'
- | '/.mcp/list-tools'
- | '/.well-known/oauth-protected-resource'
- | '/api/chat'
- | '/api/parse-l5x'
- | '/course/$courseId'
- | '/courses/$trade'
- | '/dev/motor-calcs'
- | '/share/$token'
- | '/.lovable/oauth/consent'
- | '/.mcp/invoke-tool/$tool'
- | '/api/public/share/$token'
+ | "/"
+ | "/auth"
+ | "/data-center"
+ | "/exam-prep"
+ | "/mcp"
+ | "/plc"
+ | "/practice-test"
+ | "/profile"
+ | "/sitemap.xml"
+ | "/study-tools"
+ | "/.mcp/list-tools"
+ | "/.well-known/oauth-protected-resource"
+ | "/api/chat"
+ | "/api/parse-l5x"
+ | "/course/$courseId"
+ | "/courses/$trade"
+ | "/dev/motor-calcs"
+ | "/share/$token"
+ | "/.lovable/oauth/consent"
+ | "/.mcp/invoke-tool/$tool"
+ | "/api/public/share/$token";
id:
- | '__root__'
- | '/'
- | '/auth'
- | '/data-center'
- | '/exam-prep'
- | '/mcp'
- | '/plc'
- | '/practice-test'
- | '/profile'
- | '/sitemap.xml'
- | '/study-tools'
- | '/.mcp/list-tools'
- | '/.well-known/oauth-protected-resource'
- | '/api/chat'
- | '/api/parse-l5x'
- | '/course/$courseId'
- | '/courses/$trade'
- | '/dev/motor-calcs'
- | '/share/$token'
- | '/.lovable/oauth/consent'
- | '/.mcp/invoke-tool/$tool'
- | '/api/public/share/$token'
- fileRoutesById: FileRoutesById
+ | "__root__"
+ | "/"
+ | "/auth"
+ | "/data-center"
+ | "/exam-prep"
+ | "/mcp"
+ | "/plc"
+ | "/practice-test"
+ | "/profile"
+ | "/sitemap.xml"
+ | "/study-tools"
+ | "/.mcp/list-tools"
+ | "/.well-known/oauth-protected-resource"
+ | "/api/chat"
+ | "/api/parse-l5x"
+ | "/course/$courseId"
+ | "/courses/$trade"
+ | "/dev/motor-calcs"
+ | "/share/$token"
+ | "/.lovable/oauth/consent"
+ | "/.mcp/invoke-tool/$tool"
+ | "/api/public/share/$token";
+ fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
- IndexRoute: typeof IndexRoute
- AuthRoute: typeof AuthRoute
- DataCenterRoute: typeof DataCenterRoute
- ExamPrepRoute: typeof ExamPrepRoute
- McpRoute: typeof McpRoute
- PlcRoute: typeof PlcRoute
- PracticeTestRoute: typeof PracticeTestRoute
- ProfileRoute: typeof ProfileRoute
- SitemapDotxmlRoute: typeof SitemapDotxmlRoute
- StudyToolsRoute: typeof StudyToolsRoute
- Char91DotmcpChar93ListToolsRoute: typeof Char91DotmcpChar93ListToolsRoute
- Char91DotwellKnownChar93OauthProtectedResourceRoute: typeof Char91DotwellKnownChar93OauthProtectedResourceRoute
- ApiChatRoute: typeof ApiChatRoute
- ApiParseL5xRoute: typeof ApiParseL5xRoute
- CourseCourseIdRoute: typeof CourseCourseIdRoute
- CoursesTradeRoute: typeof CoursesTradeRoute
- DevMotorCalcsRoute: typeof DevMotorCalcsRoute
- ShareTokenRoute: typeof ShareTokenRoute
- DotlovableOauthConsentRoute: typeof DotlovableOauthConsentRoute
- Char91DotmcpChar93InvokeToolToolRoute: typeof Char91DotmcpChar93InvokeToolToolRoute
- ApiPublicShareTokenRoute: typeof ApiPublicShareTokenRoute
+ IndexRoute: typeof IndexRoute;
+ AuthRoute: typeof AuthRoute;
+ DataCenterRoute: typeof DataCenterRoute;
+ ExamPrepRoute: typeof ExamPrepRoute;
+ McpRoute: typeof McpRoute;
+ PlcRoute: typeof PlcRoute;
+ PracticeTestRoute: typeof PracticeTestRoute;
+ ProfileRoute: typeof ProfileRoute;
+ SitemapDotxmlRoute: typeof SitemapDotxmlRoute;
+ StudyToolsRoute: typeof StudyToolsRoute;
+ Char91DotmcpChar93ListToolsRoute: typeof Char91DotmcpChar93ListToolsRoute;
+ Char91DotwellKnownChar93OauthProtectedResourceRoute: typeof Char91DotwellKnownChar93OauthProtectedResourceRoute;
+ ApiChatRoute: typeof ApiChatRoute;
+ ApiParseL5xRoute: typeof ApiParseL5xRoute;
+ CourseCourseIdRoute: typeof CourseCourseIdRoute;
+ CoursesTradeRoute: typeof CoursesTradeRoute;
+ DevMotorCalcsRoute: typeof DevMotorCalcsRoute;
+ ShareTokenRoute: typeof ShareTokenRoute;
+ DotlovableOauthConsentRoute: typeof DotlovableOauthConsentRoute;
+ Char91DotmcpChar93InvokeToolToolRoute: typeof Char91DotmcpChar93InvokeToolToolRoute;
+ ApiPublicShareTokenRoute: typeof ApiPublicShareTokenRoute;
}
-declare module '@tanstack/react-router' {
+declare module "@tanstack/react-router" {
interface FileRoutesByPath {
- '/study-tools': {
- id: '/study-tools'
- path: '/study-tools'
- fullPath: '/study-tools'
- preLoaderRoute: typeof StudyToolsRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/sitemap.xml': {
- id: '/sitemap.xml'
- path: '/sitemap.xml'
- fullPath: '/sitemap.xml'
- preLoaderRoute: typeof SitemapDotxmlRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/profile': {
- id: '/profile'
- path: '/profile'
- fullPath: '/profile'
- preLoaderRoute: typeof ProfileRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/practice-test': {
- id: '/practice-test'
- path: '/practice-test'
- fullPath: '/practice-test'
- preLoaderRoute: typeof PracticeTestRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/plc': {
- id: '/plc'
- path: '/plc'
- fullPath: '/plc'
- preLoaderRoute: typeof PlcRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/mcp': {
- id: '/mcp'
- path: '/mcp'
- fullPath: '/mcp'
- preLoaderRoute: typeof McpRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/exam-prep': {
- id: '/exam-prep'
- path: '/exam-prep'
- fullPath: '/exam-prep'
- preLoaderRoute: typeof ExamPrepRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/data-center': {
- id: '/data-center'
- path: '/data-center'
- fullPath: '/data-center'
- preLoaderRoute: typeof DataCenterRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/auth': {
- id: '/auth'
- path: '/auth'
- fullPath: '/auth'
- preLoaderRoute: typeof AuthRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/': {
- id: '/'
- path: '/'
- fullPath: '/'
- preLoaderRoute: typeof IndexRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/share/$token': {
- id: '/share/$token'
- path: '/share/$token'
- fullPath: '/share/$token'
- preLoaderRoute: typeof ShareTokenRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/dev/motor-calcs': {
- id: '/dev/motor-calcs'
- path: '/dev/motor-calcs'
- fullPath: '/dev/motor-calcs'
- preLoaderRoute: typeof DevMotorCalcsRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/courses/$trade': {
- id: '/courses/$trade'
- path: '/courses/$trade'
- fullPath: '/courses/$trade'
- preLoaderRoute: typeof CoursesTradeRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/course/$courseId': {
- id: '/course/$courseId'
- path: '/course/$courseId'
- fullPath: '/course/$courseId'
- preLoaderRoute: typeof CourseCourseIdRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/api/parse-l5x': {
- id: '/api/parse-l5x'
- path: '/api/parse-l5x'
- fullPath: '/api/parse-l5x'
- preLoaderRoute: typeof ApiParseL5xRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/api/chat': {
- id: '/api/chat'
- path: '/api/chat'
- fullPath: '/api/chat'
- preLoaderRoute: typeof ApiChatRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/.well-known/oauth-protected-resource': {
- id: '/.well-known/oauth-protected-resource'
- path: '/.well-known/oauth-protected-resource'
- fullPath: '/.well-known/oauth-protected-resource'
- preLoaderRoute: typeof Char91DotwellKnownChar93OauthProtectedResourceRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/.mcp/list-tools': {
- id: '/.mcp/list-tools'
- path: '/.mcp/list-tools'
- fullPath: '/.mcp/list-tools'
- preLoaderRoute: typeof Char91DotmcpChar93ListToolsRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/.mcp/invoke-tool/$tool': {
- id: '/.mcp/invoke-tool/$tool'
- path: '/.mcp/invoke-tool/$tool'
- fullPath: '/.mcp/invoke-tool/$tool'
- preLoaderRoute: typeof Char91DotmcpChar93InvokeToolToolRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/.lovable/oauth/consent': {
- id: '/.lovable/oauth/consent'
- path: '/.lovable/oauth/consent'
- fullPath: '/.lovable/oauth/consent'
- preLoaderRoute: typeof DotlovableOauthConsentRouteImport
- parentRoute: typeof rootRouteImport
- }
- '/api/public/share/$token': {
- id: '/api/public/share/$token'
- path: '/api/public/share/$token'
- fullPath: '/api/public/share/$token'
- preLoaderRoute: typeof ApiPublicShareTokenRouteImport
- parentRoute: typeof rootRouteImport
- }
+ "/study-tools": {
+ id: "/study-tools";
+ path: "/study-tools";
+ fullPath: "/study-tools";
+ preLoaderRoute: typeof StudyToolsRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/sitemap.xml": {
+ id: "/sitemap.xml";
+ path: "/sitemap.xml";
+ fullPath: "/sitemap.xml";
+ preLoaderRoute: typeof SitemapDotxmlRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/profile": {
+ id: "/profile";
+ path: "/profile";
+ fullPath: "/profile";
+ preLoaderRoute: typeof ProfileRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/practice-test": {
+ id: "/practice-test";
+ path: "/practice-test";
+ fullPath: "/practice-test";
+ preLoaderRoute: typeof PracticeTestRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/plc": {
+ id: "/plc";
+ path: "/plc";
+ fullPath: "/plc";
+ preLoaderRoute: typeof PlcRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/mcp": {
+ id: "/mcp";
+ path: "/mcp";
+ fullPath: "/mcp";
+ preLoaderRoute: typeof McpRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/exam-prep": {
+ id: "/exam-prep";
+ path: "/exam-prep";
+ fullPath: "/exam-prep";
+ preLoaderRoute: typeof ExamPrepRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/data-center": {
+ id: "/data-center";
+ path: "/data-center";
+ fullPath: "/data-center";
+ preLoaderRoute: typeof DataCenterRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/auth": {
+ id: "/auth";
+ path: "/auth";
+ fullPath: "/auth";
+ preLoaderRoute: typeof AuthRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/": {
+ id: "/";
+ path: "/";
+ fullPath: "/";
+ preLoaderRoute: typeof IndexRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/share/$token": {
+ id: "/share/$token";
+ path: "/share/$token";
+ fullPath: "/share/$token";
+ preLoaderRoute: typeof ShareTokenRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/dev/motor-calcs": {
+ id: "/dev/motor-calcs";
+ path: "/dev/motor-calcs";
+ fullPath: "/dev/motor-calcs";
+ preLoaderRoute: typeof DevMotorCalcsRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/courses/$trade": {
+ id: "/courses/$trade";
+ path: "/courses/$trade";
+ fullPath: "/courses/$trade";
+ preLoaderRoute: typeof CoursesTradeRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/course/$courseId": {
+ id: "/course/$courseId";
+ path: "/course/$courseId";
+ fullPath: "/course/$courseId";
+ preLoaderRoute: typeof CourseCourseIdRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/api/parse-l5x": {
+ id: "/api/parse-l5x";
+ path: "/api/parse-l5x";
+ fullPath: "/api/parse-l5x";
+ preLoaderRoute: typeof ApiParseL5xRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/api/chat": {
+ id: "/api/chat";
+ path: "/api/chat";
+ fullPath: "/api/chat";
+ preLoaderRoute: typeof ApiChatRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/.well-known/oauth-protected-resource": {
+ id: "/.well-known/oauth-protected-resource";
+ path: "/.well-known/oauth-protected-resource";
+ fullPath: "/.well-known/oauth-protected-resource";
+ preLoaderRoute: typeof Char91DotwellKnownChar93OauthProtectedResourceRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/.mcp/list-tools": {
+ id: "/.mcp/list-tools";
+ path: "/.mcp/list-tools";
+ fullPath: "/.mcp/list-tools";
+ preLoaderRoute: typeof Char91DotmcpChar93ListToolsRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/.mcp/invoke-tool/$tool": {
+ id: "/.mcp/invoke-tool/$tool";
+ path: "/.mcp/invoke-tool/$tool";
+ fullPath: "/.mcp/invoke-tool/$tool";
+ preLoaderRoute: typeof Char91DotmcpChar93InvokeToolToolRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/.lovable/oauth/consent": {
+ id: "/.lovable/oauth/consent";
+ path: "/.lovable/oauth/consent";
+ fullPath: "/.lovable/oauth/consent";
+ preLoaderRoute: typeof DotlovableOauthConsentRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
+ "/api/public/share/$token": {
+ id: "/api/public/share/$token";
+ path: "/api/public/share/$token";
+ fullPath: "/api/public/share/$token";
+ preLoaderRoute: typeof ApiPublicShareTokenRouteImport;
+ parentRoute: typeof rootRouteImport;
+ };
}
}
@@ -481,17 +479,17 @@ const rootRouteChildren: RootRouteChildren = {
DotlovableOauthConsentRoute: DotlovableOauthConsentRoute,
Char91DotmcpChar93InvokeToolToolRoute: Char91DotmcpChar93InvokeToolToolRoute,
ApiPublicShareTokenRoute: ApiPublicShareTokenRoute,
-}
+};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
- ._addFileTypes
()
+ ._addFileTypes();
-import type { getRouter } from './router.tsx'
-import type { startInstance } from './start.ts'
-declare module '@tanstack/react-start' {
+import type { getRouter } from "./router.tsx";
+import type { startInstance } from "./start.ts";
+declare module "@tanstack/react-start" {
interface Register {
- ssr: true
- router: Awaited>
- config: Awaited>
+ ssr: true;
+ router: Awaited>;
+ config: Awaited>;
}
}
diff --git a/codecompass-beta/src/routes/[.mcp]/invoke-tool/$tool.ts b/codecompass-beta/src/routes/[.mcp]/invoke-tool/$tool.ts
index 4f9c6a0..e631701 100644
--- a/codecompass-beta/src/routes/[.mcp]/invoke-tool/$tool.ts
+++ b/codecompass-beta/src/routes/[.mcp]/invoke-tool/$tool.ts
@@ -13,7 +13,11 @@ export const Route = createFileRoute("/.mcp/invoke-tool/$tool")({
server: {
handlers: {
// ANY: TanStack returns SPA HTML for methods not in `handlers`; the SDK 405s instead.
- ANY: createTanStackInvokeToolHandler(mcp, { resourcePath: "/mcp", metadataPath: "/.well-known/oauth-protected-resource", trustForwardedHost: true }),
+ ANY: createTanStackInvokeToolHandler(mcp, {
+ resourcePath: "/mcp",
+ metadataPath: "/.well-known/oauth-protected-resource",
+ trustForwardedHost: true,
+ }),
},
},
});
diff --git a/codecompass-beta/src/routes/[.mcp]/list-tools.ts b/codecompass-beta/src/routes/[.mcp]/list-tools.ts
index ce5de60..5104f6e 100644
--- a/codecompass-beta/src/routes/[.mcp]/list-tools.ts
+++ b/codecompass-beta/src/routes/[.mcp]/list-tools.ts
@@ -13,7 +13,11 @@ export const Route = createFileRoute("/.mcp/list-tools")({
server: {
handlers: {
// ANY: TanStack returns SPA HTML for methods not in `handlers`; the SDK 405s instead.
- ANY: createTanStackListToolsHandler(mcp, { resourcePath: "/mcp", metadataPath: "/.well-known/oauth-protected-resource", trustForwardedHost: true }),
+ ANY: createTanStackListToolsHandler(mcp, {
+ resourcePath: "/mcp",
+ metadataPath: "/.well-known/oauth-protected-resource",
+ trustForwardedHost: true,
+ }),
},
},
});
diff --git a/codecompass-beta/src/routes/[.well-known]/oauth-protected-resource.ts b/codecompass-beta/src/routes/[.well-known]/oauth-protected-resource.ts
index 87fe0ac..ac78a6d 100644
--- a/codecompass-beta/src/routes/[.well-known]/oauth-protected-resource.ts
+++ b/codecompass-beta/src/routes/[.well-known]/oauth-protected-resource.ts
@@ -12,7 +12,11 @@ import mcp from "../../lib/mcp/index";
export const Route = createFileRoute("/.well-known/oauth-protected-resource")({
server: {
handlers: {
- ANY: createTanStackOAuthProtectedResourceMetadataHandler(mcp, { resourcePath: "/mcp", metadataPath: "/.well-known/oauth-protected-resource", trustForwardedHost: true }),
+ ANY: createTanStackOAuthProtectedResourceMetadataHandler(mcp, {
+ resourcePath: "/mcp",
+ metadataPath: "/.well-known/oauth-protected-resource",
+ trustForwardedHost: true,
+ }),
},
},
});
diff --git a/codecompass-beta/src/routes/api/parse-l5x.ts b/codecompass-beta/src/routes/api/parse-l5x.ts
index c008fa2..2d1ed9d 100644
--- a/codecompass-beta/src/routes/api/parse-l5x.ts
+++ b/codecompass-beta/src/routes/api/parse-l5x.ts
@@ -62,7 +62,7 @@ export const Route = createFileRoute("/api/parse-l5x")({
const stack = error instanceof Error ? error.stack : undefined;
console.error("L5X parse error:", { message: errMsg, stack });
return new Response(
- JSON.stringify({
+ JSON.stringify({
error: "Failed to parse L5X file. Invalid format or unsupported schema.",
details: process.env.NODE_ENV === "development" ? errMsg : undefined,
}),
diff --git a/codecompass-beta/src/routes/api/stripe-checkout.ts b/codecompass-beta/src/routes/api/stripe-checkout.ts
index 9f9a753..7df09e1 100644
--- a/codecompass-beta/src/routes/api/stripe-checkout.ts
+++ b/codecompass-beta/src/routes/api/stripe-checkout.ts
@@ -7,8 +7,7 @@ import { createFileRoute } from "@tanstack/react-router";
* Env: STRIPE_SECRET_KEY, STRIPE_LIFETIME_PRICE_ID (optional, defaults to test price)
*/
-const LIFETIME_PRICE_ID =
- process.env.STRIPE_LIFETIME_PRICE_ID ?? "price_1Ten4FRjzbxMHVlJikIz0EJR";
+const LIFETIME_PRICE_ID = process.env.STRIPE_LIFETIME_PRICE_ID ?? "price_1Ten4FRjzbxMHVlJikIz0EJR";
export const Route = createFileRoute("/api/stripe-checkout")({
server: {
@@ -17,10 +16,10 @@ export const Route = createFileRoute("/api/stripe-checkout")({
try {
const stripeKey = process.env.STRIPE_SECRET_KEY;
if (!stripeKey) {
- return new Response(
- JSON.stringify({ error: "Stripe not configured" }),
- { status: 500, headers: { "content-type": "application/json" } },
- );
+ return new Response(JSON.stringify({ error: "Stripe not configured" }), {
+ status: 500,
+ headers: { "content-type": "application/json" },
+ });
}
const body = await request.json().catch(() => ({}));
@@ -28,42 +27,39 @@ export const Route = createFileRoute("/api/stripe-checkout")({
const customerEmail = body.customerEmail || undefined;
// Use Stripe REST directly — no SDK dependency needed for checkout
- const sessionRes = await fetch(
- "https://api.stripe.com/v1/checkout/sessions",
- {
- method: "POST",
- headers: {
- Authorization: `Bearer ${stripeKey}`,
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: new URLSearchParams({
- mode: "payment",
- "payment_method_types[]": "card",
- "line_items[0][price]": LIFETIME_PRICE_ID,
- "line_items[0][quantity]": "1",
- allow_promotion_codes: "true",
- success_url: `${origin}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
- cancel_url: `${origin}/payment-cancel`,
- ...(customerEmail ? { customer_email: customerEmail } : {}),
- "metadata[plan_id]": "lifetime",
- }),
+ const sessionRes = await fetch("https://api.stripe.com/v1/checkout/sessions", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${stripeKey}`,
+ "Content-Type": "application/x-www-form-urlencoded",
},
- );
+ body: new URLSearchParams({
+ mode: "payment",
+ "payment_method_types[]": "card",
+ "line_items[0][price]": LIFETIME_PRICE_ID,
+ "line_items[0][quantity]": "1",
+ allow_promotion_codes: "true",
+ success_url: `${origin}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: `${origin}/payment-cancel`,
+ ...(customerEmail ? { customer_email: customerEmail } : {}),
+ "metadata[plan_id]": "lifetime",
+ }),
+ });
if (!sessionRes.ok) {
const err = await sessionRes.text();
console.error("[Stripe Checkout] API error:", err);
- return new Response(
- JSON.stringify({ error: "Stripe API error", detail: err }),
- { status: 502, headers: { "content-type": "application/json" } },
- );
+ return new Response(JSON.stringify({ error: "Stripe API error", detail: err }), {
+ status: 502,
+ headers: { "content-type": "application/json" },
+ });
}
const session = await sessionRes.json();
- return new Response(
- JSON.stringify({ url: session.url, id: session.id }),
- { status: 200, headers: { "content-type": "application/json" } },
- );
+ return new Response(JSON.stringify({ url: session.url, id: session.id }), {
+ status: 200,
+ headers: { "content-type": "application/json" },
+ });
} catch (error) {
const errMsg = error instanceof Error ? error.message : "Unknown error";
console.error("[Stripe Checkout] Error:", errMsg);
diff --git a/codecompass-beta/src/routes/api/stripe-webhook.ts b/codecompass-beta/src/routes/api/stripe-webhook.ts
index a9ebf00..783d859 100644
--- a/codecompass-beta/src/routes/api/stripe-webhook.ts
+++ b/codecompass-beta/src/routes/api/stripe-webhook.ts
@@ -43,13 +43,10 @@ export const Route = createFileRoute("/api/stripe-webhook")({
.map((s) => s.replace("v1=", ""));
const payload = `${timestamp}.${rawBody}`;
- const computed = crypto
- .createHmac("sha256", webhookSecret)
- .update(payload)
- .digest("hex");
+ const computed = crypto.createHmac("sha256", webhookSecret).update(payload).digest("hex");
- const isValid = sigValues.some(
- (v) => crypto.timingSafeEqual(Buffer.from(v), Buffer.from(computed)),
+ const isValid = sigValues.some((v) =>
+ crypto.timingSafeEqual(Buffer.from(v), Buffer.from(computed)),
);
if (!isValid) {
@@ -81,23 +78,20 @@ export const Route = createFileRoute("/api/stripe-webhook")({
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
if (supabaseUrl && serviceKey) {
- await fetch(
- `${supabaseUrl}/rest/v1/users?id=eq.${userId}`,
- {
- method: "PATCH",
- headers: {
- apikey: serviceKey,
- Authorization: `Bearer ${serviceKey}`,
- "Content-Type": "application/json",
- Prefer: "return=minimal",
- },
- body: JSON.stringify({
- subscription_status: "active",
- subscription_plan: "lifetime",
- stripe_customer_id: session.customer,
- }),
+ await fetch(`${supabaseUrl}/rest/v1/users?id=eq.${userId}`, {
+ method: "PATCH",
+ headers: {
+ apikey: serviceKey,
+ Authorization: `Bearer ${serviceKey}`,
+ "Content-Type": "application/json",
+ Prefer: "return=minimal",
},
- );
+ body: JSON.stringify({
+ subscription_status: "active",
+ subscription_plan: "lifetime",
+ stripe_customer_id: session.customer,
+ }),
+ });
console.log(`[Webhook] Granted lifetime access to user ${userId}`);
}
}
@@ -109,10 +103,10 @@ export const Route = createFileRoute("/api/stripe-webhook")({
} catch (error) {
const errMsg = error instanceof Error ? error.message : "Unknown error";
console.error("[Webhook] Processing error:", errMsg);
- return new Response(
- JSON.stringify({ error: "Webhook processing error" }),
- { status: 500, headers: { "content-type": "application/json" } },
- );
+ return new Response(JSON.stringify({ error: "Webhook processing error" }), {
+ status: 500,
+ headers: { "content-type": "application/json" },
+ });
}
},
},
diff --git a/codecompass-beta/src/routes/mcp.ts b/codecompass-beta/src/routes/mcp.ts
index a13662d..0436bb4 100644
--- a/codecompass-beta/src/routes/mcp.ts
+++ b/codecompass-beta/src/routes/mcp.ts
@@ -12,7 +12,11 @@ import mcp from "../lib/mcp/index";
export const Route = createFileRoute("/mcp")({
server: {
handlers: {
- ANY: createTanStackMcpHandler(mcp, { resourcePath: "/mcp", metadataPath: "/.well-known/oauth-protected-resource", trustForwardedHost: true }),
+ ANY: createTanStackMcpHandler(mcp, {
+ resourcePath: "/mcp",
+ metadataPath: "/.well-known/oauth-protected-resource",
+ trustForwardedHost: true,
+ }),
},
},
});
diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json
index cc6efb8..df6c8bb 100644
--- a/drizzle/meta/0000_snapshot.json
+++ b/drizzle/meta/0000_snapshot.json
@@ -81,17 +81,13 @@
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
- "columns": [
- "id"
- ]
+ "columns": ["id"]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
- "columns": [
- "openId"
- ]
+ "columns": ["openId"]
}
},
"checkConstraint": {}
@@ -107,4 +103,4 @@
"tables": {},
"indexes": {}
}
-}
\ No newline at end of file
+}
diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json
index 71d8d90..ca5e2da 100644
--- a/drizzle/meta/0001_snapshot.json
+++ b/drizzle/meta/0001_snapshot.json
@@ -117,17 +117,13 @@
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
- "columns": [
- "id"
- ]
+ "columns": ["id"]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
- "columns": [
- "openId"
- ]
+ "columns": ["openId"]
}
},
"checkConstraint": {}
@@ -143,4 +139,4 @@
"tables": {},
"indexes": {}
}
-}
\ No newline at end of file
+}
diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json
index ea41ffc..1fcdd31 100644
--- a/drizzle/meta/0002_snapshot.json
+++ b/drizzle/meta/0002_snapshot.json
@@ -59,9 +59,7 @@
"compositePrimaryKeys": {
"copilot_usage_id": {
"name": "copilot_usage_id",
- "columns": [
- "id"
- ]
+ "columns": ["id"]
}
},
"uniqueConstraints": {},
@@ -180,17 +178,13 @@
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
- "columns": [
- "id"
- ]
+ "columns": ["id"]
}
},
"uniqueConstraints": {
"users_openId_unique": {
"name": "users_openId_unique",
- "columns": [
- "openId"
- ]
+ "columns": ["openId"]
}
},
"checkConstraint": {}
@@ -206,4 +200,4 @@
"tables": {},
"indexes": {}
}
-}
\ No newline at end of file
+}
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 82f2345..87ded69 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -24,4 +24,4 @@
"breakpoints": true
}
]
-}
\ No newline at end of file
+}
diff --git a/drizzle/schema.ts b/drizzle/schema.ts
index 951bfb5..ca24bca 100644
--- a/drizzle/schema.ts
+++ b/drizzle/schema.ts
@@ -1,4 +1,11 @@
-import { int, mysqlEnum, mysqlTable, text, timestamp, varchar } from "drizzle-orm/mysql-core";
+import {
+ int,
+ mysqlEnum,
+ mysqlTable,
+ text,
+ timestamp,
+ varchar,
+} from "drizzle-orm/mysql-core";
/**
* Core user table backing auth flow.
@@ -24,7 +31,15 @@ export const users = mysqlTable("users", {
stripeCustomerId: varchar("stripeCustomerId", { length: 128 }),
stripeSubscriptionId: varchar("stripeSubscriptionId", { length: 128 }),
// Cached subscription status for fast access — kept in sync via webhooks
- subscriptionStatus: mysqlEnum("subscriptionStatus", ["active", "trialing", "past_due", "canceled", "none"]).default("none").notNull(),
+ subscriptionStatus: mysqlEnum("subscriptionStatus", [
+ "active",
+ "trialing",
+ "past_due",
+ "canceled",
+ "none",
+ ])
+ .default("none")
+ .notNull(),
subscriptionPlan: varchar("subscriptionPlan", { length: 64 }), // e.g. "pro_monthly" | "pro_annual" | "lifetime"
subscriptionCurrentPeriodEnd: timestamp("subscriptionCurrentPeriodEnd"),
});
diff --git a/ideas.md b/ideas.md
index aa9fdb2..31d30b0 100644
--- a/ideas.md
+++ b/ideas.md
@@ -7,6 +7,7 @@
**Design Movement:** Industrial Brutalism meets Control Room UI — inspired by SCADA interfaces, electrical panel labeling, and the tactile feel of breaker boxes.
**Core Principles:**
+
1. Information density over decoration — every pixel earns its place
2. Monospaced precision — data feels engineered, not designed
3. Status-indicator color language — amber = active, green = correct, red = fault
@@ -17,6 +18,7 @@
**Layout Paradigm:** Fixed left sidebar (navigation as "breaker panel"), main content area as a "work surface" with card-based modules that snap into a grid like DIN-rail components. No scrolling hero sections — immediate utility.
**Signature Elements:**
+
1. Pulsing amber dot indicators on active modes (like a live panel)
2. "Wire" connector lines between related UI elements using SVG paths
3. Stencil-style uppercase labels reminiscent of panel schedules
@@ -37,6 +39,7 @@
**Design Movement:** Technical Blueprint Aesthetic — inspired by architectural drawings, engineering schematics, and the blue-white contrast of cyanotype prints, modernized with a dark-mode twist.
**Core Principles:**
+
1. Grid-ruled precision — faint grid lines visible in backgrounds like graph paper
2. Annotation-style UI — labels feel hand-written on a technical drawing
3. Layered depth — content sits on "sheets" that cast subtle shadows
@@ -47,6 +50,7 @@
**Layout Paradigm:** Full-width top navigation bar (like a toolbar), content organized in "blueprint sheets" — large cards with subtle dashed borders. Quiz/Exam modes use a split layout: question on left, answer workspace on right. No wasted vertical space.
**Signature Elements:**
+
1. Faint crosshatch grid pattern in section backgrounds
2. "Dimension line" dividers between sections (like architectural measurements)
3. Circular badge indicators with article numbers (like callout bubbles on a blueprint)
@@ -67,6 +71,7 @@
**Design Movement:** Cyberpunk HUD (Heads-Up Display) — inspired by video game interfaces, night-shift jobsite lighting, and the glow of multimeter screens in dark spaces.
**Core Principles:**
+
1. Glowing focal points — key data radiates light against darkness
2. Scanline texture — subtle horizontal lines create screen depth
3. Segmented displays — numbers and stats shown in LED-style readouts
@@ -77,6 +82,7 @@
**Layout Paradigm:** Centered single-column for mobile-first (tradesmen on phones at jobsites). Desktop expands to a cockpit layout — stats in corners, main content centered, persistent bottom toolbar for mode switching. No sidebar — everything accessible within thumb reach.
**Signature Elements:**
+
1. Glowing border effect on active cards (box-shadow with amber)
2. Seven-segment LED-style number displays for scores and timers
3. "Signal strength" bars for difficulty indicators
diff --git a/package.json b/package.json
index 4becc7c..3ab0b0a 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"@aws-sdk/client-s3": "^3.693.0",
"@aws-sdk/s3-request-presigner": "^3.693.0",
"@google/generative-ai": "^0.24.1",
+ "@vercel/analytics": "^2.0.1",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e600460..9bf32a5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -121,6 +121,9 @@ importers:
'@trpc/server':
specifier: ^11.6.0
version: 11.17.0(typescript@5.9.3)
+ '@vercel/analytics':
+ specifier: ^2.0.1
+ version: 2.0.1(react@19.2.1)
axios:
specifier: ^1.12.0
version: 1.12.2
@@ -2522,6 +2525,35 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
+ '@vercel/analytics@2.0.1':
+ resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
+ peerDependencies:
+ '@remix-run/react': ^2
+ '@sveltejs/kit': ^1 || ^2
+ next: '>= 13'
+ nuxt: '>= 3'
+ react: ^18 || ^19 || ^19.0.0-rc
+ svelte: '>= 4'
+ vue: ^3
+ vue-router: ^4
+ peerDependenciesMeta:
+ '@remix-run/react':
+ optional: true
+ '@sveltejs/kit':
+ optional: true
+ next:
+ optional: true
+ nuxt:
+ optional: true
+ react:
+ optional: true
+ svelte:
+ optional: true
+ vue:
+ optional: true
+ vue-router:
+ optional: true
+
'@vercel/build-utils@13.34.0':
resolution: {integrity: sha512-DhbYXymwjO6N3prqBf7UzQFNpQIcHabe1qbiXxqqfjDJ5Tca9+MMYnU/+uObHrPBfR9FAXMxAwmkIUw59zJ7cw==}
@@ -6939,6 +6971,10 @@ snapshots:
'@ungap/structured-clone@1.3.0': {}
+ '@vercel/analytics@2.0.1(react@19.2.1)':
+ optionalDependencies:
+ react: 19.2.1
+
'@vercel/build-utils@13.34.0':
dependencies:
'@vercel/python-analysis': 0.11.1
diff --git a/server/_core/dataApi.ts b/server/_core/dataApi.ts
index 7ef81b5..a5d6343 100644
--- a/server/_core/dataApi.ts
+++ b/server/_core/dataApi.ts
@@ -25,8 +25,13 @@ export async function callDataApi(
}
// Build the full URL by appending the service path to the base URL
- const baseUrl = ENV.forgeApiUrl.endsWith("/") ? ENV.forgeApiUrl : `${ENV.forgeApiUrl}/`;
- const fullUrl = new URL("webdevtoken.v1.WebDevService/CallApi", baseUrl).toString();
+ const baseUrl = ENV.forgeApiUrl.endsWith("/")
+ ? ENV.forgeApiUrl
+ : `${ENV.forgeApiUrl}/`;
+ const fullUrl = new URL(
+ "webdevtoken.v1.WebDevService/CallApi",
+ baseUrl
+ ).toString();
const response = await fetch(fullUrl, {
method: "POST",
diff --git a/server/_core/llm.ts b/server/_core/llm.ts
index 8ea4c4a..b64b9da 100644
--- a/server/_core/llm.ts
+++ b/server/_core/llm.ts
@@ -19,7 +19,12 @@ export type FileContent = {
type: "file_url";
file_url: {
url: string;
- mime_type?: "audio/mpeg" | "audio/wav" | "application/pdf" | "audio/mp4" | "video/mp4" ;
+ mime_type?:
+ | "audio/mpeg"
+ | "audio/wav"
+ | "application/pdf"
+ | "audio/mp4"
+ | "video/mp4";
};
};
@@ -296,10 +301,10 @@ export async function invokeLLM(params: InvokeParams): Promise {
payload.tool_choice = normalizedToolChoice;
}
- payload.max_tokens = 32768
+ payload.max_tokens = 32768;
payload.thinking = {
- "budget_tokens": 128
- }
+ budget_tokens: 128,
+ };
const normalizedResponseFormat = normalizeResponseFormat({
responseFormat,
diff --git a/server/_core/map.ts b/server/_core/map.ts
index ac2c28c..b571665 100644
--- a/server/_core/map.ts
+++ b/server/_core/map.ts
@@ -1,9 +1,9 @@
/**
* Google Maps API Integration for Manus WebDev Templates
- *
+ *
* Main function: makeRequest(endpoint, params) - Makes authenticated requests to Google Maps APIs
* All credentials are automatically injected. Array parameters use | as separator.
- *
+ *
* See API examples below the type definitions for usage patterns.
*/
@@ -45,7 +45,7 @@ interface RequestOptions {
/**
* Make authenticated requests to Google Maps APIs
- *
+ *
* @param endpoint - The API endpoint (e.g., "/maps/api/geocode/json")
* @param params - Query parameters for the request
* @param options - Additional request options
@@ -313,7 +313,3 @@ export type RoadsResult = {
* Output: Image URL (not JSON) - use directly in
* Note: Construct URL manually with getMapsConfig() for auth
*/
-
-
-
-
diff --git a/server/_core/notification.ts b/server/_core/notification.ts
index 889a573..61147ad 100644
--- a/server/_core/notification.ts
+++ b/server/_core/notification.ts
@@ -14,9 +14,7 @@ const isNonEmptyString = (value: unknown): value is string =>
typeof value === "string" && value.trim().length > 0;
const buildEndpointUrl = (baseUrl: string): string => {
- const normalizedBase = baseUrl.endsWith("/")
- ? baseUrl
- : `${baseUrl}/`;
+ const normalizedBase = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;
return new URL(
"webdevtoken.v1.WebDevService/SendNotification",
normalizedBase
diff --git a/server/_core/oauth.ts b/server/_core/oauth.ts
index fd45373..f216748 100644
--- a/server/_core/oauth.ts
+++ b/server/_core/oauth.ts
@@ -42,7 +42,10 @@ export function registerOAuthRoutes(app: Express) {
});
const cookieOptions = getSessionCookieOptions(req);
- res.cookie(COOKIE_NAME, sessionToken, { ...cookieOptions, maxAge: ONE_YEAR_MS });
+ res.cookie(COOKIE_NAME, sessionToken, {
+ ...cookieOptions,
+ maxAge: ONE_YEAR_MS,
+ });
res.redirect(302, "/");
} catch (error) {
diff --git a/server/_core/storageProxy.ts b/server/_core/storageProxy.ts
index d2e8978..c6ccb24 100644
--- a/server/_core/storageProxy.ts
+++ b/server/_core/storageProxy.ts
@@ -17,7 +17,7 @@ export function registerStorageProxy(app: Express) {
try {
const forgeUrl = new URL(
"v1/storage/presign/get",
- ENV.forgeApiUrl.replace(/\/+$/, "") + "/",
+ ENV.forgeApiUrl.replace(/\/+$/, "") + "/"
);
forgeUrl.searchParams.set("path", key);
@@ -27,7 +27,9 @@ export function registerStorageProxy(app: Express) {
if (!forgeResp.ok) {
const body = await forgeResp.text().catch(() => "");
- console.error(`[StorageProxy] forge error: ${forgeResp.status} ${body}`);
+ console.error(
+ `[StorageProxy] forge error: ${forgeResp.status} ${body}`
+ );
res.status(502).send("Storage backend error");
return;
}
diff --git a/server/_core/trpc.ts b/server/_core/trpc.ts
index a0bcc05..e3d5bdf 100644
--- a/server/_core/trpc.ts
+++ b/server/_core/trpc.ts
@@ -1,4 +1,4 @@
-import { NOT_ADMIN_ERR_MSG, UNAUTHED_ERR_MSG } from '@shared/const';
+import { NOT_ADMIN_ERR_MSG, UNAUTHED_ERR_MSG } from "@shared/const";
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { TrpcContext } from "./context";
@@ -31,7 +31,7 @@ export const adminProcedure = t.procedure.use(
t.middleware(async opts => {
const { ctx, next } = opts;
- if (!ctx.user || ctx.user.role !== 'admin') {
+ if (!ctx.user || ctx.user.role !== "admin") {
throw new TRPCError({ code: "FORBIDDEN", message: NOT_ADMIN_ERR_MSG });
}
@@ -41,5 +41,5 @@ export const adminProcedure = t.procedure.use(
user: ctx.user,
},
});
- }),
+ })
);
diff --git a/server/_core/voiceTranscription.ts b/server/_core/voiceTranscription.ts
index ed511f6..65ca75e 100644
--- a/server/_core/voiceTranscription.ts
+++ b/server/_core/voiceTranscription.ts
@@ -5,7 +5,7 @@
* 1. Capture audio using MediaRecorder API
* 2. Upload audio to storage (e.g., S3) to get URL
* 3. Call transcription with the URL
- *
+ *
* Example usage:
* ```tsx
* // Frontend component
@@ -16,7 +16,7 @@
* console.log(data.segments); // Timestamped segments
* }
* });
- *
+ *
* // After uploading audio to storage
* transcribeMutation.mutate({
* audioUrl: uploadedAudioUrl,
@@ -60,13 +60,18 @@ export type TranscriptionResponse = WhisperResponse; // Return native Whisper AP
export type TranscriptionError = {
error: string;
- code: "FILE_TOO_LARGE" | "INVALID_FORMAT" | "TRANSCRIPTION_FAILED" | "UPLOAD_FAILED" | "SERVICE_ERROR";
+ code:
+ | "FILE_TOO_LARGE"
+ | "INVALID_FORMAT"
+ | "TRANSCRIPTION_FAILED"
+ | "UPLOAD_FAILED"
+ | "SERVICE_ERROR";
details?: string;
};
/**
* Transcribe audio to text using the internal Speech-to-Text service
- *
+ *
* @param options - Audio data and metadata
* @returns Transcription result or error
*/
@@ -79,14 +84,14 @@ export async function transcribeAudio(
return {
error: "Voice transcription service is not configured",
code: "SERVICE_ERROR",
- details: "BUILT_IN_FORGE_API_URL is not set"
+ details: "BUILT_IN_FORGE_API_URL is not set",
};
}
if (!ENV.forgeApiKey) {
return {
error: "Voice transcription service authentication is missing",
code: "SERVICE_ERROR",
- details: "BUILT_IN_FORGE_API_KEY is not set"
+ details: "BUILT_IN_FORGE_API_KEY is not set",
};
}
@@ -99,58 +104,57 @@ export async function transcribeAudio(
return {
error: "Failed to download audio file",
code: "INVALID_FORMAT",
- details: `HTTP ${response.status}: ${response.statusText}`
+ details: `HTTP ${response.status}: ${response.statusText}`,
};
}
-
+
audioBuffer = Buffer.from(await response.arrayBuffer());
- mimeType = response.headers.get('content-type') || 'audio/mpeg';
-
+ mimeType = response.headers.get("content-type") || "audio/mpeg";
+
// Check file size (16MB limit)
const sizeMB = audioBuffer.length / (1024 * 1024);
if (sizeMB > 16) {
return {
error: "Audio file exceeds maximum size limit",
code: "FILE_TOO_LARGE",
- details: `File size is ${sizeMB.toFixed(2)}MB, maximum allowed is 16MB`
+ details: `File size is ${sizeMB.toFixed(2)}MB, maximum allowed is 16MB`,
};
}
} catch (error) {
return {
error: "Failed to fetch audio file",
code: "SERVICE_ERROR",
- details: error instanceof Error ? error.message : "Unknown error"
+ details: error instanceof Error ? error.message : "Unknown error",
};
}
// Step 3: Create FormData for multipart upload to Whisper API
const formData = new FormData();
-
+
// Create a Blob from the buffer and append to form
const filename = `audio.${getFileExtension(mimeType)}`;
- const audioBlob = new Blob([new Uint8Array(audioBuffer)], { type: mimeType });
+ const audioBlob = new Blob([new Uint8Array(audioBuffer)], {
+ type: mimeType,
+ });
formData.append("file", audioBlob, filename);
-
+
formData.append("model", "whisper-1");
formData.append("response_format", "verbose_json");
-
+
// Add prompt - use custom prompt if provided, otherwise generate based on language
- const prompt = options.prompt || (
- options.language
+ const prompt =
+ options.prompt ||
+ (options.language
? `Transcribe the user's voice to text, the user's working language is ${getLanguageName(options.language)}`
- : "Transcribe the user's voice to text"
- );
+ : "Transcribe the user's voice to text");
formData.append("prompt", prompt);
// Step 4: Call the transcription service
const baseUrl = ENV.forgeApiUrl.endsWith("/")
? ENV.forgeApiUrl
: `${ENV.forgeApiUrl}/`;
-
- const fullUrl = new URL(
- "v1/audio/transcriptions",
- baseUrl
- ).toString();
+
+ const fullUrl = new URL("v1/audio/transcriptions", baseUrl).toString();
const response = await fetch(fullUrl, {
method: "POST",
@@ -166,30 +170,30 @@ export async function transcribeAudio(
return {
error: "Transcription service request failed",
code: "TRANSCRIPTION_FAILED",
- details: `${response.status} ${response.statusText}${errorText ? `: ${errorText}` : ""}`
+ details: `${response.status} ${response.statusText}${errorText ? `: ${errorText}` : ""}`,
};
}
// Step 5: Parse and return the transcription result
- const whisperResponse = await response.json() as WhisperResponse;
-
+ const whisperResponse = (await response.json()) as WhisperResponse;
+
// Validate response structure
- if (!whisperResponse.text || typeof whisperResponse.text !== 'string') {
+ if (!whisperResponse.text || typeof whisperResponse.text !== "string") {
return {
error: "Invalid transcription response",
code: "SERVICE_ERROR",
- details: "Transcription service returned an invalid response format"
+ details: "Transcription service returned an invalid response format",
};
}
return whisperResponse; // Return native Whisper API response directly
-
} catch (error) {
// Handle unexpected errors
return {
error: "Voice transcription failed",
code: "SERVICE_ERROR",
- details: error instanceof Error ? error.message : "An unexpected error occurred"
+ details:
+ error instanceof Error ? error.message : "An unexpected error occurred",
};
}
}
@@ -199,17 +203,17 @@ export async function transcribeAudio(
*/
function getFileExtension(mimeType: string): string {
const mimeToExt: Record = {
- 'audio/webm': 'webm',
- 'audio/mp3': 'mp3',
- 'audio/mpeg': 'mp3',
- 'audio/wav': 'wav',
- 'audio/wave': 'wav',
- 'audio/ogg': 'ogg',
- 'audio/m4a': 'm4a',
- 'audio/mp4': 'm4a',
+ "audio/webm": "webm",
+ "audio/mp3": "mp3",
+ "audio/mpeg": "mp3",
+ "audio/wav": "wav",
+ "audio/wave": "wav",
+ "audio/ogg": "ogg",
+ "audio/m4a": "m4a",
+ "audio/mp4": "m4a",
};
-
- return mimeToExt[mimeType] || 'audio';
+
+ return mimeToExt[mimeType] || "audio";
}
/**
@@ -217,37 +221,37 @@ function getFileExtension(mimeType: string): string {
*/
function getLanguageName(langCode: string): string {
const langMap: Record = {
- 'en': 'English',
- 'es': 'Spanish',
- 'fr': 'French',
- 'de': 'German',
- 'it': 'Italian',
- 'pt': 'Portuguese',
- 'ru': 'Russian',
- 'ja': 'Japanese',
- 'ko': 'Korean',
- 'zh': 'Chinese',
- 'ar': 'Arabic',
- 'hi': 'Hindi',
- 'nl': 'Dutch',
- 'pl': 'Polish',
- 'tr': 'Turkish',
- 'sv': 'Swedish',
- 'da': 'Danish',
- 'no': 'Norwegian',
- 'fi': 'Finnish',
+ en: "English",
+ es: "Spanish",
+ fr: "French",
+ de: "German",
+ it: "Italian",
+ pt: "Portuguese",
+ ru: "Russian",
+ ja: "Japanese",
+ ko: "Korean",
+ zh: "Chinese",
+ ar: "Arabic",
+ hi: "Hindi",
+ nl: "Dutch",
+ pl: "Polish",
+ tr: "Turkish",
+ sv: "Swedish",
+ da: "Danish",
+ no: "Norwegian",
+ fi: "Finnish",
};
-
+
return langMap[langCode] || langCode;
}
/**
* Example tRPC procedure implementation:
- *
+ *
* ```ts
* // In server/routers.ts
* import { transcribeAudio } from "./_core/voiceTranscription";
- *
+ *
* export const voiceRouter = router({
* transcribe: protectedProcedure
* .input(z.object({
@@ -257,7 +261,7 @@ function getLanguageName(langCode: string): string {
* }))
* .mutation(async ({ input, ctx }) => {
* const result = await transcribeAudio(input);
- *
+ *
* // Check if it's an error
* if ('error' in result) {
* throw new TRPCError({
@@ -266,7 +270,7 @@ function getLanguageName(langCode: string): string {
* cause: result,
* });
* }
- *
+ *
* // Optionally save transcription to database
* await db.insert(transcriptions).values({
* userId: ctx.user.id,
@@ -276,7 +280,7 @@ function getLanguageName(langCode: string): string {
* audioUrl: input.audioUrl,
* createdAt: new Date(),
* });
- *
+ *
* return result;
* }),
* });
diff --git a/server/auth.logout.test.ts b/server/auth.logout.test.ts
index 3071539..f5e56cc 100644
--- a/server/auth.logout.test.ts
+++ b/server/auth.logout.test.ts
@@ -10,7 +10,10 @@ type CookieCall = {
type AuthenticatedUser = NonNullable;
-function createAuthContext(): { ctx: TrpcContext; clearedCookies: CookieCall[] } {
+function createAuthContext(): {
+ ctx: TrpcContext;
+ clearedCookies: CookieCall[];
+} {
const clearedCookies: CookieCall[] = [];
const user: AuthenticatedUser = {
diff --git a/server/copilot.analyze.test.ts b/server/copilot.analyze.test.ts
index c14f242..2017972 100644
--- a/server/copilot.analyze.test.ts
+++ b/server/copilot.analyze.test.ts
@@ -117,7 +117,9 @@ describe("copilot.analyze", () => {
it("reports remaining free questions via usageStatus", async () => {
usageState.count = 1;
const caller = appRouter.createCaller(makeCtx());
- const status = await caller.copilot.usageStatus({ clientId: "test-device-3" });
+ const status = await caller.copilot.usageStatus({
+ clientId: "test-device-3",
+ });
expect(status.unlimited).toBe(false);
expect(status.remaining).toBe(2);
});
diff --git a/server/db.ts b/server/db.ts
index bfacb31..9742285 100644
--- a/server/db.ts
+++ b/server/db.ts
@@ -1,7 +1,7 @@
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/mysql2";
import { InsertUser, users } from "../drizzle/schema";
-import { ENV } from './_core/env';
+import { ENV } from "./_core/env";
let _db: ReturnType | null = null;
@@ -56,8 +56,8 @@ export async function upsertUser(user: InsertUser): Promise {
values.role = user.role;
updateSet.role = user.role;
} else if (user.openId === ENV.ownerOpenId) {
- values.role = 'admin';
- updateSet.role = 'admin';
+ values.role = "admin";
+ updateSet.role = "admin";
}
if (!values.lastSignedIn) {
@@ -84,14 +84,17 @@ export async function getUserByOpenId(openId: string) {
return undefined;
}
- const result = await db.select().from(users).where(eq(users.openId, openId)).limit(1);
+ const result = await db
+ .select()
+ .from(users)
+ .where(eq(users.openId, openId))
+ .limit(1);
return result.length > 0 ? result[0] : undefined;
}
// TODO: add feature queries here as your schema grows.
-
// ----- Co-Pilot usage tracking (freemium gate) -----
import { and, eq as eqOp } from "drizzle-orm";
import { copilotUsage } from "../drizzle/schema";
@@ -109,7 +112,12 @@ export async function getCopilotUsageToday(clientId: string): Promise {
const rows = await db
.select()
.from(copilotUsage)
- .where(and(eqOp(copilotUsage.clientId, clientId), eqOp(copilotUsage.usageDay, day)))
+ .where(
+ and(
+ eqOp(copilotUsage.clientId, clientId),
+ eqOp(copilotUsage.usageDay, day)
+ )
+ )
.limit(1);
return rows.length > 0 ? rows[0].count : 0;
}
@@ -122,7 +130,12 @@ export async function incrementCopilotUsage(clientId: string): Promise {
const existing = await db
.select()
.from(copilotUsage)
- .where(and(eqOp(copilotUsage.clientId, clientId), eqOp(copilotUsage.usageDay, day)))
+ .where(
+ and(
+ eqOp(copilotUsage.clientId, clientId),
+ eqOp(copilotUsage.usageDay, day)
+ )
+ )
.limit(1);
if (existing.length > 0) {
diff --git a/server/products.ts b/server/products.ts
index 2fe28b5..39e413d 100644
--- a/server/products.ts
+++ b/server/products.ts
@@ -49,7 +49,8 @@ export const PLANS: Plan[] = [
{
id: "lifetime",
name: "Lifetime Access",
- description: "One payment. Unlimited Co-Pilot. All future updates. Forever.",
+ description:
+ "One payment. Unlimited Co-Pilot. All future updates. Forever.",
priceUsd: 39.99,
interval: "once",
stripePriceId: LIFETIME_PRICE_ID,
@@ -70,5 +71,5 @@ export const PLANS: Plan[] = [
];
export function getPlanById(id: PlanId): Plan | undefined {
- return PLANS.find((p) => p.id === id);
+ return PLANS.find(p => p.id === id);
}
diff --git a/server/routers.ts b/server/routers.ts
index 43b72fe..41b8125 100644
--- a/server/routers.ts
+++ b/server/routers.ts
@@ -13,7 +13,11 @@ import { eq } from "drizzle-orm";
function getStripe() {
const key = process.env.STRIPE_SECRET_KEY;
- if (!key) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Stripe not configured" });
+ if (!key)
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "Stripe not configured",
+ });
return new Stripe(key, { apiVersion: "2026-05-27.dahlia" });
}
@@ -60,10 +64,18 @@ export const appRouter = router({
const stripe = getStripe();
const db = await getDb();
- if (!db) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "DB unavailable" });
+ if (!db)
+ throw new TRPCError({
+ code: "INTERNAL_SERVER_ERROR",
+ message: "DB unavailable",
+ });
// Get or create Stripe customer
- const [userRow] = await db.select().from(users).where(eq(users.id, ctx.user.id)).limit(1);
+ const [userRow] = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, ctx.user.id))
+ .limit(1);
let customerId = userRow?.stripeCustomerId ?? undefined;
if (!customerId) {
@@ -73,7 +85,10 @@ export const appRouter = router({
metadata: { userId: ctx.user.id.toString() },
});
customerId = customer.id;
- await db.update(users).set({ stripeCustomerId: customerId }).where(eq(users.id, ctx.user.id));
+ await db
+ .update(users)
+ .set({ stripeCustomerId: customerId })
+ .where(eq(users.id, ctx.user.id));
}
// Only lifetime (one-time payment) exists — always use payment mode
@@ -84,7 +99,9 @@ export const appRouter = router({
line_items: [{ price: plan.stripePriceId, quantity: 1 }],
allow_promotion_codes: true,
client_reference_id: ctx.user.id.toString(),
- customer_email: !customerId ? (ctx.user.email ?? undefined) : undefined,
+ customer_email: !customerId
+ ? (ctx.user.email ?? undefined)
+ : undefined,
metadata: {
user_id: ctx.user.id.toString(),
plan_id: plan.id,
@@ -102,7 +119,11 @@ export const appRouter = router({
const db = await getDb();
if (!db) return { status: "none", plan: null, currentPeriodEnd: null };
- const [userRow] = await db.select().from(users).where(eq(users.id, ctx.user.id)).limit(1);
+ const [userRow] = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, ctx.user.id))
+ .limit(1);
return {
status: userRow?.subscriptionStatus ?? "none",
plan: userRow?.subscriptionPlan ?? null,
diff --git a/server/routers/copilot.ts b/server/routers/copilot.ts
index 38430bf..8d57bbc 100644
--- a/server/routers/copilot.ts
+++ b/server/routers/copilot.ts
@@ -16,9 +16,14 @@ import { getCopilotUsageToday, incrementCopilotUsage } from "../db";
export const FREE_DAILY_LIMIT = 3;
/** True if the user has an active or trialing subscription (unlimited access). */
-function hasUnlimitedAccess(user: { subscriptionStatus?: string } | null | undefined): boolean {
+function hasUnlimitedAccess(
+ user: { subscriptionStatus?: string } | null | undefined
+): boolean {
if (!user) return false;
- return user.subscriptionStatus === "active" || user.subscriptionStatus === "trialing";
+ return (
+ user.subscriptionStatus === "active" ||
+ user.subscriptionStatus === "trialing"
+ );
}
const SUGGESTED_QUESTIONS = [
@@ -73,9 +78,16 @@ export const copilotRouter = router({
usageStatus: publicProcedure
.input(z.object({ clientId: z.string().min(1).max(128) }))
.query(async ({ ctx, input }) => {
- const unlimited = hasUnlimitedAccess(ctx.user as { subscriptionStatus?: string } | null);
+ const unlimited = hasUnlimitedAccess(
+ ctx.user as { subscriptionStatus?: string } | null
+ );
if (unlimited) {
- return { unlimited: true, used: 0, limit: FREE_DAILY_LIMIT, remaining: FREE_DAILY_LIMIT };
+ return {
+ unlimited: true,
+ used: 0,
+ limit: FREE_DAILY_LIMIT,
+ remaining: FREE_DAILY_LIMIT,
+ };
}
const used = await getCopilotUsageToday(input.clientId);
return {
@@ -96,7 +108,9 @@ export const copilotRouter = router({
)
.mutation(async ({ ctx, input }) => {
// Freemium gate: subscribers are unlimited; everyone else gets FREE_DAILY_LIMIT/day.
- const unlimited = hasUnlimitedAccess(ctx.user as { subscriptionStatus?: string } | null);
+ const unlimited = hasUnlimitedAccess(
+ ctx.user as { subscriptionStatus?: string } | null
+ );
if (!unlimited) {
const used = await getCopilotUsageToday(input.clientId);
if (used >= FREE_DAILY_LIMIT) {
@@ -142,7 +156,12 @@ export const copilotRouter = router({
return {
keywords: parsed.keywords as string[],
- indexDrilldown: (parsed.indexDrilldown as Array<{ level: number; entry: string; description: string }>) ?? [],
+ indexDrilldown:
+ (parsed.indexDrilldown as Array<{
+ level: number;
+ entry: string;
+ description: string;
+ }>) ?? [],
article: parsed.article as string,
articleTitle: (parsed.articleTitle as string) ?? "",
lookupSteps: (parsed.lookupSteps as string[]) ?? [],
diff --git a/server/storage.ts b/server/storage.ts
index 1d88470..c96fb26 100644
--- a/server/storage.ts
+++ b/server/storage.ts
@@ -10,7 +10,7 @@ function getForgeConfig() {
if (!forgeUrl || !forgeKey) {
throw new Error(
- "Storage config missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY",
+ "Storage config missing: set BUILT_IN_FORGE_API_URL and BUILT_IN_FORGE_API_KEY"
);
}
@@ -31,7 +31,7 @@ function appendHashSuffix(relKey: string): string {
export async function storagePut(
relKey: string,
data: Buffer | Uint8Array | string,
- contentType = "application/octet-stream",
+ contentType = "application/octet-stream"
): Promise<{ key: string; url: string }> {
const { forgeUrl, forgeKey } = getForgeConfig();
const key = appendHashSuffix(normalizeKey(relKey));
@@ -71,7 +71,9 @@ export async function storagePut(
return { key, url: `/manus-storage/${key}` };
}
-export async function storageGet(relKey: string): Promise<{ key: string; url: string }> {
+export async function storageGet(
+ relKey: string
+): Promise<{ key: string; url: string }> {
const key = normalizeKey(relKey);
return { key, url: `/manus-storage/${key}` };
}
diff --git a/server/stripe.checkout.test.ts b/server/stripe.checkout.test.ts
index f2b32c2..84db9d7 100644
--- a/server/stripe.checkout.test.ts
+++ b/server/stripe.checkout.test.ts
@@ -10,7 +10,9 @@ vi.mock("stripe", () => {
},
checkout: {
sessions: {
- create: vi.fn().mockResolvedValue({ url: "https://checkout.stripe.com/test" }),
+ create: vi
+ .fn()
+ .mockResolvedValue({ url: "https://checkout.stripe.com/test" }),
},
},
};
@@ -66,7 +68,10 @@ describe("stripe.createCheckoutSession", () => {
const ctx = createAuthContext();
const caller = appRouter.createCaller(ctx);
await expect(
- caller.stripe.createCheckoutSession({ planId: "invalid_plan", origin: "https://example.com" })
+ caller.stripe.createCheckoutSession({
+ planId: "invalid_plan",
+ origin: "https://example.com",
+ })
).rejects.toThrow("Invalid plan");
});
@@ -74,7 +79,10 @@ describe("stripe.createCheckoutSession", () => {
const ctx = createAuthContext();
const caller = appRouter.createCaller(ctx);
await expect(
- caller.stripe.createCheckoutSession({ planId: "free", origin: "https://example.com" })
+ caller.stripe.createCheckoutSession({
+ planId: "free",
+ origin: "https://example.com",
+ })
).rejects.toThrow("Invalid plan");
});
});
diff --git a/server/stripeWebhook.ts b/server/stripeWebhook.ts
index 0c4cbfd..bbef42b 100644
--- a/server/stripeWebhook.ts
+++ b/server/stripeWebhook.ts
@@ -15,7 +15,9 @@ export function registerStripeWebhook(app: Express) {
(req, res, next) => {
let data = "";
req.setEncoding("utf8");
- req.on("data", (chunk) => { data += chunk; });
+ req.on("data", chunk => {
+ data += chunk;
+ });
req.on("end", () => {
(req as Request & { rawBody: string }).rawBody = data;
next();
@@ -49,7 +51,9 @@ export function registerStripeWebhook(app: Express) {
// Test event passthrough — required for Stripe webhook verification
if (event.id.startsWith("evt_test_")) {
- console.log("[Webhook] Test event detected, returning verification response");
+ console.log(
+ "[Webhook] Test event detected, returning verification response"
+ );
res.json({ verified: true });
return;
}
@@ -70,8 +74,8 @@ export function registerStripeWebhook(app: Express) {
const userId = session.metadata?.user_id
? parseInt(session.metadata.user_id)
: session.client_reference_id
- ? parseInt(session.client_reference_id)
- : null;
+ ? parseInt(session.client_reference_id)
+ : null;
const planId = session.metadata?.plan_id ?? null;
if (!userId) break;
@@ -82,7 +86,10 @@ export function registerStripeWebhook(app: Express) {
// confirms the money actually cleared — `payment_status === "paid"`.
// Without this guard, any completed-but-unpaid (or test) session
// would unlock lifetime access.
- if (session.mode === "payment" && session.payment_status === "paid") {
+ if (
+ session.mode === "payment" &&
+ session.payment_status === "paid"
+ ) {
updateData.subscriptionStatus = "active";
updateData.subscriptionPlan = planId ?? "lifetime";
}
@@ -93,7 +100,10 @@ export function registerStripeWebhook(app: Express) {
}
if (Object.keys(updateData).length > 0) {
- await db.update(users).set(updateData).where(eq(users.id, userId));
+ await db
+ .update(users)
+ .set(updateData)
+ .where(eq(users.id, userId));
}
break;
}
@@ -111,7 +121,12 @@ export function registerStripeWebhook(app: Express) {
if (!userRow) break;
- const status = sub.status as "active" | "trialing" | "past_due" | "canceled" | "none";
+ const status = sub.status as
+ | "active"
+ | "trialing"
+ | "past_due"
+ | "canceled"
+ | "none";
const firstItem = sub.items.data[0];
const periodEnd = firstItem?.current_period_end
? new Date(firstItem.current_period_end * 1000)
@@ -122,14 +137,19 @@ export function registerStripeWebhook(app: Express) {
const interval = sub.items.data[0]?.price?.recurring?.interval;
const planId = interval === "year" ? "pro_annual" : "pro_monthly";
- await db.update(users).set({
- stripeSubscriptionId: sub.id,
- subscriptionStatus: status,
- subscriptionPlan: planId,
- subscriptionCurrentPeriodEnd: periodEnd,
- }).where(eq(users.id, userRow.id));
-
- console.log(`[Webhook] Updated subscription for user ${userRow.id}: ${status}`);
+ await db
+ .update(users)
+ .set({
+ stripeSubscriptionId: sub.id,
+ subscriptionStatus: status,
+ subscriptionPlan: planId,
+ subscriptionCurrentPeriodEnd: periodEnd,
+ })
+ .where(eq(users.id, userRow.id));
+
+ console.log(
+ `[Webhook] Updated subscription for user ${userRow.id}: ${status}`
+ );
break;
}
@@ -150,12 +170,17 @@ export function registerStripeWebhook(app: Express) {
? new Date(deletedItem.current_period_end * 1000)
: null;
- await db.update(users).set({
- subscriptionStatus: "canceled",
- subscriptionCurrentPeriodEnd: deletedPeriodEnd,
- }).where(eq(users.id, userRow.id));
-
- console.log(`[Webhook] Subscription canceled for user ${userRow.id}`);
+ await db
+ .update(users)
+ .set({
+ subscriptionStatus: "canceled",
+ subscriptionCurrentPeriodEnd: deletedPeriodEnd,
+ })
+ .where(eq(users.id, userRow.id));
+
+ console.log(
+ `[Webhook] Subscription canceled for user ${userRow.id}`
+ );
break;
}
@@ -171,7 +196,10 @@ export function registerStripeWebhook(app: Express) {
if (!userRow) break;
- await db.update(users).set({ subscriptionStatus: "past_due" }).where(eq(users.id, userRow.id));
+ await db
+ .update(users)
+ .set({ subscriptionStatus: "past_due" })
+ .where(eq(users.id, userRow.id));
console.log(`[Webhook] Payment failed for user ${userRow.id}`);
break;
}
diff --git a/shared/const.ts b/shared/const.ts
index 112ffc5..fa49709 100644
--- a/shared/const.ts
+++ b/shared/const.ts
@@ -1,5 +1,5 @@
export const COOKIE_NAME = "app_session_id";
export const ONE_YEAR_MS = 1000 * 60 * 60 * 24 * 365;
export const AXIOS_TIMEOUT_MS = 30_000;
-export const UNAUTHED_ERR_MSG = 'Please login (10001)';
-export const NOT_ADMIN_ERR_MSG = 'You do not have required permission (10002)';
+export const UNAUTHED_ERR_MSG = "Please login (10001)";
+export const NOT_ADMIN_ERR_MSG = "You do not have required permission (10002)";
diff --git a/todo.md b/todo.md
index f4c2170..de869e5 100644
--- a/todo.md
+++ b/todo.md
@@ -1,11 +1,12 @@
# Code Compass TODO
## Stripe Payments
+
- [x] Add stripe_customer_id and stripe_subscription_id columns to users table in drizzle/schema.ts
- [x] Run pnpm db:push to migrate schema
- [x] Create server/products.ts with plan definitions (Free, Pro monthly, Pro annual)
- [x] Create Stripe checkout session tRPC procedure in server/routers.ts
-- [x] Create Stripe webhook handler at /api/stripe/webhook in server/_core/index.ts
+- [x] Create Stripe webhook handler at /api/stripe/webhook in server/\_core/index.ts
- [x] Build /pricing page with plan cards and checkout buttons
- [x] Build /payment-success page
- [x] Build /payment-cancel page
@@ -13,14 +14,15 @@
- [x] Write vitest for checkout session procedure
## AI Co-Pilot Page
+
- [x] Add Gemini-powered tRPC procedure: analyze any NEC question, return keywords + lookup path + article citation + explanation
- [x] Build /copilot page: dark bg, orange accent, input feed at top, suggested keyword chips, results panel, bottom tab nav (Search/Index/Co-Pilot)
- [x] Add AI icon to AppLayout sidebar nav
- [x] Wire Gemini API key via GEMINI_API_KEY env var on server (uses built-in invokeLLM)
- [x] Write vitest for the copilot.analyze procedure
-
## 24-Hour Launch (Revenue Roadmap)
+
- [x] Add light mode toggle (dark/light theme switcher in header)
- [x] Add custom back button to all pages (state-aware navigation)
- [x] Simplify landing page (remove feature overload, 3-section layout: Hero + CTA + Social Proof)
@@ -32,9 +34,13 @@
- [ ] Share Stripe payment link in Facebook electrician groups — MANUAL (marketing)
## Week 1 Refinements (POST-LAUNCH — not launch-blocking)
+
# These need Drizzle schema changes + new tRPC procedures + tests. Build them
+
# properly in a follow-up (push the repo so the app can be built/tested first),
+
# not blind into a snapshot right before the payment test.
+
- [ ] Add gamification (progress bar, study streaks, badges)
- [ ] Add tiered pricing UI ($9.99/mo, $79/year, $149 lifetime) — needs live Stripe prices per tier
- [ ] Build performance analytics dashboard (weak areas, success rate by article)
@@ -42,6 +48,7 @@
- [ ] Add social proof copy to landing page — NEEDS REAL numbers/testimonials. Do NOT ship fabricated "500+ electricians"/fake quotes (dishonest + FTC risk). Provide real ones to drop in.
## Front-End Redesign (Method-First Beta)
+
- [x] Theme switcher: exactly 2 themes — OLED Black (#000000, default) + Light (readability-first), drop Industrial Dark
- [x] Theme toggle in header on every screen (single shared toggle in root layout), persist in localStorage cc_theme, default OLED Black
- [x] Full-width welcome page: hero (name + promise line), 3 modes in a row, primary CTA (Get Started) + Sign in
@@ -51,11 +58,13 @@
- [x] Test theme persistence + welcome page + per-question guide, checkpoint, push to GitHub
## OAuth In-App Browser Fix (Error 403 disallowed_useragent)
+
- [x] Detect in-app browsers (Messenger/Instagram/Facebook/TikTok/X/LinkedIn/Snapchat/LINE) - useInAppBrowser hook + 6 passing tests
- [x] Show 'Open in Safari/Chrome' guidance banner (InAppBrowserBanner) mounted globally in AppLayout, with copy-link button
- [x] Test detection logic (15/15 tests pass), checkpoint, guide publish
## Homepage Redesign + Paywall (Jun 6 2026)
+
- [x] Find high-quality electrical/lightning/nature hero image (Unsplash/Pexels) — hero-electrician-tower wired in Dashboard.tsx
- [x] Flip default theme to Light for public pages (home, pricing, onboarding) — :root is now Light in index.css
- [x] Redesign homepage: full-bleed hero image, minimal text, single CTA — Dashboard.tsx
@@ -63,8 +72,9 @@
- [ ] Test, checkpoint, publish — MANUAL (run pnpm check + load app in Manus, then publish)
## Launch Blockers (theme + checkout) — Jun 6 2026
+
- [x] All pages default to Light theme; OLED reserved for paid users only — index.css :root=Light, OLED under .dark (paid only)
- [x] OLED toggle hidden/locked for free users (only shows after paywall) — AppLayout: {oledUnlocked && }, unlocked only on active/trialing sub
- [x] Diagnose card-declined checkout issue (mom's Visa) — ROOT CAUSE: Stripe in TEST mode; live cards rejected (only 4242... works in test). Code reads keys from env, nothing hardcoded.
-- [ ] Fix checkout so cards are accepted — CONFIG (no code): in host env set STRIPE_SECRET_KEY=sk_live_…, STRIPE_LIFETIME_PRICE_ID=, STRIPE_WEBHOOK_SECRET= after Stripe KYC. Code is now env-ready (products.ts).
+- [ ] Fix checkout so cards are accepted — CONFIG (no code): in host env set STRIPE*SECRET_KEY=sk_live*…, STRIPE*LIFETIME_PRICE_ID=, STRIPE_WEBHOOK_SECRET= after Stripe KYC. Code is now env-ready (products.ts).
- [ ] Test, checkpoint, guide publish — MANUAL (after live keys: run one real low-value charge end-to-end)
diff --git a/vite.config.ts b/vite.config.ts
index 8d4acaf..1aed729 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -56,7 +56,7 @@ function writeToLogFile(source: LogSource, entries: unknown[]) {
const logPath = path.join(LOG_DIR, `${source}.log`);
// Format entries with timestamps
- const lines = entries.map((entry) => {
+ const lines = entries.map(entry => {
const ts = new Date().toISOString();
return `[${ts}] ${JSON.stringify(entry)}`;
});
@@ -132,7 +132,7 @@ function vitePluginManusDebugCollector(): Plugin {
}
let body = "";
- req.on("data", (chunk) => {
+ req.on("data", chunk => {
body += chunk.toString();
});
@@ -150,7 +150,13 @@ function vitePluginManusDebugCollector(): Plugin {
};
}
-const plugins = [react(), tailwindcss(), jsxLocPlugin(), vitePluginManusRuntime(), vitePluginManusDebugCollector()];
+const plugins = [
+ react(),
+ tailwindcss(),
+ jsxLocPlugin(),
+ vitePluginManusRuntime(),
+ vitePluginManusDebugCollector(),
+];
export default defineConfig({
plugins,