diff --git a/CLAUDE.md b/CLAUDE.md index 03e692df..55e52015 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,15 +45,7 @@ Changemaker Lite is a self-hosted political campaign platform built with Docker - **RBAC:** `requireRole(...roles)`, `requireNonTemp`, `authenticate` middleware; `SUPER_ADMIN` implicitly bypasses all role checks - **Module-specific role groups** (defined in `api/src/utils/roles.ts`): `INFLUENCE_ROLES`, `MAP_ROLES`, `BROADCAST_ROLES`, `CONTENT_ROLES`, `MEDIA_ROLES`, `PAYMENTS_ROLES`, `EVENTS_ROLES`, `SOCIAL_ROLES`, `SYSTEM_ROLES`, `SCHEDULING_ROLES` - **User management:** `SUPER_ADMIN` always; other admins need `permissions.canManageUsers: true` for write operations -- **Security features:** - - Refresh token rotation (atomic transaction) - - User enumeration prevention (401 not 404) - - Rate limiting on auth endpoints (10/min) - - Redis authentication required - - XSS/injection prevention (HTML escaping) - - Path traversal protection - - Encryption key for DB secrets (ENCRYPTION_KEY env var) - - Security audit complete (13 findings addressed, see `SECURITY_AUDIT_2025-02-11.md`) +- **Security:** See Security & Configuration section below + `SECURITY_AUDIT_2025-02-11.md` ### Email Systems @@ -129,57 +121,13 @@ changemaker.lite/ │ │ └── observability/ # Monitoring components │ ├── pages/ │ │ ├── auth/ # LoginPage -│ │ ├── DashboardPage.tsx # Admin dashboard -│ │ ├── UsersPage.tsx # User CRUD -│ │ ├── SettingsPage.tsx # Global site settings -│ │ ├── influence/ -│ │ │ ├── CampaignsPage.tsx # Campaign management -│ │ │ ├── ResponsesPage.tsx # Response moderation -│ │ │ ├── RepresentativesPage.tsx # Rep cache admin -│ │ │ └── EmailQueuePage.tsx # Queue monitoring -│ │ ├── map/ -│ │ │ ├── LocationsPage.tsx # Location CRUD + CSV + geocoding -│ │ │ ├── CutsPage.tsx # Cut table + map drawing editor -│ │ │ ├── ShiftsPage.tsx # Shift CRUD + signups drawer -│ │ │ ├── MapSettingsPage.tsx # Map settings -│ │ │ └── DataQualityDashboardPage.tsx # Geocoding quality metrics -│ │ ├── CanvassDashboardPage.tsx # Admin canvass overview -│ │ ├── WalkSheetPage.tsx # Printable walk sheet -│ │ ├── CutExportPage.tsx # Printable location report -│ │ ├── volunteer/ -│ │ │ ├── VolunteerMapPage.tsx # Full-screen GPS canvass map -│ │ │ ├── VolunteerShiftsPage.tsx # Assigned shifts -│ │ │ ├── MyActivityPage.tsx # Visit history + outcomes -│ │ │ └── MyRoutesPage.tsx # Route history -│ │ ├── public/ -│ │ │ ├── CampaignsListPage.tsx # Public campaign listing -│ │ │ ├── CampaignPage.tsx # Campaign detail + email form -│ │ │ ├── ResponseWallPage.tsx # Public response wall -│ │ │ ├── MapPage.tsx # Public Leaflet map -│ │ │ ├── ShiftsPage.tsx # Public shift signup -│ │ │ ├── LandingPage.tsx # Rendered landing page (/p/:slug) -│ │ │ ├── MediaGalleryPage.tsx # Public video gallery -│ │ │ └── MediaViewerPage.tsx # Video detail page -│ │ ├── media/ -│ │ │ ├── LibraryPage.tsx # Video library management -│ │ │ ├── SharedMediaPage.tsx # Public gallery admin -│ │ │ └── MediaJobsPage.tsx # Job queue monitoring -│ │ ├── LandingPagesPage.tsx # Landing page manager -│ │ ├── PageEditorPage.tsx # Full-screen GrapesJS editor -│ │ ├── EmailTemplatesPage.tsx # Email template CRUD -│ │ ├── EmailTemplateEditorPage.tsx # Email template editor -│ │ ├── ListmonkPage.tsx # Newsletter sync management -│ │ ├── PangolinPage.tsx # Tunnel setup wizard -│ │ ├── DocsPage.tsx # MkDocs export management -│ │ ├── MkDocsSettingsPage.tsx # Documentation config -│ │ ├── ObservabilityPage.tsx # Monitoring dashboard -│ │ └── services/ -│ │ ├── MiniQRPage.tsx # Mini QR iframe -│ │ ├── MailHogPage.tsx # Email capture UI -│ │ ├── CodeEditorPage.tsx # Code Server management -│ │ ├── N8nPage.tsx # Workflow automation -│ │ ├── GiteaPage.tsx # Git repository hosting -│ │ └── NocoDBPage.tsx # Data browser management +│ │ ├── influence/ # CampaignsPage, ResponsesPage, RepresentativesPage, EmailQueuePage +│ │ ├── map/ # LocationsPage, CutsPage, ShiftsPage, MapSettingsPage, DataQualityDashboardPage +│ │ ├── volunteer/ # VolunteerMapPage, VolunteerShiftsPage, MyActivityPage, MyRoutesPage +│ │ ├── public/ # CampaignsListPage, CampaignPage, ResponseWallPage, MapPage, ShiftsPage, LandingPage, MediaGalleryPage, MediaViewerPage +│ │ ├── media/ # LibraryPage, SharedMediaPage, MediaJobsPage, AnalyticsDashboardPage +│ │ ├── services/ # MiniQRPage, MailHogPage, CodeEditorPage, N8nPage, GiteaPage, NocoDBPage +│ │ └── (root) # DashboardPage, UsersPage, SettingsPage, CanvassDashboardPage, WalkSheetPage, CutExportPage, LandingPagesPage, PageEditorPage, EmailTemplatesPage, ListmonkPage, PangolinPage, ObservabilityPage │ ├── stores/ # auth.store.ts, canvass.store.ts (Zustand) │ ├── lib/ # api.ts, media-api.ts, media-public-api.ts (axios) │ ├── hooks/ # useDebounce, useLocalStorage @@ -505,25 +453,12 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit - `admin/src/pages/public/MediaGalleryPage.tsx` — Public video gallery - `admin/src/components/media/` — VideoCard, VideoActions, modals, charts -**Features:** -- **Video CRUD:** Upload with FFprobe metadata extraction (duration, dimensions, orientation, quality), bulk operations -- **Quick Actions** (Feb 2026): Edit, preview, analytics, schedule, duplicate, preview links (24h JWT), reset analytics -- **Scheduled Publishing** (Feb 2026): BullMQ job queue, timezone support (11 zones), calendar view, publish/unpublish automation -- **Analytics** (Feb 2026): Views, watch time, completion rate, traffic sources, registered viewers, GDPR-compliant (IP hashing, 90-day retention) -- **Tracking:** Public endpoints for view/event recording, 10s heartbeat, navigator.sendBeacon for reliability -- **UI Features:** Keyboard shortcuts (E/P/A/S), hover overlays, skeleton loading, error handling, mobile responsive +**Features:** Video CRUD with FFprobe metadata, quick actions, scheduled publishing (BullMQ + timezones), analytics (GDPR-compliant), public tracking endpoints, keyboard shortcuts **Routes:** - Admin: `/app/media/library`, `/app/media/analytics`, `/app/media/shared`, `/app/media/jobs` -- Public: `/gallery` (public video gallery), `/gallery/watch/:id` (video viewer), `/media/:id` (backwards compatible viewer route) -- Tracking (public): `/track/view`, `/track/event`, `/track/heartbeat` - -**Note:** The public gallery is served at `/gallery` via the admin app using `MediaPublicLayout`. This provides a unified purple interface for both authenticated and unauthenticated users. The gallery supports optional authentication (session-based upvoting/commenting for anonymous users). - -**Documentation:** -- [Media Admin Features Guide](./docs/MEDIA_ADMIN_FEATURES.md) — Complete feature documentation -- [Video Analytics Guide](./docs/VIDEO_ANALYTICS_GUIDE.md) — Analytics setup and interpretation -- [Media API README](./api/src/modules/media/README.md) — Architecture overview +- Public: `/gallery`, `/gallery/watch/:id`, `/media/:id` (legacy) +- Public gallery uses `MediaPublicLayout` (purple theme, optional auth) ### Services & Integrations @@ -658,35 +593,16 @@ cd api && npx tsc --noEmit && cd ../admin && npx tsc --noEmit ### Prisma Migration Workflow - **Always use `prisma migrate dev`** for schema changes (not `prisma db push`) — `db push` applies changes directly but doesn't create migration files, causing drift - **Migration history:** 14 migrations in `api/prisma/migrations/` fully cover the schema (baseline catch-up applied Feb 2026) -- **Fixing drift:** If `db push` was used and migrations are out of sync: - 1. Drop any stray indexes/objects in DB not in schema: `DROP INDEX IF EXISTS ;` - 2. Create a temp shadow DB: `docker compose exec -T v2-postgres createdb -U changemaker prisma_shadow_diff` - 3. Generate catch-up SQL: `docker compose exec -T api npx prisma migrate diff --from-migrations ./prisma/migrations --to-schema-datamodel ./prisma/schema.prisma --shadow-database-url "postgresql://..." --script` - 4. Save to `api/prisma/migrations/_/migration.sql` - 5. Mark as applied: `docker compose exec -T api npx prisma migrate resolve --applied ` - 6. Verify: `docker compose exec -T api npx prisma migrate status` → "Database schema is up to date!" - 7. Clean up: `docker compose exec -T v2-postgres dropdb -U changemaker prisma_shadow_diff` -- **Gotcha:** `--from-migrations` replays all migration files on a shadow DB. If a migration references tables created by `db push` (no migration file), it will fail. Fix: temporarily move the dependent migration aside, generate the catch-up (which includes the missing tables), then remove the old migration -- **Production deploys:** Use `prisma migrate deploy` (not `migrate dev`) — it applies pending migrations without creating a shadow DB +- **Fixing drift:** Use `prisma migrate diff --from-migrations ... --to-schema-datamodel ... --script` with a shadow DB to generate catch-up SQL, then `prisma migrate resolve --applied`. See MEMORY.md for detailed steps +- **Production deploys:** Use `prisma migrate deploy` (not `migrate dev`) ### V2-Specific Gotchas -- **Prisma migrations:** Never use `db push` on the v2 branch — always use `migrate dev` to keep migration history in sync. The baseline catch-up migration (`20260224100000_baseline_catchup`) covers all schema changes from Feb 18–24 that were previously applied via `db push` -- Fastify media API on port 4100, separate from Express on 4000 (same DB, different ORM) -- Volunteer page naming: `VolunteerShiftsPage.tsx` (not "MyAssignmentsPage") -- Tracking module: `api/src/modules/map/tracking/` (volunteer + admin routes) -- Pages module: 3 route files (pages-admin, pages-public, blocks) -- Vite proxy: `VITE_API_URL`, `VITE_MKDOCS_URL` env vars (Docker sets to container hostnames) +- **Prisma migrations:** Never use `db push` — always `migrate dev` to keep history in sync - Nginx media API block must come BEFORE general API block -- MkDocs port 4003 (was 4000, conflicted with API) -- Media upload: requires separate RW volume mount for inbox directory (`:rw` on `/media/local/inbox`), library remains read-only -- FFmpeg/FFprobe: installed in media-api container (Alpine `apk add --no-cache ffmpeg`), used for metadata extraction -- **Registry mode:** `IMAGE_TAG=local` (default) never pulls from registry; set to a commit SHA or `latest` to use pre-built images; `--use-registry` in upgrade.sh auto-sets `IMAGE_TAG` to the new commit SHA -- **Registry auth:** `docker login gitea.bnkops.com` required once per machine; `GITEA_REGISTRY_USER`/`GITEA_REGISTRY_PASS` in `.env` used by the API's registry status endpoint only (not Docker itself) -- **Cloudflare + registry:** gitea.bnkops.com must be DNS-only (no proxy) to push image layers >100MB; Cloudflare free plan blocks large blobs with 413 errors. See `docs/REGISTRY_GUIDE.md` -- **Production image size:** API Dockerfile production stage uses `npm ci --omit=dev` + `npx prisma generate` (not copying from build stage) to avoid including TypeScript devDeps in the final image -- **Release vs source installs:** Detected by `VERSION` file + absence of `.git/`. Release installs use `docker-compose.prod.yml` (no build blocks, no source mounts, `IMAGE_TAG:-latest`). Source installs use `docker-compose.yml` (build blocks + source mounts for dev). `config.sh`, `upgrade.sh`, and `upgrade-check.sh` all auto-detect the mode -- **`api/dist/` is gitignored:** Compiled output must NOT be committed. It's generated by `npm run build` (dev) or baked into Docker images (prod). If root-owned (from container builds), fix with `docker run --rm -v ./api:/api alpine chown -R $(id -u):$(id -g) /api/dist/` -- **`api/.dockerignore` excludes dist/:** Prevents stale local compiled JS from being copied into Docker build context. Without this, cached Docker layers may serve old module sets even after adding new TypeScript modules +- `IMAGE_TAG=local` (default) never pulls from registry; set to SHA or `latest` for pre-built images +- **Release vs source installs:** Detected by `VERSION` file + absence of `.git/`; release uses `docker-compose.prod.yml`, source uses `docker-compose.yml` +- **`api/dist/` is gitignored** — never commit; if root-owned from container builds, fix with `chown` +- See MEMORY.md "Common Gotchas" for additional gotchas (ports, volumes, media upload, registry, etc.) --- @@ -784,25 +700,8 @@ Check in order: 4. **Pangolin resources configured:** All resources set to "Not Protected" 5. **Nginx running:** `docker compose ps nginx` -### Database Connection Failures - -**Symptom:** API logs show database connection errors. - -**Fix:** -1. Check PostgreSQL container: `docker compose ps v2-postgres` -2. Verify `DATABASE_URL` in `.env` matches container name and port -3. Check PostgreSQL logs: `docker compose logs v2-postgres --tail 50` -4. Test connection: `docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"` - -### Redis Connection Failures - -**Symptom:** API logs show Redis connection errors, rate limiting doesn't work. - -**Fix:** -1. Check Redis container: `docker compose ps redis-changemaker` -2. Verify `REDIS_PASSWORD` matches in `.env` and `REDIS_URL` format -3. Check Redis logs: `docker compose logs redis-changemaker --tail 50` -4. Test connection: `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping` +### Database/Redis Connection Failures +Check container status (`docker compose ps`), verify credentials in `.env`, check logs (`docker compose logs --tail 50`). Test DB: `docker compose exec api npx prisma db execute --stdin <<< "SELECT 1"`. Test Redis: `docker compose exec redis-changemaker redis-cli -a $REDIS_PASSWORD ping`. ---