Merge pull request #389 from trails-cool/stigi/local-dev-stack

Local dev stack improvements
This commit is contained in:
Ullrich Schäfer 2026-05-17 23:15:39 +02:00 committed by GitHub
commit 6c7bf6baeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 260 additions and 109 deletions

10
.env.development.example Normal file
View file

@ -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

View file

@ -191,64 +191,12 @@ jobs:
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - 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 - name: Cache BRouter segment
id: segment-cache id: segment-cache
uses: actions/cache@v5 uses: actions/cache@v5
with: with:
path: /tmp/brouter-segments path: /tmp/brouter-segments
key: brouter-segment-E10_N50 key: brouter-segment-E10_N50-v1.7.9
- name: Download Berlin segment - name: Download Berlin segment
if: steps.segment-cache.outputs.cache-hit != 'true' if: steps.segment-cache.outputs.cache-hit != 'true'
@ -256,23 +204,33 @@ jobs:
mkdir -p /tmp/brouter-segments mkdir -p /tmp/brouter-segments
wget -q "https://brouter.de/brouter/segments4/E10_N50.rd5" -O /tmp/brouter-segments/E10_N50.rd5 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: | run: |
cd /tmp/brouter docker volume create trails_brouter_segments
java -Xmx256M -Xms64M \ docker run --rm \
-DmaxRunningTime=300 \ -v /tmp/brouter-segments:/src:ro \
-cp brouter-1.7.8-all.jar \ -v trails_brouter_segments:/dst \
btools.server.RouteServer \ alpine sh -c "cp /src/*.rd5 /dst/ && chmod a+r /dst/*.rd5"
/tmp/brouter-segments profiles2 profiles2 \
17777 2 & - name: Start services
# Wait for BRouter to start run: docker compose -f docker-compose.dev.yml up -d --wait --build
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
env: env:
BROUTER_URL: http://localhost:17777 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 - name: Cache Playwright browsers
id: playwright-cache id: playwright-cache
uses: actions/cache@v5 uses: actions/cache@v5

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ dist/
.crit.json .crit.json
e2e/results/ e2e/results/
test-results/ test-results/
.env.development
playwright-report/ playwright-report/
playwright-results.json playwright-results.json
.claude/worktrees/ .claude/worktrees/

View file

@ -7,8 +7,10 @@ services:
POSTGRES_USER: trails POSTGRES_USER: trails
POSTGRES_PASSWORD: trails POSTGRES_PASSWORD: trails
POSTGRES_DB: trails POSTGRES_DB: trails
command: ["postgres", "-c", "shared_preload_libraries=pg_stat_statements"]
volumes: volumes:
- pgdata:/var/lib/postgresql/data - pgdata:/var/lib/postgresql/data
- ./infrastructure/postgres/init-grafana-user.sql:/docker-entrypoint-initdb.d/init-grafana-user.sql:ro
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U trails"] test: ["CMD-SHELL", "pg_isready -U trails"]
interval: 5s interval: 5s
@ -21,7 +23,50 @@ services:
- "17777:17777" - "17777:17777"
volumes: volumes:
- brouter_segments:/data/segments - 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: volumes:
pgdata: pgdata:
brouter_segments: brouter_segments:
prometheus_data:
grafana_data:
loki_data:

View file

@ -1,34 +1,34 @@
## 1. Docker Compose ## 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 - [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
- [ ] 1.2 Add Prometheus service with `monitoring` profile, mounting `infrastructure/prometheus/prometheus.yml` - [x] 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 - [x] 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.4 Add Loki service with `monitoring` profile, mounting `infrastructure/loki/loki-config.yml`
## 2. Database ## 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` - [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`
- [ ] 2.2 Add `pnpm db:seed` script to root package.json that runs `scripts/seed.ts` with `--experimental-strip-types` - [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. 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` - [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`
- [ ] 3.2 Replace manual BRouter download/start in `ci.yml` with the compose BRouter service plus cached segment mount - [x] 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 - [x] 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.4 Remove any E2E test skip workarounds that check for DB availability (the `withDb()` 503 skip pattern)
## 4. Scripts ## 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 - [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
- [ ] 4.2 Create `scripts/reset-dev.sh`: stop containers, remove volumes, restart — add as `pnpm dev:reset` in package.json - [x] 4.2 Create `scripts/reset-dev.sh`: stop containers, remove volumes, restart — add as `pnpm dev:reset` in package.json
## 5. Environment ## 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. 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 - [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
- [ ] 6.2 Test monitoring profile: `pnpm dev:full -- --monitoring` starts Prometheus + Grafana + Loki, verify Grafana dashboards load at localhost:3002 - [x] 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) - [x] 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.4 Test reset: `pnpm dev:reset` wipes volumes and restarts cleanly

