📋 Master Handover Document

Everything you need to pick up where we left off

Technical reference for Yarashi. Platform status, architecture, open issues, decision log, commit history, config and credentials, next steps.

Updated 23 Jun 2026
Owner Kevin Delaney
Repo agilefoundry/agf-job-match
🌿 Root to Fruit →
Overview Status Architecture Sprints Open Issues Decisions Commits Config Next Steps Split Plan Commits

Yarashi — Master Handover Document

Last updated: 20 June 2026 · Owner: Kevin Delaney · kevin@theagilefoundry.es

Overview

Single source of truth for the Yarashi platform build

Yarashi is an AI career intelligence SaaS platform targeting the Australian skilled-migrant job market. It is built as a solo project by Kevin Delaney under The Agile Foundry (ABN 60 303 086 105), operating from Sydney (Rouse Hill/Box Hill, NSW).

Platform URL
yarashi.com.au
Repo
agilefoundry/agf-job-match
Local path
C:\Users\imcte\Desktop\agf-job-match
Main file
app/platform/page.jsx
Admin account
kevin@theagilefoundry.es
Admin UUID
7e5282f6-7e5b-4c3e...
Test account
celticboxingcoach@gmail.com
Status
Beta — no paying customers

Critical rule: NEVER modify app/platform/page.jsx by downloading or rewriting from Claude outputs. Only surgical Python in-place string replacements, or upload the current deployed file for Claude to work on. Full file replace has broken the platform multiple times.

Platform Status

As of 20 June 2026
13
Sprint 0 done
1
Sprint 0 left
11
Sprint 1 done
10
S1 code items
0
Paying users

Tech Stack

LayerTechnologyNotes
FrameworkNext.js 14.2.29App Router. 20 CVEs — upgrade to 16 blocked on staging environment
FrontendReact, Tailwind CSSSingle 13,000-line page.jsx — component split is priority work
DatabaseSupabase (PostgreSQL)sdhmresojnwuvtlyoohs.supabase.co
AuthSupabase AuthEmail/password. TOTP MFA enabled at project level
AIAnthropic Claude APIclaude-sonnet-4-5, claude-haiku-4-5. 30-day data retention (ON). PII stripping active.
BillingStripeWebhook live. Customer portal live. 9 plan keys.
Job APIsAdzuna + JoobleDual feed. posted_at captured. 2-week scoring cutoff active.
EmailResendTransactional email. Welcome email route live.
HostingVercel ProAuto-deploy from main branch. CSP headers active.
DNSVentraIPyarashi.com.au, agilefoundry.com.au, celticboxingcoach.com
MonitoringUptimeRobot (free)3 monitors: yarashi.com.au, /platform, project.yarashi.com.au

Key File Paths

FilePurpose
app/platform/page.jsxEntire platform UI — 13,000+ lines. DO NOT replace wholesale.
lib/stripe.tsStripe client, PLANS definition, getOrCreateStripeCustomer
lib/rateLimit.tsRate limiting, ADMIN_BYPASS_IDS (admin UUID only)
lib/piiStrip.tsPII stripping utility — applied to all AI routes
lib/supabase.tsSupabase admin client (service role)
app/api/webhook/route.tsStripe webhook handler with signature verification
app/api/sync-jobs/route.tsManual job sync — CRON_SECRET protected
app/api/cron/sync-jobs/route.tsAutomated job feed — Vercel cron 08:00 UTC daily
app/api/cron/data-retention/route.tsData cleanup — Vercel cron 02:00 UTC daily
app/privacy/page.jsxPrivacy Policy — Anthropic subprocessor, PII stripping, ADM disclosure
app/terms/page.jsxTerms of Service — LinkedIn data warrant
vercel.jsonFunction timeouts, cron schedules, CSP security headers

Architecture

Dual Job Search System

JobFeed (passive): Vercel cron runs daily at 08:00 UTC. Fetches from Adzuna + Jooble per user saved role preferences. Stores to job_feed table. Auto-scores jobs posted within 2 weeks using one batched Claude call per user. Max 20 jobs per user per sync.

JobIntelligence (active): User-triggered search at runtime. Also fetches from Adzuna + Jooble. Uses role_alias as query, not all keywords joined. These two systems are intentionally complementary — the June 2026 consolidation plan was retired.

Billing Flow

  1. User clicks upgrade → /api/stripe/checkout creates Stripe Checkout Session with metadata: { userId, plan }
  2. User completes payment on Stripe-hosted page
  3. Stripe fires checkout.session.completed to /api/webhook
  4. Webhook verifies signature, reads metadata, calls updateUserPlan() → writes to profiles.plan in Supabase
  5. On cancellation: customer.subscription.deleted fires → revertToFree()

PII Stripping Flow

Before CV text is sent to the Anthropic API, lib/piiStrip.ts strips: name (first line), email, phone, address, postcode, LinkedIn URL. These are replaced with placeholders like [NAME], [EMAIL], [PHONE]. Applied to: cover-letter, build-cv, skills-gap, interview-prep, linkedin-optimiser, match-cv routes.

Rate Limiting

Server-side rate limiting in lib/rateLimit.ts. Per-tool limits per plan tier defined in lib/stripe.ts PLANS object. Admin account (ADMIN_BYPASS_IDS) bypasses all limits. Currently one entry: Kevin's admin UUID.

Supabase Cron Functions

FunctionSchedulePurpose
/api/cron/sync-jobs08:00 UTC dailyJob feed refresh + auto-scoring for new jobs ≤14 days old
/api/cron/data-retention02:00 UTC dailyEnforce retention policy, schedule inactive account deletion

Sprint Status

As of 20 June 2026

Sprint 0 — Security & Foundation (13/14)

CardTitleStatus
s0-1Stripe portal route filenameDone
s0-2Staging environmentDone 22 Jun
s0-3AuthScreen hydration fixDone 22 Jun (parked)
s0-4Verify Job Search/Role Match state safetyDone
s0-5Capture posted_at from Adzuna/JoobleDone
s0-62-week auto-scoring cutoff in cronDone
s0-7Deprecated meta tag + localStorage keyDone
s0-8Supabase account 2FADone
s0-92FA in project board auth flowDone
s0-10Secure /api/sync-jobsDone
s0-11Stripe webhook handlerDone
s0-12Rotate CRON_SECRETDone
s0-13SUPABASE_SERVICE_ROLE_KEY confirmed cleanDone
s0-14ADMIN_BYPASS_IDS verifiedDone

