# CLAUDE.md

Guidance for Claude Code to stay aligned with the HLS player stack (backend + frontend) and its admin/local-sharing workflows.

## Purpose

- Full-stack HLS player with admin toolbox, embed mode, upload + proxy, and local media sharing. (backend/index.js:1-217, hls-player/src/App.tsx:1-18)
- Use this file for project context, critical commands, and quick references before jumping into deeper docs.

## Commands & Tooling

### Install
```bash
cd backend && npm install
cd hls-player && npm install
# shortcut (installs missing deps and boots both apps)
./dev.sh
```

### Development
```bash
./dev.sh                  # full stack
cd backend && npm run dev  # express server w/ nodemon
cd hls-player && npm run dev -- --host 0.0.0.0 --port 7594 --strictPort
```

### Build / Preview / Lint / Test
```bash
cd hls-player && npm run build
cd hls-player && npm run preview
cd hls-player && npm run lint
cd hls-player && npm test       # Vitest + Testing Library
cd backend && npm test          # Node built-in test runner
./prod.sh                       # orchestrates prod-like startup
```

## Architecture at a Glance

### Backend Modules
| Module | Role | Notes |
|--------|------|-------|
| `backend/index.js` | Express, security, rate limit, file upload, share APIs, ffmpeg packager hooks | sets trust proxy, helmet, cors, multer, rate-limit (1 limit/15m). (backend/index.js:1-217) |
| `adminAuth.js` | Timing-safe admin login, session cookies, CORS-aware middleware | handles `/admin/*` auth, SameSite cookie rules (None for cross-site). (backend/adminAuth.js:1-278) |
| `adminSessionStore.js` | JSON-backed session persistence with pruning | stores `{ token, createdAt, expiresAt }`, auto-cleans expired entries. (backend/adminSessionStore.js:1-127) |
| `localMedia.js` | Path validation, MIME detection, playlist rewriting | protects against traversal, rewrites m3u8 URLs. (backend/localMedia.js:1-200) |
| `localMediaStore.js` | Share persistence (create/list/delete share records) | TTL enforcement, shareId metadata. (backend/localMediaStore.js:1-127) |
| `hlsPackager.js` | ffmpeg-based packaging for `.ts` + caching | caches under `data/hls-cache/<shareId>`. (backend/hlsPackager.js:1-200) |
| `proxy.js` | URL validation + playlist rewriting for `/proxy` | blocks non-http(s)/private targets, rewrites playlists. (backend/proxy.js:1-120) |

### Frontend Stack
- `hls-player/src/main.tsx`: mounts `<App />` under `BrowserRouter`. (hls-player/src/main.tsx:1-9)
- `App.tsx`: routes `/`, `/embed`, `/admin` (redirects to `/`), wildcard to `/`. (hls-player/src/App.tsx:1-18)
- `ToolboxPage`: admin interface w/ `HlsPlayer`, `ConfigPanel`, `FileBrowserDialog`, `EmbedSnippet`, `StatusBanner`. (hls-player/src/routes/ToolboxPage.tsx)
- `EmbedPage`: clean player powered by querystring config. (hls-player/src/routes/EmbedPage.tsx)
- `HlsPlayer`: Plyr + `hls.js`, config encoding via `configCodec.ts`, backend URL resolution via `backendUrl.ts`. (hls-player/src/components/HlsPlayer.tsx:1-214)

## API Endpoint Snapshot
| Area | Method | Path | Purpose |
|------|--------|------|---------|
| Health | GET | `/health` | `{ status: 'ok' }` health probe |
| Admin | POST | `/admin/login` | Sets admin session cookie |
| Admin | POST | `/admin/logout` | Clears session cookie |
| Admin | GET | `/admin/me` | Tells if admin is authenticated/configured |
| Embed | POST | `/embed/tokens` | Create embed token (stores config + TTL) |
| Embed | GET | `/embed/tokens` | List embed tokens + audit fields |
| Embed | DELETE | `/embed/tokens/:tokenId` | Revoke embed token |
| Embed | GET | `/embed/token/:tokenId` | Resolve token, record usage (404/410 on missing/expired) |
| Local Media | GET | `/local/browse?dir=...` | Admin-only file browse |
| Local Media | POST | `/local/shares` | Create share token (`{ path, ttlSeconds }`) |
| Local Media | GET | `/local/shares` | List shares |
| Local Media | DELETE | `/local/shares/:id` | Revoke share |
| Local Media | GET | `/local/share/file/:id` | Serve file (supports Range) |
| Local Media | GET | `/local/share/hls/:id/index.m3u8` | Shared playlist |
| Local Media | GET | `/local/share/hls/:id/:asset` | Shared HLS segment |
| Upload | POST | `/upload` | Image upload (`multipart/form-data`, `file`) |
| Upload | GET | `/uploads/:filename` | Serve uploaded image |
| Proxy | GET | `/proxy?url=...` | Cross-origin playlist proxy, rewrites URLs |