View file

@ -51,3 +51,31 @@ The Planner SHALL be able to compute routes locally in the dev environment.
#### Scenario: Compute a route in Berlin #### Scenario: Compute a route in Berlin
- **WHEN** a developer sends a route request with two Berlin waypoints to the local Planner - **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 - **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

View file

@ -20,8 +20,10 @@
"db:push": "drizzle-kit push --config packages/db/drizzle.config.ts --force", "db:push": "drizzle-kit push --config packages/db/drizzle.config.ts --force",
"db:migrate-data": "tsx packages/db/src/migrate-data.ts", "db:migrate-data": "tsx packages/db/src/migrate-data.ts",
"db:studio": "drizzle-kit studio --config packages/db/drizzle.config.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:services": "docker compose -f docker-compose.dev.yml up -d",
"dev:full": "./scripts/dev.sh", "dev:full": "./scripts/dev.sh",
"dev:reset": "./scripts/reset-dev.sh",
"dev:ios": "pnpm --filter @trails-cool/mobile ios", "dev:ios": "pnpm --filter @trails-cool/mobile ios",
"dev:android": "pnpm --filter @trails-cool/mobile android", "dev:android": "pnpm --filter @trails-cool/mobile android",
"openspec": "openspec" "openspec": "openspec"

View file

@ -6,50 +6,59 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_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 "=== trails.cool dev environment ==="
echo "" echo ""
# 1. Start Docker services # 1. Start Docker services
echo "Starting Docker services..." if [ "$MONITORING" = "true" ]; then
docker compose -f docker-compose.dev.yml up -d 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 # 2. Push database schema
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
echo "Pushing database schema..." echo "Pushing database schema..."
pnpm db:push 2>&1 | tail -3 pnpm db:push 2>&1 | tail -3
echo "✓ Database schema up to date" echo "✓ Database schema up to date"
# 3. Seed database
echo "Seeding database..."
pnpm db:seed
echo "✓ Database seeded"
# 4. Download BRouter segments if needed # 4. Download BRouter segments if needed
echo "Checking BRouter segments..." echo "Checking BRouter segments..."
"$SCRIPT_DIR/download-dev-segments.sh" "$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 ""
echo "=== Starting apps ===" echo "=== Starting apps ==="
echo " Journal: http://localhost:3000" echo " Journal: http://localhost:3000"
echo " Planner: http://localhost:3001" echo " Planner: http://localhost:3001"
echo " BRouter: http://localhost:17777" 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 "" echo ""
# 6. Start both apps with turbo # 5. Start both apps with turbo
exec pnpm dev exec pnpm dev

26
scripts/reset-dev.sh Executable file
View file

@ -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."

72
scripts/seed.ts Normal file
View file

@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="trails.cool" xmlns="http://www.topografix.com/GPX/1/1">
<trk>
<name>Tiergarten Loop</name>
<trkseg>
<trkpt lat="52.5145" lon="13.3501"><ele>34</ele></trkpt>
<trkpt lat="52.5150" lon="13.3560"><ele>35</ele></trkpt>
<trkpt lat="52.5170" lon="13.3620"><ele>35</ele></trkpt>
<trkpt lat="52.5190" lon="13.3580"><ele>36</ele></trkpt>
<trkpt lat="52.5200" lon="13.3510"><ele>35</ele></trkpt>
<trkpt lat="52.5145" lon="13.3501"><ele>34</ele></trkpt>
</trkseg>
</trk>
</gpx>`;
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);