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..0494470 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,64 +191,12 @@ 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 with: path: /tmp/brouter-segments - key: brouter-segment-E10_N50 + key: brouter-segment-E10_N50-v1.7.9 - name: Download Berlin segment if: steps.segment-cache.outputs.cache-hit != 'true' @@ -256,23 +204,33 @@ 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 + - name: Pre-seed BRouter segment volume 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 + docker volume create trails_brouter_segments + docker run --rm \ + -v /tmp/brouter-segments:/src:ro \ + -v trails_brouter_segments:/dst \ + alpine sh -c "cp /src/*.rd5 /dst/ && chmod a+r /dst/*.rd5" + + - name: Start services + run: docker compose -f docker-compose.dev.yml up -d --wait --build env: BROUTER_URL: http://localhost:17777 + - name: Wait for BRouter routing + run: | + for i in $(seq 1 60); do + curl -s 'http://localhost:17777/brouter?lonlats=13.4,52.5|13.5,52.5&profile=trekking&format=geojson' 2>/dev/null | grep -q "FeatureCollection" && echo "BRouter ready" && break + [ "$i" = "60" ] && echo "BRouter not ready after 120s" && exit 1 + sleep 2 + done + + - 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.dev.yml b/docker-compose.dev.yml index f65f243..ff29112 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 @@ -21,7 +23,50 @@ services: - "17777:17777" volumes: - brouter_segments:/data/segments + healthcheck: + test: ["CMD-SHELL", "curl -s http://localhost:17777/ > /dev/null 2>&1"] + interval: 5s + timeout: 5s + retries: 30 + start_period: 10s + + 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/datasources:/etc/grafana/provisioning/datasources:ro + - ./infrastructure/grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards: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/.openspec.yaml b/openspec/changes/archive/2026-05-17-local-dev-stack/.openspec.yaml similarity index 100% rename from openspec/changes/local-dev-stack/.openspec.yaml rename to openspec/changes/archive/2026-05-17-local-dev-stack/.openspec.yaml diff --git a/openspec/changes/local-dev-stack/design.md b/openspec/changes/archive/2026-05-17-local-dev-stack/design.md similarity index 100% rename from openspec/changes/local-dev-stack/design.md rename to openspec/changes/archive/2026-05-17-local-dev-stack/design.md diff --git a/openspec/changes/local-dev-stack/proposal.md b/openspec/changes/archive/2026-05-17-local-dev-stack/proposal.md similarity index 100% rename from openspec/changes/local-dev-stack/proposal.md rename to openspec/changes/archive/2026-05-17-local-dev-stack/proposal.md diff --git a/openspec/changes/local-dev-stack/specs/local-dev-environment/spec.md b/openspec/changes/archive/2026-05-17-local-dev-stack/specs/local-dev-environment/spec.md similarity index 100% rename from openspec/changes/local-dev-stack/specs/local-dev-environment/spec.md rename to openspec/changes/archive/2026-05-17-local-dev-stack/specs/local-dev-environment/spec.md diff --git a/openspec/changes/local-dev-stack/tasks.md b/openspec/changes/archive/2026-05-17-local-dev-stack/tasks.md similarity index 53% rename from openspec/changes/local-dev-stack/tasks.md rename to openspec/changes/archive/2026-05-17-local-dev-stack/tasks.md index c2a0973..bc7ebe6 100644 --- a/openspec/changes/local-dev-stack/tasks.md +++ b/openspec/changes/archive/2026-05-17-local-dev-stack/tasks.md @@ -1,34 +1,34 @@ ## 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 -- [ ] 6.1 Test full local dev flow: `pnpm dev:full` starts services, pushes schema, seeds data, launches apps — create session, compute route, verify seeded data visible -- [ ] 6.2 Test monitoring profile: `pnpm dev:full -- --monitoring` starts Prometheus + Grafana + Loki, verify Grafana dashboards load at localhost:3002 -- [ ] 6.3 Test CI flow: push branch, verify e2e job uses compose, all ~20 E2E tests run (none skipped) -- [ ] 6.4 Test reset: `pnpm dev:reset` wipes volumes and restarts cleanly +- [x] 6.1 Test full local dev flow: `pnpm dev:full` starts services, pushes schema, seeds data, launches apps — create session, compute route, verify seeded data visible +- [x] 6.2 Test monitoring profile: `pnpm dev:full -- --monitoring` starts Prometheus + Grafana + Loki, verify Grafana dashboards load at localhost:3002 +- [x] 6.3 Test CI flow: push branch, verify e2e job uses compose, all ~20 E2E tests run (none skipped) +- [x] 6.4 Test reset: `pnpm dev:reset` wipes volumes and restarts cleanly diff --git a/openspec/specs/local-dev-environment/spec.md b/openspec/specs/local-dev-environment/spec.md index 53853c0..2edd2d2 100644 --- a/openspec/specs/local-dev-environment/spec.md +++ b/openspec/specs/local-dev-environment/spec.md @@ -51,3 +51,31 @@ The Planner SHALL be able to compute routes locally in the dev environment. #### Scenario: Compute a route in Berlin - **WHEN** a developer sends a route request with two Berlin waypoints to the local Planner - **THEN** the Planner proxies to local BRouter and returns a valid GeoJSON route + +### Requirement: Optional local monitoring stack +The dev environment SHALL support an optional monitoring profile matching the production stack. + +#### Scenario: Start with monitoring +- **WHEN** a developer runs `pnpm dev:full` with `--monitoring` +- **THEN** Prometheus, Grafana, and Loki start alongside the app services + +### Requirement: Production-aligned PostgreSQL config +The dev PostgreSQL SHALL match production configuration including pg_stat_statements. + +#### Scenario: pg_stat_statements available +- **WHEN** the dev PostgreSQL container starts +- **THEN** pg_stat_statements is enabled via initialization scripts + +### Requirement: Database seed script +The dev environment SHALL provide a seed script for consistent test data. + +#### Scenario: Seed database +- **WHEN** a developer runs `pnpm db:seed` +- **THEN** test users, routes, and activities are created idempotently in the local database + +### Requirement: Dev environment reset +The dev environment SHALL provide a command to tear down and recreate the local stack. + +#### Scenario: Reset dev environment +- **WHEN** a developer runs `pnpm dev:reset` +- **THEN** all Docker volumes are removed and containers are recreated 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);