Last weird branch commit
Some checks failed
CI / backend-tests (pull_request) Failing after 1m16s
CI / frontend-tests (pull_request) Successful in 28s

This commit is contained in:
2026-03-20 22:11:39 +01:00
parent 2364922b58
commit 3d362a8314
10 changed files with 141 additions and 36 deletions

View File

@@ -1,11 +1,11 @@
---
# nuzlocke-tracker-1y09
title: Enforce feature branch workflow for agents
status: todo
status: completed
type: task
priority: high
created_at: 2026-03-20T20:48:21Z
updated_at: 2026-03-20T20:59:21Z
updated_at: 2026-03-20T21:01:47Z
---
## Problem

View File

@@ -1,10 +1,11 @@
---
# nuzlocke-tracker-he1n
title: Add local GoTrue container for dev auth testing
status: todo
status: in-progress
type: feature
priority: normal
created_at: 2026-03-20T20:57:04Z
updated_at: 2026-03-20T20:57:04Z
updated_at: 2026-03-20T21:08:22Z
---
## Problem
@@ -34,17 +35,17 @@ GoTrue will use the existing `db` PostgreSQL container, creating its own `auth`
## Checklist
- [ ] Research GoTrue Docker image and required env vars (JWT secret, DB connection, SMTP disabled, etc.)
- [ ] Add `gotrue` service to `docker-compose.yml` using the existing `db` container
- [ ] Configure GoTrue to use the same PostgreSQL with its own `auth` schema
- [ ] Set local JWT secret (e.g. `super-secret-jwt-token-for-local-dev`) shared between GoTrue and the backend
- [ ] Update `.env.example` with local GoTrue defaults (`SUPABASE_URL=http://localhost:9999`, local JWT secret, local anon key)
- [ ] Update `frontend/src/lib/supabase.ts` to use `http://localhost:9999` in dev (GoTrue's local port)
- [ ] Verify backend JWT verification works with GoTrue-issued tokens (same HS256 + shared secret)
- [x] Research GoTrue Docker image and required env vars (JWT secret, DB connection, SMTP disabled, etc.)
- [x] Add `gotrue` service to `docker-compose.yml` using the existing `db` container
- [x] Configure GoTrue to use the same PostgreSQL with its own `auth` schema
- [x] Set local JWT secret (e.g. `super-secret-jwt-token-for-local-dev`) shared between GoTrue and the backend
- [x] Update `.env.example` with local GoTrue defaults (`SUPABASE_URL=http://localhost:9999`, local JWT secret, local anon key)
- [x] Update `frontend/src/lib/supabase.ts` to use `http://localhost:9999` in dev (GoTrue's local port)
- [x] Verify backend JWT verification works with GoTrue-issued tokens (same HS256 + shared secret)
- [ ] Test email/password signup and login flow end-to-end locally
- [ ] Verify OAuth buttons gracefully handle missing providers in local dev (show disabled state or helpful message)
- [ ] Update `docker-compose.yml` healthcheck for GoTrue readiness
- [ ] Document the local auth setup in README or contributing guide
- [x] Verify OAuth buttons gracefully handle missing providers in local dev (show disabled state or helpful message)
- [x] Update `docker-compose.yml` healthcheck for GoTrue readiness
- [x] Document the local auth setup in README or contributing guide
## Notes

View File

@@ -5,6 +5,17 @@
],
"PreCompact": [
{ "hooks": [{ "type": "command", "command": "beans prime" }] }
],
"PreToolCall": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'if echo \"$TOOL_INPUT\" | grep -q \"git commit\"; then BRANCH=$(git branch --show-current); if [ \"$BRANCH\" = \"develop\" ] || [ \"$BRANCH\" = \"main\" ]; then echo \"BLOCK: Cannot commit directly to $BRANCH. Create a feature branch first: git checkout -b feature/<name>\"; exit 2; fi; fi'"
}
]
}
]
}
}

View File

