diff --git a/.gitignore b/.gitignore
index 8d221f6..daeb0d7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,9 @@ dist/
build/
.vite/
+# Local-only Personal Mode state and generated credentials
+.careeros/
+
# Supabase local/temporary state
.supabase/
supabase/.temp/
diff --git a/README.md b/README.md
index fbe5ff2..38410ee 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,9 @@ hackathons, scholarships, and other resume-building opportunities; compares them
detailed saved profile; explains where the student is competitive and where they are not; and
tracks the full lifecycle from "found it" to "applied" to "interviewed."
-Phase 0 is implemented. Phase 1A currently provides routed manual profile, primary-education,
-and experience/research workflows: a React/TypeScript/Vite frontend, a local Supabase stack
+Phase 2A is implemented as a local personal workspace. It provides a one-command Personal Mode,
+local Greenhouse discovery, opportunity/application tracking, and routed manual profile,
+primary-education, and experience/research workflows: a React/TypeScript/Vite frontend, a local Supabase stack
(Postgres/Auth/PostgREST), `profiles`, `education_entries`, and `work_experience` protected by
Row Level Security, and CI. Projects, links, skills, preferences, and resume features remain
deferred. See [Local development setup](#local-development-setup) below to run it.
@@ -57,6 +58,29 @@ matter of enabling sign-ups, not rearchitecting the schema or the pipeline. See
## Local development setup
+### Personal Mode — normal daily use
+
+With Docker or Colima running, use just:
+
+```bash
+npm run personal
+```
+
+It starts the local stack when needed, applies only pending migrations (never `supabase db reset`),
+generates gitignored local runtime configuration, reuses the existing `dev-owner@careeros.local`
+identity when present, refreshes the configured Greenhouse sources, and starts CareerOS at
+`http://127.0.0.1:5173`. Personal Mode signs that local owner in invisibly; it never shows email
+or password UI, and `sign-in` routes back into the workspace. The generated local session
+credential lives only in gitignored `.careeros/` and `app/.env.local`; it is not a service-role
+credential and public authentication remains dormant/recoverable for a later deployment.
+
+The page-level **Refresh opportunities** action talks only to the loopback companion started by
+this command. It runs the trusted local ingestion CLI without exposing its service-role key to the
+browser. Re-running `npm run personal` is safe: existing opportunities, saved state, applications,
+interview notes, profile data, and version history are preserved.
+
+### Advanced maintainer and isolated-test setup
+
Prerequisites:
- Node.js `22.23.1` (the root [.nvmrc](.nvmrc) is authoritative). Run `nvm use` from the
@@ -87,7 +111,8 @@ supabase start
# bound to 127.0.0.1.
./supabase/scripts/dev-tunnel.sh
-# 4. Apply every migration from an empty database
+# 4. Apply every migration from an empty database (isolated test setup only;
+# never use this during Personal Mode or against personal data)
supabase db reset
# 5. Create the two local-only development login users (idempotent).
diff --git a/app/README.md b/app/README.md
index 34ce4cf..25450f5 100644
--- a/app/README.md
+++ b/app/README.md
@@ -4,8 +4,10 @@ React + TypeScript + Vite single-page app. Talks directly to the local Supabase
(Postgres/Auth/PostgREST) — no backend server in this project (see
[../docs/ARCHITECTURE.md](../docs/ARCHITECTURE.md), [ADR-002](../docs/adr/ADR-002-frontend-and-backend-boundaries.md)).
-See the repository root [README.md](../README.md) for full local setup instructions
-(Colima/Docker, Supabase CLI, the loopback dev tunnel).
+For normal personal use, run `npm run personal` from the repository root. It writes this app's
+gitignored `.env.local`, establishes an invisible local Personal Mode session, refreshes configured
+sources, and starts Vite. See the repository root [README.md](../README.md) for the full local
+runtime notes and advanced maintainer setup.
## Scripts
@@ -23,6 +25,8 @@ See the repository root [README.md](../README.md) for full local setup instructi
## Environment variables
-Copy `.env.example` to `.env.local` and fill in the values `supabase status` prints
-for the local stack. Only `VITE_`-prefixed variables are exposed to the browser bundle —
-never put a secret/service-role key in one.
+`npm run personal` creates `.env.local` automatically for the local personal runtime. Only
+`VITE_`-prefixed values are exposed to the browser bundle — never put a secret/service-role key in
+one. `VITE_PERSONAL_MODE=true` is a local-only convenience boundary: it uses a generated local
+owner credential to establish the existing RLS-scoped session without rendering authentication UI.
+Set it false or omit it when returning to the dormant public-auth route.
diff --git a/app/src/App.test.tsx b/app/src/App.test.tsx
index f7487f5..4ffb4c6 100644
--- a/app/src/App.test.tsx
+++ b/app/src/App.test.tsx
@@ -1,17 +1,20 @@
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
-import { describe, expect, it, vi, beforeEach } from 'vitest'
+import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'
import App from './App'
const mockGetSession = vi.fn()
const mockOnAuthStateChange = vi.fn()
const mockMaybeSingle = vi.fn()
+const mockSignInWithPassword = vi.fn()
vi.mock('./lib/supabaseClient', () => ({
supabase: {
auth: {
getSession: () => mockGetSession(),
onAuthStateChange: (...args: unknown[]) => mockOnAuthStateChange(...args),
+ signInWithPassword: (...args: unknown[]) =>
+ mockSignInWithPassword(...args),
signOut: vi.fn(),
},
from: () => ({
@@ -44,16 +47,23 @@ vi.mock('./lib/profileReviewRepository', () => ({
state: () => Promise.resolve({ data: [], error: null }),
},
}))
+vi.mock('./pages/DashboardPage', () => ({
+ DashboardPage: () =>