Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,13 @@
# Full documentation: README.md → "Environment variables"
# =============================================================================

# --- Site & OAuth -------------------------------------------------------------
# Production: set to your canonical origin (e.g. https://kaichen.dev) in Vercel.
# Used by Supabase OAuth redirect URLs from the browser client.
NEXT_PUBLIC_SITE_URL=

# --- Supabase ----------------------------------------------------------------
# Project URL and anon key (safe to expose in the browser bundle).
# Used by getSupabaseAnon(), guestbook POST, gallery reads, /admin client, etc.
# Only used by /api/lastfm/now-playing for optional listening history.
# NEXT_PUBLIC_SUPABASE_URL is paired with the service role key for server-side
# reads/writes against `listening_history` / `listening_stats`.
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=

# Server-only: bypasses Row Level Security — NEVER expose to the client.
# Required for /api/lastfm/now-playing DB writes (listening_history / listening_stats).
SUPABASE_SERVICE_ROLE_KEY=

# --- Last.fm -----------------------------------------------------------------
Expand Down
5 changes: 0 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@ on:
jobs:
ci:
runs-on: ubuntu-latest
env:
# `next build` prerenders pages that call `createBrowserClient` at module scope (`/admin`, `/gallery`).
# Real secrets are not required for compile; placeholders satisfy the client constructor.
NEXT_PUBLIC_SUPABASE_URL: https://ci-build-placeholder.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiJ9.ci-build-placeholder
steps:
- uses: actions/checkout@v6

