From 228032c913446e87239d7272cace14f5ab294ddc Mon Sep 17 00:00:00 2001 From: "(jenkins)" <(jenkins)> Date: Wed, 1 Apr 2026 11:04:16 +0100 Subject: [PATCH] Update application ports to 3050 and enhance Playwright tests with WebGL mock --- .claude/settings.local.json | 9 +++ .github/copilot-instructions.md | 70 ++++++++++++++++ CLAUDE.md | 112 ++++++++++++++++++++++++++ README.md | 4 +- docker-compose.yml | 2 +- frontend/Dockerfile | 2 +- frontend/e2e/app.spec.js | 74 +++++++++++++++-- frontend/playwright-report/index.html | 2 +- frontend/playwright.config.js | 12 ++- frontend/test-results/.last-run.json | 8 +- frontend/vite.config.js | 2 +- 11 files changed, 278 insertions(+), 19 deletions(-) create mode 100644 .claude/settings.local.json create mode 100644 .github/copilot-instructions.md create mode 100644 CLAUDE.md diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..ba2a8ac --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(npm test:*)", + "Bash(npm run:*)", + "Bash(npx playwright:*)" + ] + } +} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7c5164e --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,70 @@ +# Copilot Instructions for line-of-sight + +## Purpose +This repository is a full-stack geospatial app: +- `frontend/`: React + Vite + MapLibre UI +- `backend/`: Express API with PostGIS queries +- `docker/` and `docker-compose.yml`: local orchestration and DB initialization + +Prefer small, focused changes that keep frontend/backend contracts stable. + +## First Commands to Know +Use these commands first when validating changes. + +### Docker-first workflow (preferred) +- Start stack: `docker-compose up --build` +- Stop stack: `docker-compose down` +- Backend tests: `docker-compose exec backend npm test` +- Frontend unit tests: `docker-compose exec frontend npm test -- --run` +- Frontend E2E tests: `docker-compose exec frontend npm run test:e2e` +- Seed geodata: `docker-compose exec backend npm run seed-data` + +### Local workflow (without Docker) +- Root Node version: `nvm use` (uses `.nvmrc`, currently Node 24) +- Backend dev: `cd backend && npm install && npm run dev` +- Frontend dev: `cd frontend && npm install && npm run start` + +## Architecture and Boundaries +- Backend API entrypoint: `backend/app/server.js` + - Defines `GET /api/line-of-sight` and `GET /api/health` + - Performs destination/path generation and PostGIS querying +- Frontend app entrypoint: `frontend/src/App.jsx` + - Owns map lifecycle, animation, and UI state + - Calls backend via `frontend/src/services/api.js` +- Database bootstrap: `docker/init.sql` + - Enables PostGIS and creates `cities` with spatial index + +When changing behavior, prefer backend geospatial logic over duplicating calculations in the frontend. + +## Project Conventions +- Keep geospatial coordinates in WGS84 (`SRID 4326`) and preserve existing lat/lon parameter naming. +- Keep API responses JSON-compatible and backward compatible unless explicitly requested. +- Keep frontend styles in `frontend/src/styles/` (project uses custom CSS, not utility CSS frameworks). +- Avoid broad refactors in `App.jsx` unless task requires it; map lifecycle and refs are tightly coupled. + +## Testing Expectations +- Backend tests live in `backend/tests/` and use Jest + Supertest. +- Frontend unit tests live in `frontend/src/__tests__/` and use Vitest + Testing Library. +- E2E tests live in `frontend/e2e/` and use Playwright. +- For feature changes: + - Run the closest unit tests first. + - Run E2E tests when user flows or map interactions are changed. + +## Common Pitfalls +- PostGIS must be available before backend geospatial endpoints are exercised. +- `docker-compose.yml` pins Postgres service to `linux/arm64`; this can affect non-ARM hosts. +- Data import script (`backend/scripts/import_cities.js`) depends on `wget` and `unzip` availability. +- `README.md` contains roadmap/history notes; trust current scripts/config in source files first. + +## CI Notes +CI config is in `.gitea/workflows/test.yml`. +- Backend and frontend tests run on Node 24. +- Frontend unit tests run with `npm test -- --run`. +- E2E runs in Playwright container. + +## Link, Do Not Duplicate +For detailed setup and product context, reference: +- `README.md` +- `GEMINI.md` + +Keep this file focused on actionable agent guidance and repo-specific guardrails. \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..28bca26 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,112 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Project Is + +**Line of Sight** is a geospatial web app that draws a great-circle line from a user-selected point on Earth in a chosen direction, then finds all cities along that path. It includes a flight-over animation that speeds up 20x over water and slows to normal speed over land. + +## Commands + +### Docker (preferred workflow) + +```bash +docker-compose up --build # Start all services (frontend :3050, backend :3001, postgres) +docker-compose down # Stop all services +docker-compose logs -f # View logs + +# Run tests inside containers +docker-compose exec backend npm test +docker-compose exec frontend npm test -- --run +docker-compose exec frontend npm run test:e2e +``` + +### Local development (without Docker) + +```bash +# Backend +cd backend && npm run dev # Nodemon dev server on :3001 + +# Frontend +cd frontend && npm run start # Vite dev server on :3050 +``` + +### Tests + +```bash +# Backend unit tests (Jest + Supertest) +cd backend && npm test + +# Frontend unit tests (Vitest) +cd frontend && npm test # Watch mode +cd frontend && npm test -- --run # Single run (CI mode) + +# E2E tests (Playwright) +cd frontend && npm run test:e2e + +# Seed the database with GeoNames city data +cd backend && npm run seed-data +``` + +### Build + +```bash +cd frontend && npm run build # Vite production build → frontend/build/ +``` + +## Architecture + +This is a monorepo with three services orchestrated by Docker Compose: + +``` +frontend/ → React 19 + Vite 6 SPA (MapLibre GL, Turf.js, Axios) +backend/ → Node 24 + Express 5 REST API +docker/ → PostgreSQL + PostGIS initialization +``` + +### Data flow + +1. User clicks map → sets start point (lat/lon) +2. User selects direction (0–360°) and tolerance (km radius) +3. Frontend calls `GET /api/line-of-sight?lat=&lon=&direction=&tolerance=` +4. Backend generates 80 path points along a great-circle arc (up to 20,000 km) +5. For each point, a PostGIS `ST_DWithin()` query finds cities within tolerance +6. Backend also checks whether each point is over land (500 km radius land check) +7. Response includes line coordinates with per-point water flags and matching cities +8. Frontend renders the path on a 3D globe map and shows city markers + +### Backend: `backend/app/server.js` + +Single-file Express app. Key internals: +- `calculateDestination()` — Haversine formula for great-circle destination points +- `GET /api/line-of-sight` — main endpoint: path generation + PostGIS city lookup +- `GET /api/health` — health check +- Returns up to 200 cities ordered by position along the line + +### Frontend: `frontend/src/App.jsx` + +Single large React component (~687 lines) managing all state and map logic: +- MapLibre GL map with 3D globe projection and terrain/sky effects +- Direction slider drives a live preview line before the user commits +- After "Show Line of Sight", map locks (prevents moving start point) +- Flight animation uses Turf.js to interpolate positions along the path; speed is 1× over land, 20× over water with smooth acceleration (0.005 step), predictive look-ahead of 2000 km +- New cities are fetched every 2000 km during flight via the same API +- Three map styles: light, dark, satellite (Esri tiles) + +### Database + +PostGIS `cities` table with a GIST index on `geom GEOGRAPHY(POINT, 4326)`. All coordinates use WGS84 / SRID 4326. The init SQL seeds 10 major cities; the `seed-data` script imports from GeoNames for a fuller dataset. + +## Conventions + +- **Geospatial**: WGS84 / SRID 4326 everywhere; PostGIS `GEOGRAPHY` type for distance queries +- **API**: RESTful; query params for `GET /api/line-of-sight`; env var `VITE_API_URL` (frontend) or `DATABASE_URL` (backend) +- **Styling**: Plain CSS files in `frontend/src/styles/` — no CSS framework +- **Tests**: Backend mocks the `pg` Pool; frontend mocks MapLibre GL and the API service. E2E tests use Playwright with a mock API response + +## CI + +Gitea workflow (`.gitea/workflows/test.yml`) runs on push/PR to `main`: +1. **backend-test** — `npm ci && npm test` +2. **frontend-test** — `npm ci && npm test -- --run` +3. **e2e-test** — uses `mcr.microsoft.com/playwright:v1.50.1` container diff --git a/README.md b/README.md index e1ee42e..b740c2d 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ docker-compose up --build ### 3. Access Application -- **Frontend**: http://localhost:3000 +- **Frontend**: http://localhost:3050 - **Backend API**: http://localhost:3001 - **Database**: localhost:5432 (PostgreSQL with PostGIS) @@ -219,7 +219,7 @@ npm run dev ### Port Already in Use ```bash # Find process using port -lsof -i :3000 +lsof -i :3050 lsof -i :3001 # Kill process diff --git a/docker-compose.yml b/docker-compose.yml index af9e1a8..ed2d685 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: dockerfile: Dockerfile container_name: line-of-sight-frontend ports: - - "3000:3000" + - "3050:3050" environment: - VITE_API_URL=http://localhost:3001/api depends_on: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6379caa..78f23c4 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -10,7 +10,7 @@ RUN npm install COPY . . # Expose port -EXPOSE 3000 +EXPOSE 3050 # Set environment variable for API URL ENV VITE_API_URL=http://localhost:3001/api diff --git a/frontend/e2e/app.spec.js b/frontend/e2e/app.spec.js index 3ad70f1..cd14b93 100644 --- a/frontend/e2e/app.spec.js +++ b/frontend/e2e/app.spec.js @@ -1,14 +1,80 @@ import { test, expect } from '@playwright/test'; +// MapLibre GL requires WebGL which isn't available in headless Chromium. +// Intercept the module request and return a lightweight mock so React can render. +const MAPLIBRE_MOCK = ` +class Map { + constructor(opts) { + this._listeners = {}; + Promise.resolve().then(() => this._emit('style.load')); + } + _emit(event) { + (this._listeners[event] || []).forEach(fn => fn()); + } + on(event, fn) { + if (!this._listeners[event]) this._listeners[event] = []; + this._listeners[event].push(fn); + return this; + } + off(event, fn) { + this._listeners[event] = (this._listeners[event] || []).filter(l => l !== fn); + return this; + } + remove() {} + getSource() { return null; } + getLayer() { return null; } + addSource() {} + addLayer() {} + removeLayer() {} + removeSource() {} + setSky() {} + setTerrain() {} + setLayoutProperty() {} + flyTo() {} + jumpTo() {} + setStyle() { Promise.resolve().then(() => this._emit('style.load')); } + getCanvas() { return document.createElement('canvas'); } + project() { return { x: 0, y: 0 }; } + unproject() { return { lng: 0, lat: 0 }; } +} +class Marker { + constructor(opts) { + this._el = (opts && opts.element) || document.createElement('div'); + } + setLngLat() { return this; } + addTo() { return this; } + remove() {} + getLngLat() { return { lng: 0, lat: 0 }; } + getElement() { return this._el; } +} +class Popup { + setLngLat() { return this; } + setDOMContent() { return this; } + addTo() { return this; } + remove() {} +} +class LngLatBounds { + extend() { return this; } +} +export default { Map, Marker, Popup, LngLatBounds }; +`; + +test.beforeEach(async ({ page }) => { + await page.route('**maplibre-gl.js*', async route => { + await route.fulfill({ contentType: 'application/javascript', body: MAPLIBRE_MOCK }); + }); +}); + test.describe('Line of Sight Application', () => { test('should load the home page and show settings', async ({ page }) => { await page.goto('/'); await expect(page.locator('text=Line of Sight Settings')).toBeVisible(); - await expect(page.locator('text=Direction')).toBeVisible(); + await expect(page.locator('label:has-text("Direction")')).toBeVisible(); }); test('should be able to toggle map style', async ({ page }) => { await page.goto('/'); + await page.waitForSelector('h3:text("Line of Sight Settings")', { timeout: 10000 }); const darkButton = page.locator('button:text("Dark")'); await darkButton.click(); await expect(darkButton).toHaveClass(/active-style/); @@ -16,10 +82,8 @@ test.describe('Line of Sight Application', () => { test('should show results when clicking the search button', async ({ page }) => { await page.goto('/'); - - // Mock the API response to avoid dependency on backend for E2E if desired, - // but here we let it hit the real backend if it's up. - // For a robust E2E in CI, we usually mock. + await page.waitForSelector('h3:text("Line of Sight Settings")', { timeout: 10000 }); + await page.route('**/api/line-of-sight*', async route => { const json = { success: true, diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html index 2e24970..6a8013c 100644 --- a/frontend/playwright-report/index.html +++ b/frontend/playwright-report/index.html @@ -82,4 +82,4 @@ Error generating stack: `+a.message+`