## Environment & Runtime Config
| Variable | Default | Purpose |
|----------|---------|---------|
| `BACKEND_PORT` | `2738` | Express host port |
| `BACKEND_HOST` | `0.0.0.0` | Express bind address |
| `FRONTEND_URL` / `PUBLIC_URL` | `http://127.0.0.1:7594` | Frontend CORS origins |
| `FRONTEND_ALLOWED_HOSTS` | `''` | Additional allowed hosts for Vite dev/preview |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | *required for admin* | Username/password for `/admin/*` endpoints |
| `ADMIN_SESSION_TTL_SECONDS` | `0` (no expiry) | Session lifetime |
| `ENABLE_LOCAL_MEDIA` | `'0'` | Set to `1` to enable `/local/*` APIs and file sharing |
| `LOCAL_MEDIA_ROOT` | `''` | Local media root path (must be inside MEDIA_ROOT) |
| `LOCAL_SHARES_DB_PATH` | `backend/data/local-shares.json` | Persistent share store |
| `LOCAL_SHARE_TTL_SECONDS` | `86400` | Default share TTL |
| `LOCAL_SHARE_MAX_TTL_SECONDS` | `2592000` | Max share TTL (30 days) |
| `FFMPEG_PATH` | `ffmpeg` | ffmpeg binary used for packaging |
| `FFMPEG_MAX_JOBS` | `1` | Concurrency for packager |
| `LOCAL_TRANSCODE_TARGET` | `auto` | `copy`, `h264`, `hevc` overrides |
| `TRUST_PROXY` | `1` | Respects X-Forwarded-* headers |

## Security & Design Notes
- Helmet hardens headers (CSP disabled for playback). (backend/index.js:140-144)
- Frameguard disabled to allow cross-site embedding via `/embed` iframes. (backend/index.js:140-145)
- Rate limiting: 100 requests per 15 minutes per IP. (backend/index.js:147-152)
- Path validation enforces no traversal before reading the filesystem. (backend/localMedia.js:1-200)
- Share tokens are UUIDs saved with TTL in `local-shares.json`; revoked shares removed immediately. (backend/localMediaStore.js:1-127)
- Embed tokens are opaque IDs stored server-side with TTL + audit; `/embed` is token-only. (backend/embedTokenStore.js:1-120)
- Proxy blocks non-http(s) and private hosts to prevent SSRF. (backend/proxy.js:19-31)
- Frontend design must stick to Vietnam flag palette: red surfaces, yellow accents only. (AGENTS.md:73-84)

## Testing & Quality
- Backend: `node --test` runner (tests in `backend/tests/`). (backend/tests/proxy.test.js)
- Frontend: Vitest + Testing Library; global cleanup in `src/test/setup.ts`. (hls-player/src/test/setup.ts:1-10)
- Linting: `cd hls-player && npm run lint`. (hls-player/package.json:9)

## Key Files @ a Glance
| File | Role |
|------|------|
| `backend/index.js` | Express server bootstrapping all middleware + route setup |
| `backend/adminAuth.js` | Admin session auth + middleware |
| `backend/adminSessionStore.js` | JSON-based session persistence |
| `backend/localMedia*.js` | Path helpers + share persistence |
| `backend/hlsPackager.js` | ffmpeg packaging for `.ts` files |
| `backend/proxy.js` | Playlist rewriting logic |
| `hls-player/src/App.tsx` | Router for `/`, `/embed`, `/admin` |
| `hls-player/src/routes/ToolboxPage.tsx` | Admin UI container |
| `hls-player/src/routes/EmbedPage.tsx` | Embed-mode player |
| `hls-player/src/components/HlsPlayer.tsx` | Player widget w/ Plyr + hls.js |
| `hls-player/src/lib/configCodec.ts` | Config encode / decode helpers |
| `hls-player/src/lib/backendUrl.ts` | Runtime backend URL resolution |

## Recent Updates
- Admin auth now relies on `adminAuth.js` plus persistent `adminSessionStore.js` for timing-safe login and session cookies. (backend/adminAuth.js:1-278)
- Local media share APIs (`/local/*`) plus ffmpeg packaging hooks now live in `backend/index.js`, backed by `localMediaStore.js`. (backend/index.js:94-217, backend/localMediaStore.js:1-127)
- Frontend introduced `FileBrowserDialog` and admin share flows for the Toolbox. (hls-player/src/components/toolbox/FileBrowserDialog.tsx:1-200)