Expand Down
3 changes: 1 addition & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ Canonical documentation: **[README.md](README.md)** (setup, routes, APIs, env, C
| **Client polling** | `app/hooks/use-now-playing.ts` — polls every **10s**; `Cache-Control` on API allows short CDN cache |
| **GitHub** | `GET /api/github/contributions` — GraphQL calendar + REST; `GET /api/github/stars` — star counts |
| **Weather** | `GET /api/weather` — Open-Meteo, fixed coordinates (Berkeley), revalidated fetch |
| **Gallery** | Supabase Storage + `gallery_photos`; admin at `/admin` (OAuth + allowlisted email in app — **RLS must match in Supabase**) |
| **Notes** | MDX under `app/notes/**/page.mdx`; pipeline in `next.config.ts` (Webpack + `@mdx-js/loader`); components in `mdx-components.tsx` and `components/notes/` |
| **Observability** | Sentry via `instrumentation*.ts` + `sentry.*.config.ts` (DSN optional); Vercel Analytics / Speed Insights in root layout |
| **Theme** | `app/components/theme-provider.tsx` + inline script in `app/layout.tsx` — default **light** when unset |
Expand All @@ -39,8 +38,8 @@ npm run lint && npm run typecheck && npm run test && npm run build

## Important paths

- [`lib/supabase.ts`](lib/supabase.ts) — `getSupabaseAnon()` lazy singleton
- [`lib/now-playing.ts`](lib/now-playing.ts) — types for Last.fm payload
- [`lib/lastfm-now-playing-helpers.ts`](lib/lastfm-now-playing-helpers.ts) — pure helpers used by the now-playing route (tested)
- [`app/lib/substack.ts`](app/lib/substack.ts) — RSS fetch + parse (tested)
- [`next.config.ts`](next.config.ts) — MDX webpack rule, `withSentryConfig`

Expand Down
41 changes: 15 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,11 @@ This repository is a **[Next.js 16](https://nextjs.org/)** application using the
The site combines:

- A **marketing-style home page** (identity, listening status, weather, **GitHub pinned repositories** via GraphQL with a static fallback, Substack headlines, and links to **news.kaichen.dev** and the **Berkeley library hours** page).
- **Dynamic data** from Last.fm, GitHub, Open-Meteo, optional Supabase-backed gallery and listening history, and **UC Berkeley library hours** scraped from the official hours page (cached, JSON API available).
- **Dynamic data** from Last.fm, GitHub, Open-Meteo, optional Supabase-backed listening history, and **UC Berkeley library hours** scraped from the official hours page (cached, JSON API available).
- **MDX-powered notes** under `/notes` with math (KaTeX), GitHub-flavored Markdown, and syntax-highlighted code blocks — multiple course segments (UC Berkeley and SUSTech); see [MDX lecture notes](#mdx-lecture-notes).
- **Optional observability** via Sentry (client, server, edge) and Vercel Analytics / Speed Insights.

There is **no** `middleware.ts` in this repo; auth for admin flows uses Supabase OAuth and route handlers under `app/auth/`.
There is **no** `middleware.ts` (or `proxy.ts`) in this repo — every route is publicly accessible and rendered by the App Router directly.

---

Expand Down Expand Up @@ -160,18 +160,15 @@ kaichen.dev/
│ ├── about/ # Bio / CV-style page + OG
│ ├── projects/ # Projects + GitHub heatmap + OG
│ ├── notes/ # Notes index, course pages, MDX note routes
│ ├── gallery/ # Public gallery + OG
│ ├── admin/ # Supabase-auth gallery admin; /admin/gallery → redirect /admin
│ ├── api/ # Route handlers (Last.fm, GitHub, weather, guestbook, UCB libraries)
│ ├── api/ # Route handlers (Last.fm, GitHub, weather, UCB libraries)
│ ├── berkeley-libraries/ # UC Berkeley library hours (HTML from lib.berkeley.edu)
│ ├── auth/callback/ # Supabase OAuth exchange → redirect
│ ├── components/ # UI: nav, cards, theme, weather, listening, GitHub heatmap, …
│ ├── hooks/ # e.g. use-now-playing.ts
│ └── lib/ # og.tsx, substack RSS, GitHub pinned repos (GraphQL)
├── components/notes/ # MDX shortcodes: Theorem, Proof, Definition, Example, NoteBlock
├── lib/ # Shared server-oriented helpers + Vitest tests
│ ├── supabase.ts # Lazy anon Supabase client (getSupabaseAnon)
│ ├── now-playing.ts # Types for Last.fm payload
│ ├── lastfm-now-playing-helpers.ts
│ ├── ucb-library-hours.ts # Fetch + parse lib.berkeley.edu/hours (Cheerio)
│ ├── weather-open-meteo.ts
│ └── *.test.ts
Expand Down Expand Up @@ -209,7 +206,7 @@ kaichen.dev/
| Content | **MDX** via `@mdx-js/loader` + `remark-gfm`, `remark-math`, `rehype-katex`, `rehype-highlight` |
| Scraping | **cheerio** — parses UC Berkeley library hours HTML server-side |
| Fonts | `@fontsource/*` (Nunito, Bitter, JetBrains Mono), `geist` (sans/mono CSS variables) |
| Auth / data | Supabase (`@supabase/supabase-js`, `@supabase/ssr`) for OAuth, gallery, optional listening DB writes |
| Data | Supabase (`@supabase/supabase-js`) — optional listening history DB writes (service role) for `/api/lastfm/now-playing` |
| Monitoring | `@sentry/nextjs` (optional DSN), Vercel Analytics + Speed Insights |
| Testing | Vitest **3** |

Expand All @@ -227,9 +224,6 @@ Pinned versions are in [`package.json`](package.json).
| `/notes` | Index of courses (see [MDX lecture notes](#mdx-lecture-notes)); links to external [SUSTech-Kai-Notes](https://github.com/kaiiiichen/SUSTech-Kai-Notes) for broader collections |
| `/notes/...` | Nested segments per course (e.g. `cs61a`, `data100`, `cs217`, `ma121`–`ma337`); individual notes are `page.mdx` |
| `/berkeley-libraries` | UC Berkeley library **open/closed** status and hours, parsed from [lib.berkeley.edu/hours](https://www.lib.berkeley.edu/hours); data revalidates every **15 minutes** |
| `/gallery` | Photo grid + lightbox; data from Supabase `gallery_photos` + Storage |
| `/admin` | Google OAuth via Supabase; **restricted to an allowlisted email** in client code — **you must enforce the same rules in Supabase RLS** for production safety |
| `/admin/gallery` | Redirects to `/admin` |

**External nav (no in-app route):** the main nav includes **News** → [news.kaichen.dev](https://news.kaichen.dev) and **Blog** → [Substack](https://kaiiiichen.substack.com/); there is no `/blog` or `/news` route in this repo.

Expand All @@ -248,9 +242,6 @@ All handlers live under `app/api/`.
| `GET /api/github/stars?repo=owner/name` | Returns `stargazers_count` and `archived` for a repo | `revalidate = 3600`; optional `GITHUB_TOKEN` for rate limits |
| `GET /api/weather` | Open-Meteo forecast for fixed Berkeley coordinates | `fetch` with `next.revalidate = 600` |
| `GET /api/ucb-libraries` | Same payload as `/berkeley-libraries`: JSON with `libraries`, `fetchedAt`, `sourceUrl`, or `ok: false` + `error` | Uses `getUCBLibraryHours()`; upstream fetch `revalidate: 900` (15 minutes) |
| `POST /api/guestbook` | JSON body `{ email, message }` → insert into Supabase `guestbook` via anon client | No auth; relies on **Supabase RLS** and sensible limits in the database |

**Guestbook** is only referenced from API + docs; ensure any front-end or future form respects abuse concerns (rate limits, validation) at the edge or in Supabase policies.

**Berkeley libraries:** parsing depends on the HTML structure of lib.berkeley.edu. If the upstream page changes, [`lib/ucb-library-hours.ts`](lib/ucb-library-hours.ts) may need updates (see error responses when zero libraries parse).

Expand All @@ -264,10 +255,8 @@ Copy [`.env.example`](.env.example) to `.env.local`. **Never commit** real secre

| Variable | Role |
| --- | --- |
| `NEXT_PUBLIC_SITE_URL` | Canonical site origin; used for Supabase OAuth `redirectTo` (set production URL on Vercel). |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL. |
| `NEXT_PUBLIC_SUPABASE_ANON_KEY` | Supabase anonymous key (browser + server routes using `getSupabaseAnon()`). |
| `SUPABASE_SERVICE_ROLE_KEY` | **Server-only.** Used by `/api/lastfm/now-playing` for DB writes and any server path that must bypass RLS — keep off the client bundle. |
| `NEXT_PUBLIC_SUPABASE_URL` | Supabase project URL. Paired with `SUPABASE_SERVICE_ROLE_KEY` inside `/api/lastfm/now-playing` for the optional listening history. |
| `SUPABASE_SERVICE_ROLE_KEY` | **Server-only.** Used by `/api/lastfm/now-playing` for DB writes/reads against `listening_history` / `listening_stats` — keep off the client bundle. |
| `LASTFM_API_KEY` | Last.fm API. If unset, the now-playing API returns a graceful “not playing” / DB fallback without calling Last.fm. |
| `GITHUB_TOKEN` | Fine-grained or classic PAT for GitHub API (contributions + stars + **pinned repos** on the home page). If missing, contribution/stars features may error or return empty data; pinned projects fall back to a **static list** in [`app/lib/github-pinned.ts`](app/lib/github-pinned.ts). |
| `GITHUB_LOGIN` | Optional. GitHub username for **pinned repositories** and related API calls (defaults to `kaiiiichen` if unset). Set when forking so the home page shows your pins. |
Expand All @@ -287,9 +276,9 @@ vercel env pull .env.vercel.check

That path is gitignored — do not commit it.

### CI placeholders
### CI

[`.github/workflows/ci.yml`](.github/workflows/ci.yml) sets dummy `NEXT_PUBLIC_SUPABASE_*` values so `next build` can prerender pages that call `createBrowserClient` at module scope (e.g. `/admin`). These are **not** real credentials.
CI does not need any real Supabase keys to build — every Supabase client in this repo is constructed lazily inside a function body, so `next build` succeeds without `NEXT_PUBLIC_SUPABASE_*` set. See [`.github/workflows/ci.yml`](.github/workflows/ci.yml).

---

Expand All @@ -316,7 +305,7 @@ To add a new course: add a card on the notes index, create `app/notes/<slug>/pag
| **GitHub GraphQL** | Contribution calendar |
| **GitHub REST** | Repo stars, commit search |
| **Open-Meteo** | Weather (no API key) |
| **Supabase** | Auth, gallery tables + storage, guestbook insert, listening history (optional) |
| **Supabase** | Optional `listening_history` / `listening_stats` writes (service role) for the now-playing route |
| **Substack RSS** | Home page “latest posts” (`app/lib/substack.ts`) |
| **lib.berkeley.edu** | Library hours HTML (scraped server-side; not an official API) |

Expand All @@ -326,15 +315,15 @@ To add a new course: add a card on the notes index, create `app/notes/<slug>/pag

- **Node 20**, **npm install** then **`npm run dev`**.
- If MDX fails to compile, confirm you did not remove the `--webpack` flag from scripts.
- **Supabase:** for full gallery/admin behavior, configure a project and env vars; for static pages only, you can omit keys where the build allows (see CI placeholders for build-time behavior).
- **Supabase:** only `/api/lastfm/now-playing` uses Supabase (service role) for the optional listening history. The site builds and serves every page without any Supabase env set; the route just falls back to live Last.fm + in-memory cache when the DB is unreachable.

### Common issues

| Symptom | Things to check |
| --- | --- |
| MDX differs between dev and prod | Ensure both use Webpack (`--webpack`). |
| GitHub widgets empty | `GITHUB_TOKEN` set and not expired; API rate limits. |
| OAuth redirect wrong host | `NEXT_PUBLIC_SITE_URL` and Supabase redirect URLs match Vercel domain. |
| "Recently played" never persists across deploys | `NEXT_PUBLIC_SUPABASE_URL` / `SUPABASE_SERVICE_ROLE_KEY` set; tables `listening_history` / `listening_stats` exist with the expected columns. |
| Sentry noisy locally | DSN unset disables reporting; or lower sample rate in `instrumentation-client.ts`. |

---
Expand All @@ -348,7 +337,7 @@ npm run test
npm run test:watch
```

There are currently **no** Playwright/E2E tests in this repo; manual browser checks matter for layout and OAuth flows.
There are currently **no** Playwright/E2E tests in this repo; manual browser checks matter for layout and visual polish.

---

Expand Down Expand Up @@ -397,7 +386,7 @@ to non-merge commits via `git interpret-trailers` (idempotent). Automation that
## Deployment

1. Connect the GitHub repository to **Vercel**.
2. Set environment variables in the Vercel project (production + preview as needed), especially `NEXT_PUBLIC_SITE_URL` and Supabase URLs for OAuth.
2. Set environment variables in the Vercel project (production + preview as needed), especially `GITHUB_TOKEN`, `LASTFM_API_KEY`, and the Supabase keys if you want listening history persistence.
3. Pushes to `main` typically deploy production; preview deployments use PR branches.

Manual CLI (after `vercel link`):
Expand Down Expand Up @@ -435,7 +424,7 @@ Replace at minimum:
| Last.fm username | `app/api/lastfm/now-playing/route.ts` |
| GitHub login / repos / pins | `app/api/github/contributions/route.ts`, `app/components/project-stars.tsx`, [`app/lib/github-pinned.ts`](app/lib/github-pinned.ts), env `GITHUB_LOGIN` |
| Berkeley library page | `lib/ucb-library-hours.ts`, `app/berkeley-libraries/page.tsx`, `app/api/ucb-libraries/route.ts` |
| Supabase project + admin allowlist | `lib/supabase.ts`, `app/admin/page.tsx`, Supabase dashboard (RLS, Storage) |
| Supabase tables | `app/api/lastfm/now-playing/route.ts`, Supabase dashboard (`listening_history`, `listening_stats`) |
| Substack feeds | `app/lib/substack.ts` |
| Weather location | `app/api/weather/route.ts`, weather UI components |
| Theme / fonts | `app/layout.tsx`, `app/globals.css`, `app/components/theme-provider.tsx` |
Expand Down
3 changes: 1 addition & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ We care about vulnerabilities that affect:

- **Production** behavior at `kaichen.dev` (including Vercel-hosted Next.js routes and static assets).
- **Repository automation** (GitHub Actions workflows, Dependabot-related automation) when they could lead to secret exfiltration or unauthorized repository changes.
- **User data** processed through **Supabase** (auth sessions, gallery metadata, guestbook rows, listening history) when issues stem from **this application’s** code or documented deployment practices.
- **User data** processed through **Supabase** (listening history) when issues stem from **this application’s** code or documented deployment practices.

We do **not** provide a formal bug bounty program. Reports are handled **best-effort**.

Expand Down Expand Up @@ -64,7 +64,6 @@ Reports may be **declined** or redirected when they concern:
- Third-party services’ policies (Last.fm, GitHub, Vercel, Supabase product bugs) — use their official channels.
- **Social engineering** or account takeover of maintainer accounts outside this codebase.
- **Theoretical** issues without a plausible attack path against deployed configuration.
- Content **spam** on optional features (e.g. guestbook) unless tied to a clear application defect; operational mitigations (rate limits, RLS) may be tracked separately.

---

Expand Down
5 changes: 0 additions & 5 deletions app/admin/gallery/page.tsx

This file was deleted.

Loading
Loading