/ 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  ```