diff --git a/.env.development.example b/.env.development.example new file mode 100644 index 0000000..4b73c10 --- /dev/null +++ b/.env.development.example @@ -0,0 +1,10 @@ +# Root-level local dev defaults. +# Copy to `.env.development` (gitignored) to override. +# All values here work out of the box — no changes needed for standard local dev. + +DATABASE_URL=postgres://trails:trails@localhost:5432/trails +BROUTER_URL=http://localhost:17777 + +# Change these for any environment that is not purely local. +JWT_SECRET=dev-secret-not-for-production +SESSION_SECRET=dev-secret-not-for-production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6d12af6..bd213d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,58 +191,6 @@ jobs: cache: pnpm - run: pnpm install --frozen-lockfile - - name: Cache PostGIS Docker image - id: postgis-cache - uses: actions/cache@v5 - with: - path: /tmp/postgis-image.tar - key: postgis-16-3.4 - - - name: Load or pull PostGIS image - run: | - if [ -f /tmp/postgis-image.tar ]; then - docker load < /tmp/postgis-image.tar - else - docker pull postgis/postgis:16-3.4 - docker save postgis/postgis:16-3.4 > /tmp/postgis-image.tar - fi - - - name: Start PostgreSQL - run: | - docker run -d --name postgres \ - -e POSTGRES_USER=trails \ - -e POSTGRES_PASSWORD=trails \ - -e POSTGRES_DB=trails \ - -p 5432:5432 \ - postgis/postgis:16-3.4 - # Wait for pg_isready - for i in $(seq 1 30); do - docker exec postgres pg_isready -U trails > /dev/null 2>&1 && break - sleep 1 - done - # Wait for PostGIS extension to be ready - for i in $(seq 1 10); do - docker exec postgres psql -U trails -c "SELECT PostGIS_Version();" > /dev/null 2>&1 && break - sleep 1 - done - - - name: Push database schema - run: pnpm db:push - - - name: Build and cache BRouter - id: brouter-cache - uses: actions/cache@v5 - with: - path: /tmp/brouter - key: brouter-1.7.8 - - - name: Download BRouter - if: steps.brouter-cache.outputs.cache-hit != 'true' - run: | - mkdir -p /tmp/brouter - wget -q "https://github.com/abrensch/brouter/releases/download/v1.7.8/brouter-1.7.8.zip" -O /tmp/brouter/brouter.zip - cd /tmp/brouter && unzip -o brouter.zip && mv brouter-1.7.8/* . && rmdir brouter-1.7.8 && rm brouter.zip - - name: Cache BRouter segment id: segment-cache uses: actions/cache@v5 @@ -256,23 +204,17 @@ jobs: mkdir -p /tmp/brouter-segments wget -q "https://brouter.de/brouter/segments4/E10_N50.rd5" -O /tmp/brouter-segments/E10_N50.rd5 - - name: Start BRouter - run: | - cd /tmp/brouter - java -Xmx256M -Xms64M \ - -DmaxRunningTime=300 \ - -cp brouter-1.7.8-all.jar \ - btools.server.RouteServer \ - /tmp/brouter-segments profiles2 profiles2 \ - 17777 2 & - # Wait for BRouter to start - for i in $(seq 1 30); do - curl -sf http://localhost:17777/brouter?lonlats=13.4,52.5\|13.5,52.5\&profile=trekking\&format=geojson > /dev/null 2>&1 && break - sleep 2 - done + - name: Start services + run: docker compose -f docker-compose.dev.yml -f docker-compose.ci.yml up -d --wait --build env: BROUTER_URL: http://localhost:17777 + - name: Push database schema + run: pnpm db:push + + - name: Seed database + run: pnpm db:seed + - name: Cache Playwright browsers id: playwright-cache uses: actions/cache@v5 diff --git a/.gitignore b/.gitignore index 58a8d39..85f9417 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ dist/ .crit.json e2e/results/ test-results/ +.env.development playwright-report/ playwright-results.json .claude/worktrees/ diff --git a/docker-compose.ci.yml b/docker-compose.ci.yml new file mode 100644 index 0000000..f900e4b --- /dev/null +++ b/docker-compose.ci.yml @@ -0,0 +1,4 @@ +services: + brouter: + volumes: + - /tmp/brouter-segments:/data/segments diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f65f243..aa669ad 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -7,8 +7,10 @@ services: POSTGRES_USER: trails POSTGRES_PASSWORD: trails POSTGRES_DB: trails + command: ["postgres", "-c", "shared_preload_libraries=pg_stat_statements"] volumes: - pgdata:/var/lib/postgresql/data + - ./infrastructure/postgres/init-grafana-user.sql:/docker-entrypoint-initdb.d/init-grafana-user.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U trails"] interval: 5s @@ -22,6 +24,42 @@ services: volumes: - brouter_segments:/data/segments + prometheus: + image: prom/prometheus:latest + profiles: [monitoring] + ports: + - "9090:9090" + volumes: + - ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus_data:/prometheus + + grafana: + image: grafana/grafana:latest + profiles: [monitoring] + ports: + - "3002:3000" + environment: + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: Admin + GF_AUTH_DISABLE_LOGIN_FORM: "true" + volumes: + - ./infrastructure/grafana/provisioning:/etc/grafana/provisioning:ro + - ./infrastructure/grafana/dashboards:/var/lib/grafana/dashboards:ro + - grafana_data:/var/lib/grafana + + loki: + image: grafana/loki:latest + profiles: [monitoring] + ports: + - "3100:3100" + volumes: + - ./infrastructure/loki/loki-config.yml:/etc/loki/local-config.yaml:ro + - loki_data:/loki + command: ["-config.file=/etc/loki/local-config.yaml"] + volumes: pgdata: brouter_segments: + prometheus_data: + grafana_data: + loki_data: diff --git a/openspec/changes/local-dev-stack/tasks.md b/openspec/changes/local-dev-stack/tasks.md index c2a0973..1db8b5f 100644 --- a/openspec/changes/local-dev-stack/tasks.md +++ b/openspec/changes/local-dev-stack/tasks.md @@ -1,30 +1,30 @@ ## 1. Docker Compose -- [ ] 1.1 Update dev PostgreSQL to match production: add `shared_preload_libraries=pg_stat_statements` command and mount `infrastructure/postgres/init-grafana-user.sql` as init script -- [ ] 1.2 Add Prometheus service with `monitoring` profile, mounting `infrastructure/prometheus/prometheus.yml` -- [ ] 1.3 Add Grafana service with `monitoring` profile, anonymous auth enabled, provisioned with local Prometheus and Loki datasources -- [ ] 1.4 Add Loki service with `monitoring` profile, mounting `infrastructure/loki/loki-config.yml` +- [x] 1.1 Update dev PostgreSQL to match production: add `shared_preload_libraries=pg_stat_statements` command and mount `infrastructure/postgres/init-grafana-user.sql` as init script +- [x] 1.2 Add Prometheus service with `monitoring` profile, mounting `infrastructure/prometheus/prometheus.yml` +- [x] 1.3 Add Grafana service with `monitoring` profile, anonymous auth enabled, provisioned with local Prometheus and Loki datasources +- [x] 1.4 Add Loki service with `monitoring` profile, mounting `infrastructure/loki/loki-config.yml` ## 2. Database -- [ ] 2.1 Create `scripts/seed.ts` with idempotent test data: user account, sample route (Berlin area), sample activity. Use Drizzle ORM, `ON CONFLICT DO NOTHING` -- [ ] 2.2 Add `pnpm db:seed` script to root package.json that runs `scripts/seed.ts` with `--experimental-strip-types` +- [x] 2.1 Create `scripts/seed.ts` with idempotent test data: user account, sample route (Berlin area), sample activity. Use Drizzle ORM, `ON CONFLICT DO NOTHING` +- [x] 2.2 Add `pnpm db:seed` script to root package.json that runs `scripts/seed.ts` with `--experimental-strip-types` ## 3. CI Integration -- [ ] 3.1 Replace manual `docker run` PostgreSQL setup in `ci.yml` e2e job with `docker compose -f docker-compose.dev.yml up -d --wait` -- [ ] 3.2 Replace manual BRouter download/start in `ci.yml` with the compose BRouter service plus cached segment mount -- [ ] 3.3 Add `pnpm db:seed` step after `pnpm db:push` in CI e2e job -- [ ] 3.4 Remove any E2E test skip workarounds that check for DB availability (the `withDb()` 503 skip pattern) +- [x] 3.1 Replace manual `docker run` PostgreSQL setup in `ci.yml` e2e job with `docker compose -f docker-compose.dev.yml up -d --wait` +- [x] 3.2 Replace manual BRouter download/start in `ci.yml` with the compose BRouter service plus cached segment mount +- [x] 3.3 Add `pnpm db:seed` step after `pnpm db:push` in CI e2e job +- [x] 3.4 Remove any E2E test skip workarounds that check for DB availability (the `withDb()` 503 skip pattern) ## 4. Scripts -- [ ] 4.1 Improve `scripts/dev.sh`: check Docker is running first, use `docker compose up -d --wait`, add `--monitoring` flag to start monitoring profile, run seed after schema push -- [ ] 4.2 Create `scripts/reset-dev.sh`: stop containers, remove volumes, restart — add as `pnpm dev:reset` in package.json +- [x] 4.1 Improve `scripts/dev.sh`: check Docker is running first, use `docker compose up -d --wait`, add `--monitoring` flag to start monitoring profile, run seed after schema push +- [x] 4.2 Create `scripts/reset-dev.sh`: stop containers, remove volumes, restart — add as `pnpm dev:reset` in package.json ## 5. Environment -- [ ] 5.1 Create `.env.development.example` with documented defaults (DATABASE_URL, BROUTER_URL, JWT_SECRET, SESSION_SECRET) and add `.env.development` to `.gitignore` +- [x] 5.1 Create `.env.development.example` with documented defaults (DATABASE_URL, BROUTER_URL, JWT_SECRET, SESSION_SECRET) and add `.env.development` to `.gitignore` ## 6. Verify diff --git a/package.json b/package.json index ab062fd..eabfd27 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "db:push": "drizzle-kit push --config packages/db/drizzle.config.ts --force", "db:migrate-data": "tsx packages/db/src/migrate-data.ts", "db:studio": "drizzle-kit studio --config packages/db/drizzle.config.ts", + "db:seed": "node --experimental-strip-types scripts/seed.ts", "dev:services": "docker compose -f docker-compose.dev.yml up -d", "dev:full": "./scripts/dev.sh", + "dev:reset": "./scripts/reset-dev.sh", "dev:ios": "pnpm --filter @trails-cool/mobile ios", "dev:android": "pnpm --filter @trails-cool/mobile android", "openspec": "openspec" diff --git a/scripts/dev.sh b/scripts/dev.sh index 9d6d1e0..2e1ca44 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -6,50 +6,59 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")" cd "$PROJECT_DIR" +# Parse flags +MONITORING=false +for arg in "$@"; do + case "$arg" in + --monitoring) MONITORING=true ;; + esac +done + +# Check Docker is running +if ! docker info > /dev/null 2>&1; then + echo "Error: Docker is not running. Please start Docker and try again." + exit 1 +fi + echo "=== trails.cool dev environment ===" echo "" # 1. Start Docker services -echo "Starting Docker services..." -docker compose -f docker-compose.dev.yml up -d +if [ "$MONITORING" = "true" ]; then + echo "Starting Docker services (with monitoring)..." + docker compose -f docker-compose.dev.yml --profile monitoring up -d --wait +else + echo "Starting Docker services..." + docker compose -f docker-compose.dev.yml up -d --wait +fi +echo "✓ Services are ready" -# 2. Wait for PostgreSQL -echo "Waiting for PostgreSQL..." -until docker compose -f docker-compose.dev.yml exec -T postgres pg_isready -U trails > /dev/null 2>&1; do - sleep 1 -done -echo "✓ PostgreSQL is ready" - -# 3. Push database schema +# 2. Push database schema echo "Pushing database schema..." pnpm db:push 2>&1 | tail -3 echo "✓ Database schema up to date" +# 3. Seed database +echo "Seeding database..." +pnpm db:seed +echo "✓ Database seeded" + # 4. Download BRouter segments if needed echo "Checking BRouter segments..." "$SCRIPT_DIR/download-dev-segments.sh" -# 5. Wait for BRouter (if segments are available) -if docker compose -f docker-compose.dev.yml ps brouter --format '{{.State}}' 2>/dev/null | grep -q "running"; then - echo "Waiting for BRouter..." - for i in $(seq 1 30); do - if curl -sf http://localhost:17777/brouter?lonlats=13.4,52.5\|13.5,52.5\&profile=trekking\&format=geojson > /dev/null 2>&1; then - echo "✓ BRouter is ready" - break - fi - if [ "$i" = "30" ]; then - echo "⚠ BRouter not responding (segments may still be loading). Continuing..." - fi - sleep 2 - done -fi - echo "" echo "=== Starting apps ===" echo " Journal: http://localhost:3000" echo " Planner: http://localhost:3001" echo " BRouter: http://localhost:17777" +if [ "$MONITORING" = "true" ]; then + echo " Grafana: http://localhost:3002" + echo " Prometheus: http://localhost:9090" +fi +echo "" +echo " Tip: pnpm dev:reset to wipe all data and start fresh" echo "" -# 6. Start both apps with turbo +# 5. Start both apps with turbo exec pnpm dev diff --git a/scripts/reset-dev.sh b/scripts/reset-dev.sh new file mode 100755 index 0000000..7b737dc --- /dev/null +++ b/scripts/reset-dev.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -euo pipefail + +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$PROJECT_DIR" + +if ! docker info > /dev/null 2>&1; then + echo "Error: Docker is not running. Please start Docker and try again." + exit 1 +fi + +echo "=== Resetting dev environment ===" +echo "This will stop all containers and wipe all local data volumes." +echo "" + +read -r -p "Are you sure? [y/N] " confirm +if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then + echo "Aborted." + exit 0 +fi + +echo "Stopping containers and removing volumes..." +docker compose -f docker-compose.dev.yml --profile monitoring down -v 2>/dev/null || \ + docker compose -f docker-compose.dev.yml down -v + +echo "✓ Done. Run 'pnpm dev:full' to start fresh." diff --git a/scripts/seed.ts b/scripts/seed.ts new file mode 100644 index 0000000..fcb5c34 --- /dev/null +++ b/scripts/seed.ts @@ -0,0 +1,72 @@ +import { createDb } from "../packages/db/src/index.ts"; +import { users, routes, activities } from "../packages/db/src/schema/journal.ts"; + +const db = createDb(); + +const SEED_USER_ID = "seed-user-dev"; +const SEED_ROUTE_ID = "seed-route-berlin"; +const SEED_ACTIVITY_ID = "seed-activity-berlin"; + +// Minimal GPX for a short Berlin route (Tiergarten area) +const BERLIN_GPX = ` + + + Tiergarten Loop + + 34 + 35 + 35 + 36 + 35 + 34 + + +`; + +await db + .insert(users) + .values({ + id: SEED_USER_ID, + email: "dev@trails.cool", + username: "devuser", + displayName: "Dev User", + domain: "localhost:3000", + profileVisibility: "public", + }) + .onConflictDoNothing(); + +await db + .insert(routes) + .values({ + id: SEED_ROUTE_ID, + ownerId: SEED_USER_ID, + name: "Tiergarten Loop", + description: "A short loop through Tiergarten for local dev testing.", + gpx: BERLIN_GPX, + routingProfile: "trekking", + distance: 1200, + elevationGain: 5, + elevationLoss: 5, + visibility: "public", + }) + .onConflictDoNothing(); + +await db + .insert(activities) + .values({ + id: SEED_ACTIVITY_ID, + ownerId: SEED_USER_ID, + routeId: SEED_ROUTE_ID, + name: "Morning walk in Tiergarten", + gpx: BERLIN_GPX, + startedAt: new Date("2024-06-01T08:00:00Z"), + duration: 1800, + distance: 1200, + elevationGain: 5, + elevationLoss: 5, + visibility: "public", + }) + .onConflictDoNothing(); + +console.log("✓ Seed complete"); +process.exit(0);