Sprint 1 — Quick Wins (11/13 done, 10 code items remaining)

CardTitleStatus
s0-24lib/locationResolver.ts deployedDone 21 Jun
s1-1Build lib/ai.ts model assignment layerPending
s1-2Confirm and downgrade model assignmentsPending
s1-7npm auditBlocked on staging
s1-8CSP and security headersDone
s1-9Auth brute-force limits verifiedDone
s1-10Anthropic API logging auditedDone
s1-11Data retention route verifiedDone
s1-12PII stripping before Claude API callsDone
s1-15Privacy Policy — Anthropic subprocessorDone
s1-18LinkedIn data warrant in ToSDone
s1-19ADM disclosure for Privacy Act 2024Done
s1-20Jooble commercial terms reviewedDone
s1-23Uptime monitoringDone
s1-25Beta exit criteria documentDone

20 June 2026 Session

Security hardening, legal compliance, project board overhaul

Security Changes

  • Stripe webhook — replaced 4-line stub with full handler: signature verification, plan updates on checkout/subscription events, revert-to-free on cancellation
  • Stripe portal route — renamed from route (31).ts to route.ts so Next.js can see it
  • /api/sync-jobs — added CRON_SECRET Bearer token auth to GET and POST
  • CRON_SECRET rotated — new value in Vercel only
  • Supabase account 2FA — TOTP MFA enabled at app.supabase.com
  • CSP headers — added to vercel.json covering all routes
  • PII strippinglib/piiStrip.ts created and applied to 6 AI routes

Legal & Compliance

  • Privacy Policy: Anthropic 30-day data retention disclosed
  • Privacy Policy: Full ADM disclosure (5 tools) for Privacy Act 2024 — ahead of Dec 2026 deadline
  • Privacy Policy: PII stripping disclosed
  • Terms of Service: LinkedIn PDF upload warrant added
  • Jooble commercial terms reviewed — clean, no restrictions

Project Board

  • Full-page TOTP 2FA auth gate — board hidden until signed in
  • Sign out button in topbar
  • Nav links across all three pages (board ↔ risk register ↔ beta exit ↔ handover)
  • Card IDs visible on every card
  • Status visual treatment — done/in progress/won't do with completion dates
  • Beta exit criteria live at project.yarashi.com.au/beta-exit
  • Fixed pullFromSupabase bug — card data was accidentally embedded inside function body

Open Issues

Not regressed — carried forward from earlier sessions

AuthScreen hydration errors #418/#423/#425 — still firing in production. Root cause: success state (line 10943 in page.jsx) uses typeof window === 'undefined' check that evaluates differently SSR vs client. Fix pattern known (initialize to '', move URL check to useEffect). Dedicated session required — s0-3.

CVTailorModule Regenerate button ReferenceError — bare job variable out of scope in the Regenerate handler. All users are affected — cannot regenerate CV tailoring results.

Manual CV Rewrite silent failurecreate-manual succeeds, build-cv never fires. Correlated with hydration errors.

Next.js CVEs — 20 vulnerabilities in Next.js 14 (high + moderate). Fix requires 14→16 upgrade (breaking change). Blocked on s0-2 staging environment. Do not run npm audit fix --force on production.

page.jsx component split — 13,000+ lines. The largest single technical debt item. Must be split before any significant new UI work. This is the prerequisite for adding PII stripping UI badge, 2FA option, and other user-facing improvements cleanly.

Adzuna commercial use — must confirm in writing with Adzuna before first paying customer. Beta risk accepted.

locationResolver.ts deployed - verify next sync — Jooble was returning US results (Jamestown ND) because pref.location was null. Canonical library deployed commit c87f9af with Country-first format across 57 locations. Verify next cron sync returns Sydney NSW roles only.

saved_jobs cleanup done 21 Jun — 100+ duplicate test entries removed via SQL. Real saved jobs from 13 June intact. No dedup constraint exists yet - monitor per-user counts.

Project board static HTML rule — docs deploy dd4da22 caused 404 on entire board by adding Next.js folders to static HTML repo. Reverted. Rule enforced: yarashi-project is static HTML only, no app/ or pages/ directories ever.

Admin new-user notification email (23 Jun 2026) — /api/email/admin-notify/route.ts created. Fires non-blocking from app/api/auth/signup/route.ts alongside welcome email on every new signup. Sends branded HTML email to kevin@theagilefoundry.es with user name, email, plan, total user count, AEST timestamp, and Supabase quick link. Commits: 98ebcf6 (route), 050df04 (signup patch). Tested and confirmed working.

Signout crash fixed (23 Jun 2026) — handleSignOut was clearing authUser to null and wiping all agf_ localStorage keys but not redirecting — React tried to re-render the entire platform with null state, causing error #300 which cascaded into #418/#423/#425. Fix: handleSignOut is now async, calls /api/auth/signout to invalidate Supabase session server-side, then hard redirects to yarashi.com.au. Commit: 5a4c36f.

Signout error flash fixed (23 Jun 2026) — After the crash fix, a 2-second error flash remained because React still attempted one re-render before the navigation completed. Fix: isSigningOut useRef added, setMounted(false) called at the very start of handleSignOut — React immediately renders the loading screen instead of the platform, error boundary never triggered. Commit: bf95f1f.

Onboarding screen re-appearing on every login (23 Jun 2026) — handleSignOut wiped all agf_ keys including agf_onboarding_seen. On next login, profile fetch hadn't completed before the onboarding check fired, so it showed regardless of onboarding_complete in Supabase. Fix: agf_onboarding_seen, agf_lang, agf_country, agf_ip_detected now preserved across signout — these are user preferences, not session data.

Protocol breach recorded (23 Jun 2026) — Three fixes (signout crash, signout flash, admin-notify) were pushed directly to main before staging was tested. Staging was 4 commits behind main at session end. Root cause: pressure to fix live crash overrode the staging-first rule. Corrected by merging main into staging (fast-forward). New non-negotiable protocol documented in Next Steps. Will not happen again.

