diff --git a/.factory/analysis/api-edge-report.md b/.factory/analysis/api-edge-report.md new file mode 100644 index 00000000..b0c80eb1 --- /dev/null +++ b/.factory/analysis/api-edge-report.md @@ -0,0 +1,330 @@ +# API Edge Migration Analysis + +## Edge-Safe Routes (convert to `export const runtime = "edge"`) + +These routes do pure Supabase reads/writes without any Node.js-specific APIs, file processing, AI calls, or large payloads. They can safely run on Edge. + +| Route | Methods | Reasoning | +|-------|---------|-----------| +| `app/api/version/` | GET | Returns env vars + timestamp, no external deps | +| `app/api/user/next-nodes/` | GET | Supabase read-only, small response (≤5 items) | +| `app/api/hero-galaxy/` | GET | Supabase read with in-memory math transforms | +| `app/api/app-data/` | GET | Supabase reads, already uses `Cache-Control` header | +| `app/api/check-node/` | GET | Supabase single-row lookup via debug guard | +| `app/api/pre-questionnaire/` | GET, POST | Supabase read/write, small JSON body | +| `app/api/maps/route.ts` | GET | Supabase read, small response | +| `app/api/maps/list/` | GET | Supabase paginated read, already uses `Cache-Control` | +| `app/api/maps/[id]/route.ts` | GET | Supabase single-map read | +| `app/api/maps/[id]/enroll/` | POST | Supabase insert, small body | +| `app/api/maps/[id]/enrollment/` | GET | Supabase read | +| `app/api/maps/[id]/progress/` | GET | Supabase aggregated read | +| `app/api/maps/[id]/nodes/` | GET | Supabase read | +| `app/api/classrooms/route.ts` | GET | Supabase aggregated reads (memberships + counts) | +| `app/api/classrooms/join/` | POST | Supabase insert, small body | +| `app/api/classrooms/[id]/route.ts` | GET | Supabase single-row read | +| `app/api/classrooms/[id]/stats/` | GET | Supabase aggregated reads | +| `app/api/classrooms/[id]/teams/` | GET, POST | Supabase read/write teams | +| `app/api/classrooms/[id]/students/` | GET | Supabase read | +| `app/api/classrooms/[id]/assignments/` | GET | Supabase read | +| `app/api/classrooms/[id]/settings/` | GET | Supabase read | +| `app/api/classrooms/[id]/teams/[teamId]/route.ts` | GET, PUT, DELETE | Supabase CRUD on teams | +| `app/api/classrooms/[id]/teams/unassigned/` | GET | Supabase filtered read | +| `app/api/classrooms/[id]/regenerate-code/` | POST | Supabase update, small mutation | +| `app/api/classrooms/[id]/grading/` | GET, POST | Supabase grading CRUD | +| `app/api/classrooms/[id]/grading/group-members/` | GET, POST | Supabase group grading | +| `app/api/classrooms/[id]/grading/bulk/` | POST | Supabase bulk update | +| `app/api/classrooms/[id]/group-grading/` | GET | Supabase read | +| `app/api/classrooms/[id]/debug-groups/` | GET, POST | Debug routes, Supabase operations | +| `app/api/classrooms/[id]/maps/[mapId]/fork-to-team/` | POST | Supabase clone operation | +| `app/api/assignments/route.ts` | GET, POST, PUT, DELETE | Supabase CRUD, no large payloads | +| `app/api/assignments/[id]/nodes/` | GET | Supabase read | +| `app/api/assignments/[id]/progress/` | GET | Supabase aggregated read | +| `app/api/assignments/[id]/enrollments/` | GET | Supabase read | +| `app/api/assignments/[id]/enroll/` | POST | Supabase insert | +| `app/api/profile/dashboard/` | GET | Supabase aggregated reads (many parallel queries) | +| `app/api/tcas/projection/` | GET | Supabase read with in-memory normalization | +| `app/api/tcas/categories/` | GET | Supabase read | +| `app/api/pathlab/library/` | GET, POST | Supabase CRUD activity templates | +| `app/api/pathlab/npc-conversations/[conversationId]/route.ts` | GET | Supabase read with relational join | +| `app/api/pathlab/npc-conversations/choice/` | POST | Supabase insert (choice selection) | +| `app/api/pathlab/npc-conversations/progress/[progressId]/` | GET | Supabase read | +| `app/api/pathlab/activities/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/days/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/assessments/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/content/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/preview/` | GET | Supabase read | +| `app/api/pathlab/reports/` | GET | Supabase aggregated read | +| `app/api/pathlab/pages/[id]/activities/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/paths/[pathId]/days/` | GET, POST | Supabase CRUD | +| `app/api/pathlab/paths/[pathId]/days/[dayNumber]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/pathlab/paths/[pathId]/export/` | GET | Supabase read, JSON export | +| `app/api/pathlab/enroll/` | POST | Supabase insert | +| `app/api/pathlab/reflect/` | POST | Supabase insert (path reflection), no AI | +| `app/api/onboarding/state/` | POST | Supabase upsert, small body | +| `app/api/onboarding/complete/` | POST | Supabase upsert, small body | +| `app/api/onboarding/tcas/` | GET | Supabase read | +| `app/api/onboarding/reset/` | POST | Supabase delete | +| `app/api/auth/forgot-password/` | POST | Supabase auth helper | +| `app/api/seeds/create/` | POST | Supabase insert | +| `app/api/seeds/clone-map/` | POST | Supabase clone operation | +| `app/api/seeds/reset-progress/` | POST | Supabase delete/update | +| `app/api/seeds/rooms/[roomId]/grading/` | GET, POST | Supabase CRUD | +| `app/api/hackathon/login/` | POST | Supabase auth, sets cookie | +| `app/api/hackathon/logout/` | POST | Clears cookie, DB session delete | +| `app/api/hackathon/register/` | POST | Supabase insert, small body | +| `app/api/hackathon/register/special/` | POST | Supabase insert | +| `app/api/hackathon/register/link/[token]/` | GET | Supabase read (token validation) | +| `app/api/hackathon/register/invite/[token]/` | GET | Supabase read (token validation) | +| `app/api/hackathon/forgot-password/` | POST | Supabase auth helper | +| `app/api/hackathon/reset-password/` | POST | Supabase auth helper | +| `app/api/hackathon/pre-questionnaire/` | GET, POST | Supabase read/write | +| `app/api/hackathon/me/` | GET | Supabase read (session lookup) | +| `app/api/hackathon/team/me/` | GET | Supabase read (session lookup) | +| `app/api/hackathon/team/create/` | POST | Supabase insert | +| `app/api/hackathon/team/join/` | POST | Supabase insert | +| `app/api/hackathon/team/leave/` | POST | Supabase update | +| `app/api/hackathon/team/kick/` | POST | Supabase update/delete | +| `app/api/hackathon/team/interests/` | POST | Supabase update | +| `app/api/hackathon/team/invite/create/` | POST | Supabase insert | +| `app/api/hackathon/team/invite/status/` | POST | Supabase read/write | +| `app/api/hackathon/team/match/status/` | GET | Supabase read | +| `app/api/hackathon/team/match/join/` | POST | Supabase update | +| `app/api/hackathon/team/match/cancel/` | POST | Supabase update | +| `app/api/hackathon/student/team/` | GET | Supabase read | +| `app/api/hackathon/student/mentor-quota/` | GET | Supabase read | +| `app/api/hackathon/student/mentor-slots/` | GET | Supabase read | +| `app/api/hackathon/student/book-mentor/` | POST | Supabase insert | +| `app/api/hackathon/student/cancel-booking/[id]/` | POST | Supabase update | +| `app/api/hackathon/mentor/me/` | GET | Supabase read (session lookup) | +| `app/api/hackathon/mentor/login/` | POST | Supabase auth, sets cookie | +| `app/api/hackathon/mentor/logout/` | POST | Clears cookie, DB session delete | +| `app/api/hackathon/mentor/register/` | POST | Supabase insert | +| `app/api/hackathon/mentor/public/` | GET | Supabase read | +| `app/api/hackathon/mentor/availability/` | GET, POST | Supabase read/write | +| `app/api/hackathon/mentor/availability-toggle/` | POST | Supabase update | +| `app/api/hackathon/mentor/bookings/` | GET | Supabase read | +| `app/api/hackathon/mentor/bookings/[id]/` | GET | Supabase read | +| `app/api/hackathon/mentor/teams/` | GET | Supabase read | +| `app/api/hackathon/mentor/teams/assign/` | POST | Supabase insert | +| `app/api/hackathon/mentor/line-connect/` | POST | Supabase update | +| `app/api/hackathon/bookings/` | GET, POST | Supabase read/write | +| `app/api/hackathon/submissions/` | GET | Supabase paginated read | +| `app/api/hackathon/inbox/` | GET | Supabase read | +| `app/api/hackathon/push-subscribe/` | POST | Supabase insert | +| `app/api/hackathon/team-finder/status/` | GET | Supabase read | +| `app/api/hackathon/team-finder/join/` | POST | Supabase insert | +| `app/api/hackathon/team-finder/preferences/` | POST | Supabase upsert | +| `app/api/hackathon/team-matching/participants/` | GET | Supabase read | +| `app/api/hackathon/team-matching/met/` | GET, POST | Supabase read/write | +| `app/api/hackathon/team-matching/rankings/` | GET | Supabase read | +| `app/api/hackathon/team-matching/event/` | GET, POST | Supabase read/write | +| `app/api/hackathon/matching/met/` | GET, POST | Supabase read/write | +| `app/api/hackathon/matching/rankings/` | GET | Supabase read | +| `app/api/hackathon/matching/event/` | GET, POST | Supabase read/write | +| `app/api/hackathon/mentor/submissions/[scope]/[id]/grade/` | POST | Supabase update (manual grade) | +| `app/api/hackathon/mentor/submissions/[scope]/[id]/comment/` | POST | Supabase insert (comment) | +| `app/api/mentor-sessions/schedule/` | POST | Supabase insert | +| `app/api/mentor-sessions/[id]/cancel/` | POST | Supabase update | +| `app/api/groups/[id]/progress/` | GET | Supabase read | +| `app/api/ps/projects/[id]/members/` | GET | Supabase read | +| `app/api/ps/b2b/phase1/` | POST | Supabase insert | +| `app/api/expert-interview/submit/` | POST | Supabase insert | +| `app/api/expert-interview/claim/` | POST | Supabase update | +| `app/api/expert-interview/force-complete/` | POST | Supabase update | +| `app/api/test-db/` | GET | Supabase read (test) | +| `app/api/check-tables/` | GET | Supabase read (test) | +| `app/api/check-reflections/` | GET | Supabase read (test) | +| `app/api/test-classroom/` | GET | Supabase read (test) | +| `app/api/direction/enqueue/` | POST | Supabase insert (creates job), no AI | +| `app/api/direction/status/[jobId]/` | GET | Supabase read (poll job) | +| `app/api/embeddings/enqueue/` | POST | Supabase insert (creates job) | +| `app/api/embeddings/status/[jobId]/` | GET | Supabase read (poll job) | +| `app/api/admin/analytics/` | GET | Supabase aggregated reads (many tables) | +| `app/api/admin/stats/` | GET | Supabase reads | +| `app/api/admin/onboarding/` | GET | Supabase read | +| `app/api/admin/beta-registrations/` | GET | Supabase read | +| `app/api/admin/users/` | GET | Supabase read | +| `app/api/admin/users/roles/` | POST | Supabase insert | +| `app/api/admin/experts/` | GET, POST | Supabase CRUD | +| `app/api/admin/experts/[id]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/experts/[id]/approve/` | POST | Supabase update | +| `app/api/admin/experts/[id]/reject/` | POST | Supabase update | +| `app/api/admin/universities/` | GET, POST | Supabase CRUD | +| `app/api/admin/universities/[id]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/maps/` | GET, POST | Supabase CRUD | +| `app/api/admin/test-connection/` | GET | Supabase connectivity test | +| `app/api/admin/track-payment/` | POST | Supabase insert | +| `app/api/admin/event-tracker/` | GET | Supabase read | +| `app/api/admin/npc-conversations/` | GET, POST | Supabase CRUD | +| `app/api/admin/npc-conversations/[id]/import/` | POST | Supabase insert | +| `app/api/admin/hackathon/activities/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/activities/editor/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/activities/editor/[id]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/hackathon/activities/editor/[id]/content/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/activities/editor/[id]/content/[contentId]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/hackathon/activities/editor/[id]/assessment/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/activities/[activityId]/submissions/` | GET | Supabase read | +| `app/api/admin/hackathon/activities/[activityId]/clusters/` | GET | Supabase read | +| `app/api/admin/hackathon/teams/` | GET | Supabase read | +| `app/api/admin/hackathon/teams/[teamId]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/hackathon/teams/[teamId]/message/` | POST | Supabase insert | +| `app/api/admin/hackathon/teams/[teamId]/mentor-assignments/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/teams/submissions/` | GET | Supabase read | +| `app/api/admin/hackathon/submissions/[scope]/[id]/comment/` | POST | Supabase insert | +| `app/api/admin/hackathon/submissions/[scope]/[id]/review/` | POST | Supabase update | +| `app/api/admin/hackathon/submissions/[scope]/[id]/ai-draft/discard/` | POST | Supabase update | +| `app/api/admin/hackathon/mentors/` | GET | Supabase read | +| `app/api/admin/hackathon/mentors/[id]/route.ts` | GET, PUT, DELETE | Supabase CRUD | +| `app/api/admin/hackathon/mentors/[id]/approve/` | POST | Supabase update | +| `app/api/admin/hackathon/mentors/[id]/availability/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/mentors/[id]/assignments/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/mentors/bookings/` | GET | Supabase read | +| `app/api/admin/hackathon/mentor-bookings/reset-quota/` | POST | Supabase update | +| `app/api/admin/hackathon/mentor-bookings/reset-quota-team/` | POST | Supabase update | +| `app/api/admin/hackathon/mentor-bookings/set-quota/` | POST | Supabase update | +| `app/api/admin/hackathon/participants/` | GET | Supabase read | +| `app/api/admin/hackathon/participants/set-password/` | POST | Supabase auth helper | +| `app/api/admin/hackathon/register-links/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/invite-toggle/` | POST | Supabase update | +| `app/api/admin/hackathon/questionnaires/` | GET | Supabase read | +| `app/api/admin/hackathon/analytics/` | GET | Supabase read | +| `app/api/admin/hackathon/grading-prompt/` | GET, POST | Supabase read/write | +| `app/api/admin/hackathon/push-sender/` | POST | Supabase read (fetches subscribers) + external push API | +| `app/api/admin/hackathon/push-sender/receipts/` | GET | Supabase read | +| `app/api/admin/hackathon/push-sender/test/` | POST | Supabase read + external push API | +| `app/api/admin/hackathon/email-sender/` | POST | Supabase read + external email API | +| `app/api/admin/hackathon/team-finder/reset-preferences/` | POST | Supabase update | +| `app/api/admin/hackathon/team-finder/participants/` | GET | Supabase read | +| `app/api/admin/hackathon/team-finder/create-teams/` | POST | Supabase insert | +| `app/api/admin/hackathon/team-matching/run/` | POST | Supabase batch operations | +| `app/api/admin/hackathon/matching/run/` | POST | Supabase batch operations | +| `app/api/admin/hackathon/matching/event/` | GET, POST | Supabase CRUD | +| `app/api/admin/hackathon/team-directions/search/` | GET | Supabase read (vector search? small) | +| `app/api/admin/hackathon/team-directions/clusters/` | GET | Supabase read | +| `app/api/admin/hackathon/team-directions/health/` | GET | Health check | +| `app/api/admin/hackathon/team-directions/rag/` | POST | Supabase operations | +| `app/api/admin/hackathon/team-directions/debug/` | GET | Supabase read | +| `app/api/admin/hackathon/team-directions/recluster/` | POST | Supabase operations | +| `app/api/admin/hackathon/team-directions/process-pending/` | POST | Supabase operations | +| `app/api/debug/classrooms/` | GET | Debug route, Supabase read | +| `app/api/debug/user-role/` | GET | Debug route, Supabase read | +| `app/api/debug/user/[id]/route.ts` | GET, POST | Debug route, Supabase read/write | +| `app/api/debug/test-grade/` | POST | Debug route, Supabase insert | +| `app/api/debug/grading/` | GET, POST | Debug route, Supabase CRUD | +| `app/api/debug/batch-update/` | POST | Debug route, Supabase batch update | +| `app/api/apple-music/search/` | GET | External API proxy (Apple Music), small payload | +| `app/api/deezer/search/` | GET | External API proxy (Deezer), small payload | + +## Edge-Possible Routes (could convert with minor changes) + +These routes are mostly read or simple mutations, but have one or two considerations that need addressing before Edge migration. + +| Route | Methods | Reasoning | Changes Needed | +|-------|---------|-----------|----------------| +| `app/api/soundcloud/search/` | GET | External API proxy to SoundCloud. Uses `fetch()` which works on Edge. No Node.js APIs. | None — already Edge-compatible (uses `fetch`). Could add `runtime = "edge"`. | +| `app/api/deezer/search/` | GET | External API proxy to Deezer. Uses `fetch()`. No Node.js APIs. | Already marked as Edge-safe above. | +| `app/api/apple-music/search/` | GET | External API proxy to Apple Music. Uses `fetch()`. | Already marked as Edge-safe above. | +| `app/api/spotify/search/` | GET | External API proxy, but uses `Buffer.from()` for base64 encoding of credentials. | Replace `Buffer.from()` with `btoa()` which is a Web API available on Edge. | +| `app/api/music/process/` | POST | External API calls (Spotify + Deezer + audio features). Uses `Buffer.from()` for base64. | Replace `Buffer.from()` with `btoa()`. POST body is small JSON. No file uploads in this route. | +| `app/api/hackathon/track-view/` | POST | Uses `createHash("sha256")` from Node.js `crypto`. | Replace with `crypto.subtle.digest("SHA-256", ...)` from Web Crypto API. | +| `app/api/expert-interview/session/` | POST | Uses `crypto.randomUUID()` from Node.js `crypto`. | Replace with `crypto.randomUUID()` from Web Crypto API (available globally on Edge). | +| `app/api/admin/hackathon/participants/export/` | GET | Returns CSV. Very simple — builds CSV string from Supabase query. No Node.js APIs. | None — already Edge-compatible. Could add `runtime = "edge"`. | +| `app/api/maps/[id]/route.ts` PUT | PUT | Map update with batch operations to Supabase. Moderately complex writes but all Supabase. | Already marked as Edge-safe for GET. PUT has more complex logic but still Supabase-only. | +| `app/api/admin/hackathon/team-directions/search/` | GET | Supabase vector search, small payloads. | Already marked as Edge-safe. | +| `app/api/admin/hackathon/submissions/[scope]/[id]/ai-grade/` GET | GET | Returns prompt context (no AI call). Read-only Supabase fetches. | Already Edge-safe for GET. POST variant is Node-required. | +| `app/api/pathlab/migrate/` | POST | Migration helper, Supabase operations. | Probably already Edge-safe. | +| `app/api/maps/create-from-json/` | POST | Creates maps from JSON body. Supabase inserts. | Already Edge-safe. | +| `app/api/admin/npc-conversations/[id]/import/` | POST | Imports NPC conversation data. Supabase operations. | Already Edge-safe. | + +## Node-Required Routes (must stay Node.js) + +These routes use Node.js-specific APIs, handle file uploads, make AI/LLM calls with streaming, process large payloads, or use SDKs incompatible with the Edge runtime. + +| Route | Methods | Reasoning | +|-------|---------|-----------| +| **Upload Routes (Backblaze B2 + File Processing)** | | | +| `app/api/upload/route.ts` | POST, DELETE, GET | File upload to B2, uses `Buffer`, multipart form data | +| `app/api/upload/avatar/` | POST | Avatar upload to B2, form data + Buffer | +| `app/api/upload/badge/` | POST | Badge upload to B2, uses `Buffer` | +| `app/api/upload/documents/` | POST | Document upload to B2 | +| `app/api/upload/certificate-template/` | POST | Certificate template upload, uses `Buffer` | +| `app/api/upload/stream/` | (streaming) | Streaming file operations | +| `app/api/upload/presigned/` | (presigned URLs) | B2 presigned URL generation | +| `app/api/upload/images/` | POST | Image upload to B2 | +| `app/api/upload/chunk/` | POST | Chunked file upload to B2 | +| `app/api/categories/upload-logo/` | POST | Logo upload to B2, uses `Buffer` | +| `app/api/maps/upload-cover-image/` | POST | Cover image upload, uses `Buffer` + `StorageManager` (sharp processing) | +| `app/api/seeds/upload-cover-image/` | POST | Seed cover image upload, uses `Buffer` + `StorageManager` (sharp processing + blurhash) | +| `app/api/expert-interview/upload-photo/` | POST | Expert photo upload, uses `Buffer` + `StorageManager` | +| `app/api/hackathon/mentor/photo/` | POST | Mentor photo upload to B2, uses `Buffer` | +| **AI/LLM Routes (streaming, ML models)** | | | +| `app/api/onboarding/chat/` | POST | Uses `streamText` from `ai` SDK, creates `ReadableStream`, NDJSON streaming | +| `app/api/expert-interview/chat/` | POST | Calls `processInterviewMessage` (AI chat service), uses LLM | +| `app/api/expert-interview/transcribe/` | POST | Uses `groq-sdk` (Whisper + LLM post-processing), audio blob processing | +| `app/api/pathlab/chat/` | POST | Uses `generateText` from `ai` SDK with Gemini model | +| `app/api/pathlab/generate/` | POST | Uses `generatePathLabDraft` (AI generation), calls LLM | +| `app/api/pathlab/generate/regenerate/` | POST | Same as above — regenerates draft via AI | +| `app/api/pathlab/generate/validate/` | POST | Validates AI-generated output | +| `app/api/pathlab/preview/` | POST | Uses `generatePathLabDraft` (AI generation) | +| `app/api/pathlab/ai-chat/[activityId]/` | (AI chat) | AI-powered activity chat | +| `app/api/pathlab/ai-chat/test/` | (AI test) | AI chat test endpoint | +| `app/api/education/roadmap/generate/` | POST | AI roadmap generation (currently has TODO for real AI, but structured for LLM call) | +| `app/api/direction/process/[jobId]/` | (background) | Runs AI direction profile engine (`generateDirectionProfileCore`, `generatePrograms`, etc.) — multi-step AI pipeline | +| `app/api/embeddings/process/[jobId]/` | (background) | Runs embedding generation pipeline | +| `app/api/admin/hackathon/submissions/[scope]/[id]/ai-grade/` | POST | Uses `streamText` from `ai` SDK with MiniMax M2.7, NDJSON streaming, `maxDuration = 60`, image analysis | +| `app/api/admin/hackathon/submissions/[scope]/[id]/rewrite-feedback/` | POST | Uses `streamText` from `ai` SDK, NDJSON streaming | +| `app/api/hackathon/submit/` | POST | File upload to B2 + embedding enqueue + `Buffer` operations | +| `app/api/admin/hackathon/team-directions/embed-all/` | POST | Batch enqueue embedding jobs (can process many teams) | +| `app/api/admin/hackathon/team-directions/embed-team/` | POST | Embedding generation for a single team | +| `app/api/admin/hackathon/team-directions/process/[jobId]/` | (background) | Embedding processing pipeline | +| `app/api/admin/experts/[id]/approve/` POST | POST | Calls `generatePathLabDraft` (AI generation) as part of expert approval | +| **Crypto-Required Routes** | | | +| `app/api/hackathon/mentor/line-webhook/` | POST | Uses `crypto.randomBytes()` (Node.js crypto), LINE signature validation | +| **Large Data / CSV / Special Routes** | | | +| `app/api/cc-research/run/` | POST | AI orchestration (campaign runner with LLM calls) | +| `app/api/cc-research/campaigns/` | GET, POST | CC research campaign CRUD (Supabase, but part of AI pipeline ecosystem) | +| `app/api/cc-research/campaigns/[campaignId]/` | GET, PUT, DELETE | Campaign management | +| `app/api/cc-research/campaigns/[campaignId]/seed-leads/` | POST | Seed lead generation | +| `app/api/cc-research/interviews/` | GET | Interview data | +| `app/api/cc-research/insights/` | GET | Insights data | +| `app/api/cc-research/outreach/` | POST | Outreach operations | +| `app/api/cc-research/dashboard/[campaignId]/` | GET | Dashboard data | +| `app/api/cc-research/leads/[leadId]/status/` | PUT | Lead status updates | + +## Summary + +- **Total routes analyzed**: 210+ route files across `app/api/` +- **Edge-safe**: ~160 routes (76%) — Pure Supabase read/write operations, no Node.js APIs, no AI, no file processing +- **Edge-possible**: ~10 routes (5%) — Need minor changes (e.g., `Buffer.from()` → `btoa()`, Node.js `crypto` → Web Crypto) +- **Node-required**: ~40 routes (19%) — File uploads (B2 + Buffer), AI/LLM streaming, Groq SDK, embedding pipelines + +### Migration Priority Recommendations + +1. **High-value, low-effort** (convert immediately): + - `app/api/app-data/` — Used on every page load, already cached + - `app/api/user/next-nodes/` — Core dashboard data + - `app/api/maps/list/` — Public map browsing (already has Cache-Control) + - All hackathon GET endpoints (login page loads, dashboard data) + - `app/api/version/` — Deploy health check + +2. **Fix then convert** (minor `Buffer`/`crypto` replacements): + - `app/api/spotify/search/` — Replace `Buffer.from()` with `btoa()` + - `app/api/music/process/` — Replace `Buffer.from()` with `btoa()` + - `app/api/hackathon/track-view/` — Replace `createHash` with Web Crypto + - `app/api/expert-interview/session/` — Replace `crypto.randomUUID()` with Web Crypto + +3. **Stay on Node.js** (fundamental limitations): + - All `/upload/` routes — File uploads with B2 + - All AI/LLM routes — `streamText`, `generateText`, Groq SDK + - `app/api/admin/hackathon/submissions/[scope]/[id]/ai-grade/` — Long-running AI (60s max) + - `app/api/direction/process/[jobId]/` — Multi-step AI pipeline + - `app/api/hackathon/mentor/line-webhook/` — LINE webhook with raw body access + +### Project-wide Observations + +- **Supabase client works on Edge**: The `createServerClient` from `@supabase/ssr` with `cookies()` API is Edge-compatible (uses `getAll`/`setAll`). +- **`next/headers` cookies() works on Edge**: Next.js 15 supports `cookies()` in Edge runtime. +- **Most routes use `createClient()` from `@/utils/supabase/server`**: This is Edge-compatible as it uses Supabase SSR pattern. +- **`Buffer` is the main blocker**: ~8 routes use `Buffer.from()` for file processing. Edge has `Uint8Array` and `btoa()` as alternatives for non-file use cases. +- **Node.js `crypto` is used in 4 routes**: Can be replaced with Web Crypto API (`crypto.subtle.digest`, `crypto.randomUUID()`). +- **No `fs`, `path`, `child_process`, or `stream` (Node.js) usage found** in API routes. diff --git a/.factory/analysis/db-image-columns.md b/.factory/analysis/db-image-columns.md new file mode 100644 index 00000000..c24aa5c9 --- /dev/null +++ b/.factory/analysis/db-image-columns.md @@ -0,0 +1,261 @@ +# Database Image URL Columns Report + +Generated: 2026-05-05 + +## Summary + +This report lists every database column that stores B2 (Backblaze) image/file URLs, and identifies Supabase Storage URLs that should NOT be converted. + +## B2 Environment + +| Config | Value | +|--------|-------| +| Bucket | `pseed-dev` | +| Endpoint | `s3.us-east-005.backblazeb2.com` (prod) / `s3.us-west-000.backblazeb2.com` (default) | +| URL Pattern | `https://pseed-dev.s3.{region}.backblazeb2.com/{path}` | +| Friendly URL | `https://f005.backblazeb2.com/file/pseed-dev/{path}` | +| CDN (exists, unused) | `https://cdn.passionseed.org/file/pseed-dev/{path}` | + +All B2 URLs are constructed by `lib/backblaze.ts`, `lib/storage/storage-manager.ts`, and `app/api/upload/presigned/route.ts`. + +--- + +## Category 1: Tables/Columns Storing B2 Image URLs (should be converted to CDN URLs) + +### Core Map/Learning Content + +``` +Table: learning_maps + - cover_image_url (likely stores B2 URLs because: explicitly commented "Public URL to the optimized cover image stored in Backblaze B2" - migration 20250920000000_optimize_image_storage.sql) + - cover_image_key (likely stores B2 URLs because: "Backblaze B2 file key for deletion and management") + +Table: node_content (also referenced as map_content) + - content_url (likely stores B2 URLs because: stores video/canva links; can be B2-hosted content or external links like YouTube) + +Table: learning_maps + - sprite_url (likely stores B2 URLs because: gamification boss/sprite images stored in B2) +``` + +### Seeds / Game System + +``` +Table: seeds + - cover_image_url (likely stores B2 URLs because: same B2 pattern as learning_maps, seeded from maps, uses StorageManager for upload - migration 20251202000001_create_seeds.sql + 20251203000001_add_seed_image_columns.sql) + - cover_image_key (likely stores B2 URLs because: B2 file key, same pattern as learning_maps) + +Table: seed_categories + - logo_url (likely stores B2 URLs because: category logos uploaded to B2 - migration 20251206000002_add_category_logos.sql) + +Table: seed_certificates + - signature_image_url (likely stores B2 URLs because: certificate signature images stored in B2) + - logo_url (likely stores B2 URLs because: certificate logo images stored in B2 - migration 20251213000001_add_certificate_system.sql) + +Table: issued_certificates + - certificate_url (likely stores B2 URLs because: URL to stored PNG/PDF in B2 - migration 20251213000001_add_certificate_system.sql) + +Table: seed_badges + - badge_image_url (likely stores B2 URLs because: badge images uploaded to B2 - migration 20251213000002_add_badge_system.sql) + - badge_image_key (likely stores B2 URLs because: Storage key for B2 management) +``` + +### Assessment / Submissions (User-Generated Content) + +``` +Table: assessment_submissions + - image_url (likely stores B2 URLs because: image upload assessments stored in B2 - migration 20250725080607_add_map_content.sql; documented in docs/file-upload-system.md) + - file_urls (likely stores B2 URLs because: array of B2 file URLs for multi-file submissions - migration 20250730114553_update_file_urls.sql; documented in docs/file-upload-system.md) + +Table: hackathon_phase_activity_submissions + - image_url (likely stores B2 URLs because: hackathon participant image submissions go to B2 - migration 20260403000000_hackathon_submissions.sql) + - file_urls (likely stores B2 URLs because: array of hackathon file submission B2 URLs - migration 20260403000000_hackathon_submissions.sql) + - revisions (likely stores B2 URLs because: JSONB array; each element has image_url + file_urls fields referencing B2 - migration 20260420074921_hackathon_submission_revisions.sql) + +Table: hackathon_phase_activity_team_submissions + - image_url (likely stores B2 URLs because: team hackathon image submissions go to B2 - migration 20260403000002_hackathon_activity_scope_and_team_submissions.sql) + - file_urls (likely stores B2 URLs because: team hackathon file submission B2 URLs - migration 20260403000002_hackathon_activity_scope_and_team_submissions.sql) + - revisions (likely stores B2 URLs because: JSONB array; each element has image_url + file_urls fields referencing B2 - migration 20260420074921_hackathon_submission_revisions.sql) + +Table: path_assessment_submissions + - image_url (likely stores B2 URLs because: PathLab image submissions go to B2 - migration 20260317000000_create_pathlab_content_system.sql) + - file_urls (likely stores B2 URLs because: PathLab file submission B2 URLs - migration 20260317000000_create_pathlab_content_system.sql) +``` + +### PathLab / Phase Activity Content + +``` +Table: path_content + - content_url (likely stores B2 URLs because: stores video/short_video/PDF content URLs from B2 - migration 20260317000000_create_pathlab_content_system.sql) + - image_url (likely stores B2 URLs because: PathLab image content stored in B2 - migration 20260317000000_create_pathlab_content_system.sql) + - file_urls (likely stores B2 URLs because: PathLab file content stored in B2 - migration 20260317000000_create_pathlab_content_system.sql) + +Table: hackathon_phase_activity_content + - content_url (likely stores B2 URLs because: hackathon phase video/short_video content from B2 - migration 20260401000000_hackathon_phase_activities.sql) +``` + +### Community / Social + +``` +Table: community_images + - url (likely stores B2 URLs because: community profile and cover photos stored in B2 - migration 20250630015258_update_communities.sql) + +Table: post_media + - url (likely stores B2 URLs because: post images/files stored in B2 - migration 20250630015258_update_communities.sql) +``` + +### Profiles / People + +``` +Table: mentor_profiles + - photo_url (likely stores B2 URLs because: mentor profile photos uploaded to B2 - migration 20260403100000_mentor_booking_system.sql) + +Table: expert_profiles + - photo_url (likely stores B2 URLs because: expert profile photos stored in B2 - migration 20260313100000_create_expert_interview_tables.sql) +``` + +### University / Education + +``` +Table: universities + - logo_url (likely stores B2 URLs because: university logo images stored in B2 - migration 20251119000002_create_educational_pathway_tables.sql) +``` + +### Projects / Reflections + +``` +Table: projects + - image_url (likely stores B2 URLs because: project image uploads stored in B2 - migration 20250702010000_refactor_reflection_system_revised.sql) + +Table: reflections + - image_url (likely stores B2 URLs because: reflection image uploads stored in B2 - migration 20250702010000_refactor_reflection_system_revised.sql) +``` + +### Hackathon Participant Avatars (ambiguous - see notes) + +``` +Table: hackathon_participants + - avatar_url (likely stores B2 URLs because: "URL to participant avatar image in storage bucket" - migration 20260406230001_add_comment_fields_to_participants.sql; NOTE: "storage bucket" could mean Supabase Storage, but since these participants are not Supabase Auth users, uploads likely go to B2 via the same upload pipeline) +``` + +### JSONB-Stored URLs (embedded in data) + +``` +Table: hackathon_phase_activity_submissions.revisions (JSONB column) + - Each revision element contains: image_url (text), file_urls (text[]) + (likely stores B2 URLs because: revision snapshots reference same B2 URLs as the parent row) + +Table: hackathon_phase_activity_team_submissions.revisions (JSONB column) + - Each revision element contains: image_url (text), file_urls (text[]) + (likely stores B2 URLs because: same as individual submissions) + +Table: issued_certificates.certificate_data (JSONB column) + - Contains: logo_url, signature_image_url (as part of rendered certificate data) + (likely stores B2 URLs because: certificate template references stored in the JSONB snapshot) +``` + +--- + +## Category 2: Supabase Storage URLs — DO NOT TOUCH + +These columns store Supabase Storage URLs (supabase.co/storage/v1/...) and should NOT be converted to CDN URLs: + +``` +Table: profiles + - avatar_url (Supabase Storage URL because: Managed by Supabase Auth; stores avatar URLs from auth.users user_metadata. format: https://.supabase.co/storage/v1/object/... — confirmed by image-delivery-report.md) + +Table: hackathon_teams + - team_avatar_url (Supabase Storage URL because: Uses dedicated Supabase Storage bucket `hackathon-team-avatars` created in migration 20260406000000_add_hackathon_profile_social_fields.sql. Storage policies reference storage.objects, not B2) +``` + +--- + +## Category 3: Non-Image URLs — IGNORE (not image/file storage) + +These columns store URLs but are NOT image/file storage URLs. They should NOT be converted: + +``` +Table: universities.website_url — University website link +Table: universities.admission_url — University admission page link +Table: expert_profiles.linkedin_url — LinkedIn profile link +Table: expert_profiles.booking_url — Meeting booking link +Table: mentor_profiles.instagram_url — Social media handle +Table: mentor_profiles.linkedin_url — Social media profile +Table: mentor_profiles.website_url — Personal website +Table: job_listings.company_url — Company website +Table: job_listings.apply_url — Job application link +Table: job_listings.url — Job listing source URL +Table: job_listings.source_url — Job data source +Table: projects.link — Project link (external) +Table: projects.spotify_album_cover_url — External album art (Spotify CDN) +Table: projects.preview_url — Spotify preview audio +Table: song_of_the_day.song_url — Spotify song URL +Table: song_of_the_day.album_cover_url — External album art +Table: cc_research_namespace.linkedin_url — Research profile link +Table: hackathon_journey_alien_clicks.target_url — External redirect +Table: inbox_notifications.action_url — Action link URL +Table: gdrive_analysis_results.drive_folder_url — Google Drive folder link +Table: educational_pathways.universities.website_url — University website +``` + +--- + +## Category 4: Columns Using External CDNs (Already Not B2) + +``` +Table: projects.spotify_album_cover_url — Spotify CDN (i.scdn.co) +Table: song_of_the_day.album_cover_url — Spotify CDN (i.scdn.co) +``` + +--- + +## Migration Files Referencing B2 Patterns + +- No migration files directly reference `backblazeb2.com` in column defaults or data +- No migration files directly reference `pseed-dev` in column defaults +- The B2 bucket/endpoint is configured entirely via environment variables (`B2_BUCKET_NAME`, `B2_ENDPOINT`) +- URLs are constructed at runtime in `lib/backblaze.ts`, `lib/storage/storage-manager.ts`, and `app/api/upload/presigned/route.ts` + +--- + +## TypeScript Type Definitions (types/) Confirming These Columns + +| Type File | URL Columns | +|-----------|-------------| +| `types/map.ts` | `cover_image_url`, `cover_image_blurhash`, `cover_image_key`, `sprite_url`, `content_url`, `file_urls`, `image_url` | +| `types/seeds.ts` | `cover_image_url`, `cover_image_blurhash`, `cover_image_key`, `logo_url`, `signature_image_url`, `certificate_template_url`, `certificate_url` | +| `types/badges.ts` | `badge_image_url`, `badge_image_key`, `cover_image_url` | +| `types/community.ts` | `cover_image_url`, `profile_image_url`, `avatar_url`, `url` (media) | +| `types/admin-hackathon.ts` | `image_url`, `file_urls`, `participant_avatar_url`, `photo_url` | +| `types/hackathon-phase-activity.ts` | `content_url` | +| `types/pathlab-content.ts` | `content_url`, `file_urls`, `image_url` | +| `types/education.ts` | `logo_url` | +| `types/mentor.ts` | `photo_url` | +| `types/expert-interview.ts` | `photoUrl` | +| `types/teams.ts` | `avatar_url` | +| `types/journey.ts` | `cover_image_url`, `cover_image_blurhash`, `cover_image_key` | +| `types/project.ts` | `image_url` | +| `types/classroom.ts` | `avatar_url` (from profiles, NOT B2) | +| `types/npc-conversations.ts` | `npc_avatar_id` (not a URL, references seed_npc_avatars table) | + +--- + +## Key Findings + +1. **Two distinct storage systems exist:** B2 for user-generated content/images, Supabase Storage for auth avatars and hackathon team avatars. + +2. **No existing backblazeb2.com references in database data or migrations.** All B2 URLs are built at runtime. + +3. **The CDN infrastructure exists** (`cdn.passionseed.org` → `f005.backblazeb2.com`) but is NOT currently used by the application code. All code constructs raw B2 bucket URLs. + +4. **The heavy-lifting tables for CDN migration are:** + - `assessment_submissions` (image_url, file_urls) + - `hackathon_phase_activity_submissions` (image_url, file_urls, revisions) + - `hackathon_phase_activity_team_submissions` (image_url, file_urls, revisions) + - `learning_maps` (cover_image_url) + - `seeds` (cover_image_url) + - `path_assessment_submissions` (image_url, file_urls) + - `community_images` (url) + - `post_media` (url) + +5. **Columns using `_key` suffix** (cover_image_key, badge_image_key, certificate_key) store B2 file keys, not full URLs. These don't need URL transformation but are part of the B2 storage system. + +6. **JSONB columns** (`revisions`, `certificate_data`) contain embedded B2 URLs that would also need transformation if doing a DB-level migration. diff --git a/.factory/analysis/dependency-readiness.md b/.factory/analysis/dependency-readiness.md new file mode 100644 index 00000000..f5aadf47 --- /dev/null +++ b/.factory/analysis/dependency-readiness.md @@ -0,0 +1,37 @@ +# Dependency Readiness Report + +## Packages: PASS +- pnpm install: Completed successfully in 782ms. Lockfile up to date, no errors. + +## Tools: PASS +- node: v25.9.0 +- supabase CLI: v2.95.4 (newer v2.98.1 available, but current version is functional) +- curl: 8.7.1 +- gh: authenticated (account: xb1g, scopes: admin:public_key, gist, read:org, repo) + +## Environment: PASS +- NEXT_PUBLIC_SUPABASE_URL: set (http://127.0.0.1:54321) +- NEXT_PUBLIC_SUPABASE_ANON_KEY: set (masked) +- SUPABASE_SERVICE_ROLE_KEY: set (masked) +- B2_BUCKET_NAME: set (pseed-dev) — matches expected value ✓ +- B2_ENDPOINT: set (s3.us-east-005.backblazeb2.com) — matches expected value ✓ +- B2_APPLICATION_KEY_ID: set (masked) +- B2_APPLICATION_KEY: set (masked) + +## Supabase: NOT RUNNING +- `supabase status` failed: Docker daemon is not running (OrbStack socket unreachable). +- Local Supabase at http://127.0.0.1:54321 is unavailable for direct DB work. +- Workers needing local DB access must either start Docker/OrbStack or operate against a remote Supabase instance. + +## GitHub: PASS +- Authenticated via SSH as xb1g with `repo` scope. Git operations will work. + +## Blockers +- Docker/OrbStack daemon is not running, which prevents `supabase status`, `supabase db push`, and any local Supabase operations. Workers needing to apply SQL migrations or test DB changes locally will need Docker started first. + +## Recommendations +1. Start Docker/OrbStack daemon before any worker that needs local Supabase DB access (SQL migration workers, DB testing workers). +2. Supabase CLI upgrade (v2.95.4 → v2.98.1) is optional but not blocking. +3. Environment variables are all configured — no .env.local changes needed before starting. +4. For Cloudflare API and Vercel Analytics workers, ensure Cloudflare API token and Vercel token are available (not checked here — may need separate verification if those workers require them). +5. The SQL migration worker can draft the B2→CDN URL rewrite script without a running Supabase, but testing it will require a DB connection. diff --git a/.factory/analysis/image-delivery-report.md b/.factory/analysis/image-delivery-report.md new file mode 100644 index 00000000..1f953397 --- /dev/null +++ b/.factory/analysis/image-delivery-report.md @@ -0,0 +1,296 @@ +# Image Delivery Analysis Report + +## Current Image Components + +### next/image users +- `components/main-nav.tsx` → `/passionseed-logo.svg`, `/hackathon/HackLogo.png` (local static files in `/public`) +- `components/landing-page-wrapper.tsx` → imports `Image` but delegates to child components (no direct src usage) +- `components/map/OptimizedImage.tsx` → wraps `next/image` for all map/seed cover images; `src` and `blurhash` props dynamically passed from B2 URLs or local defaults +- `components/map/AnimatedMapPreview.tsx` → `map.metadata.coverImage` (B2 URL from DB) +- `components/ps/CreateProjectModal.tsx` → `selectedSong.albumCover` (external album art URLs) +- `components/journey/SocialSharePreview.tsx` → dynamic `img` prop (passed in) +- `components/education/direction-finder/AIConversation.tsx` → `/passionseed-logo.svg` (local static) +- `components/landing-hero.tsx` → imports `Image` (used in child section of LandingDemoPaths, not directly) +- `components/landing-logos.tsx` → `/universities/chula-logo.png`, `/universities/tu-logo.png`, `/universities/KU-logo.jpg`, `/universities/kmutt-logo.png`, `/universities/csii-logo.png` (local static) +- `components/landing-hackathon-banner.tsx` → `/hackathon/HackLogo.png` (local static) +- `app/expert-interview/page.tsx` → `/passionseed-logo.svg` (local static) +- `app/hackathon/line-oa/page.tsx` → `/hackathon/LineQR.jpg` (local static) +- `app/app/beta/success/page.tsx` → `https://qr-official.line.me/gs/M_161irjbq_GW.png?oat_content=qr` (external URL, with `unoptimized`) +- `app/app/beta/page.tsx` → `/CBT/Person1.svg`, `/CBT/Person2.svg`, `/CBT/UI1_1.png`, `/CBT/UI1_2.png`, `/CBT/UI2_1.png`, `/CBT/UI2_2.png` (local static) +- `app/pitch/page.tsx` → `/passionseed-logo.svg` (local static) +- `app/pitch/3/page.tsx` → `/passionseed-logo.svg` (local static — used twice) + +**Summary of next/image source types:** +- **Local static (~80% of next/image instances):** `/public/` directory files (logos, SVGs, PNGs) — these are bundled with the app and served by Vercel +- **Dynamic B2 URLs (~15%):** `cover_image_url` from `learning_maps` and `seeds` tables — these point to `https://pseed-dev.s3.us-east-005.backblazeb2.com/images/maps/...` +- **External URLs (~5%):** QR code URLs, album art URLs + +### tag users +- `components/community/CommunityCard.tsx` → `community.profile_image_url` and `community.cover_image_url` as CSS `backgroundImage` (URLs from DB, likely B2) +- `components/community/PostCard.tsx` → `media.url` (hackathon submission media URLs from B2) +- `components/hackathon/ImpactLandingPage.tsx` → local static sponsor logos (`/hackathon/PS.png`, `/hackathon/AMSA.png`, `/hackathon/StemLike.png`) +- `components/hackathon/LandingPage.tsx` → local static SVGs (`/hackathon/Creature/*.svg`) and sponsor PNGs — ~15 separate `` tags on this page +- `components/hackathon/CelebrationPage.tsx` → local static SVGs (`/hackathon/Creature/Jellyfish 1.svg`, etc.) — ~4 images +- `components/hackathon/ChallengePage.tsx` → `/images/hackathon_*.png` (local static thumbnails) +- `components/seeds/GameBoxCard.tsx` → `seed.cover_image_url` (B2 URL), `seed.category.logo_url` (URL from DB) +- `components/seeds/CategoryManagementModal.tsx` → `logoPreview` from file input (local data URL) +- `components/seeds/certificates/templates/*.tsx` → `data.logo_url` (certificate logo URL from DB) +- `components/admin/AdminHackathonActivities.tsx` → `ans.image_url` (hackathon submission image B2 URL) +- `components/admin/UniversityManagement.tsx` → university logo URLs +- `components/song-of-the-day/portal-vinyl.tsx` → `currentSong.albumCover` (external album art) +- `components/map/OptimizedImage.tsx` → blurhash placeholder uses `` internally + +**Summary of tag source types:** +- **Local static (~50%):** SVGs, PNGs in `/public/hackathon/`, `/public/images/` +- **Dynamic B2 URLs (~40%):** `cover_image_url`, `image_url`, `media.url`, `profile_image_url` +- **External URLs (~10%):** Album covers, data URLs + +### AvatarImage users (shadcn/ui) +All use `profile.avatar_url` from the Supabase `profiles` table via DB queries. These URLs originate from Supabase Auth's built-in avatar storage (`*.supabase.co/storage/v1/object/...`). + +Files with AvatarImage: +- `components/community/PostCard.tsx` → `post.author.avatar_url` +- `components/teams/TeamMembersPanel.tsx` → `profile?.avatar_url` +- `components/teams/TeamOverviewCard.tsx` → `team.team_metadata.avatar_url` +- `components/teams/StudentsWithoutTeamsPanel.tsx` → `student.avatar_url` +- `components/teams/JoinTeamModal.tsx` → import only (likely in child) +- `components/teams/AllTeamsGrid.tsx` → `team.leader.profiles.avatar_url`, `member.profiles.avatar_url` +- `components/seeds/LobbyView.tsx` → import only +- `components/seeds/SeedRoomDashboard.tsx` → `profile?.avatar_url` +- `components/map/SubmissionList.tsx` → empty string `""` (placeholder) +- `components/map/InstructorGradingPanel.tsx` → empty string `""` (placeholder) +- `components/map/TeamNodeViewPanel.tsx` → `prof?.avatar_url` +- `components/ps/task-list.tsx` → `m.user?.avatar_url` (3 instances) +- `components/ps/build-leaderboard.tsx` → `user.avatarUrl` +- `components/ps/draggable-task.tsx` → `assignee.user?.avatar_url` +- `components/classroom/StudentProgressTable.tsx` → `student.user?.avatar_url` +- `components/classroom/StudentProgressView.tsx` → `student.user?.avatar_url` +- `components/classroom/InstructorManagement.tsx` → `instructor.profiles.avatar_url` +- `components/classroom/TeamDetailsModal.tsx` → avatar URL (team member) +- `components/classroom/ClassroomTeamsManager.tsx` → `student.avatar_url` +- `components/education/direction-finder/AIConversation.tsx` → AI avatar URL +- `components/user-nav.tsx` → user metadata avatar URL +- `components/journey/nodes/UserCenterNode.tsx` → `data.userAvatar` +- `app/profile/page.tsx` → `profile.avatar_url` +- `app/communities/page.tsx` → empty string (placeholder) +- `app/communities/[slug]/page.tsx` → empty string (placeholder) +- `app/settings/page.tsx` → `user?.user_metadata?.avatar_url` +- `app/map/[id]/grading/grading-table.tsx` → import only + +**Note:** All `avatar_url` values originate from **Supabase Auth/Storage** (not B2). The `profiles` table stores them as raw Supabase storage URLs. + +--- + +## B2 URL Construction + +### How URLs Are Built + +Three locations construct B2 URLs identically: + +1. **`lib/backblaze.ts` (BackblazeB2 class):** + ```typescript + const fileUrl = `https://${this.bucketName}.${this.endpoint}/${fileName}`; + // e.g. https://pseed-dev.s3.us-east-005.backblazeb2.com/submissions/user123/node456/1712345678_abc123.jpg + ``` + +2. **`lib/storage/storage-manager.ts` (StorageManager.getImageUrl):** + ```typescript + return `https://${bucketName}.${endpoint}/${fileName}`; + ``` + +3. **`app/api/upload/presigned/route.ts`:** + ```typescript + const fileUrl = `https://${process.env.B2_BUCKET_NAME}.${process.env.B2_ENDPOINT}/${presignedData.fileKey}`; + ``` + +### Current Endpoint Patterns + +| Config | Value | +|--------|-------| +| Bucket name (`B2_BUCKET_NAME`) | `pseed-dev` | +| Endpoint (`B2_ENDPOINT`) | `s3.us-east-005.backblazeb2.com` (production) / `s3.us-west-000.backblazeb2.com` (default/fallback) | +| URL pattern | `https://pseed-dev.s3.{region}.backblazeb2.com/{path}` | +| f005 (friendly URL) | `https://f005.backblazeb2.com/file/pseed-dev/{path}` — used for guidebook PDF and APK download | + +### Storage Paths + +| Purpose | Path | +|---------|------| +| Participant submissions | `submissions/{userId}/{nodeId}/{timestamp}_{random}.{ext}` | +| Map content files | `maps/{nodeId}/content/{timestamp}_{random}.{ext}` | +| Map cover images | `images/maps/{timestamp}_{fileName}` | +| Seed cover images | Same B2 upload, URL stored in `seeds.cover_image_url` | + +### CDN Equivalent Patterns + +A Cloudflare CDN at `cdn.passionseed.org` exists (per task context) that proxies to `f005.backblazeb2.com`. + +**Current B2 URL → CDN URL transformation:** +- `https://pseed-dev.s3.us-east-005.backblazeb2.com/images/maps/123_cover.webp` +- → `https://cdn.passionseed.org/file/pseed-dev/images/maps/123_cover.webp` + +**No existing CDN URL usage in codebase.** All current code uses the raw `bucket.endpoint` B2 URL format. The CDN infrastructure exists but is **not being used** in the application code. + +--- + +## Migration Impact + +### Impact of `images.unoptimized: true` in Production + +Current config: +```javascript +// next.config.mjs +images: { + unoptimized: process.env.NODE_ENV === "development", + // ... +} +``` + +This means **production (`NODE_ENV !== "development"`) has `unoptimized: false`** — Vercel's image optimization is **active** for ALL `next/image` requests. + +**What happens when we change to `unoptimized: true`:** +1. **Local static images** (`/passionseed-logo.svg`, `/hackathon/*.png`, etc.) — No negative impact. They are already served directly by Vercel. `next/image` will render `` tags with correct sizes/sourceset attributes pointing to the origin. +2. **B2 images (`cover_image_url`, `cover_image_blurhash`)** — `OptimizedImage` component wraps `next/image`. With `unoptimized: true`, Vercel will NOT proxy/transform these. The raw B2 URLs will be served directly from Backblaze. This is actually **desired** since the CDN should handle caching/optimization. +3. **External URLs** (`https://qr-official.line.me/...`) — Already uses `unoptimized` prop in some cases (`app/app/beta/success/page.tsx`). Others would need the prop added or just serve directly. +4. **Supabase storage URLs** (`avatar_url`) — Supabase serves images directly, no Vercel optimization needed. + +**Performance implication:** Without Vercel's optimization, you lose: +- Automatic WebP/AVIF conversion for JPEG/PNG images +- Automatic responsive sizing +- On-the-fly resizing + +**These must be compensated for:** +- Map/seed cover images are already pre-processed to WebP with blurhash via `image-processor.ts` before upload to B2 +- The CDN can handle caching and format negotiation +- `sizes` attributes should be set properly on `next/image` components + +### Required URL Transformations + +To use the CDN, B2 URLs must be rewritten: + +```typescript +// Utility needed +function toCdnUrl(b2Url: string): string { + // https://pseed-dev.s3.us-east-005.backblazeb2.com/images/maps/123_cover.webp + // → https://cdn.passionseed.org/file/pseed-dev/images/maps/123_cover.webp + const match = b2Url.match(/^https:\/\/pseed-dev\.s3\.[^/]+\.backblazeb2\.com\/(.+)$/); + if (!match) return b2Url; + return `https://cdn.passionseed.org/file/pseed-dev/${match[1]}`; +} +``` + +### Pages with Heavy Image Usage (Potential Edge Cases) + +| Page | Image Count | Type | Risk | +|------|------------|------|------| +| `/map` (MapGallery) | Up to 20-50 MapCards per page, each with OptimizedImage + VinylRecord background | B2 cover images, local fallbacks | **HIGH** — heaviest page; each `next/image` call hits Vercel optimization in production | +| `/seeds` (SeedGallery) | Up to 20-50 GameBoxCards, each with `seed.cover_image_url` (plain ``) | B2 cover images | **HIGH** — uses plain `` tags so unaffected by `unoptimized` change, but still many external image requests | +| `/hackathon/*` (LandingPage, CelebrationPage) | ~10-15 local SVGs/PNGs | Local static | **MEDIUM** — local files, no optimization impact | +| `/communities` | CommunityCards with `cover_image_url` as CSS `background-image` | B2 URLs | **LOW** — CSS background, not next/image | +| `/app/beta` | ~6 local PNGs/SVGs | Local static | **LOW** — local files | +| `/hackathon/challenge` | Team submission images via `` tags | B2 URLs | **MEDIUM** — but uses plain ``, not next/image | +| Admin grading panels | Individual submission images (1 at a time) | B2 URLs | **LOW** — single image views | + +**Key observation:** The heaviest image pages (`/map`, `/seeds`) display **B2 cover images that are already pre-processed** (WebP, blurhash, optimized sizes). They don't need Vercel's optimization — they need the CDN to serve them fast. + +--- + +## Recommended Changes + +### 1. Config Changes + +**`next.config.mjs`:** +```javascript +images: { + unoptimized: true, // Disable Vercel image optimization entirely + // Keep remotePatterns for Allowlist (required even when unoptimized) + remotePatterns: [ + { protocol: "https", hostname: "*.backblazeb2.com" }, + { protocol: "https", hostname: "*.supabase.co" }, + { protocol: "https", hostname: "cdn.passionseed.org" }, // NEW + ], + // Remove formats — not needed when unoptimized +} +``` + +### 2. Code Changes Needed + +#### A. Create CDN URL utility — HIGH PRIORITY + +**New file: `lib/cdn-url.ts`** +```typescript +const CDN_BASE = "https://cdn.passionseed.org"; +const B2_BUCKET = process.env.B2_BUCKET_NAME || "pseed-dev"; +const B2_ENDPOINT = process.env.B2_ENDPOINT || "s3.us-west-000.backblazeb2.com"; + +export function toCdnUrl(b2Url: string): string { + if (!b2Url) return ""; + + // Already a CDN URL — pass through + if (b2Url.startsWith(CDN_BASE)) return b2Url; + + // B2 raw URL → CDN URL + const prefix = `https://${B2_BUCKET}.${B2_ENDPOINT}/`; + if (b2Url.startsWith(prefix)) { + const path = b2Url.slice(prefix.length); + return `${CDN_BASE}/file/${B2_BUCKET}/${path}`; + } + + // f005 friendly URL → CDN URL + if (b2Url.match(/^https:\/\/f\d+\.backblazeb2\.com\/file\//)) { + const url = new URL(b2Url); + const path = url.pathname.replace(/^\/file\//, ""); + return `${CDN_BASE}/${path}`; + } + + return b2Url; +} +``` + +#### B. Update B2 client to emit CDN URLs + +**`lib/backblaze.ts`** — Modify URL construction in `uploadFile()`, `uploadImageBuffer()`, and `getImageUrl()` to return CDN URLs instead of raw B2 URLs. Or add a config flag: +```typescript +const USE_CDN = process.env.B2_USE_CDN === "true"; +// Then: fileUrl = USE_CDN ? toCdnUrl(rawB2Url) : rawB2Url; +``` + +#### C. Update `lib/storage/storage-manager.ts` + +`getImageUrl()` method needs to return CDN URLs when the CDN is configured. + +#### D. Update `lib/hackathon/image-analysis.ts` + +The `transformToBackblazeUrl()` function should also transform to CDN URLs (or the new `toCdnUrl` utility should be applied after the Supabase→B2 transformation). + +#### E. Add `unoptimized` prop to external URL images + +**`app/app/beta/success/page.tsx`** — Already has `unoptimized`. Good. +**`components/ps/CreateProjectModal.tsx`** — External album art URLs. Add `unoptimized`. +**`components/song-of-the-day/portal-vinyl.tsx`** — External album art. Uses plain ``, no change needed. + +#### F. Optimize `sizes` attributes on OptimizedImage + +**`components/map/OptimizedImage.tsx`** — Ensure proper `sizes` prop to help the browser fetch the right resolution since Vercel won't resize. The images are already optimized (1200x800 WebP with blurhash), so this is mostly about correct display sizing. + +### Files NOT needing changes +- `components/main-nav.tsx`, `components/landing-*.tsx`, `app/pitch/*.tsx` — Local static images. No change needed. +- All `AvatarImage` components — Supabase storage URLs. No optimization on those anyway. +- `` tag users — Already bypass Vercel optimization. + +### 3. Environment Variables Required + +| Variable | Purpose | Value | +|----------|---------|-------| +| `B2_BUCKET_NAME` | Already exists | `pseed-dev` | +| `B2_ENDPOINT` | Already exists | `s3.us-east-005.backblazeb2.com` | +| `NEXT_PUBLIC_CDN_BASE` | **NEW** — CDN base URL for client-side URL construction | `https://cdn.passionseed.org` | +| `B2_USE_CDN` | **NEW** — Toggle CDN URL construction server-side | `true` | + +### 4. Migration Rollout Strategy + +1. **Phase 1:** Create `lib/cdn-url.ts` utility. Write tests. +2. **Phase 2:** Set `images.unoptimized: true` in next.config.mjs. +3. **Phase 3:** Update B2 client + StorageManager to emit CDN URLs. +4. **Phase 4:** Verify all pages render images correctly in staging. +5. **Phase 5:** Deploy to production with monitoring on image load times. diff --git a/.factory/analysis/static-pages-report.md b/.factory/analysis/static-pages-report.md new file mode 100644 index 00000000..28a2e475 --- /dev/null +++ b/.factory/analysis/static-pages-report.md @@ -0,0 +1,123 @@ +# Static Page Analysis Report + +Generated: 2026-05-05 | Project: PassionSeed (Next.js 15.4.5 App Router + Supabase SSR) + +--- + +## Can Convert to force-static + +- **app/map/page.tsx**: No auth check, no cookies, no redirects, no searchParams. Calls `getMapsWithStatsServer(0, 20)` which fetches public map listing data. No `createClient()` call at all — uses a server-side Supabase query only. Renders a client component wrapper (`MapsClientPage`) that handles interactivity client-side. The server pre-fetch is purely for hydration performance. Can be `export const dynamic = "force-static"`. + +--- + +## Can Convert to ISR + +- **app/hackathon/sponsorship/page.tsx**: Public page. Uses `createAdminClient()` (service role key, no cookies). Fetches participant/team counts and grade-level breakdowns. No user auth, no redirects, no cookie reads. Aggregated stats that change slowly. Suggested: `export const revalidate = 300` (5 minutes). + +- **app/epic-sprint/page.tsx**: Public report page. Uses `createAdminClient()` only. Fetches hackathon participants and beta registrations for a static report (Epic Sprint March 2026). The underlying data is historical (linked to a past event) and rarely changes. No user auth, no redirects. Suggested: `export const revalidate = 3600` (1 hour) — or even force-static since this is a historical report. + +- **app/fi/exec/page.tsx**: Public executive dashboard. Uses `createAdminClient()` only. Fetches hackathon and beta data for summary stats. No user auth, no redirects, no cookies. Suggested: `export const revalidate = 300` (5 minutes). + +--- + +## Must Stay force-dynamic + +### Auth-gated pages (read cookies, redirect based on user state) + +- **app/page.tsx** (root): Reads cookies via `createClient()`, calls `getUser()`, checks `isAnonymousUser()`, queries `profiles` table, conditionally redirects to `/me`, `/onboard`, `/auth/finish-profile`, or renders landing page. This is the authentication routing hub — must be per-request. + +- **app/login/page.tsx**: Reads cookies via `createClient()`, calls `getUser()`. If already logged in, checks `profiles` table for completion and redirects to `/auth/finish-profile` or `/`. Must be per-request (auth guard before showing login form). + +- **app/me/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login` if no user. Fetches user-specific dashboard data via `getUserDashboardData(supabase)`. Renders personalized portal with reflections, next steps, etc. Must be per-user. + +- **app/me/journey/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login` if no user. Passes `userId` to client wrapper for personalized learning map. Must be per-user. + +- **app/onboard/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login` if no user. Queries `onboarding_state` and `profiles` for that specific user. Redirects to `/me` if already onboarded. Must be per-user. + +- **app/classrooms/join/page.tsx**: Reads cookies, calls `getUser()`, returns `notFound()` if no user. Renders join form. Must be per-request (auth required to join). + +- **app/teams/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login`. Fetches user-specific classroom memberships, team data, and maps. Complex per-user data aggregation. Must be per-user. + +- **app/feedback/[token]/page.tsx**: Uses `params.token`. Conditionally reads cookies/auth if `form.require_auth` is true — redirects unauthenticated users to login. Has a server action (`handleSubmit`) that processes form submissions. Uses `createClient()` in the JSX (auth-dependent footer). Must be per-request. + +### PS (Passion Seed) team-gated pages + +- **app/build/page.tsx**: Calls `getPSAccess()` which reads cookies and checks `user_roles` table. Renders "Build Access Required" for unauthorized. Fetches user-specific tasks, leaderboard, focus stats, project memberships. Must be per-user. + +- **app/ps/projects/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login`. Fetches user-specific projects, tasks, pending request counts, and project memberships. Must be per-user. + +- **app/ps/projects/[id]/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login`. Uses `params.id`. Fetches project details, stats, members, requests — all user-specific and project-specific. Must be per-user. + +- **app/ps/projects/[id]/feedback/page.tsx**: Uses `params.id`. No direct `createClient()` call, but calls server actions (`getProjectForms`, `getProjectSubmissions`) that internally auth-check via `getPSAccess()` or `checkPSRole()`. Contains inline server action (`createForm`). Data is project-specific and user-gated. Caching would leak form data between users. Must stay dynamic. + +- **app/ps/projects/[id]/feedback/[formId]/page.tsx**: Uses `params.id` and `params.formId`. Calls server actions (`getFormWithFields`, `getProjectTasks`) that auth-check internally. Form editor page with user-gated data. Must stay dynamic. + +- **app/ps/b2b/page.tsx**: Calls `getPSAccess()` which reads cookies and checks roles. Renders "Build Access Required" for unauthorized. Must be per-request. + +- **app/ps/hackathon/page.tsx**: Reads cookies, calls `getUser()`, redirects to `/login`. Fetches user-specific hackathon data, memberships, and pending counts. Must be per-user. + +### CC Research pages (team-gated) + +- **app/cc-research/layout.tsx**: Calls `requireCCResearchAccess()` which reads cookies, checks auth, queries `user_roles`. Returns different UI for unauthorized vs authorized. All child pages inherit this auth gate. Must stay dynamic. + +- **app/cc-research/campaigns/page.tsx**: Calls `requireCCResearchAccess()` and `listCcCampaigns()`. Team-gated data. Even though layout also checks auth, the page has its own auth call. Must stay dynamic. + +- **app/cc-research/campaigns/[id]/page.tsx**: Calls `requireCCResearchAccess()` and `getCampaignDashboardPayload(id)` using `params.id`. Team-gated, campaign-specific data. Must stay dynamic. + +### Workshop page (auth-aware rendering) + +- **app/workshops/page.tsx**: Calls `createClient()` and `getUser()` but does NOT redirect. Uses `user` boolean to conditionally render buttons ("Suggest a Path" links to `/suggest-path` vs `/login?modal=suggest`, "Vote for Next Paths" shown only when logged in). Fetches workshop list from DB. Could theoretically work with force-static + client-side auth check for the conditional UI, but currently relies on server-side auth read. Must stay dynamic for now (would need refactoring to go static). + +### Admin pages (all behind requireAdmin layout) + +All 25 admin pages are protected by `app/admin/layout.tsx` which calls `requireAdmin()` — this reads cookies, checks auth, queries `user_roles` for admin role, and redirects unauthorized users to `/login` or `/me`. The layout's `force-dynamic` makes the entire admin subtree dynamic. Individual pages also mostly use `createAdminClient()` (service key, no cookies) to fetch data, but their access control depends on the layout's cookie-based auth. + +- **app/admin/layout.tsx**: `requireAdmin()` — reads cookies, checks user_roles, redirects. Must stay dynamic (auth gate). +- **app/admin/page.tsx**: Admin dashboard with nav cards. Protected by layout. Page itself is a pure component (no data fetching), but behind auth gate. +- **app/admin/analytics/page.tsx**: Protected by layout. +- **app/admin/users/page.tsx**: Protected by layout. +- **app/admin/experts/page.tsx**: Protected by layout. +- **app/admin/hackathon/page.tsx**: Protected by layout. +- **app/admin/hackathon/analytics/page.tsx**: Protected by layout. +- **app/admin/hackathon/team-directions/page.tsx**: Protected by layout. +- **app/admin/hackathon/activities/editor/page.tsx**: Protected by layout. +- **app/admin/hackathon/activities/page.tsx**: Protected by layout. +- **app/admin/hackathon/activities/[activityId]/clusters/page.tsx**: Protected by layout. Uses params. +- **app/admin/hackathon/teams/page.tsx**: Protected by layout. +- **app/admin/hackathon/mentors/page.tsx**: Protected by layout. +- **app/admin/hackathon/participants/page.tsx**: Protected by layout. +- **app/admin/hackathon/email-sender/page.tsx**: Protected by layout. +- **app/admin/hackathon/team-finder/page.tsx**: Protected by layout. +- **app/admin/hackathon/team-submissions/page.tsx**: Protected by layout. +- **app/admin/hackathon/push-sender/page.tsx**: Protected by layout. +- **app/admin/hackathon/questionnaire/page.tsx**: Protected by layout. +- **app/admin/ceo/page.tsx**: Protected by layout. +- **app/admin/ceo/payments/page.tsx**: Protected by layout. +- **app/admin/maps/page.tsx**: Protected by layout. +- **app/admin/beta/page.tsx**: Protected by layout. +- **app/admin/direction-finder/page.tsx**: Protected by layout. +- **app/admin/team-matching-simulator/page.tsx**: Protected by layout. +- **app/admin/event-tracker/page.tsx**: Protected by layout. + +--- + +## Summary + +- **Total pages analyzed**: 49 (48 pages + 2 layouts) +- **Can go force-static**: 1 (`app/map/page.tsx`) +- **Can go ISR**: 3 (`app/hackathon/sponsorship/page.tsx`, `app/epic-sprint/page.tsx`, `app/fi/exec/page.tsx`) +- **Must stay force-dynamic**: 45 (all auth-gated, team-gated, admin pages, plus the auth-aware workshops page) + +--- + +## Key Takeaways + +1. **The vast majority (92%) must stay dynamic** because they depend on Supabase SSR cookies for authentication and user-specific data fetching. This is inherent to a user-gated application. + +2. **Only 4 pages (8%) can be optimized** — three ISR candidates and one force-static candidate. All four share the same pattern: public pages using `createAdminClient()` (service role key) with no user auth, no cookies, no redirects. + +3. **`createAdminClient()` vs `createClient()` is the key discriminator.** Pages using the admin client (service role) can be static/ISR. Pages using the SSR client (cookies) cannot. + +4. **The admin layout** (`app/admin/layout.tsx`) is the single auth gate for 25 pages. If that layout could be refactored to not require cookies (e.g., using a different auth mechanism), all admin pages could theoretically become ISR. However, Supabase SSR auth is fundamentally cookie-based, so this is not feasible without a major architectural change. + +5. **`app/map/page.tsx`** is an anomaly — it has `force-dynamic` but performs no auth check at all. It simply fetches public map data. The `force-dynamic` appears to be a leftover. Converting to `force-static` is safe and would eliminate serverless invocations for a potentially high-traffic page. diff --git a/.factory/analysis/validation-readiness.md b/.factory/analysis/validation-readiness.md new file mode 100644 index 00000000..39980ca8 --- /dev/null +++ b/.factory/analysis/validation-readiness.md @@ -0,0 +1,66 @@ +# Validation Readiness Report + +Generated: 2026-05-05 + +## Build: PASS +- **Exit code**: 0 +- **Summary**: `pnpm build` completed successfully. All routes compiled (static + dynamic). Build succeeded despite local Supabase being unavailable (Docker daemon down), which means the build does not depend on Supabase being reachable at build time. +- **Key observation**: This is excellent news for the Vercel cost optimization mission — the build pipeline is robust and changes to `next.config.mjs`, static page conversions, etc. can be verified via `pnpm build` without needing a running Supabase instance. + +## Lint: FAIL +- **Exit code**: 1 +- **Error**: `Invalid project directory provided, no such directory: /Users/bunyasit/.warp/worktrees/pseed/thermal-mogote/lint` +- **Root cause**: Next.js lint configuration is pointing to a non-existent `/lint` directory. This appears to be a pre-existing configuration issue unrelated to the mission. +- **Impact**: Lint cannot be used as a validation gate without fixing this config first. + +## Test: FAIL +- **Exit code**: 1 +- **Results**: 4 test suites failed, 1 skipped, 19 passed (23 of 24 total suites) +- **Test counts**: 5 failed, 6 skipped, 53 passed (64 total) +- **Primary failure**: `lib/hackathon/__tests__/line-notification.test.ts` — crashes due to a CJS/ESM incompatibility in the `postal-mime` dependency (imported transitively through `resend` → `postal-mime`). The error is a `TypeError` in `decode-strings.cjs` triggered at module load time. +- **Impact**: Tests that don't transitively import the broken module chain should still pass. Workers can run focused test subsets (e.g., `pnpm jest --testPathPattern=...`). + +## TypeCheck: FAIL +- **Exit code**: 1 +- **Error scope**: All 20 TypeScript errors are confined to `supabase/functions/` directory (Deno Edge Functions): + - `portfolio-fit/index.ts`: `Deno` global not found, implicit `any` types + - `score-engine/index.ts`: Deno module imports not resolvable, `Deno` global, `unknown` error + - `viability-webhook/index.ts`: Same Deno pattern +- **Next.js app**: Clean — no TypeScript errors in `app/`, `components/`, `lib/`, or `types/`. +- **Impact**: The Next.js application code is type-safe. The Deno function errors are expected (tsc doesn't understand Deno runtime). This is not a blocker. + +## agent-browser: available +- **Path**: `/Users/bunyasit/.factory/bin/agent-browser` +- **Version**: 0.17.1 +- **Status**: Fully available for browser-based verification (screenshot diffs, page load checks, interactive testing). + +## Resources +- **Before build**: 869 processes, pages free: 4,108 (67 MB), pages active: 436,557 (7.1 GB) +- **After build**: 876 processes, pages free: 78,201 (1.3 GB), pages active: 384,011 (6.3 GB) +- **Delta**: +7 processes, memory freed up post-build (likely GC/temp file cleanup). + +## Port 3000: occupied +- **By**: node (PID 10802), listening on TCP `*:hbci` (port 3000) +- **Note**: This is likely an existing `pnpm dev` or Next.js process. Workers attempting to start a dev server on port 3000 will need to either kill PID 10802 or use an alternate port. + +## Blockers +- **Lint config is broken**: `next lint` looks for a directory `/lint` that doesn't exist. Needs investigation of `next.config.mjs` or `.eslintrc` to fix the lint directory configuration. +- **Test suite has pre-existing CJS/ESM failures**: The `postal-mime` / `resend` compatibility issue blocks 1 test suite (4 suites total fail). This is pre-existing and unrelated to the cost optimization mission. +- **Port 3000 is occupied**: Cannot start a dev server on the default port without killing the existing process. + +## Recommendations for Workers + +### What workers CAN verify (given the environment): +1. **Build integrity**: `pnpm build` works end-to-end. Any mission changes to `next.config.mjs`, page exports (`export const dynamic = 'force-static'`), or Edge runtime config can be validated via `pnpm build`. +2. **Targeted tests**: Run specific test files that don't import the broken module chain: + ``` + pnpm jest --testPathIgnorePatterns='line-notification' + ``` +3. **TypeScript for app code**: Use `npx tsc --noEmit --exclude 'supabase/functions/**'` to verify Next.js app types only. +4. **agent-browser visual verification**: With agent-browser 0.17.1 available, workers can spin up `pnpm dev` on an alternate port (e.g., 3001) and do visual checks of static pages, verify Cloudflare cache headers, and take before/after screenshots. +5. **Static page verification**: Workers can inspect `.next/server/app/` for `.html` files to confirm static generation succeeded for converted pages. + +### What workers CANNOT verify (blocked): +1. **Supabase-dependent runtime behavior**: No local Supabase means any page that calls `supabase.from(...)` at runtime cannot be functionally verified end-to-end. +2. **Lint compliance**: Broken lint config needs fixing first. +3. **Full test suite pass**: The CJS/ESM issue blocks a subset of tests. diff --git a/.gitignore b/.gitignore index 6a65f8de..92fa854a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,11 @@ coverage # local worktrees .worktrees/ .gstack/ + +# OpenNext +.open-next + +# wrangler files +.wrangler +.dev.vars* +!.dev.vars.example diff --git a/.sisyphus/ralph-loop.local.md b/.sisyphus/ralph-loop.local.md deleted file mode 100644 index 40838ed7..00000000 --- a/.sisyphus/ralph-loop.local.md +++ /dev/null @@ -1,30 +0,0 @@ ---- -active: true -iteration: 11 -max_iterations: 500 -completion_promise: "DONE" -initial_completion_promise: "DONE" -started_at: "2026-04-14T16:32:13.805Z" -session_id: "ses_274ea3f77ffeHNjp3V45iM707p" -ultrawork: true -strategy: "continue" -message_count_at_start: 96 ---- -Exceeded free resources -Build Minutes -2h 6m / 0s -Speed Insights Data Points -53K / 10K -Edge Requests -1.6M / 1M -10M -Function Invocations -1.4M / 1M -Fast Origin Transfer -7.71 GB / 10 GB -100 GB -Web Analytics Events -36K / 50K -100K -Function Duration -66.3 GB-Hrs / 100 GB-Hrs diff --git a/app/api/admin/hackathon/email-sender/route.ts b/app/api/admin/hackathon/email-sender/route.ts index 5d092e48..e480974f 100644 --- a/app/api/admin/hackathon/email-sender/route.ts +++ b/app/api/admin/hackathon/email-sender/route.ts @@ -9,9 +9,16 @@ import { } from "@/lib/hackathon/email-templates"; import { type EmailTemplateVars } from "@/lib/hackathon/email"; -const resend = new Resend(process.env.RESEND_API_KEY); const FROM_EMAIL = process.env.RESEND_FROM_EMAIL || "hi@noreply.passionseed.org"; +function getResendClient() { + const apiKey = process.env.RESEND_API_KEY; + if (!apiKey) { + throw new Error("Missing env.RESEND_API_KEY"); + } + return new Resend(apiKey); +} + function getServiceClient() { return createServiceClient( process.env.HACKATHON_SUPABASE_URL ?? @@ -263,6 +270,7 @@ export async function POST(req: NextRequest) { for (let i = 0; i < emailList.length; i += BATCH_SIZE) { const chunk = emailList.slice(i, i + BATCH_SIZE); + const resend = getResendClient(); try { const { error } = await resend.batch.send(chunk); diff --git a/app/api/check-node/route.ts b/app/api/check-node/route.ts index 95a86b43..ba4f1d47 100644 --- a/app/api/check-node/route.ts +++ b/app/api/check-node/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { requireDebugAccess, safeServerError } from "@/lib/security/route-guards"; + export async function GET(request: Request) { const debug = await requireDebugAccess(); if (!debug.ok) return debug.response; diff --git a/app/api/classrooms/route.ts b/app/api/classrooms/route.ts index 1843a3f7..0cb434c6 100644 --- a/app/api/classrooms/route.ts +++ b/app/api/classrooms/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/utils/supabase/server"; + export async function GET(request: NextRequest) { try { const supabase = await createClient(); diff --git a/app/api/expert-interview/session/route.ts b/app/api/expert-interview/session/route.ts index 6ed6443d..5629ee2a 100644 --- a/app/api/expert-interview/session/route.ts +++ b/app/api/expert-interview/session/route.ts @@ -2,7 +2,6 @@ import { NextRequest, NextResponse } from "next/server"; import { checkRateLimit } from "@/lib/expert-interview/rate-limiter"; import { getFirstQuestion, getTotalQuestions } from "@/lib/expert-interview/chat-service"; import type { InterviewType } from "@/types/expert-interview"; -import crypto from "crypto"; function getClientIp(request: NextRequest): string { return ( diff --git a/app/api/hackathon/inbox/route.ts b/app/api/hackathon/inbox/route.ts index 4da6f4e6..3aa9325c 100644 --- a/app/api/hackathon/inbox/route.ts +++ b/app/api/hackathon/inbox/route.ts @@ -1,8 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient as createServiceClient } from "@supabase/supabase-js"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; + function getHackathonServiceClient() { return createServiceClient( process.env.HACKATHON_SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/app/api/hackathon/login/route.ts b/app/api/hackathon/login/route.ts index e94b59c4..240fcf77 100644 --- a/app/api/hackathon/login/route.ts +++ b/app/api/hackathon/login/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { verifyPassword, generateSessionToken, SESSION_COOKIE, SESSION_EXPIRY_DAYS, getCorsHeaders } from "@/lib/hackathon/auth"; +import { verifyPassword, generateSessionToken } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE, SESSION_EXPIRY_DAYS, getCorsHeaders } from "@/lib/hackathon/auth-edge"; import { findParticipantByEmail, createSession } from "@/lib/hackathon/db"; export async function POST(req: NextRequest) { diff --git a/app/api/hackathon/logout/route.ts b/app/api/hackathon/logout/route.ts index 719e4f73..09c684be 100644 --- a/app/api/hackathon/logout/route.ts +++ b/app/api/hackathon/logout/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { deleteSession } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; diff --git a/app/api/hackathon/matching/event/route.ts b/app/api/hackathon/matching/event/route.ts index 2cb619e6..e345c18b 100644 --- a/app/api/hackathon/matching/event/route.ts +++ b/app/api/hackathon/matching/event/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { getParticipantMatchingState } from "@/lib/hackathon/matching-service"; diff --git a/app/api/hackathon/matching/met/route.ts b/app/api/hackathon/matching/met/route.ts index f6bde7e2..f09af7fc 100644 --- a/app/api/hackathon/matching/met/route.ts +++ b/app/api/hackathon/matching/met/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { saveMetConnections } from "@/lib/hackathon/matching-service"; diff --git a/app/api/hackathon/matching/rankings/route.ts b/app/api/hackathon/matching/rankings/route.ts index bd674d4b..a0fabc16 100644 --- a/app/api/hackathon/matching/rankings/route.ts +++ b/app/api/hackathon/matching/rankings/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { saveRankings } from "@/lib/hackathon/matching-service"; diff --git a/app/api/hackathon/me/route.ts b/app/api/hackathon/me/route.ts index afe74ec0..2037f610 100644 --- a/app/api/hackathon/me/route.ts +++ b/app/api/hackathon/me/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionParticipant, updateParticipant } from "@/lib/hackathon/db"; import { createClient } from "@/utils/supabase/server"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; + export async function GET(req: NextRequest) { const corsHeaders = getCorsHeaders(req); diff --git a/app/api/hackathon/mentor/public/route.ts b/app/api/hackathon/mentor/public/route.ts index 20d65744..3b9a919a 100644 --- a/app/api/hackathon/mentor/public/route.ts +++ b/app/api/hackathon/mentor/public/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@supabase/supabase-js"; import type { MentorProfile } from "@/types/mentor"; + function getClient() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, diff --git a/app/api/hackathon/pre-questionnaire/route.ts b/app/api/hackathon/pre-questionnaire/route.ts index 75f4d369..ceb06de3 100644 --- a/app/api/hackathon/pre-questionnaire/route.ts +++ b/app/api/hackathon/pre-questionnaire/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { createClient } from "@supabase/supabase-js"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; function getAdminClient() { return createClient( diff --git a/app/api/hackathon/push-subscribe/route.ts b/app/api/hackathon/push-subscribe/route.ts index 3f523df5..6eecc3b8 100644 --- a/app/api/hackathon/push-subscribe/route.ts +++ b/app/api/hackathon/push-subscribe/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionParticipant } from "@/lib/hackathon/db"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; import { createClient as createServiceClient } from "@supabase/supabase-js"; function getServiceClient() { diff --git a/app/api/hackathon/register/invite/[token]/route.ts b/app/api/hackathon/register/invite/[token]/route.ts index b41fb706..58a6153a 100644 --- a/app/api/hackathon/register/invite/[token]/route.ts +++ b/app/api/hackathon/register/invite/[token]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { hashPassword, generateSessionToken, SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth"; +import { hashPassword, generateSessionToken } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth-edge"; import { findParticipantByEmail, createParticipant, createSession, getHackathonAdminClient } from "@/lib/hackathon/db"; import { getInviteByToken, claimInvite, isInviteEnabled } from "@/lib/hackathon/invites"; diff --git a/app/api/hackathon/register/link/[token]/route.ts b/app/api/hackathon/register/link/[token]/route.ts index 6ca7ac4f..917097ee 100644 --- a/app/api/hackathon/register/link/[token]/route.ts +++ b/app/api/hackathon/register/link/[token]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { hashPassword, generateSessionToken, SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth"; +import { hashPassword, generateSessionToken } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth-edge"; import { findParticipantByEmail, createParticipant, createSession } from "@/lib/hackathon/db"; import { getRegisterLinkByToken, claimRegisterLink } from "@/lib/hackathon/register-links"; diff --git a/app/api/hackathon/register/route.ts b/app/api/hackathon/register/route.ts index 61e25b0f..da7b7d05 100644 --- a/app/api/hackathon/register/route.ts +++ b/app/api/hackathon/register/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { hashPassword, generateSessionToken, SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth"; +import { hashPassword, generateSessionToken } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth-edge"; import { findParticipantByEmail, createParticipant, createSession } from "@/lib/hackathon/db"; import { createClient } from "@supabase/supabase-js"; diff --git a/app/api/hackathon/register/special/route.ts b/app/api/hackathon/register/special/route.ts index 2c719872..cbfb0475 100644 --- a/app/api/hackathon/register/special/route.ts +++ b/app/api/hackathon/register/special/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; -import { hashPassword, generateSessionToken, SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth"; +import { hashPassword, generateSessionToken } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE, SESSION_EXPIRY_DAYS } from "@/lib/hackathon/auth-edge"; import { findParticipantByEmail, createParticipant, createSession, getHackathonAdminClient } from "@/lib/hackathon/db"; const SPECIAL_CAP = 0; diff --git a/app/api/hackathon/student/team/route.ts b/app/api/hackathon/student/team/route.ts index 9bd85e63..52d3faf8 100644 --- a/app/api/hackathon/student/team/route.ts +++ b/app/api/hackathon/student/team/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@supabase/supabase-js"; import { getSessionParticipant } from "@/lib/hackathon/db"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; function getClient() { return createClient( diff --git a/app/api/hackathon/submissions/route.ts b/app/api/hackathon/submissions/route.ts index a0b17a49..7aaaad1c 100644 --- a/app/api/hackathon/submissions/route.ts +++ b/app/api/hackathon/submissions/route.ts @@ -1,7 +1,8 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { createClient } from "@supabase/supabase-js"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; + function getHackathonAuthClient() { const url = process.env.HACKATHON_SUPABASE_URL ?? process.env.NEXT_PUBLIC_SUPABASE_URL!; diff --git a/app/api/hackathon/team-finder/join/route.ts b/app/api/hackathon/team-finder/join/route.ts index 627a9276..690fd19a 100644 --- a/app/api/hackathon/team-finder/join/route.ts +++ b/app/api/hackathon/team-finder/join/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { upsertTeamFinderEntry } from "@/lib/hackathon/team-finder"; diff --git a/app/api/hackathon/team-finder/preferences/route.ts b/app/api/hackathon/team-finder/preferences/route.ts index 40086da2..feac0c00 100644 --- a/app/api/hackathon/team-finder/preferences/route.ts +++ b/app/api/hackathon/team-finder/preferences/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { getTeamFinderEntry, diff --git a/app/api/hackathon/team-finder/status/route.ts b/app/api/hackathon/team-finder/status/route.ts index 2cc7a588..66503046 100644 --- a/app/api/hackathon/team-finder/status/route.ts +++ b/app/api/hackathon/team-finder/status/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { getSessionParticipant } from "@/lib/hackathon/db"; -import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth"; +import { getCorsHeaders, extractHackathonToken } from "@/lib/hackathon/auth-edge"; import { getTeamFinderEntry, listTeamFinderParticipantsExcluding, diff --git a/app/api/hackathon/team-matching/event/route.ts b/app/api/hackathon/team-matching/event/route.ts index 12ab3c70..73dc795f 100644 --- a/app/api/hackathon/team-matching/event/route.ts +++ b/app/api/hackathon/team-matching/event/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getLatestHackathonMatchingEvent, getParticipantHackathonMatchingState, diff --git a/app/api/hackathon/team-matching/met/route.ts b/app/api/hackathon/team-matching/met/route.ts index 3d204dba..8ac96883 100644 --- a/app/api/hackathon/team-matching/met/route.ts +++ b/app/api/hackathon/team-matching/met/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getHackathonMatchingEvent, getParticipantHackathonMatchingState, diff --git a/app/api/hackathon/team-matching/participants/route.ts b/app/api/hackathon/team-matching/participants/route.ts index d1d4986d..c33514fc 100644 --- a/app/api/hackathon/team-matching/participants/route.ts +++ b/app/api/hackathon/team-matching/participants/route.ts @@ -1,12 +1,12 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getLatestHackathonMatchingEvent, getParticipantTeam, getSessionParticipant, listHackathonMatchingCandidates, -} from "@/lib/hackathon/db"; +} from "@/lib/hackathon/db-edge"; export async function GET() { try { diff --git a/app/api/hackathon/team-matching/rankings/route.ts b/app/api/hackathon/team-matching/rankings/route.ts index 5885cff1..a5ba7250 100644 --- a/app/api/hackathon/team-matching/rankings/route.ts +++ b/app/api/hackathon/team-matching/rankings/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getHackathonMatchingEvent, getParticipantHackathonMatchingState, diff --git a/app/api/hackathon/team/create/route.ts b/app/api/hackathon/team/create/route.ts index c290a1b8..df382ade 100644 --- a/app/api/hackathon/team/create/route.ts +++ b/app/api/hackathon/team/create/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, createTeam } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; diff --git a/app/api/hackathon/team/interests/route.ts b/app/api/hackathon/team/interests/route.ts index fe499e1c..560697ae 100644 --- a/app/api/hackathon/team/interests/route.ts +++ b/app/api/hackathon/team/interests/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, getParticipantTeam } from "@/lib/hackathon/db"; import { createClient } from "@supabase/supabase-js"; diff --git a/app/api/hackathon/team/invite/create/route.ts b/app/api/hackathon/team/invite/create/route.ts index 379a6dea..6a2e8678 100644 --- a/app/api/hackathon/team/invite/create/route.ts +++ b/app/api/hackathon/team/invite/create/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, getParticipantTeam } from "@/lib/hackathon/db"; import { isInviteEnabled, getExistingInvite, createInvite, deleteTeamInvites, teamAlreadyUsedInvite } from "@/lib/hackathon/invites"; diff --git a/app/api/hackathon/team/invite/status/route.ts b/app/api/hackathon/team/invite/status/route.ts index 167fdad3..7caca7cd 100644 --- a/app/api/hackathon/team/invite/status/route.ts +++ b/app/api/hackathon/team/invite/status/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { cookies } from "next/headers"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, getParticipantTeam } from "@/lib/hackathon/db"; import { getExistingInvite, isInviteEnabled, isInvitedMember, teamAlreadyUsedInvite } from "@/lib/hackathon/invites"; diff --git a/app/api/hackathon/team/join/route.ts b/app/api/hackathon/team/join/route.ts index 07037a50..34dd21e1 100644 --- a/app/api/hackathon/team/join/route.ts +++ b/app/api/hackathon/team/join/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, joinTeam } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; diff --git a/app/api/hackathon/team/kick/route.ts b/app/api/hackathon/team/kick/route.ts index fb7ef7a5..bda8b1bf 100644 --- a/app/api/hackathon/team/kick/route.ts +++ b/app/api/hackathon/team/kick/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; import { createClient } from "@supabase/supabase-js"; diff --git a/app/api/hackathon/team/leave/route.ts b/app/api/hackathon/team/leave/route.ts index 9db9469d..e6dc7085 100644 --- a/app/api/hackathon/team/leave/route.ts +++ b/app/api/hackathon/team/leave/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { isInvitedMember } from "@/lib/hackathon/invites"; import { cookies } from "next/headers"; diff --git a/app/api/hackathon/team/match/cancel/route.ts b/app/api/hackathon/team/match/cancel/route.ts index f40ece3a..bc98f7ee 100644 --- a/app/api/hackathon/team/match/cancel/route.ts +++ b/app/api/hackathon/team/match/cancel/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; import { createClient } from "@/utils/supabase/server"; diff --git a/app/api/hackathon/team/match/join/route.ts b/app/api/hackathon/team/match/join/route.ts index f1617201..d021c320 100644 --- a/app/api/hackathon/team/match/join/route.ts +++ b/app/api/hackathon/team/match/join/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; import { createClient } from "@/utils/supabase/server"; diff --git a/app/api/hackathon/team/match/status/route.ts b/app/api/hackathon/team/match/status/route.ts index 00ab0164..5713f336 100644 --- a/app/api/hackathon/team/match/status/route.ts +++ b/app/api/hackathon/team/match/status/route.ts @@ -1,9 +1,10 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; -import { getSessionParticipant } from "@/lib/hackathon/db"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; +import { getSessionParticipant } from "@/lib/hackathon/db-edge"; import { cookies } from "next/headers"; import { createClient } from "@/utils/supabase/server"; + export async function GET() { try { const cookieStore = await cookies(); diff --git a/app/api/hackathon/team/me/route.ts b/app/api/hackathon/team/me/route.ts index d82a9fb9..915f206d 100644 --- a/app/api/hackathon/team/me/route.ts +++ b/app/api/hackathon/team/me/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from "next/server"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, getParticipantTeam } from "@/lib/hackathon/db"; import { cookies } from "next/headers"; + export async function GET() { try { const cookieStore = await cookies(); diff --git a/app/api/hackathon/track-view/route.ts b/app/api/hackathon/track-view/route.ts index 792bf494..8a354adf 100644 --- a/app/api/hackathon/track-view/route.ts +++ b/app/api/hackathon/track-view/route.ts @@ -1,6 +1,13 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/utils/supabase/server"; -import { createHash } from "crypto"; + +async function sha256(message: string): Promise { + const encoder = new TextEncoder(); + const data = encoder.encode(message); + const hashBuffer = await crypto.subtle.digest("SHA-256", data); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); +} /** * Track hackathon page views for analytics @@ -26,9 +33,7 @@ export async function POST(request: NextRequest) { // Create a privacy-preserving visitor fingerprint // Hash IP + user agent to anonymize while still tracking unique visitors - const visitorFingerprint = createHash("sha256") - .update(`${ip}-${userAgent}`) - .digest("hex"); + const visitorFingerprint = await sha256(`${ip}-${userAgent}`); // Generate a session ID if not provided (browser-based tracking) const sessionId = body.session_id || visitorFingerprint; diff --git a/app/api/hero-galaxy/route.ts b/app/api/hero-galaxy/route.ts index c134b0de..85f8d562 100644 --- a/app/api/hero-galaxy/route.ts +++ b/app/api/hero-galaxy/route.ts @@ -1,6 +1,7 @@ import { createClient } from "@/utils/supabase/server"; import { NextResponse } from "next/server"; + // University brand colors for visualization const UNIVERSITY_COLORS: Record = { "จุฬาลงกรณ์มหาวิทยาลัย": "#E5007D", // Pink diff --git a/app/api/maps/list/route.ts b/app/api/maps/list/route.ts index deb9304c..1f199c0b 100644 --- a/app/api/maps/list/route.ts +++ b/app/api/maps/list/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getMapsWithStats } from "@/lib/supabase/maps"; + // OPTIMIZATION: Dedicated API endpoint for paginated map data export async function GET(request: NextRequest) { try { diff --git a/app/api/maps/route.ts b/app/api/maps/route.ts index 58635e8a..48998f18 100644 --- a/app/api/maps/route.ts +++ b/app/api/maps/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getMapsWithStats } from "@/lib/supabase/maps"; + export async function GET(request: NextRequest) { try { const maps = await getMapsWithStats(); diff --git a/app/api/music/process/route.ts b/app/api/music/process/route.ts index 2eeac0db..1adc1dee 100644 --- a/app/api/music/process/route.ts +++ b/app/api/music/process/route.ts @@ -25,7 +25,7 @@ async function getSpotifyAccessToken(): Promise { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, }, body: 'grant_type=client_credentials', }); diff --git a/app/api/onboarding/tcas/route.ts b/app/api/onboarding/tcas/route.ts index 02cc29c4..70cfae6d 100644 --- a/app/api/onboarding/tcas/route.ts +++ b/app/api/onboarding/tcas/route.ts @@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createClient } from "@/utils/supabase/server"; + const UNIVERSITY_LIMIT = 8; const PROGRAM_LIMIT = 10; diff --git a/app/api/profile/dashboard/route.ts b/app/api/profile/dashboard/route.ts index c7cf9ba8..364ba346 100644 --- a/app/api/profile/dashboard/route.ts +++ b/app/api/profile/dashboard/route.ts @@ -8,6 +8,7 @@ import { type DashboardProgress, } from "@/components/profile/profile-dashboard-utils"; + async function safeQuery( label: string, run: () => PromiseLike<{ data: T | null; error: { message?: string } | null }>, diff --git a/app/api/spotify/search/route.ts b/app/api/spotify/search/route.ts index ebcb4e61..625f056b 100644 --- a/app/api/spotify/search/route.ts +++ b/app/api/spotify/search/route.ts @@ -62,7 +62,7 @@ async function getAccessToken(): Promise { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`, + 'Authorization': `Basic ${btoa(`${clientId}:${clientSecret}`)}`, }, body: 'grant_type=client_credentials', }); diff --git a/app/api/tcas/categories/route.ts b/app/api/tcas/categories/route.ts index 50439dd0..e41f0137 100644 --- a/app/api/tcas/categories/route.ts +++ b/app/api/tcas/categories/route.ts @@ -1,6 +1,7 @@ import { createClient } from "@/utils/supabase/server"; import { NextResponse } from "next/server"; + export async function GET() { const supabase = await createClient(); diff --git a/app/api/tcas/projection/route.ts b/app/api/tcas/projection/route.ts index f6416669..1ae00ea9 100644 --- a/app/api/tcas/projection/route.ts +++ b/app/api/tcas/projection/route.ts @@ -1,6 +1,7 @@ import { createClient } from "@/utils/supabase/server"; import { NextResponse } from "next/server"; + export async function GET(request: Request) { const { searchParams } = new URL(request.url); const category = searchParams.get("category"); diff --git a/app/api/test-db/route.ts b/app/api/test-db/route.ts index ce6fb23f..7e9529aa 100644 --- a/app/api/test-db/route.ts +++ b/app/api/test-db/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { requireDebugAccess, safeServerError } from "@/lib/security/route-guards"; + export async function GET() { const debug = await requireDebugAccess(); if (!debug.ok) return debug.response; diff --git a/app/api/user/next-nodes/route.ts b/app/api/user/next-nodes/route.ts index 7454a78f..3e20f175 100644 --- a/app/api/user/next-nodes/route.ts +++ b/app/api/user/next-nodes/route.ts @@ -3,6 +3,7 @@ import { createServerClient } from "@supabase/ssr"; import { cookies } from "next/headers"; import { MapNode } from "@/types/map"; + export async function GET() { try { const cookieStore = await cookies(); diff --git a/app/api/version/route.ts b/app/api/version/route.ts index fd6ff980..4c5e5718 100644 --- a/app/api/version/route.ts +++ b/app/api/version/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server'; + export async function GET() { return NextResponse.json({ version: process.env.VERCEL_GIT_COMMIT_SHA || 'local', diff --git a/app/epic-sprint/page.tsx b/app/epic-sprint/page.tsx index 2e1c21e8..6e274d27 100644 --- a/app/epic-sprint/page.tsx +++ b/app/epic-sprint/page.tsx @@ -1,7 +1,8 @@ import { createAdminClient } from "@/utils/supabase/admin"; import { ParticipantTable, BetaTable } from "./ParticipantTable"; -export const dynamic = "force-dynamic"; +export const dynamic = "force-static"; +export const revalidate = 3600; const BETA_FORM_TOKEN = "2d1a7a73-e3dd-4c5a-b0d5-1b7f5a5c2e11"; @@ -169,10 +170,17 @@ function KpiRow({ } export default async function EpicSprintPage() { - const [participants, betaRegistrations] = await Promise.all([ - fetchHackathonParticipants(), - fetchBetaRegistrations(), - ]); + let participants: Awaited> = []; + let betaRegistrations: Awaited> = []; + + try { + [participants, betaRegistrations] = await Promise.all([ + fetchHackathonParticipants(), + fetchBetaRegistrations(), + ]); + } catch (error) { + console.error("Error fetching epic sprint data:", error); + } const withTeam = participants.filter((p) => p.team !== null).length; const withoutTeam = participants.filter((p) => p.team === null).length; diff --git a/app/fi/exec/page.tsx b/app/fi/exec/page.tsx index cb0e0dcd..93caf0cd 100644 --- a/app/fi/exec/page.tsx +++ b/app/fi/exec/page.tsx @@ -15,7 +15,8 @@ import { Lightbulb, } from "lucide-react"; -export const dynamic = "force-dynamic"; +export const dynamic = "force-static"; +export const revalidate = 300; // ============================================================================ // DATA FETCHING @@ -130,10 +131,21 @@ function SprintBadge({ status }: { status: string }) { // ============================================================================ export default async function FIExecPage() { - const [hackathonData, betaData] = await Promise.all([ - fetchHackathonData(), - fetchBetaData(), - ]); + let hackathonData: Awaited> = { + participants: [], + withTeam: 0, + teamRate: "0", + }; + let betaData: Awaited> = { count: 0 }; + + try { + [hackathonData, betaData] = await Promise.all([ + fetchHackathonData(), + fetchBetaData(), + ]); + } catch (error) { + console.error("Error fetching FI exec data:", error); + } const { participants, withTeam, teamRate } = hackathonData; const totalActions = participants.length + betaData.count; diff --git a/app/hackathon/matching/page.tsx b/app/hackathon/matching/page.tsx index 068d5433..9c3a9f62 100644 --- a/app/hackathon/matching/page.tsx +++ b/app/hackathon/matching/page.tsx @@ -1,6 +1,6 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant } from "@/lib/hackathon/db"; import HackathonMatchingPage from "@/components/hackathon/HackathonMatchingPage"; diff --git a/app/hackathon/page.tsx b/app/hackathon/page.tsx index a6b2162e..ea0dde10 100644 --- a/app/hackathon/page.tsx +++ b/app/hackathon/page.tsx @@ -1,6 +1,6 @@ import { cookies } from "next/headers"; import { getSessionParticipant } from "@/lib/hackathon/db"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import LandingPage from "@/components/hackathon/LandingPage"; import { Metadata } from "next"; diff --git a/app/hackathon/sponsorship/page.tsx b/app/hackathon/sponsorship/page.tsx index da611b29..cd14411f 100644 --- a/app/hackathon/sponsorship/page.tsx +++ b/app/hackathon/sponsorship/page.tsx @@ -1,7 +1,8 @@ import { createAdminClient } from "@/utils/supabase/admin"; import { SponsorshipContent } from "./SponsorshipContent"; -export const dynamic = "force-dynamic"; +export const dynamic = "force-static"; +export const revalidate = 300; async function getHackathonStats() { const admin = createAdminClient(); @@ -48,7 +49,13 @@ async function getHackathonStats() { } export default async function SponsorshipPage() { - const stats = await getHackathonStats(); + let stats; + try { + stats = await getHackathonStats(); + } catch (error) { + console.error("Error fetching hackathon stats:", error); + stats = { participants: 0, teams: 0, gradeLevels: {} }; + } return ; } diff --git a/app/hackathon/team/page.tsx b/app/hackathon/team/page.tsx index 03ecdbf4..d8ce7628 100644 --- a/app/hackathon/team/page.tsx +++ b/app/hackathon/team/page.tsx @@ -1,6 +1,6 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; -import { SESSION_COOKIE } from "@/lib/hackathon/auth"; +import { SESSION_COOKIE } from "@/lib/hackathon/auth-edge"; import { getSessionParticipant, getParticipantTeam } from "@/lib/hackathon/db"; import TeamDashboard from "@/components/hackathon/TeamDashboard"; diff --git a/app/layout.tsx b/app/layout.tsx index 93654aed..5ed7ab0d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,7 +12,6 @@ import { Noto_Sans_Thai, } from "next/font/google"; import "./globals.css"; -import { Analytics } from "@vercel/analytics/next"; import { ThemeProvider } from "@/components/theme-provider"; import { Toaster as SonnerToaster } from "@/components/ui/sonner"; import { createClient } from "@/utils/supabase/server"; @@ -169,7 +168,13 @@ export default async function RootLayout({ - + {/* Cloudflare Web Analytics */} +