@@ -3,11 +3,20 @@ DEBUG=true
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nuzlocke
# Supabase Auth (backend)
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_JWT_SECRET=your-jwt-secret
# For local dev with GoTrue container:
SUPABASE_URL=http://localhost:9999
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
# For production, replace with your Supabase cloud values:
# SUPABASE_URL=https://your-project.supabase.co
# SUPABASE_ANON_KEY=your-anon-key
# SUPABASE_JWT_SECRET=your-jwt-secret
# Frontend settings (used by Vite)
VITE_API_URL=http://localhost:8000
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
# For local dev with GoTrue container:
VITE_SUPABASE_URL=http://localhost:9999
VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
# For production, replace with your Supabase cloud values:
# VITE_SUPABASE_URL=https://your-project.supabase.co
# VITE_SUPABASE_ANON_KEY=your-anon-key

View File

@@ -1,12 +1,11 @@
# Branching Strategy
- **Never commit directly to `main`.** `main` is always production-ready.
- Day-to-day work happens on `develop`.
- New work is done on `feature/*` branches off `develop`.
- Merge flow: `feature/*``develop``main`.
- **Squash merge** `feature/*` into `develop` (one clean commit per feature).
- **Merge commit** `develop` into `main` (marks deploy points).
- Always `git pull` the target branch before merging into it.
- **Never commit directly to `develop` or `main`.** Always create a `feature/*` branch first.
- When starting an **epic**, create `feature/<epic-title-slug>` off `develop`
- When starting a **standalone task/bug** (no parent epic), create `feature/<task-title-slug>` off `develop`
- Each task within an epic gets its own commit(s) on the epic's feature branch
- Branch naming: use a kebab-case slug of the bean title (e.g., `feature/add-auth-system`)
- When the epic/task is complete, squash merge into `develop`
# Pre-commit Hooks

View File

@@ -14,15 +14,29 @@ A full-stack Nuzlocke run tracker for Pokemon games.
docker compose up
```
This starts three services:
This starts four services:
| Service | URL |
|------------|--------------------------|
| Service | URL |
|------------|---------------------------|
| Frontend | http://localhost:5173 |
| API | http://localhost:8000 |
| API Docs | http://localhost:8000/docs|
| API | http://localhost:8080 |
| API Docs | http://localhost:8080/docs|
| GoTrue | http://localhost:9999 |
| PostgreSQL | localhost:5432 |
### Local Authentication
The stack includes a local GoTrue container for auth testing. Email/password signup and login work out of the box with auto-confirmation (no email verification needed).
**OAuth providers (Google, Discord) are disabled in local dev.** The login/signup pages show OAuth buttons as disabled with a tooltip explaining this. For OAuth testing, deploy to an environment with Supabase cloud configured.
The local JWT secret and anon key are pre-configured in `.env.example` and `docker-compose.yml`. Copy `.env.example` to `.env` before starting:
```bash
cp .env.example .env
docker compose up
```
### Run Migrations
```bash

View File

