diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 253a052..e4708a3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -67,6 +67,8 @@ jobs:
name: E2E Tests
needs: build
runs-on: ubuntu-latest
+ env:
+ DATABASE_URL: postgres://trails:trails@localhost:5432/trails
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v4
@@ -75,8 +77,109 @@ jobs:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- - run: pnpm exec playwright install --with-deps chromium
- - run: pnpm test:e2e
+
+ - name: Cache PostGIS Docker image
+ id: postgis-cache
+ uses: actions/cache@v4
+ 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@v4
+ 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@v4
+ with:
+ path: /tmp/brouter-segments
+ key: brouter-segment-E10_N50
+
+ - name: Download Berlin segment
+ if: steps.segment-cache.outputs.cache-hit != 'true'
+ run: |
+ 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
+ env:
+ BROUTER_URL: http://localhost:17777
+
+ - name: Cache Playwright browsers
+ id: playwright-cache
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ hashFiles('pnpm-lock.yaml') }}
+
+ - name: Install Playwright
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: pnpm exec playwright install --with-deps chromium
+
+ - name: Install Playwright deps only
+ if: steps.playwright-cache.outputs.cache-hit == 'true'
+ run: pnpm exec playwright install-deps chromium
+
+ - name: Run E2E tests
+ run: pnpm test:e2e
+ env:
+ BROUTER_URL: http://localhost:17777
+
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx
index ca53230..f68979f 100644
--- a/apps/journal/app/root.tsx
+++ b/apps/journal/app/root.tsx
@@ -1,5 +1,6 @@
-import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router";
+import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from "react-router";
import type { LinksFunction } from "react-router";
+import type { Route } from "./+types/root";
import stylesheet from "@trails-cool/ui/styles.css?url";
export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }];
@@ -25,3 +26,33 @@ export function Layout({ children }: { children: React.ReactNode }) {
export default function App() {
return
+ {error.status === 404 && "Page not found"} + {error.status === 503 && "Service temporarily unavailable. Please try again later."} + {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} +
+ + Go home + ++ {error instanceof Error ? error.message : "An unexpected error occurred"} +
+ + Go home + ++ {error.status === 404 && "Page not found"} + {error.status === 503 && "Service temporarily unavailable"} + {error.status !== 404 && error.status !== 503 && (error.statusText || "Something went wrong")} +
++ {error instanceof Error ? error.message : "An unexpected error occurred"} +
+