/ FINALSPRINT.md
FINALSPRINT.md
1 # FINALSPRINT — GapZero Monetization, Profile Hub & Quality Sprint 2 3 > **Created:** 2026-03-05 4 > **Scope:** 11 features across 6 work streams 5 > **Estimated Commits:** 25–30 6 7 --- 8 9 ## Table of Contents 10 11 1. [Requirements Map](#1-requirements-map) 12 2. [Architecture Overview](#2-architecture-overview) 13 3. [Work Stream A: Profile Hub Dashboard](#3-work-stream-a-profile-hub-dashboard) 14 4. [Work Stream B: Wizard Flow Integration](#4-work-stream-b-wizard-flow-integration) 15 5. [Work Stream C: Stripe Monetization & Quotas](#5-work-stream-c-stripe-monetization--quotas) 16 6. [Work Stream D: Output Tagging for Retraining](#6-work-stream-d-output-tagging-for-retraining) 17 7. [Work Stream E: CV Generation ATS Fix](#7-work-stream-e-cv-generation-ats-fix) 18 8. [Work Stream F: Career Coach Removal](#8-work-stream-f-career-coach-removal) 19 9. [Database Migrations](#9-database-migrations) 20 10. [i18n Keys](#10-i18n-keys) 21 11. [Commit Sequence](#11-commit-sequence) 22 12. [Testing Checklist](#12-testing-checklist) 23 13. [Security & Privacy Audit Checklist](#13-security--privacy-audit-checklist) 24 25 --- 26 27 ## 1. Requirements Map 28 29 | # | Requirement | Work Stream | 30 |---|-------------|-------------| 31 | 1 | Logged-in user sees Profile Hub Dashboard as landing page | A | 32 | 2 | Profile Hub holds CV, GitHub, LinkedIn + context textarea with examples | A | 33 | 3 | Profile Hub shows analysis history | A | 34 | 4 | No prior analysis → wizard as-is | B | 35 | 5 | Stripe payment integration | C | 36 | 6 | Free tier: 1 analysis/week, initial analysis free (doesn't count) | C | 37 | 7 | Free analysis: 1 CV generation + 1 cover letter generation | C | 38 | 8 | No AI Career Coach window | F | 39 | 9 | Paid plan: 10 weekly analyses, 10 CV, 10 cover letter, 10 coach | C | 40 | 10 | Output token/section tagging for retraining feedback | D | 41 | 11 | Fix CV generation producing lower ATS scores | E | 42 43 --- 44 45 ## 2. Architecture Overview 46 47 ### Current State 48 49 ``` 50 Landing (/) → Marketing page 51 Dashboard (/dashboard) → Tabs: Profile | Analyses | Jobs 52 Analyze (/analyze) → Wizard → Streaming → Results 53 ``` 54 55 ### Target State 56 57 ``` 58 Logged-in user: 59 / or /dashboard → Profile Hub (merged landing + dashboard) 60 ├─ Profile Card (CV, LinkedIn, GitHub, context textarea) 61 ├─ Quick Analysis (job description → run with stored profile) 62 ├─ Analysis History (list of past analyses with tags) 63 └─ Jobs Tab (existing Kanban) 64 65 Not logged in: 66 / → Marketing landing page (unchanged) 67 68 First-time user (logged in, no profile): 69 /dashboard → Redirects to /analyze (wizard) 70 71 Payments: 72 /pricing → Plan comparison + Stripe checkout 73 /api/stripe/checkout → Create Stripe session 74 /api/stripe/webhook → Handle payment events 75 /api/stripe/portal → Customer portal redirect 76 ``` 77 78 ### New Database Tables 79 80 ```sql 81 user_quotas — Tracks weekly usage per user 82 user_subscriptions — Stripe subscription state 83 output_tags — User-applied tags on analysis tokens/sections 84 ``` 85 86 --- 87 88 ## 3. Work Stream A: Profile Hub Dashboard 89 90 ### A1. Redesign ProfileTab → Profile Hub 91 92 **File:** `components/dashboard/ProfileTab.tsx` 93 94 **Current:** Simple form card with career fields + file upload + quick re-analysis button. 95 96 **Target:** Full profile landing page with 4 sections: 97 98 #### Section 1: Profile Card 99 - Display: name (from auth), current role, target role, years of experience, country, work preference 100 - Files: CV PDF (view/replace/remove), LinkedIn PDF (view/replace/remove) 101 - GitHub: clickable link, show connected status 102 - Edit button → inline editing mode 103 - Visual indicators: green checkmark for uploaded files, gray placeholder for missing 104 105 #### Section 2: Additional Context Textarea 106 New `additional_context` column in `career_profiles` table (TEXT, max 5000 chars). 107 108 **UI:** 109 ``` 110 ┌─────────────────────────────────────────────────────────────────┐ 111 │ Additional Context ✏️ │ 112 │ │ 113 │ Help us understand your full story. Write in your own words │ 114 │ about experiences not captured in your CV or LinkedIn: │ 115 │ │ 116 │ ┌─────────────────────────────────────────────────────────────┐ │ 117 │ │ │ │ 118 │ │ [textarea — autosaves on blur] │ │ 119 │ │ │ │ 120 │ └─────────────────────────────────────────────────────────────┘ │ 121 │ │ 122 │ 💡 Examples of useful context: │ 123 │ │ 124 │ • "Between 2022-2023 I took a career break to complete the │ 125 │ AWS Solutions Architect certification and built 3 full-stack │ 126 │ projects: a real-time chat app (React + Node + Socket.io), │ 127 │ an e-commerce platform (Next.js + Stripe), and a job │ 128 │ board aggregator using Python scrapers. All are on my │ 129 │ GitHub." │ 130 │ │ 131 │ • "While freelancing on Upwork (2021-2023), I delivered 15+ │ 132 │ projects for clients in fintech and healthcare, including a │ 133 │ HIPAA-compliant patient portal and a real-time stock │ 134 │ dashboard. My JSS was 97% with $45k+ earned." │ 135 │ │ 136 │ • "I've been contributing to open-source projects including │ 137 │ React Query (3 merged PRs) and Supabase (documentation │ 138 │ improvements). I also mentor 2 junior developers through │ 139 │ ADPList and run a tech blog with 5k monthly readers." │ 140 │ │ 141 │ • "After being laid off in 2024, I used the 6-month gap to │ 142 │ pivot from backend to full-stack: completed the Meta │ 143 │ Frontend Developer certificate, learned TypeScript + │ 144 │ React, and built a personal finance tracker used by 200+ │ 145 │ beta users." │ 146 │ │ 147 │ • "I have 3 years of hobby game development experience with │ 148 │ Unity and C# — I've published 2 mobile games with 10k+ │ 149 │ combined downloads. This taught me performance optimization, │ 150 │ event-driven architecture, and user analytics." │ 151 │ │ 152 └─────────────────────────────────────────────────────────────────┘ 153 ``` 154 155 **Auto-save:** Debounced PUT to `/api/profile` on 2-second idle after typing. 156 157 **Pipeline integration:** Pass `additional_context` to the analysis pipeline as extra context in the skill extraction and gap analysis prompts. 158 159 #### Section 3: Quick Analysis Card 160 - Job description textarea + "Run Analysis" button 161 - Shows quota status: "2 of 10 analyses used this week" or "Free analysis available" 162 - If quota exceeded → shows upgrade CTA with pricing link 163 - On submit → navigate to `/analyze?fromProfile=true` with job description in sessionStorage 164 165 #### Section 4: Analysis History 166 - List of saved analyses (from existing AnalysesTab) 167 - Each card: target role, fit score badge, date, language flag 168 - Click → navigate to `/analyze?saved=<id>` 169 - Delete button with confirmation 170 - Empty state: "No analyses yet. Run your first analysis above." 171 172 ### A2. Dashboard Page Updates 173 174 **File:** `app/dashboard/page.tsx` 175 176 **Changes:** 177 - Remove the 3-tab switcher for Profile/Analyses (merge into one page) 178 - Keep Jobs as a separate tab or link 179 - Smart routing: if no `career_profiles` row exists AND no `analyses` rows → redirect to `/analyze` 180 - If profile exists → show Profile Hub 181 182 ### A3. Root Route for Logged-In Users 183 184 **File:** `app/page.tsx` or `middleware.ts` 185 186 **Logic:** 187 ```typescript 188 // In middleware or page: 189 if (user.isAuthenticated) { 190 redirect('/dashboard'); 191 } else { 192 // Show marketing landing page 193 } 194 ``` 195 196 --- 197 198 ## 4. Work Stream B: Wizard Flow Integration 199 200 ### B1. Wizard Skips Profile Steps for Returning Users 201 202 **File:** `app/analyze/page.tsx` 203 204 **When `?fromProfile=true`:** 205 1. Fetch profile from `/api/profile` 206 2. Download stored CV/LinkedIn from Supabase Storage 207 3. Read job description from sessionStorage 208 4. Skip wizard entirely → auto-start streaming analysis 209 210 **When no query params (first-time user):** 211 - Show full wizard as-is (LinkedIn → CV → Target Job → Review) 212 - On completion, save profile as existing logic does 213 214 ### B2. Pass Additional Context to Analysis 215 216 **File:** `app/api/analyze-stream/route.ts` 217 218 **Changes:** 219 - Accept `additionalContext` field in FormData 220 - Pass to `buildSkillExtractionPrompt()` and `buildGapAnalysisPrompt()` as supplementary context block: 221 ``` 222 ADDITIONAL CONTEXT FROM CANDIDATE (treat as supplementary background): 223 --- 224 {additionalContext} 225 --- 226 ``` 227 - Validate: max 5000 chars, sanitize for prompt injection markers 228 229 **Files:** `lib/prompts/skill-extraction.ts`, `lib/prompts/gap-analysis.ts` 230 - Add optional `additionalContext` parameter 231 - Insert after CV text block with clear boundary markers 232 233 --- 234 235 ## 5. Work Stream C: Stripe Monetization & Quotas 236 237 ### C1. Pricing Model 238 239 | Feature | Free Tier | Pro Plan | 240 |---------|-----------|----------| 241 | **Price** | $0 | $9.99/week or $29.99/month | 242 | **Career analyses** | 1/week | 10/week | 243 | **Initial analysis** | Free (uncounted) | Free (uncounted) | 244 | **CV generations** | 1/week | 10/week | 245 | **Cover letter generations** | 1/week | 10/week | 246 | **Career coach requests** | 0 | 10/week | 247 | **Output tagging** | Yes | Yes | 248 | **Analysis history** | Last 5 | Unlimited | 249 | **PDF report download** | Yes | Yes | 250 251 **Notes on pricing rationale:** 252 - $9.99/week targets active job seekers (typical job search = 4-8 weeks → $40-$80 total) 253 - $29.99/month targets longer searches with a discount (~$7.50/week) 254 - Initial analysis is always free to demonstrate value before paywall 255 - The "initial analysis" = first-ever analysis for a user (flagged `is_initial = true`) 256 257 ### C2. Database Schema: `user_quotas` 258 259 ```sql 260 create table if not exists public.user_quotas ( 261 user_id uuid references auth.users(id) on delete cascade primary key, 262 plan text not null default 'free' check (plan in ('free', 'pro')), 263 264 -- Weekly counters (reset every Monday 00:00 UTC) 265 week_start date not null default (date_trunc('week', now())::date), 266 analyses_used integer not null default 0, 267 cv_generations_used integer not null default 0, 268 cover_letters_used integer not null default 0, 269 coach_requests_used integer not null default 0, 270 271 -- Limits (derived from plan, stored for flexibility) 272 analyses_limit integer not null default 1, 273 cv_limit integer not null default 1, 274 cover_letter_limit integer not null default 1, 275 coach_limit integer not null default 0, 276 277 -- Initial analysis flag 278 has_used_initial_analysis boolean not null default false, 279 280 -- Stripe references 281 stripe_customer_id text, 282 stripe_subscription_id text, 283 subscription_status text check (subscription_status in ('active', 'past_due', 'canceled', 'trialing', null)), 284 subscription_period_end timestamptz, 285 286 created_at timestamptz default now() not null, 287 updated_at timestamptz default now() not null 288 ); 289 290 -- RLS 291 alter table public.user_quotas enable row level security; 292 create policy "Users can read own quotas" on public.user_quotas 293 for select using (auth.uid() = user_id); 294 -- No direct user writes — quotas updated by server only 295 296 -- Auto-create on first auth 297 create or replace function public.create_user_quota() 298 returns trigger as $$ 299 begin 300 insert into public.user_quotas (user_id) values (new.id) on conflict do nothing; 301 return new; 302 end; 303 $$ language plpgsql security definer; 304 305 create trigger on_auth_user_created_quota 306 after insert on auth.users 307 for each row execute function public.create_user_quota(); 308 309 -- Weekly reset function (call via Supabase cron or pg_cron) 310 create or replace function public.reset_weekly_quotas() 311 returns void as $$ 312 begin 313 update public.user_quotas 314 set analyses_used = 0, 315 cv_generations_used = 0, 316 cover_letters_used = 0, 317 coach_requests_used = 0, 318 week_start = date_trunc('week', now())::date, 319 updated_at = now() 320 where week_start < date_trunc('week', now())::date; 321 end; 322 $$ language plpgsql security definer; 323 ``` 324 325 ### C3. Quota Middleware 326 327 **New file:** `lib/quota.ts` 328 329 ```typescript 330 export type QuotaType = 'analysis' | 'cv_generation' | 'cover_letter' | 'coach_request'; 331 332 export interface QuotaCheck { 333 allowed: boolean; 334 used: number; 335 limit: number; 336 plan: 'free' | 'pro'; 337 isInitialAnalysis?: boolean; 338 resetAt: string; // next Monday 00:00 UTC 339 } 340 341 export async function checkQuota(client, userId, type: QuotaType): Promise<QuotaCheck>; 342 export async function incrementQuota(client, userId, type: QuotaType): Promise<void>; 343 export async function getQuotaStatus(client, userId): Promise<UserQuotaStatus>; 344 ``` 345 346 **Integration points:** 347 - `POST /api/analyze-stream` → `checkQuota(client, userId, 'analysis')` before starting 348 - `POST /api/rewrite-cv` → `checkQuota(client, userId, 'cv_generation')` 349 - `POST /api/generate-cover-letter` → `checkQuota(client, userId, 'cover_letter')` 350 - `POST /api/chat` → `checkQuota(client, userId, 'coach_request')` 351 352 **Initial analysis logic:** 353 ```typescript 354 if (type === 'analysis' && !quota.has_used_initial_analysis) { 355 // Allow and mark as used, but don't increment analyses_used 356 await markInitialAnalysisUsed(client, userId); 357 return { allowed: true, isInitialAnalysis: true, ... }; 358 } 359 ``` 360 361 ### C4. Stripe Integration 362 363 **New dependencies:** `stripe`, `@stripe/stripe-js` 364 365 **New environment variables:** 366 ```env 367 STRIPE_SECRET_KEY=sk_live_... 368 STRIPE_PUBLISHABLE_KEY=pk_live_... 369 STRIPE_WEBHOOK_SECRET=whsec_... 370 STRIPE_PRICE_WEEKLY=price_... 371 STRIPE_PRICE_MONTHLY=price_... 372 ``` 373 374 **New API routes:** 375 376 #### `POST /api/stripe/checkout` 377 ```typescript 378 // Creates Stripe Checkout session 379 // Input: { priceId: 'weekly' | 'monthly' } 380 // Output: { url: string } — redirect URL to Stripe Checkout 381 // Attaches userId as metadata for webhook matching 382 ``` 383 384 #### `POST /api/stripe/webhook` 385 ```typescript 386 // Handles Stripe webhook events: 387 // - checkout.session.completed → create/update user_quotas with pro limits 388 // - customer.subscription.updated → update subscription_status 389 // - customer.subscription.deleted → downgrade to free tier 390 // - invoice.payment_failed → mark as past_due 391 // 392 // CRITICAL: Verify webhook signature using STRIPE_WEBHOOK_SECRET 393 // Must be idempotent (handle duplicate events gracefully) 394 ``` 395 396 #### `POST /api/stripe/portal` 397 ```typescript 398 // Creates Stripe Customer Portal session for subscription management 399 // Output: { url: string } — redirect URL to Stripe portal 400 ``` 401 402 #### `GET /api/quota` 403 ```typescript 404 // Returns current user's quota status 405 // Output: QuotaCheck for each type + plan info + subscription details 406 // Used by frontend to show usage bars and upgrade prompts 407 ``` 408 409 ### C5. Pricing Page 410 411 **New file:** `app/pricing/page.tsx` 412 413 Simple pricing comparison page: 414 - Free tier card (current features) 415 - Pro tier card ($9.99/week or $29.99/month toggle) 416 - Feature comparison table 417 - "Get Started" → Stripe checkout 418 - "Current Plan" badge for logged-in users 419 - FAQ section 420 421 ### C6. Quota UI Components 422 423 **New file:** `components/shared/QuotaBar.tsx` 424 - Small horizontal bar showing "3 of 10 analyses used" 425 - Color: green (0-50%), yellow (50-80%), red (80-100%) 426 - Shows on Profile Hub + before analysis start 427 428 **New file:** `components/shared/UpgradePrompt.tsx` 429 - Shown when quota exceeded 430 - "You've used your free analysis this week. Upgrade to Pro for 10 weekly analyses." 431 - CTA button → /pricing 432 433 --- 434 435 ## 6. Work Stream D: Output Tagging for Retraining 436 437 ### D1. Tagging System Design 438 439 Allow users to tag **any visible token or section** in the analysis output with quality labels. 440 441 **Tag types:** 442 | Tag | Color | Meaning | 443 |-----|-------|---------| 444 | `accurate` | Green | This output is correct and useful | 445 | `inaccurate` | Red | This output is factually wrong | 446 | `irrelevant` | Orange | Correct info but not relevant to my situation | 447 | `missing_context` | Blue | Output would be better with more context | 448 | `too_generic` | Purple | Not specific enough, feels template-like | 449 450 ### D2. Database Schema: `output_tags` 451 452 ```sql 453 create table if not exists public.output_tags ( 454 id uuid default gen_random_uuid() primary key, 455 user_id uuid references auth.users(id) on delete cascade not null, 456 analysis_id uuid references public.analyses(id) on delete cascade not null, 457 458 -- What was tagged 459 section text not null, -- 'fitScore', 'strengths', 'gaps', 'actionPlan', 'salary', 'roles', 'ats', 'jobMatch', 'linkedin', 'cv' 460 element_index integer, -- index within array (e.g., gaps[2]) 461 element_key text, -- specific sub-element (e.g., 'skill', 'closing_plan', 'resource') 462 tagged_text text, -- the actual text that was tagged (for keyword-level tags) 463 464 -- The tag 465 tag text not null check (tag in ('accurate', 'inaccurate', 'irrelevant', 'missing_context', 'too_generic')), 466 comment text, -- optional user comment (max 500 chars) 467 468 created_at timestamptz default now() not null 469 ); 470 471 -- RLS 472 alter table public.output_tags enable row level security; 473 create policy "Users can CRUD own tags" on public.output_tags 474 for all using (auth.uid() = user_id); 475 476 -- Index for retraining data export 477 create index idx_output_tags_section_tag on public.output_tags(section, tag); 478 create index idx_output_tags_analysis on public.output_tags(analysis_id); 479 ``` 480 481 ### D3. API Route 482 483 **New file:** `app/api/tags/route.ts` 484 485 ```typescript 486 // POST /api/tags — Create a tag 487 // Input: { analysisId, section, elementIndex?, elementKey?, taggedText?, tag, comment? } 488 // Output: { id, tag } 489 490 // GET /api/tags?analysisId=<uuid> — Get all tags for an analysis 491 // Output: { tags: OutputTag[] } 492 493 // DELETE /api/tags?id=<uuid> — Remove a tag 494 // Output: { success: true } 495 ``` 496 497 ### D4. Tagging UI Component 498 499 **New file:** `components/shared/TaggableToken.tsx` 500 501 Wraps any text span/keyword/section. On click or long-press: 502 1. Shows a small popover with 5 tag buttons (colored circles) 503 2. User selects a tag → optional comment input appears 504 3. On submit → POST to `/api/tags` 505 4. Token gets a colored underline/dot indicator showing the applied tag 506 5. Click again to change or remove tag 507 508 ```tsx 509 // Usage in result panels: 510 <TaggableToken 511 analysisId={analysisId} 512 section="gaps" 513 elementIndex={2} 514 elementKey="skill" 515 text="Large Team Leadership" 516 > 517 Large Team Leadership 518 </TaggableToken> 519 ``` 520 521 ### D5. Integration Points 522 523 Apply `TaggableToken` wrapper to: 524 525 | Component | Elements to Wrap | 526 |-----------|-----------------| 527 | `FitScore.tsx` | Score label, summary text, each matching/missing skill pill | 528 | `StrengthsPanel.tsx` | Each strength title, description, relevance text | 529 | `GapsPanel.tsx` | Each gap skill name, severity badge, closing plan text, each resource | 530 | `ActionPlan.tsx` | Each action item text, resource link, expected impact | 531 | `RoleRecommendations.tsx` | Each role title, reasoning, salary range | 532 | `LinkedInPlan.tsx` | Each headline option, about section, skill suggestion | 533 | `CVOptimizerPanel.tsx` | Each CV suggestion (current vs suggested), ATS keyword | 534 | `CoverLetterPanel.tsx` | Generated letter sections | 535 | `SalaryPanel.tsx` | Salary ranges, percentiles, negotiation tips | 536 537 ### D6. Retraining Data Export (Admin) 538 539 **New file:** `app/api/admin/export-tags/route.ts` 540 541 Protected admin-only endpoint that exports all tags in JSONL format for fine-tuning: 542 ```jsonl 543 {"analysis_id":"...","section":"gaps","element":"skill","text":"Large Team Leadership","tag":"inaccurate","comment":"I manage 20 people","prompt_context":"...","model_output":"..."} 544 ``` 545 546 --- 547 548 ## 7. Work Stream E: CV Generation ATS Fix 549 550 ### E1. Root Cause Analysis 551 552 The CV rewrite prompt (`lib/prompts/cv-rewriter.ts`) can lower ATS scores because: 553 554 1. **Keyword removal:** Suggestions may rephrase exact keyword matches into synonyms that ATS parsers don't recognize 555 2. **Section restructuring:** Changing section headers from ATS-standard names breaks parser expectations 556 3. **Over-summarization:** Condensing bullet points can drop required keywords 557 4. **No feedback loop:** The rewrite happens without checking if the result would score higher or lower on ATS 558 559 ### E2. Fix: ATS-Aware CV Rewrite Pipeline 560 561 **Changes to `lib/prompts/cv-rewriter.ts`:** 562 563 Add ATS keyword preservation rules to the system prompt: 564 565 ``` 566 ATS KEYWORD PRESERVATION (CRITICAL): 567 - You will receive a list of MATCHED KEYWORDS from the ATS analysis. 568 - These keywords MUST appear in the rewritten CV. Never remove or replace a matched keyword with a synonym. 569 - For MISSING keywords: only add them if the candidate genuinely has the skill (evidenced in their CV or additional context). 570 - For SEMANTIC MATCHES: keep the original phrasing if the ATS matched it; only standardize if the exact industry term would score higher. 571 - PRESERVE all section headers as ATS-standard names. 572 - NEVER reduce the number of bullet points under Experience sections — you may rephrase but not remove. 573 ``` 574 575 **Changes to `app/api/rewrite-cv/route.ts`:** 576 577 Add ATS score pre-check and post-check: 578 579 ```typescript 580 // Step 1: Score original CV against job posting 581 const originalAtsScore = await scoreATS(cvText, jobPosting); 582 583 // Step 2: Generate CV rewrite suggestions (with matched keywords injected) 584 const rewrite = await generateRewrite(cvText, targetRole, gaps, jobPosting, { 585 matchedKeywords: originalAtsScore.keywords.matched, 586 missingKeywords: originalAtsScore.keywords.missing, 587 }); 588 589 // Step 3: Apply suggestions to CV text and re-score 590 const rewrittenText = applyRewriteSuggestions(cvText, rewrite.suggestions); 591 const newAtsScore = await scoreATS(rewrittenText, jobPosting); 592 593 // Step 4: If ATS score dropped, filter out harmful suggestions 594 if (newAtsScore.keywordScore < originalAtsScore.keywordScore) { 595 rewrite.suggestions = filterHarmfulSuggestions( 596 rewrite.suggestions, originalAtsScore, newAtsScore 597 ); 598 rewrite.atsWarning = 'Some suggestions were filtered to preserve your ATS score.'; 599 } 600 601 // Return with ATS comparison 602 return { 603 ...rewrite, 604 atsComparison: { 605 before: originalAtsScore.overallScore, 606 after: newAtsScore.overallScore, 607 keywordsBefore: originalAtsScore.keywordScore, 608 keywordsAfter: newAtsScore.keywordScore, 609 } 610 }; 611 ``` 612 613 **Changes to `components/results/CVOptimizerPanel.tsx`:** 614 615 Show ATS score comparison: 616 ``` 617 Before: ATS 62/100 → After: ATS 78/100 (+16 points) 618 Keywords: 45% → 72% (+27%) 619 ``` 620 621 Warn if any suggestion would lower the score. 622 623 ### E3. Keyword Injection in Rewrite Prompt 624 625 **Update `buildCVRewritePrompt` signature:** 626 627 ```typescript 628 export function buildCVRewritePrompt( 629 cvText: string, 630 targetRole: string, 631 gaps: Gap[], 632 jobPosting?: string, 633 knowledgeContext?: string, 634 atsKeywords?: { matched: string[]; missing: string[]; semantic: string[] } 635 ): { system: string; userMessage: string } 636 ``` 637 638 Add to user message: 639 ``` 640 ATS KEYWORD DATA: 641 - MATCHED (MUST PRESERVE): ${atsKeywords.matched.join(', ')} 642 - MISSING (add ONLY if candidate has the skill): ${atsKeywords.missing.join(', ')} 643 - SEMANTIC MATCHES (keep or standardize): ${atsKeywords.semantic.join(', ')} 644 ``` 645 646 --- 647 648 ## 8. Work Stream F: Career Coach Removal 649 650 ### F1. Remove ChatPanel from Results 651 652 **File:** `app/analyze/page.tsx` 653 - Remove ChatPanel import and rendering 654 - Remove any "Career Coach" tab from results 655 656 **File:** `components/results/ChatPanel.tsx` 657 - Keep file (don't delete) but stop importing it 658 - Will be re-enabled for Pro users in quota-gated version later 659 660 **File:** `app/api/chat/route.ts` 661 - Add quota check: `if (quota.plan === 'free') return 403` 662 - Keep endpoint for future Pro access 663 664 ### F2. Remove Chat References from UI 665 666 - Remove chat tab/button from result navigation 667 - Remove any "Ask the coach" CTAs 668 - Clean up i18n keys related to chat/coach for free tier messaging 669 670 --- 671 672 ## 9. Database Migrations 673 674 ### Migration 003: `user_quotas` + `output_tags` + `additional_context` 675 676 **File:** `lib/supabase/migrations/003_quotas_tags_context.sql` 677 678 ```sql 679 -- 1. User Quotas 680 create table if not exists public.user_quotas ( 681 user_id uuid references auth.users(id) on delete cascade primary key, 682 plan text not null default 'free' check (plan in ('free', 'pro')), 683 week_start date not null default (date_trunc('week', now())::date), 684 analyses_used integer not null default 0, 685 cv_generations_used integer not null default 0, 686 cover_letters_used integer not null default 0, 687 coach_requests_used integer not null default 0, 688 analyses_limit integer not null default 1, 689 cv_limit integer not null default 1, 690 cover_letter_limit integer not null default 1, 691 coach_limit integer not null default 0, 692 has_used_initial_analysis boolean not null default false, 693 stripe_customer_id text, 694 stripe_subscription_id text, 695 subscription_status text check (subscription_status in ('active', 'past_due', 'canceled', 'trialing', null)), 696 subscription_period_end timestamptz, 697 created_at timestamptz default now() not null, 698 updated_at timestamptz default now() not null 699 ); 700 701 alter table public.user_quotas enable row level security; 702 create policy "Users read own quotas" on public.user_quotas for select using (auth.uid() = user_id); 703 704 -- Auto-create quotas for new users 705 create or replace function public.create_user_quota() 706 returns trigger as $$ 707 begin 708 insert into public.user_quotas (user_id) values (new.id) on conflict do nothing; 709 return new; 710 end; 711 $$ language plpgsql security definer; 712 713 create trigger on_auth_user_created_quota 714 after insert on auth.users 715 for each row execute function public.create_user_quota(); 716 717 -- Weekly reset (schedule via pg_cron: SELECT cron.schedule('weekly-quota-reset', '0 0 * * 1', 'SELECT reset_weekly_quotas()')) 718 create or replace function public.reset_weekly_quotas() 719 returns void as $$ 720 begin 721 update public.user_quotas 722 set analyses_used = 0, cv_generations_used = 0, 723 cover_letters_used = 0, coach_requests_used = 0, 724 week_start = date_trunc('week', now())::date, 725 updated_at = now() 726 where week_start < date_trunc('week', now())::date; 727 end; 728 $$ language plpgsql security definer; 729 730 -- 2. Output Tags 731 create table if not exists public.output_tags ( 732 id uuid default gen_random_uuid() primary key, 733 user_id uuid references auth.users(id) on delete cascade not null, 734 analysis_id uuid references public.analyses(id) on delete cascade not null, 735 section text not null, 736 element_index integer, 737 element_key text, 738 tagged_text text, 739 tag text not null check (tag in ('accurate', 'inaccurate', 'irrelevant', 'missing_context', 'too_generic')), 740 comment text check (char_length(comment) <= 500), 741 created_at timestamptz default now() not null 742 ); 743 744 alter table public.output_tags enable row level security; 745 create policy "Users CRUD own tags" on public.output_tags for all using (auth.uid() = user_id); 746 create index idx_output_tags_analysis on public.output_tags(analysis_id); 747 create index idx_output_tags_section_tag on public.output_tags(section, tag); 748 749 -- 3. Additional context on career_profiles 750 alter table public.career_profiles add column if not exists additional_context text; 751 ``` 752 753 --- 754 755 ## 10. i18n Keys 756 757 New keys needed across all 6 language files: 758 759 ```json 760 { 761 "profile": { 762 "additionalContext": { 763 "title": "Additional Context", 764 "description": "Help us understand your full story...", 765 "placeholder": "Write about experiences not in your CV...", 766 "examples": { 767 "title": "Examples of useful context:", 768 "careerBreak": "Between 2022-2023 I took a career break...", 769 "freelance": "While freelancing on Upwork (2021-2023)...", 770 "openSource": "I've been contributing to open-source...", 771 "layoff": "After being laid off in 2024...", 772 "hobby": "I have 3 years of hobby game development..." 773 } 774 } 775 }, 776 "quota": { 777 "analysesUsed": "{{used}} of {{limit}} analyses used this week", 778 "cvUsed": "{{used}} of {{limit}} CV generations used", 779 "coverLetterUsed": "{{used}} of {{limit}} cover letters used", 780 "coachUsed": "{{used}} of {{limit}} coach requests used", 781 "freeAnalysis": "Free initial analysis available", 782 "exceeded": "Weekly limit reached", 783 "upgrade": "Upgrade to Pro", 784 "upgradePrompt": "You've used your free analysis this week. Upgrade to Pro for 10 weekly analyses.", 785 "resetInfo": "Resets every Monday at midnight UTC" 786 }, 787 "pricing": { 788 "title": "Simple Pricing", 789 "subtitle": "Start free, upgrade when you need more", 790 "free": { "name": "Free", "price": "$0", "period": "forever" }, 791 "pro": { "name": "Pro", "priceWeekly": "$9.99/week", "priceMonthly": "$29.99/month" }, 792 "features": { ... }, 793 "cta": { "free": "Get Started", "pro": "Upgrade Now" } 794 }, 795 "tags": { 796 "accurate": "Accurate", 797 "inaccurate": "Inaccurate", 798 "irrelevant": "Irrelevant", 799 "missingContext": "Missing Context", 800 "tooGeneric": "Too Generic", 801 "tagThis": "Tag this output", 802 "addComment": "Add a comment (optional)", 803 "saved": "Tag saved", 804 "removed": "Tag removed" 805 }, 806 "cv": { 807 "atsComparison": { 808 "before": "Before: ATS {{score}}/100", 809 "after": "After: ATS {{score}}/100", 810 "improvement": "+{{points}} points", 811 "warning": "Some suggestions were filtered to preserve your ATS score" 812 } 813 } 814 } 815 ``` 816 817 --- 818 819 ## 11. Commit Sequence 820 821 ### Phase 1: Foundation (Commits 1–5) 822 823 | # | Commit Message | Files | Depends On | 824 |---|---------------|-------|------------| 825 | 1 | `feat: add migration 003 — user_quotas, output_tags, additional_context` | `migrations/003_quotas_tags_context.sql` | — | 826 | 2 | `feat: add quota types + quota check/increment helpers` | `lib/types.ts`, `lib/quota.ts` | 1 | 827 | 3 | `feat: add quota API route (GET /api/quota)` | `app/api/quota/route.ts` | 2 | 828 | 4 | `feat: add output tags types + API route (POST/GET/DELETE)` | `lib/types.ts`, `app/api/tags/route.ts` | 1 | 829 | 5 | `feat: add additional_context to CareerProfile type + profile API` | `lib/types.ts`, `app/api/profile/route.ts` | 1 | 830 831 ### Phase 2: Stripe (Commits 6–9) 832 833 | # | Commit Message | Files | Depends On | 834 |---|---------------|-------|------------| 835 | 6 | `feat: add stripe dependency + env config` | `package.json`, `.env.example` | — | 836 | 7 | `feat: add Stripe checkout + webhook + portal API routes` | `app/api/stripe/checkout/route.ts`, `app/api/stripe/webhook/route.ts`, `app/api/stripe/portal/route.ts`, `lib/stripe.ts` | 6 | 837 | 8 | `feat: add pricing page with plan comparison` | `app/pricing/page.tsx` | 7 | 838 | 9 | `feat: gate analysis/CV/cover-letter endpoints behind quota checks` | `app/api/analyze-stream/route.ts`, `app/api/rewrite-cv/route.ts`, `app/api/generate-cover-letter/route.ts`, `app/api/chat/route.ts` | 2, 7 | 839 840 ### Phase 3: Profile Hub (Commits 10–14) 841 842 | # | Commit Message | Files | Depends On | 843 |---|---------------|-------|------------| 844 | 10 | `feat: redesign ProfileTab as Profile Hub with context textarea` | `components/dashboard/ProfileTab.tsx` | 5 | 845 | 11 | `feat: merge AnalysesTab into Profile Hub` | `components/dashboard/ProfileTab.tsx`, `components/dashboard/AnalysesTab.tsx` | 10 | 846 | 12 | `feat: add QuotaBar + UpgradePrompt shared components` | `components/shared/QuotaBar.tsx`, `components/shared/UpgradePrompt.tsx` | 3 | 847 | 13 | `feat: add quota display to Profile Hub + analysis flow` | `components/dashboard/ProfileTab.tsx`, `app/analyze/page.tsx` | 12 | 848 | 14 | `feat: redirect logged-in users to dashboard, first-time to wizard` | `app/dashboard/page.tsx`, `middleware.ts` | 10 | 849 850 ### Phase 4: Output Tagging (Commits 15–18) 851 852 | # | Commit Message | Files | Depends On | 853 |---|---------------|-------|------------| 854 | 15 | `feat: add TaggableToken component with tag popover` | `components/shared/TaggableToken.tsx` | 4 | 855 | 16 | `feat: wrap FitScore + StrengthsPanel + GapsPanel with tagging` | `components/results/FitScore.tsx`, `components/results/StrengthsPanel.tsx`, `components/results/GapsPanel.tsx` | 15 | 856 | 17 | `feat: wrap ActionPlan + RoleRecommendations + LinkedInPlan with tagging` | `components/results/ActionPlan.tsx`, `components/results/RoleRecommendations.tsx`, `components/results/LinkedInPlan.tsx` | 15 | 857 | 18 | `feat: wrap CVOptimizer + CoverLetter + SalaryPanel with tagging` | `components/results/CVOptimizerPanel.tsx`, `components/results/CoverLetterPanel.tsx`, `components/results/SalaryPanel.tsx` | 15 | 858 859 ### Phase 5: CV ATS Fix (Commits 19–21) 860 861 | # | Commit Message | Files | Depends On | 862 |---|---------------|-------|------------| 863 | 19 | `fix: add ATS keyword preservation rules to CV rewrite prompt` | `lib/prompts/cv-rewriter.ts` | — | 864 | 20 | `fix: add pre/post ATS scoring loop to CV rewrite pipeline` | `app/api/rewrite-cv/route.ts` | 19 | 865 | 21 | `feat: show ATS score comparison in CV optimizer panel` | `components/results/CVOptimizerPanel.tsx` | 20 | 866 867 ### Phase 6: Coach Removal + Pipeline Context (Commits 22–24) 868 869 | # | Commit Message | Files | Depends On | 870 |---|---------------|-------|------------| 871 | 22 | `feat: remove Career Coach from free tier results` | `app/analyze/page.tsx`, `components/results/ChatPanel.tsx` | 9 | 872 | 23 | `feat: pass additional_context through analysis pipeline` | `app/api/analyze-stream/route.ts`, `lib/prompts/skill-extraction.ts`, `lib/prompts/gap-analysis.ts` | 5 | 873 | 24 | `feat: add i18n keys for profile, quota, pricing, tags, cv across all 6 languages` | `lib/i18n/translations/*.json` (6 files) | all above | 874 875 ### Phase 7: Verification (Commit 25) 876 877 | # | Commit Message | Files | Depends On | 878 |---|---------------|-------|------------| 879 | 25 | `chore: clean up unused imports, verify build + types` | various | all above | 880 881 --- 882 883 ## 12. Testing Checklist 884 885 ### Agent Output Quality Tests 886 887 - [ ] **FitScore coherence:** Run 5 analyses with same CV + different jobs. Verify: 888 - Score 8+ never appears with critical gaps 889 - Score 9+ never appears with 3+ moderate gaps 890 - Score matches the rubric definitions in `gap-analysis.ts` 891 - [ ] **Gap severity consistency:** Run same analysis 3 times. Verify: 892 - Same skills flagged as gaps each time 893 - Severity doesn't flip between critical and minor across runs 894 - Gap count stays within ±1 across runs 895 - [ ] **Strength hallucination check:** Compare extracted strengths against CV text: 896 - Every strength title should have a corresponding mention in CV/LinkedIn 897 - No fabricated technologies or certifications 898 - [ ] **Action plan validity:** Verify all resource URLs in 30/90/12-month plans: 899 - URLs resolve (HTTP 200) 900 - URLs point to relevant learning resources 901 - No broken links or placeholder URLs 902 - [ ] **Role recommendation realism:** Check that: 903 - Role titles are real job titles (not fabricated) 904 - FitScores are consistent with overall fitScore 905 - Salary ranges are market-realistic for the country 906 - [ ] **CV rewrite ATS check:** For 3 different CVs: 907 - Run ATS score on original 908 - Apply CV rewrite suggestions 909 - Run ATS score on rewritten version 910 - Verify rewritten score ≥ original score (NEVER lower) 911 - Verify no fabricated skills/experience added 912 - [ ] **Cover letter factual accuracy:** Generate 3 cover letters: 913 - All claims match CV content 914 - No hallucinated company names or achievements 915 - Tone matches selected option 916 - [ ] **Salary data accuracy:** Compare against known salary ranges: 917 - Test with US/UK/DE/RO markets 918 - Verify currency matches country 919 - Verify ranges are within ±20% of known benchmarks 920 - [ ] **LinkedIn plan quality:** Verify: 921 - Headline options include relevant keywords 922 - About section references actual experience 923 - Recommended skills match target role 924 - [ ] **Translation quality:** Run analysis in all 6 languages: 925 - Numeric scores preserved exactly 926 - Technical terms not over-translated 927 - UI labels correct in each language 928 929 ### Quota & Payment Tests 930 931 - [ ] **Free tier limits:** Verify: 932 - First analysis succeeds (initial, uncounted) 933 - Second analysis succeeds (1 of 1 weekly) 934 - Third analysis blocked with upgrade prompt 935 - CV generation: 1 allowed, 2nd blocked 936 - Cover letter: 1 allowed, 2nd blocked 937 - Career coach: blocked entirely 938 - [ ] **Pro tier limits:** After Stripe checkout: 939 - 10 analyses in one week succeed 940 - 11th analysis blocked 941 - Same for CV, cover letter, coach 942 - [ ] **Weekly reset:** Verify quotas reset on Monday 00:00 UTC: 943 - Before reset: counters at limit → blocked 944 - After reset: counters at 0 → allowed 945 - [ ] **Stripe checkout flow:** End-to-end: 946 - Click upgrade → Stripe Checkout loads 947 - Complete payment → redirected back 948 - Quotas updated to Pro limits 949 - Subscription status shows "active" 950 - [ ] **Stripe webhook handling:** 951 - Payment success → quota upgrade 952 - Subscription canceled → downgrade to free 953 - Payment failed → mark past_due, still allow existing quota 954 - [ ] **Stripe portal:** Customer can manage subscription, change plan, cancel 955 956 ### Output Tagging Tests 957 958 - [ ] **Tag creation:** Click token → select tag → verify saved in DB 959 - [ ] **Tag display:** Refresh page → tags still visible with correct colors 960 - [ ] **Tag update:** Click tagged token → change tag → verify updated 961 - [ ] **Tag deletion:** Click tagged token → remove → verify deleted 962 - [ ] **Tag on all components:** Test tagging works in every result panel 963 - [ ] **Multi-tag:** Apply different tags to different tokens in same analysis 964 - [ ] **Comment:** Add comment to tag → verify saved and displayed 965 - [ ] **Cross-analysis isolation:** Tags from analysis A don't appear in analysis B 966 967 ### Profile Hub Tests 968 969 - [ ] **First-time user:** Logged in, no profile → redirected to wizard 970 - [ ] **Returning user:** Profile exists → shows Profile Hub 971 - [ ] **Context textarea:** Type → auto-saves → refresh → content preserved 972 - [ ] **File management:** Upload CV → shows filename → replace → shows new filename 973 - [ ] **Quick analysis:** Enter job desc → click Run → analysis starts with stored profile 974 - [ ] **Analysis history:** Shows past analyses → click → loads results 975 - [ ] **Quota display:** Shows correct usage numbers 976 977 ### Build & Integration Tests 978 979 - [ ] `npx tsc --noEmit` — zero new errors 980 - [ ] `npm run build` — clean production build 981 - [ ] `npx vitest run` — all tests pass 982 - [ ] Lighthouse score ≥ 80 on dashboard page 983 - [ ] All API routes return proper error codes (400, 401, 403, 429, 500) 984 - [ ] No console errors in browser during full user flow 985 986 --- 987 988 ## 13. Security & Privacy Audit Checklist 989 990 ### Authentication & Authorization 991 992 - [ ] **All new API routes require auth:** `/api/quota`, `/api/tags`, `/api/stripe/*` (except webhook) 993 - [ ] **Webhook signature verification:** Stripe webhook validates `stripe-signature` header 994 - [ ] **RLS on new tables:** `user_quotas` (read-only for users), `output_tags` (full CRUD for owner only) 995 - [ ] **No cross-user data leakage:** User A cannot read User B's tags, quotas, or analyses 996 - [ ] **Admin endpoints protected:** `/api/admin/export-tags` requires admin role check 997 - [ ] **Session token validation:** All authenticated routes call `getAuthenticatedClient(req)` 998 999 ### Data Privacy 1000 1001 - [ ] **CV/LinkedIn PDFs:** Stored in private Supabase Storage with per-user folder RLS 1002 - [ ] **Additional context:** Stored in `career_profiles` table with existing RLS 1003 - [ ] **No PII in logs:** Verify `logger.*` calls never log CV text, user names, or email addresses 1004 - [ ] **No PII in error responses:** API errors return generic messages, never internal details 1005 - [ ] **Data deletion:** User can delete: 1006 - [x] Career profile (existing) 1007 - [x] Individual analyses (existing) 1008 - [ ] Output tags (new — verify cascade on analysis delete) 1009 - [ ] Account + all data (GDPR — needs implementation) 1010 - [ ] **Stripe PCI compliance:** Never store card numbers server-side (Stripe Checkout handles this) 1011 - [ ] **Additional context sanitization:** Validate max 5000 chars, strip potential injection markers 1012 1013 ### Input Validation & Injection Prevention 1014 1015 - [ ] **Prompt injection:** Additional context is wrapped in boundary markers (`---CONTEXT START---`/`---END---`) with defense instructions 1016 - [ ] **SQL injection:** All DB queries use parameterized Supabase client (never raw SQL) 1017 - [ ] **XSS prevention:** `TaggableToken` renders user text via React (auto-escaped), never `dangerouslySetInnerHTML` 1018 - [ ] **CSRF protection:** Stripe webhook uses signature verification, all other routes use Bearer token 1019 - [ ] **Rate limiting on new endpoints:** `/api/tags` (50/hour), `/api/quota` (100/hour), `/api/stripe/*` (20/hour) 1020 1021 ### Payment Security 1022 1023 - [ ] **Stripe secret key:** Only in server-side env (`STRIPE_SECRET_KEY`), never in `NEXT_PUBLIC_*` 1024 - [ ] **Webhook secret:** Stored as `STRIPE_WEBHOOK_SECRET` env var, never logged 1025 - [ ] **Idempotent webhooks:** Same event processed twice produces same result (no double-charge or double-upgrade) 1026 - [ ] **Subscription validation:** Before granting Pro access, verify subscription is `active` (not just `created`) 1027 - [ ] **Downgrade handling:** When subscription canceled, quotas revert to free tier at period end 1028 - [ ] **No free tier bypass:** Guest users (no auth) get rate limiting only, no quota (can't run analysis without account) 1029 - [ ] **Price manipulation:** Stripe Checkout uses server-side price IDs (not client-supplied amounts) 1030 1031 ### Infrastructure 1032 1033 - [ ] **Environment variables:** New vars added to Vercel dashboard: 1034 - `STRIPE_SECRET_KEY` 1035 - `STRIPE_PUBLISHABLE_KEY` 1036 - `STRIPE_WEBHOOK_SECRET` 1037 - `STRIPE_PRICE_WEEKLY` 1038 - `STRIPE_PRICE_MONTHLY` 1039 - [ ] **Database migration applied:** Run migration 003 on Supabase dashboard 1040 - [ ] **pg_cron scheduled:** Weekly quota reset: `SELECT cron.schedule('weekly-quota-reset', '0 0 * * 1', 'SELECT reset_weekly_quotas()')` 1041 - [ ] **Stripe webhook URL configured:** `https://yourdomain.com/api/stripe/webhook` in Stripe dashboard 1042 - [ ] **`.env.local` in `.gitignore`:** Verified not tracked in git 1043 - [ ] **API key rotation:** Anthropic API key rotated if previously exposed 1044 1045 ### Monitoring & Alerting 1046 1047 - [ ] **Stripe webhook failures:** Monitor Stripe dashboard for failed deliveries 1048 - [ ] **Quota abuse detection:** Log when users hit quota limits repeatedly 1049 - [ ] **Error rate monitoring:** Track 500 errors on new endpoints 1050 - [ ] **Payment success rate:** Alert if payment success rate drops below 95% 1051 1052 --- 1053 1054 ## Appendix: File Inventory 1055 1056 ### New Files 1057 1058 ``` 1059 lib/quota.ts 1060 lib/stripe.ts 1061 lib/supabase/migrations/003_quotas_tags_context.sql 1062 app/api/quota/route.ts 1063 app/api/tags/route.ts 1064 app/api/stripe/checkout/route.ts 1065 app/api/stripe/webhook/route.ts 1066 app/api/stripe/portal/route.ts 1067 app/api/admin/export-tags/route.ts 1068 app/pricing/page.tsx 1069 components/shared/TaggableToken.tsx 1070 components/shared/QuotaBar.tsx 1071 components/shared/UpgradePrompt.tsx 1072 ``` 1073 1074 ### Modified Files 1075 1076 ``` 1077 lib/types.ts 1078 lib/prompts/cv-rewriter.ts 1079 lib/prompts/skill-extraction.ts 1080 lib/prompts/gap-analysis.ts 1081 app/api/analyze-stream/route.ts 1082 app/api/rewrite-cv/route.ts 1083 app/api/generate-cover-letter/route.ts 1084 app/api/chat/route.ts 1085 app/api/profile/route.ts 1086 app/analyze/page.tsx 1087 app/dashboard/page.tsx 1088 components/dashboard/ProfileTab.tsx 1089 components/dashboard/AnalysesTab.tsx 1090 components/results/FitScore.tsx 1091 components/results/StrengthsPanel.tsx 1092 components/results/GapsPanel.tsx 1093 components/results/ActionPlan.tsx 1094 components/results/RoleRecommendations.tsx 1095 components/results/LinkedInPlan.tsx 1096 components/results/CVOptimizerPanel.tsx 1097 components/results/CoverLetterPanel.tsx 1098 components/results/SalaryPanel.tsx 1099 middleware.ts 1100 package.json 1101 .env.example 1102 lib/i18n/translations/en.json 1103 lib/i18n/translations/ro.json 1104 lib/i18n/translations/de.json 1105 lib/i18n/translations/fr.json 1106 lib/i18n/translations/es.json 1107 lib/i18n/translations/it.json 1108 ```