@@ -12,9 +12,14 @@ services:
environment:
- DEBUG=true
- DATABASE_URL=postgresql://postgres:postgres@db:5432/nuzlocke
# Auth - must match GoTrue's JWT secret
- SUPABASE_URL=http://gotrue:9999
- SUPABASE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
depends_on:
db:
condition: service_healthy
gotrue:
condition: service_healthy
restart: unless-stopped
frontend:
@@ -29,8 +34,12 @@ services:
- ./frontend/index.html:/app/index.html:cached
environment:
- VITE_API_URL=http://localhost:8080
# Local GoTrue auth
- VITE_SUPABASE_URL=http://localhost:9999
- VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzc0MDQwNjEzLCJleHAiOjIwODk0MDA2MTN9.EV6tRj7gLqoiT-l2vDFw_67myqRjwpcZTuRb3Xs1nr4
depends_on:
- api
- gotrue
restart: unless-stopped
db:
@@ -43,6 +52,7 @@ services:
- POSTGRES_DB=nuzlocke
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
@@ -50,5 +60,43 @@ services:
retries: 5
restart: unless-stopped
gotrue:
image: supabase/gotrue:v2.186.0
ports:
- "9999:9999"
environment:
# API settings
- GOTRUE_API_HOST=0.0.0.0
- GOTRUE_API_PORT=9999
- API_EXTERNAL_URL=http://localhost:9999
- GOTRUE_SITE_URL=http://localhost:5173
# Database
- GOTRUE_DB_DRIVER=postgres
- GOTRUE_DB_DATABASE_URL=postgres://postgres:postgres@db:5432/nuzlocke?sslmode=disable
# JWT - must match backend's SUPABASE_JWT_SECRET
- GOTRUE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long
- GOTRUE_JWT_AUD=authenticated
- GOTRUE_JWT_EXP=3600
- GOTRUE_JWT_ADMIN_ROLES=service_role
# Email auth (auto-confirm for local dev)
- GOTRUE_EXTERNAL_EMAIL_ENABLED=true
- GOTRUE_MAILER_AUTOCONFIRM=true
- GOTRUE_MAILER_SECURE_EMAIL_CHANGE_ENABLED=false
# Disable external OAuth providers (not configured locally)
- GOTRUE_EXTERNAL_GOOGLE_ENABLED=false
- GOTRUE_EXTERNAL_DISCORD_ENABLED=false
# Disable phone auth
- GOTRUE_EXTERNAL_PHONE_ENABLED=false
- GOTRUE_SMS_AUTOCONFIRM=false
depends_on:
db:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9999/health"]
interval: 5s
timeout: 5s
retries: 3
restart: unless-stopped
volumes:
postgres_data:

View File

@@ -6,7 +6,8 @@ const supabaseAnonKey = import.meta.env['VITE_SUPABASE_ANON_KEY'] ?? ''
function createSupabaseClient(): SupabaseClient {
if (!supabaseUrl || !supabaseAnonKey) {
// Return a stub client for tests/dev without Supabase configured
return createClient('http://localhost:54321', 'stub-key')
// Uses port 9999 to match local GoTrue container
return createClient('http://localhost:9999', 'stub-key')
}
return createClient(supabaseUrl, supabaseAnonKey)
}

View File

@@ -2,6 +2,8 @@ import { useState } from 'react'
import { Link, useNavigate, useLocation } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const isLocalDev = import.meta.env['VITE_SUPABASE_URL']?.includes('localhost') ?? false
export function Login() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -108,7 +110,9 @@ export function Login() {
<button
type="button"
onClick={handleGoogleLogin}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
disabled={isLocalDev}
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
@@ -133,7 +137,9 @@ export function Login() {
<button
type="button"
onClick={handleDiscordLogin}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
disabled={isLocalDev}
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
@@ -141,6 +147,11 @@ export function Login() {
Discord
</button>
</div>
{isLocalDev && (
<p className="text-center text-xs text-text-tertiary">
OAuth providers are not available in local development. Use email/password instead.
</p>
)}
<p className="text-center text-sm text-text-secondary">
Don&apos;t have an account?{' '}

View File

@@ -2,6 +2,8 @@ import { useState } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const isLocalDev = import.meta.env['VITE_SUPABASE_URL']?.includes('localhost') ?? false
export function Signup() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -172,7 +174,9 @@ export function Signup() {
<button
type="button"
onClick={handleGoogleSignup}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
disabled={isLocalDev}
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24">
<path
@@ -197,7 +201,9 @@ export function Signup() {
<button
type="button"
onClick={handleDiscordSignup}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors"
disabled={isLocalDev}
title={isLocalDev ? 'OAuth not available in local dev' : undefined}
className="flex items-center justify-center gap-2 py-2 px-4 bg-surface-2 hover:bg-surface-3 border border-border-default rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-surface-2"
>
<svg className="w-5 h-5" viewBox="0 0 24 24" fill="currentColor">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
@@ -205,6 +211,11 @@ export function Signup() {
Discord
</button>
</div>
{isLocalDev && (
<p className="text-center text-xs text-text-tertiary">
OAuth providers are not available in local development. Use email/password instead.
</p>
)}
<p className="text-center text-sm text-text-secondary">
Already have an account?{' '}