Personal website. Django + Next.js, deployed to Hetzner VPS (nam685.de).
- Backend: Python 3.12+, Django 6.0+, PostgreSQL 16, Redis — managed with
uv - Frontend: Next.js 16 (App Router), React 19, Tailwind CSS v4, TypeScript — managed with
pnpm - Deploy: Caddy (auto-TLS), systemd, GitHub Actions CI → SSH deploy
# Backend (run from repo root)
uv run python manage.py runserver # dev server → http://localhost:8000
uv run python manage.py makemigrations # create migrations
uv run python manage.py migrate # apply migrations
uv run pytest # tests (conftest.py fixtures in root)
uvx ruff check . && uvx ruff format . # lint+format (hook auto-fixes on save)
# Frontend (cd frontend)
pnpm dev # dev server (Turbopack) → http://localhost:3001
pnpm build # prod build
pnpm lint # ESLint (custom flat config)
pnpm format # Prettier
pnpm test # vitest
# Infra
docker compose up -d # PostgreSQL + Redis- MUST use worktrees for every feature/fix — always work in
.claude/worktrees/, never directly on main. Create with:git worktree add .claude/worktrees/my-feature -b feat/my-feature origin/main - Visually verify UI changes with
pnpm dev+ Playwright screenshots before pushing
The website/ app uses split subdirectories (not flat files):
website/models/<name>.py— one file per model, exported viamodels/__init__.pywebsite/views/<name>.py— one file per view group, exported viaviews/__init__.pywebsite/urls.py— all routes under/api/website/auth.py—require_admindecorator,create_token/verify_tokenwebsite/utils.py— shared helpers (get_client_ip,parse_json_body)website/tests/test_<name>.py— pytest tests
Never create flat website/models.py or website/views.py — they conflict with the __init__.py re-exports.
When adding a new model: create website/models/<name>.py, add import to models/__init__.py, add to __all__.
When adding a new view: create website/views/<name>.py, add imports to views/__init__.py, add URL in urls.py.
All under /api/ (Caddy proxies to Django :8000):
GET /api/health/
POST /api/auth/login/ body: {"secret": "<ADMIN_SECRET>"} → {"token": "..."}
GET /api/auth/check/ Authorization: Bearer <token>
GET /api/thoughts/?page=N
POST /api/thoughts/create/ auth required, body: {"content": "..."}
GET /api/drawings/
POST /api/drawings/upload/ auth required, multipart (image + category)
POST /api/drawings/<id>/delete/ auth required
POST /api/feedback/ body: {"message": "..."}, rate-limited per IP
GET /api/projects/
GET /api/todo/
GET /api/github/contributions/
GET /api/github/auth/ auth required, initiates GitHub OAuth
GET /api/github/callback/
GET /api/github/refresh-status/ auth required
GET /api/listens/?limit=N&offset=N
GET /api/listens/tracks/ top tracks by play count
GET /api/listens/artists/ top artists by play count
GET /api/listens/albums/ top albums by play count
GET /api/listens/recommended/ recommended track (rediscovery algorithm)
GET /api/listens/stats/
POST /api/listens/sync/ auth required, triggers YTM history sync
GET /api/listens/sync-status/ auth required
POST /api/listens/import/ auth required, Google Takeout file upload
GET /api/watches/?limit=N&offset=N
GET /api/watches/staging/ auth required
POST /api/watches/channels/<id>/tier/ auth required, body: {"tier": "..."}
POST /api/watches/channels/<id>/order/ auth required, body: {"display_order": N}
POST /api/watches/channels/<id>/delete/ auth required
POST /api/watches/videos/<id>/pin/ auth required, toggles pin+visible
POST /api/watches/videos/<id>/note/ auth required, body: {"note": "..."}
POST /api/watches/videos/<id>/delete/ auth required
GET /api/watches/auth/ auth required, initiates Google OAuth
GET /api/watches/callback/ Google OAuth callback
POST /api/watches/sync/ auth required, triggers YouTube sync
GET /api/watches/sync-status/ auth required
GET /api/watches/recommended/ random weighted pinned video for hero
POST /api/watches/backfill-stats/ auth required, backfills stale video stats
GET /api/bets/ all tickers with latest price + sparkline
GET /api/bets/<id>/history/ price history, ?period=1W|1M|3M|1Y|ALL
POST /api/bets/create/ auth required, body: {symbol, name, asset_type, provider, provider_id, currency}
POST /api/bets/<id>/delete/ auth required
POST /api/bets/sync/ auth required, triggers price fetch
GET /api/bets/sync-status/ auth required
GET /api/bets/search/?q=... auth required, searches Alpha Vantage + CoinGecko
GET /api/lichess/auth/ auth required, initiates Lichess OAuth (PKCE)
GET /api/lichess/callback/ Lichess OAuth callback
GET /api/lichess/token/ auth required, returns stored Lichess token
GET /api/lichess/status/ public, returns connection status
GET /api/slops/ session list (paginated, with turns)
GET /api/slops/<id>/ single session detail with turns
GET /api/slops/<id>/trace/ ATIF trace file contents
POST /api/slops/submit/ submit prompt (1/hr/IP + 10/hr global), optional session_id, optional multipart `files[]`
GET /api/slops/attachments/<id>/preview/ auth required, UTF-8 text content (64 KB cap)
POST /api/slops/turns/<id>/approve/ auth required, approve turn + queue
POST /api/slops/turns/<id>/reject/ auth required, reject turn
GET /api/slops/stats/ aggregate stats (from turns)
Custom token auth (not Django users). Login: POST /api/auth/login/ with JSON body {"secret": "<ADMIN_SECRET>"}. Rate-limited to 15 attempts / 15 min per IP via Redis. Token via Django signing, 7-day TTL. Bearer header for protected endpoints. Frontend: /sudo login page, token stored under adminToken key in localStorage.
Frontend auth helpers in frontend/src/lib/auth.ts:
store(key)/storeDel(key)— SSR-safe localStorage wrappers (always use these, never rawlocalStorage)getAdminToken()— returns token or redirects to/sudo
Import from @/lib/api:
API— empty string for client-side (Caddy proxies/api/*)API_INTERNAL—http://localhost:8000for server-side Next.js fetches- Always use
${API}/api/<endpoint>/for client-side fetches
Each nav page has a unique --accent CSS variable. When adding a page to the nav:
- Add entry to
NAV_ITEMSinfrontend/src/lib/navWheel.ts - Add to the
mmap in the inline<script>infrontend/src/app/layout.tsx
Missing either step causes an accent color flash on uncached loads.
components/CyberGrid.tsx— SVG grid background pattern (used by codes + reads)components/PageBackground.tsx— page-specific background imagescomponents/Navbar.tsx— nav wheel + mobile navcomponents/FeedbackButton.tsx— floating feedback form
- Inline styles used for dynamic/accent-colored elements
- Tailwind used for utility layout
globals.csshas shared classes:.tag,.corner-tl/.corner-tr/.corner-bl/.corner-br- Shared keyframes in
globals.css:fadeIn,fadeUp,hexFloat - Pure utility functions go in
frontend/src/lib/for testability
- Run from repo root:
uv run pytest - Test files:
website/tests/test_<name>.py - Shared fixtures in
conftest.py:admin_token,auth_headers,_disable_ssl_redirect - Use
@pytest.mark.django_dbfor DB tests
- Tests in
frontend/src/lib/__tests__/ - Node environment for pure logic tests
- Only test exported pure functions in
src/lib/
- Python: Ruff (line-length=120). PostToolUse hook auto-runs
ruff check --fix+ruff formaton.pysaves. - Frontend: Prettier (semi, double quotes, 2-space indent, trailing commas) + ESLint (custom flat config in
eslint.config.mjs) - Caddy routes:
/api/*,/admin/*→ Django :8000;/media/*→ file_server;/*→ Next.js :3000
Backend (.env, see .env.example):
SECRET_KEY— Django secret key (required, no default)DATABASE_URL— PostgreSQL connection stringREDIS_URL— Redis URL (default:redis://localhost:6379/0)ADMIN_SECRET— secret for/api/auth/login/(required, no default)ALLOWED_HOSTS— comma-separated (must includenam685.dein prod)CORS_ALLOWED_ORIGINS— comma-separated allowed originsCSRF_TRUSTED_ORIGINS— comma-separated trusted originsDEBUG— boolean (default: False)
Frontend:
NEXT_PUBLIC_API_URL— API base URL; empty in prod (Caddy proxies),http://localhost:8000for local dev without Caddy
docs/README.md— Customer-facing description of what the website is and does. Update when adding/removing pages or features.docs/QA-CHECKLIST.md— Manual QA checklist for quality audits. Add corresponding items when adding new features or pages.docs/infrastructure.md— Server setup and deployment instructions.
Important: When adding a new page or feature, you MUST:
- Update
docs/README.mdwith a description of the new section - Add QA test items to
docs/QA-CHECKLIST.md - Follow the existing QA checklist when verifying your changes work correctly
Issue tracker lives in backlog/ — one .md file per ticket with YAML frontmatter (status, priority, labels). See backlog/README.md for conventions. Prefer this over TODO.md for anything that spans multiple sessions.
Common tasks are available via make:
make help # show all available commands
make up # full dev boot: containers + migrate + seed + dev servers
make down # stop containers
make dev # start Django + Next.js dev servers (no setup)
make db-reset # drop + recreate database
make db-seed # run migrations + seed
make dumpseed # export current DB to fixtures/seed.json
make sync # show instructions for live API syncs
make test # run all tests (backend + frontend)
make lint # run all linters
make format # format all code