page.jsx split plan locked (23 Jun 2026) — Full 25-session execution plan documented in handover. Triple-checked against live 13,205-line file. War room conducted with three AI engineers (network, security, data). 5 risks identified, 5 protocol additions agreed. Unanimous go — Session 1 clear to execute. See split plan section below.

— staging branch created in agf-job-match. Vercel auto-deploys previews. staging.yarashi.com.au CNAME added via VentraIP DNS -> cname.vercel-dns.com. Closes R-006 (no staging). All future feature work develops on staging first. For Next.js major upgrades, a full Supabase staging DB will also be needed (s6-40).

/api/account/delete merged to main (23 Jun 2026) — POST schedules deletion (deletion_requested_at + deletion_scheduled_at = now+30d). GET returns status. DELETE cancels during grace period. Supabase migration complete. Both UI buttons wired. Built on staging, tested, merged to main. Closes s0-19 and bug-6 beta exit criterion.

React hydration errors eliminated (22 Jun 2026) — 13 commits to fix #418/#423/#425. Root cause: useState lazy initialisers reading localStorage during SSR. All fixed across page.jsx, AuthScreen, JobFeed, AdminFeatureBoard, RolePrefsPanel, MyHub. force-dynamic added. Landing page @import font broken fixed. Profile 14% flash fixed.

Hydration errors #418/#423/#425 - PARKED — After 13 commits eliminating all identifiable localStorage SSR mismatches, errors persist. Root chunk fd9d1056 is React itself (react-dom 18.3.0-canary). The errors are coming from React's hydration reconciler detecting tree differences. Decision: park this, move forward. Will be resolved as part of Next.js 14->15->16 upgrade which switches to React 19 with a redesigned hydration system. Does not affect platform functionality.

pending: userPlan + jobs lazy inits — Two final lazy initialisers found in main component (lines 11508 + 11594). Fixed in page_final.jsx. Deploy pending. These may be the last source of #425 errors.

CV Tailor create-manual -> build-cv fixed — commit 2d0ae06. forceNew=true on manual job generate button bypasses localStorage cache. Previously cached result from same job title caused early return before build-cv fired.

Decision Log

DecisionRationaleDate
Anthropic 30-day retention left ONBeta, no paying customers. ZDR requires support request. Revisit before first paying customer.20 Jun
npm audit fix deferredNext.js 14→16 is breaking change. Must go through staging first.20 Jun
Sentry moved to backlogKevin's preference. Must-have in beta exit criteria before first paying customer.20 Jun
Jooble terms acceptedClean, no commercial restrictions. No action ever needed.20 Jun
Platform 2FA deferredConsumer product — mandatory is too much friction. Optional is right for job search tool.20 Jun
PII stripping UI badge deferredAdding to 13,000-line page.jsx is inadvisable. Build during component split.20 Jun
Dual job system retainedJobFeed (passive cron) and JobIntelligence (active search) are complementary. June consolidation plan retired.Jun
AuthScreen hydration deferredSticky issue — reserved for dedicated session. s0-3.Jun

Commit Log -- 20-22 June 2026

cf2188blegal: disclose PII stripping before Anthropic API calls in Privacy Policy
6dd9c9bsecurity: strip PII from CV text before all Claude API calls (s1-12)
1ffb53asecurity: add CSP and security headers via vercel.json (s1-8)
d1e2163legal: add LinkedIn data warrant to Terms of Service (s1-18)
98b4246legal: add full ADM disclosure for Privacy Act 2024 compliance (s1-19)
bfcc0d9legal: disclose Anthropic 30-day data retention in Privacy Policy (s1-15)
5499d7afix: implement Stripe webhook handler with signature verification and plan updates
23fc4a6feat: limit auto-scoring to jobs posted within 2 weeks (s0-6)
61b56d5feat: capture posted_at from Adzuna (created) and Jooble (updated) at ingestion
be09a30fix: rename yarashi_ip_detected localStorage key to agf_ip_detected
befd0adfix: rename Stripe portal route file so Next.js can see it
21 Jun-- 10-hour session --
8cced28feat: canonical locationResolver.ts - single source of truth for all job API integrations (Adzuna + Jooble + future)
c87f9affix: strip non-ASCII from locationResolver causing webpack syntax error at build
0a2ddccdocs: add complete project hub docs.html to yarashi-project (project board)
sprint-bdfeat: add 22 new sprint cards - expanded scope (21 Jun 2026) - 115 to 137 cards
22 Jun-- Hydration elimination session (13 commits) --
2d0ae06fix: CV Tailor create-manual forceNew=true, duplicate @import removed, create-manual errors logged
0fd24ddfix(landing): font @import -> link tags, lang/country SSR hydration on yarashi.com.au
2159be8fix(hydration): NewUserChecklist localStorage -> useState + useEffect
6e721ecfix(ssr): authUser/isAuthenticated/userCV/lang window guards added
618b0a9fix(hydration): AuthScreen lang/country selects -> controlled components
cf5f1defix(hydration): suppressHydrationWarning on platform root div
aee458cfix(hydration): JobFeed savedSearches lazy init -> useState([]) + useEffect
cd1ac9ffix(hydration): AdminFeatureBoard features, RolePrefsPanel userId, JobFeed
3097992fix(ssr): force-dynamic to disable SSR on /platform route entirely
8d9d992fix: profile completeness 14% flash + ProfileModule localStorage lazy init
pendingfix(hydration): MyHub.jsx missing use client directive added
fc01443fix(hydration): userPlan + jobs lazy inits in main component
d0f8a69fix(hydration): MyHub.jsx missing use client + Boolean(userId) mismatch
96684e6fix(landing): footer year new Date() -> 2026, add force-dynamic on landing page
Session end-- Architecture planning --
22 Jun-- Staging + account deletion session --
stagingfeat: staging branch created in agf-job-match repo, staging.yarashi.com.au CNAME configured in VentraIP DNS
sql-delfeat: Supabase migration — deletion_requested_at and deletion_scheduled_at columns added to profiles table
s0-19feat: /api/account/delete — POST schedules (30d grace), GET returns status, DELETE cancels. Privacy tab + Danger Zone wired. Tested on staging.
23 Jun-- Admin notify + signout fixes session --
98ebcf6feat: /api/email/admin-notify/route.ts — branded HTML email to kevin@theagilefoundry.es on every new signup with user name, email, plan, total user count, AEST timestamp, Supabase link
050df04fix: signup route now fires welcome email + admin-notify non-blocking on every new user registration
5a4c36ffix: signout crash (#300/#418/#423/#425) — handleSignOut now async, calls /api/auth/signout, hard redirects to yarashi.com.au preventing null authUser re-render
bf95f1ffix: signout 2-second error flash — isSigningOut ref + setMounted(false) shows loading screen immediately before state clears, eliminates error boundary trigger
16 Jul pmfix(s0-28): preserve agf_onboarding_done + agf_started_dismissed across signout — the earlier preserve-list fix shipped the WRONG KEY (agf_onboarding_seen), which is why onboarding kept re-appearing. OnboardingFlow.jsx reads/writes agf_onboarding_done. One line. Verified staging then prod. s0-28 CLOSED
e3ba69estyle(onboarding): restore deep navy palette to OnboardingFlow modal + swap baked-background /logo_Y.png for transparent LogoSVG (v7 sweep damage — cream text rgba(226,224,213,.65) was stranded on a white card)
99e4970feat(auth): show/hide password toggle on AuthScreen password fields (shared state drives both password and confirm on the reset tab)
ecff8d8style(auth): restore deep navy site palette to AuthScreen — 24 colour edits. Removed orphan boxShadow duplicate keys. Cream text at lines 622/812 needed no change: it was correct all along, just stranded on white
d9f8896fix(auth): remove SplashGrabWidget render call from AuthScreen (definition left intact — it is exported; see s0-46)
18b785cchore: gitignore session patch scripts and delivery zips
739055fRevert Session-1 deviation ceb79a5 (Chips extracted out of locked order) — realigned to plan
14ca6a8page.jsx split Sessions 1-25 COMPLETE: 780,660B monolith -> 112KB shell + 24 modules. Named exports. Sandbox-verified (per-file compile + whole-tree bundle) + no-undef lint sweep
HOTFIX1_HASHSplit hotfix 1: CV_LIMIT import in exports.js; LOCATIONS_BY_REGION relocated to ui/Shared (cycle avoidance). Found via Vercel build failure — runtime gate was skipped; lesson recorded
4105cd5s8-2 phase 1: Next.js 14.2 -> 15, React 18 -> 19 (app code pre-scanned clean)
b6cd9fds8-2: Stripe pair react-stripe-js@6 + stripe-js@9 for React 19 peers (fixes Vercel ERESOLVE)
362db20s8-2: lazy-init Stripe client — kills the /api/stripe/* build-time collection failure (latent pre-existing bug, misdiagnosed all night as a benign env error — see R-064)
75eb719s8-2 fix: strip UTF-8 BOM from lib/stripe.ts (PS5.1 artifact). PROMOTED: Next 15 + React 19 to production (main ff to 75eb719)
3a13499s8-2 phase 2: Next.js 16.2.10 LTS, Turbopack default, middleware.ts -> proxy.ts rename. Staging verified then PROMOTED to production 12 Jul

Config & Credentials

All secrets are in Vercel environment variables only — never in source code

Never commit secrets to the repo. All keys live in Vercel → yarashi-project → Settings → Environment Variables.

Required Environment Variables

VariableWhereNotes
NEXT_PUBLIC_SUPABASE_URLVercelPublic — safe in frontend code
NEXT_PUBLIC_SUPABASE_ANON_KEYVercelPublic — safe in frontend code
SUPABASE_SERVICE_ROLE_KEYVercel onlyBypasses all RLS — never commit
ANTHROPIC_API_KEYVercel only
STRIPE_SECRET_KEYVercel only
STRIPE_WEBHOOK_SECRETVercel onlywhsec_… from Stripe dashboard
CRON_SECRETVercel onlyRotated 20 June 2026. Auth for /api/sync-jobs
ADZUNA_APP_IDVercel only
ADZUNA_APP_KEYVercel only
JOOBLE_API_KEYVercel only
RESEND_API_KEYVercel only
NEXT_PUBLIC_APP_URLVercelhttps://yarashi.com.au

Supabase RPC Functions Required

FunctionCalled byStatus
enforce_data_retention()data-retention cronConfirmed exists
schedule_inactive_account_deletion()data-retention cronConfirmed exists

Stripe Webhook

Endpoint: https://www.yarashi.com.au/api/webhook
Events: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted

Next Steps

Updated 23 Jun 2026 — page.jsx split plan locked

Immediate — do these first

  1. Platform baseline COMPLETE (12 Jul 2026) — split (25/25), Next 15 + React 19, Next 16.2.10 (Turbopack, proxy.ts) ALL LIVE IN PRODUCTION. Stripe latent build bug fixed (lazy-init). s0-27/s0-28 confirmed NOT fixed by React 19 — real fixes now Batch 1. EXECUTION BATCHES (locked 12 Jul): B1 Stabilise (~17e): s0 bug cluster incl. signout + onboarding real fixes, dead-code s0-29..34, lint-in-CI, SupportBot extraction s8-5. B2 Protect (~30e): s1 security/cost core, s11-7/8/9 cost framework + cache double-billing + instrumentation, s12-4 sign-up gate, s6-15 2FA, s1-22 Sentry, s6-13 uptime. B3 AI cost engine (~22e): s1-26/27/28/29 cache + CV versioning, s1-1 lib/ai.ts, s1-3 Haiku eval, s6-4 prompt caching. B4 UX & trust (~30e): s2 accessibility/onboarding/mobile/observability + s6-9/10/17. B5 Oracle & CV Maestro (s3, 83e): decisions s3-2/3 first, phases 1-7, renderer, vault, destination engine, taxonomy, market intel, roadmap, learning navigator. B6 Engagement & growth (~60e): s4 alerts/digest + s11 health score, timeline, free match hook, mobile-first. B7 Platform expansion (~90e): s7 companion, s10 inbox, s12 benchmarking/company intel/cost dashboard. B8 Strategic bets (~100e): s13 autonomous agent + employer product, s6 China/i18n/team plan, s9 AU-specific. CLOSED AS SUPERSEDED: s5-6, s5-8, s5-9, s6-1 (~42e). DUPLICATES MERGED: s6-12->s1-22, s6-14->s2-12, s4-7->s1-13. Effective remaining ~585e.
  2. Next.js 14 → 15 → 16 upgrade — After split complete. Staging only. Build s6-40 (full Supabase staging DB) before starting. React 19 will resolve the hydration-related crashes at the root.
  3. Bug fixes — Signout crash, onboarding flash, and all other outstanding bugs — after Next.js upgrade. Many are caused by the same hydration/null-state pattern that React 19 redesigned. Fixing in a 13,000-line monolithic file before upgrade is high risk and fixes may not survive the upgrade.

Decision logged 23 Jun 2026. The signout crash and onboarding flash bugs are deliberately deferred. They are caused by the same hydration/localStorage pattern that Next.js 15+ and React 19 address at the framework level. Attempting to fix them now in the monolithic file adds risk with low durability. Fix after upgrade.

Protocol additions from war room (mandatory before Session 1)

  1. P-01 — Encoding check every session: After every git diff, before git push — scan for â€" /  / . Any hit: abort, restore via git checkout, rewrite in Python with encoding='utf-8' newline='\n'.
  2. P-02 — Session 7 atomic commit: Stage all four non-contiguous Shared.jsx blocks locally. Single git add -A and one commit. No partial pushes.
  3. P-03 — Before Session 13: Grep agf_ localStorage keys across CVMatchEngine, CVTailorModule, InterviewPrep, CoverLetterBuilder. Confirm no key name divergence.
  4. P-04 — Session 24: Post-extraction sign in as non-admin, confirm admin panel inaccessible before committing.
  5. P-05 — Before Sprint 8: Build s6-40 full Supabase staging DB before any Next.js upgrade session.

Session protocol — non-negotiable from 23 Jun 2026

Staging first, always. No exceptions — not for crash fixes, not for one-liners, not under pressure. git checkout staging is the first command of every session.

Check diff before every commit. git diff after every patch, read it fully, confirm only intended lines changed before git add.

Encoding check on every diff. Scan for corruption before committing.

One change per commit. No bundling multiple fixes. Each fix is its own commit.

Verify branch sync after every session. End every session with git log staging..main --oneline. If not empty, sync before closing.

Test checklist on staging before merging to main. Page loads, sign in works, changed feature works, no new console errors, sign out clean.

Before First Paying Customer (unchanged)

  • Fix CVTailorModule Regenerate ReferenceError
  • Install Sentry — currently blind on production errors
  • Confirm Adzuna commercial use in writing
  • Test Stripe billing end-to-end with a real test card
  • Upgrade Next.js 14 → 15 → 16 (after split complete)

See Beta Exit Criteria for the full checklist.

page.jsx Component Split — Full Execution Plan

Locked 23 Jun 2026 · Verified against live file (13,205 lines / 762KB) · 25 sessions

Non-negotiable rules. Staging branch only — never work directly on main. One extraction per session — no combining. Zero logic changes — pure file reorganisation only. Always run git diff before every push. Never replace page.jsx with a full file copy — surgical edits only.

Why this order matters. Dependencies flow downward — a file can only be extracted after everything it calls already exists as a standalone import. Utils go first (no JSX, no React, nothing can break). Shared UI components go second (used by multiple modules). Feature modules go last, ordered by their own cross-dependencies. The shell is cleaned up in the final session once every import resolves.

Import alias. Use @/ throughout — already confirmed working in the live file (@/components/JobFeed etc). Maps to the app/ root. New paths: @/utils/colors, @/components/ui/Toast etc. Never use relative paths like ../../utils.

Pre-Session Checklist (run before starting any session)

# 1. Confirm you are on staging — never on main
git branch

# 2. Pull latest staging
git pull origin staging

# 3. Get current live line count (shifts after each session — never trust the plan's numbers)
(Get-Content app\platform\page.jsx).Count

# 4. Grep for the function you are about to extract to get its real current line
Select-String -Path app\platform\page.jsx -Pattern "^function ComponentName|^const ComponentName"

# 5. Verify staging.yarashi.com.au is live and the previous session's module still works
# Open browser, sign in, click through the module extracted last session

Per-Session Protocol (repeat exactly, every session)

1.  git checkout staging
2.  git pull origin staging
3.  CREATE the new file — copy extracted lines verbatim, add export keywords, add required imports at top
4.  ADD the import line at the top of page.jsx (after existing imports)
5.  DELETE the extracted block from page.jsx (PowerShell str_replace — never manual edit)
6.  git diff app/platform/page.jsx     ← verify ONLY the deleted block and new import line changed
7.  git diff app/[new-file-path]       ← verify new file matches extracted content exactly
8.  git add -A
9.  git commit -m "refactor: extract [Name] to [filepath] (split session N)"
10. git push origin staging
11. Open staging.yarashi.com.au — run the Session Test Checklist below
12. IF all tests pass: git checkout main && git merge staging && git push origin main
13. IF any test fails: git revert HEAD on staging, investigate, do NOT merge to main

Session Test Checklist (run on staging after every push)

TestHow to verifyBlocks merge?
Page loadsstaging.yarashi.com.au loads without white screen or console error stormYES
Sign in worksSign in with celticboxingcoach@gmail.com test account — platform loadsYES
Extracted module rendersClick to the module extracted this session — it displays correctlyYES
No new console errorsF12 → Console — compare to pre-session baseline. Zero new errors.YES
One AI call completesRun any AI tool (Skills Gap or Job Analyser) — result returns successfullyYES
Auth flow intactSign out, sign back in — session restores correctlyYES
No hydration errors beyond baselineHard refresh (Ctrl+Shift+R), check console — no new hydration warningsYES

Known Issues to Correct in Session 1

Stray semicolon on line 52. A lone ; exists on line 52 after useColors. Delete it when extracting colors.js. It is dead syntax and should not be copied to the new file.

Verified Dependency Map (from triple-check 23 Jun 2026)

Every dependency below was verified against the live file with comment lines stripped. False positives from adjacent comment blocks have been removed.

DefinitionReal dependencies (verified)
C / useColors(none — root of the tree)
safeFetch / aiFetch(none — pure async functions)
saveAnalysisToProfilesetCachedAnalysis
loadSavedAnalysissafeFetch, getCachedAnalysis, setCachedAnalysis
withRetry(none)
CV_LIMIT / JOB_LIMITS / isPlanX / getLocalPlan / checkJobLimit / incrementJobLimit / recordJobUsage(none — pure functions)
JobLimitGatecheckJobLimit — also needs React import (uses useState + useEffect)
InfoBtn / HELP_CONTENTC
Tag / Pill / MatchRing / Stat / MiniBar / SOURCE / SourceTagC (SOURCE is self-referencing within the group)
FLabel / CopyBtn / FieldC
LogoSVG(none — pure SVG)
YarashiMark / LogoWatermarkC, LogoSVG
DashCard / DashSectionTitle / DashPill(none — pure JSX)
Chips / LocationPicker / ListField / SectionHead / FC — Chips also used by SkillsGapAnalyser, must be in Shared.jsx not ProfileModule
AddToKanbanBtn / SaveToProfileBtn(none — uses hardcoded colours, no C dependency)
UsageBadgecheckJobLimit (from utils/plan)
ToastContainer / showToast(none)
SkeletonCard(none)
LoadingScreenLogoSVG, SkeletonCard
GlobalStyles(none)
NewUserChecklistC, useColors
DashboardsaveAnalysisToProfile, loadSavedAnalysis, LogoSVG, printDashboardReport
CVUpload(none — uses hardcoded colours)
PLAN_DATA / PricingModalisPlanStarter, isPlanPro, isPlanSuperPro, LogoSVG
CVMaestroModuleC, useColors, recordJobUsage, JobLimitGate, showToast
CVTailorModuleC, useColors, safeFetch, saveAnalysisToProfile, getCachedAnalysis, setCachedAnalysis, LogoSVG, printToPDF, showToast, autoSaveToTracker — must extract AFTER ApplicationTracker
SupportBotC, LogoSVG
JobAnalyserScoreRing / JobAnalyserC, useColors, saveAnalysisToProfile, setCachedAnalysis, printJobAnalysis, LoadingScreen, withRetry
JobIntelligenceC, safeFetch, JobLimitGate, LogoSVG, LOC_GROUPS, LOC_OPTS, DATE_OPTS, SALARY_OPTS, AddToKanbanBtn — LOC_GROUPS etc move WITH this module
CVMatchEngineC, safeFetch, aiFetch, saveAnalysisToProfile, getCachedAnalysis, setCachedAnalysis, CV_LIMIT, recordJobUsage, buildFilename, exportRecommendationsPDF, exportRecommendationsWord, exportMatchPDF, exportMatchWord, Tag, showToast, SaveToProfileBtn, autoSaveToTracker — must extract AFTER ApplicationTracker
InterviewPrepC, safeFetch, saveAnalysisToProfile, getCachedAnalysis, setCachedAnalysis, recordJobUsage, JobLimitGate, Tag, LogoSVG, printInterviewPrep, showToast, AddToKanbanBtn, SaveToProfileBtn, autoSaveToTracker — must extract AFTER ApplicationTracker
TRACKER_COLS / INTERVIEW_TYPES / EMPTY_APP / autoSaveToTracker / useTracker / ApplicationTrackerC, Tag, Field, TRACKER_COLS, INTERVIEW_TYPES, EMPTY_APP, useTracker — autoSaveToTracker needs EMPTY_APP so they move together
CoverLetterBuilderC, aiFetch, getCachedAnalysis, setCachedAnalysis, recordJobUsage, JobLimitGate, exportWord, Tag, printCoverLetter, withRetry, AddToKanbanBtn, SaveToProfileBtn, autoSaveToTracker — must extract AFTER ApplicationTracker
ProfileModule + sub-componentsC, safeFetch, showToast, ALL_INDUSTRIES, TEMPLATE_TYPES, dbRowToProfile, EMPTY_PROFILE, F, LocationPicker, Chips, ListField, SectionHead — Chips is shared, imports from Shared.jsx
SkillsGapAnalyserC, safeFetch, aiFetch, saveAnalysisToProfile, setCachedAnalysis, recordJobUsage, InfoBtn, HELP_CONTENT, Tag, printSkillsGap, showToast, UsageBadge, Chips, STATUS_COLORS, PRIORITY_ORDER
LinkedInOptimiserC, safeFetch, aiFetch, saveAnalysisToProfile, getCachedAnalysis, setCachedAnalysis, recordJobUsage, JobLimitGate, CopyBtn, printLinkedIn
AdminPanelC, Tag, Stat
OnboardingGuideC — confirmed dead code, never called anywhere in the file
SplashGrabWidget / AuthScreen / JobsWrapperLogoSVG, LogoWatermark, SplashGrabWidget (internal)

The 25 Sessions — Definitive Extraction Order

Line numbers are from the file as of 23 Jun 2026. Grep for function name at session start — numbers shift after each extraction.

STarget fileLinesWhat movesKey constraint
Phase 1 — Pure utils (no JSX, zero render risk)
1app/utils/colors.js15–54C object, useColors. Delete stray ; on line 52.Nothing depends on this yet — safest possible first move
2app/utils/fetch.js55–152 + 1278–1305safeFetch, aiFetch, saveAnalysisToProfile, loadSavedAnalysis, getCachedAnalysis, setCachedAnalysis, withRetryTwo non-contiguous blocks into one file. withRetry is at line 1278 — grab it in this session.
3app/utils/plan.js153–222CV_LIMIT, JOB_LIMITS, isPlanStarter, isPlanPro, isPlanSuperPro, getLocalPlan, checkJobLimit, incrementJobLimit, recordJobUsage, JobLimitGateplan.js needs import React, { useState, useEffect } from 'react' — JobLimitGate is a React component
4app/utils/exports.js361–574 + 877–999useCVCount, buildFilename, exportPDF, exportWord, exportRecommendationsPDF, exportRecommendationsWord, exportMatchPDF, exportMatchWord, printToPDF, printCoverLetter, printInterviewPrep, printSkillsGap, printDashboardReport, printJobAnalysis, printLinkedIn, useIsMobileTwo non-contiguous blocks. printToPDF is at 877 — all print functions depend on it so they all move together.
Phase 2 — Shared UI (used by multiple modules — must exist before feature modules move)
5app/components/ui/Toast.jsx1065–1132ToastContainer, showToastNo deps. Standalone. showToast is called everywhere — page.jsx keeps it in scope via import until each module is extracted.
6app/components/ui/Logo.jsx816–851LogoSVG, YarashiMark, LogoWatermarkDepends on C (Session 1). LogoSVG is pure SVG — no React hooks.
7app/components/ui/Shared.jsx575–646 + 793–876 + 1133–1274 + 2101–2187Tag, Pill, MatchRing, Stat, MiniBar, SOURCE, SourceTag, FLabel, CopyBtn, Field, DashCard, DashSectionTitle, DashPill, SkeletonCard, LoadingScreen, GlobalStyles, AddToKanbanBtn, SaveToProfileBtn, Chips, LocationPicker, ListField, SectionHead, FFOUR non-contiguous blocks into one file. Chips and LocationPicker normally sit inside ProfileModule range but are moved here because SkillsGapAnalyser uses Chips independently. CopyBtn moves here because LinkedInOptimiser uses it. LoadingScreen depends on LogoSVG and SkeletonCard — both in this file.
8app/components/ui/InfoBtn.jsx223–360InfoBtn, HELP_CONTENTDepends on C. Used by SkillsGapAnalyser — must exist before Session 19.
9app/components/ui/CVUpload.jsx2203–2450CVUpload modalNo colour dependencies — uses hardcoded values. Self-contained.
10app/components/ui/PricingModal.jsx2451–2863PLAN_DATA, PricingModalDepends on isPlanX (Session 3) and LogoSVG (Session 6).
Phase 3 — Feature modules (ordered by dependency chain)
11app/components/NewUserChecklist.jsx1306–1518NewUserChecklistDepends on C and useColors only. Simple. Used directly by the shell.
12app/components/Dashboard.jsx1519–2000DashboardDepends on saveAnalysisToProfile, loadSavedAnalysis (Session 2), LogoSVG (Session 6), printDashboardReport (Session 4).
13app/components/ApplicationTracker.jsx7005–7577TRACKER_COLS, INTERVIEW_TYPES, EMPTY_APP, autoSaveToTracker, useTracker, ApplicationTrackerMust happen before Sessions 14, 15, 17, 18, 23 — all call autoSaveToTracker. autoSaveToTracker and EMPTY_APP are coupled and move together. Export both ApplicationTracker (default) and autoSaveToTracker (named).
14app/components/CVMatchEngine.jsx5759–6447CVMatchEngineDepends on autoSaveToTracker (Session 13). Must happen before Sessions 15 and 16 — both render CVMatchEngine.
15app/components/JobAnalyser.jsx4312–4967JobAnalyserScoreRing, JobAnalyserDepends on LoadingScreen (Session 7), withRetry (Session 2). Note: references to "JobAnalyser" in CVTailor/InterviewPrep/CoverLetterBuilder are localStorage key names in comments only — not component imports.
16app/components/JobIntelligence.jsx4968–5758 + LOC constants 2001–2100LOC_GROUPS, LOC_OPTS, DATE_OPTS, SALARY_OPTS, CONTRACT_OPTS, WORKTYPE_OPTS, SENIORITY_OPTS, JobIntelligenceLOC_GROUPS and all filter constants only used by JobIntelligence — move with it. Two non-contiguous blocks. Depends on CVMatchEngine (Session 14).
17app/components/InterviewPrep.jsx6448–7004InterviewPrepDepends on autoSaveToTracker (Session 13), AddToKanbanBtn + SaveToProfileBtn (Session 7), Tag (Session 7).
18app/components/CoverLetterBuilder.jsx7578–8060COVER_TONES, COVER_LENGTHS, CoverLetterBuilderDepends on autoSaveToTracker (Session 13), aiFetch (Session 2), exportWord (Session 4), withRetry (Session 2).
19app/components/CVMaestroModule.jsx2864–3335CVMaestroModuleDepends on C, useColors, recordJobUsage (Session 3), JobLimitGate (Session 3), showToast (Session 5). No autoSaveToTracker — can go before Session 13 but simpler to do here.
20app/components/CVTailorModule.jsx3336–4101CVTailorModuleMust be after Session 13 — calls autoSaveToTracker. Also depends on safeFetch (S2), getCachedAnalysis/setCachedAnalysis (S2), printToPDF (S4), LogoSVG (S6), showToast (S5).
21app/components/ProfileModule.jsx8061–9426LOCATIONS_BY_REGION, AU_CITIES, ALL_INDUSTRIES, EMP_TYPES, WORK_TYPES, CLEARANCES, EXP_OPTS, SENIORITY, NOTICE_OPTS, TEMPLATE_TYPES, dbRowToProfile, EMPTY_PROFILE, ProfileModule. Sub-components F, SectionHead already in Shared.jsx.Largest single extraction (1,071 lines of ProfileModule + constants). Chips, LocationPicker, ListField, F, SectionHead are already in Shared.jsx by this point — import from there. Export ProfileModule (default) and dbRowToProfile (named — used by shell).
22app/components/SkillsGapAnalyser.jsx9427–10304STATUS_COLORS, PRIORITY_ORDER, UsageBadge, SkillsGapAnalyserUsageBadge (line 2188, 15 lines) also moves here — only used by SkillsGapAnalyser. Depends on InfoBtn (S8), HELP_CONTENT (S8), Chips (S7), Tag (S7). Import Chips from @/components/ui/Shared.
23app/components/LinkedInOptimiser.jsx10305–10691LinkedInOptimiserDepends on CopyBtn (Session 7). CopyBtn is in Shared.jsx — import from @/components/ui/Shared.
24app/components/AuthScreen.jsx10692–11529AdminPanel, SplashGrabWidget, AuthScreen, JobsWrapper, OnboardingGuideOnboardingGuide is confirmed dead code (never called) — move with AuthScreen to clean it out of page.jsx. AdminPanel depends on Tag and Stat from Shared.jsx.
25app/platform/page.jsx (shell cleanup)11530–13205AgileFoundryPlatform shell — reduce from 1,676 lines to ~300By this session page.jsx contains only imports + the shell function. Clean up the import block, remove any dead imports, verify all 29 module imports resolve. Full end-to-end test of every module before final merge.

Final File Structure After Session 25

app/
  platform/
    page.jsx                     (~300 lines — shell + imports only)
    MyHub.jsx                    (already separate — untouched)
  utils/
    colors.js                    (54 lines)
    fetch.js                     (includes withRetry — ~130 lines)
    plan.js                      (includes JobLimitGate — ~70 lines)
    exports.js                   (~220 lines)
  components/
    ui/
      Toast.jsx                  (68 lines)
      Logo.jsx                   (59 lines)
      Shared.jsx                 (~350 lines — Tag, Pill, MatchRing, Stat, MiniBar,
                                   SourceTag, FLabel, CopyBtn, Field, DashCard,
                                   DashSectionTitle, DashPill, SkeletonCard,
                                   LoadingScreen, GlobalStyles, AddToKanbanBtn,
                                   SaveToProfileBtn, Chips, LocationPicker,
                                   ListField, SectionHead, F)
      InfoBtn.jsx                (138 lines)
      CVUpload.jsx               (248 lines)
      PricingModal.jsx           (~413 lines)
    NewUserChecklist.jsx         (213 lines)
    Dashboard.jsx                (482 lines)
    ApplicationTracker.jsx       (573 lines — exports autoSaveToTracker)
    CVMatchEngine.jsx            (689 lines)
    JobAnalyser.jsx              (631 lines)
    JobIntelligence.jsx          (~891 lines — includes LOC constants)
    InterviewPrep.jsx            (557 lines)
    CoverLetterBuilder.jsx       (483 lines)
    CVMaestroModule.jsx          (472 lines)
    CVTailorModule.jsx           (766 lines)
    ProfileModule.jsx            (~1,100 lines — exports dbRowToProfile)
    SkillsGapAnalyser.jsx        (~885 lines — includes UsageBadge)
    LinkedInOptimiser.jsx        (387 lines)
    AuthScreen.jsx               (~860 lines — includes AdminPanel, SplashGrabWidget,
                                   JobsWrapper, OnboardingGuide)
  (already external — untouched)
    components/JobFeed.jsx
    components/MyMatches.jsx
    components/JobActionHub.jsx
    components/OnboardingFlow.jsx
    components/RolePrefsPanel.jsx
    components/AdminFeatureBoard.jsx

Shell Import Block After Session 25

These are the exact imports the shell will need, verified from the live dependency check.

'use client';
export const dynamic = 'force-dynamic';

// Already external
import JobFeed from '@/components/JobFeed';
import MyMatches from '@/components/MyMatches';
import JobActionHub from '@/components/JobActionHub';
import OnboardingFlow, { GettingStarted } from '@/components/OnboardingFlow';
import RolePrefsPanel from '@/components/RolePrefsPanel';
import AdminFeatureBoard from '@/components/AdminFeatureBoard';
import MyHub from './MyHub';
import React, { useState, useMemo, useCallback, useEffect, useRef, forwardRef, useImperativeHandle } from 'react';

// Extracted in split sessions
import { C } from '@/utils/colors';
import { safeFetch, setCachedAnalysis } from '@/utils/fetch';
import { isPlanPro, useCVCount } from '@/utils/plan';
import { LogoSVG, YarashiMark } from '@/components/ui/Logo';
import { ToastContainer, showToast } from '@/components/ui/Toast';
import { LoadingScreen, GlobalStyles } from '@/components/ui/Shared';
import CVUpload from '@/components/ui/CVUpload';
import { PricingModal } from '@/components/ui/PricingModal';
import NewUserChecklist from '@/components/NewUserChecklist';
import Dashboard from '@/components/Dashboard';
import ApplicationTracker from '@/components/ApplicationTracker';
import CVMatchEngine from '@/components/CVMatchEngine';
import { JobAnalyser } from '@/components/JobAnalyser';
import JobIntelligence from '@/components/JobIntelligence';
import InterviewPrep from '@/components/InterviewPrep';
import CoverLetterBuilder from '@/components/CoverLetterBuilder';
import CVMaestroModule from '@/components/CVMaestroModule';
import CVTailorModule from '@/components/CVTailorModule';
import ProfileModule, { dbRowToProfile } from '@/components/ProfileModule';
import SkillsGapAnalyser from '@/components/SkillsGapAnalyser';
import LinkedInOptimiser from '@/components/LinkedInOptimiser';
import { AdminPanel, AuthScreen, JobsWrapper } from '@/components/AuthScreen';
import SupportBot from '@/components/SupportBot';

SupportBot — Separate File

SupportBot (lines 4102–4311, 210 lines) was not assigned a session in the table above because it sits between CVTailorModule and JobAnalyser in the file and has no dependency on autoSaveToTracker. Extract it in its own session between Session 20 (CVTailorModule) and Session 21 (ProfileModule), or bundle with Session 20. It depends only on C and LogoSVG.

Decision Log — Split Plan

DecisionRationale
Chips moves to Shared.jsx not ProfileModuleSkillsGapAnalyser uses Chips independently. If left in ProfileModule, SkillsGapAnalyser would create a circular-style dependency on a feature module.
autoSaveToTracker stays with ApplicationTrackerIt spreads EMPTY_APP which is a tracker-specific constant. Cannot cleanly separate into fetch utils without bringing tracker constants with it.
LOC_GROUPS and filter constants move with JobIntelligenceOnly used by JobIntelligence. No justification for a shared constants file.
UsageBadge moves with SkillsGapAnalyserOnly called by SkillsGapAnalyser. No reason to put it in Shared.jsx.
OnboardingGuide bundled with AuthScreenConfirmed dead code — never called anywhere. Moves to get it out of page.jsx. Can be deleted or wired up later.
CopyBtn goes in Shared.jsx not Logo.jsxLinkedInOptimiser uses CopyBtn. Logo.jsx is for brand assets only.
JobLimitGate stays in utils/plan.jsIt is tightly coupled to checkJobLimit and the limit functions. Only 7 lines. Needs a React import but that is a minor cost for keeping related logic together.
plan.js imports ReactJobLimitGate uses useState + useEffect. Confirmed in live file.
withRetry moves with fetch.js in Session 2It is a fetch retry utility. Same file is the right home even though it is at line 1278 — non-contiguous blocks are acceptable for a utility grouping this clear.
CVTailorModule after ApplicationTracker (Session 20, not earlier)CVTailorModule calls autoSaveToTracker at line 3498. autoSaveToTracker does not exist as a standalone import until Session 13.