diff --git a/.env.example b/.env.example index 355ff86..3ee9464 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,17 @@ MAIL_DEFAULT_SENDER=no-reply@example.com CELERY_BROKER_URL=redis://redis:6379/0 CELERY_RESULT_BACKEND=redis://redis:6379/0 +# --- Frontend (NEXT_PUBLIC_* vars are baked into browser-side code) --- +# API base URL the frontend calls. In local dev, requests go through nginx on +# localhost, so the default below is what you want. +NEXT_PUBLIC_DEVELOP_BACKEND_URL=http://localhost +# MapTiler style URLs for the basemap layers (need a free MapTiler API key, +# e.g. https://api.maptiler.com/maps/streets-v2/style.json?key=YOUR_KEY). +# Optional: the app boots without them; the basemap is just blank. +NEXT_PUBLIC_DEVELOP_MAPTILE_STREET= +NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE= +NEXT_PUBLIC_DEVELOP_MAPTILE_DARK= + # --- Ports --- DEVELOP_BACKEND_PORT=8000 DEVELOP_FRONTEND_PORT=3000 diff --git a/Makefile b/Makefile index 21bc801..0e46a03 100644 --- a/Makefile +++ b/Makefile @@ -44,10 +44,10 @@ fmt: ## Apply ruff formatting in Docker $(TEST_COMPOSE) run --rm --no-deps test sh -c "uv sync --frozen && uv run ruff format ." seed: ## Seed a dev org + verified user + sample filing (stack must be up) - $(COMPOSE) exec backend python scripts/seed.py + $(COMPOSE) exec -e PYTHONPATH=/app backend python scripts/seed.py grant-admin: ## Grant platform-admin to a user: make grant-admin email=you@example.com - $(COMPOSE) exec backend python scripts/grant_platform_admin.py $(email) + $(COMPOSE) exec -e PYTHONPATH=/app backend python scripts/grant_platform_admin.py $(email) migrate: ## Apply DB migrations (alembic upgrade head) $(COMPOSE) exec backend alembic upgrade head diff --git a/docker-compose.yml b/docker-compose.yml index 2012c5e..c070563 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -52,8 +52,11 @@ services: volumes: - ./back-end:/app depends_on: - - redis - + db: + condition: service_healthy + redis: + condition: service_started + backend: image: ${DOCKER_IMAGE_BACKEND} build: @@ -86,14 +89,24 @@ services: volumes: - ./back-end:/app depends_on: - - db - - redis + db: + condition: service_healthy + redis: + condition: service_started ports: - ${DEVELOP_BACKEND_PORT}:${DEVELOP_BACKEND_PORT} frontend: image: ${DOCKER_IMAGE_FRONTEND} build: ./front-end + environment: + # API base URL the browser-side code calls; defaults to nginx on localhost. + - NEXT_PUBLIC_DEVELOP_BACKEND_URL=${NEXT_PUBLIC_DEVELOP_BACKEND_URL:-http://localhost} + # MapTiler basemap style URLs (optional — the app boots without them, + # the basemap is just blank until a key is configured). + - NEXT_PUBLIC_DEVELOP_MAPTILE_STREET=${NEXT_PUBLIC_DEVELOP_MAPTILE_STREET:-} + - NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE=${NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE:-} + - NEXT_PUBLIC_DEVELOP_MAPTILE_DARK=${NEXT_PUBLIC_DEVELOP_MAPTILE_DARK:-} volumes: - ./front-end:/app depends_on: diff --git a/front-end/__tests__/settings-basemap-fallback.test.js b/front-end/__tests__/settings-basemap-fallback.test.js new file mode 100644 index 0000000..dc14506 --- /dev/null +++ b/front-end/__tests__/settings-basemap-fallback.test.js @@ -0,0 +1,45 @@ +/** + * The basemap settings must fall back to a keyless OSM raster style when the + * MapTiler env vars are unset, so the map renders in dev without a key. + * With a key configured, the style URL passes through unchanged. + */ + +const loadSettings = (env) => { + let mod; + jest.isolateModules(() => { + const prev = {}; + for (const [k, v] of Object.entries(env)) { + prev[k] = process.env[k]; + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + mod = require("../utils/settings"); + for (const [k, v] of Object.entries(prev)) { + if (v === undefined) delete process.env[k]; + else process.env[k] = v; + } + }); + return mod; +}; + +describe("basemap fallback", () => { + it("falls back to an OSM raster style object when no key is set", () => { + const s = loadSettings({ + NEXT_PUBLIC_DEVELOP_MAPTILE_STREET: "", + NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE: undefined, + NEXT_PUBLIC_DEVELOP_MAPTILE_DARK: undefined, + }); + for (const style of [s.maptile_street, s.maptile_satelite, s.maptile_dark]) { + expect(typeof style).toBe("object"); + expect(style.version).toBe(8); + expect(style.sources.osm.type).toBe("raster"); + expect(style.sources.osm.tiles[0]).toContain("openstreetmap.org"); + } + }); + + it("passes the configured style URL through unchanged", () => { + const url = "https://api.maptiler.com/maps/streets/style.json?key=abc"; + const s = loadSettings({ NEXT_PUBLIC_DEVELOP_MAPTILE_STREET: url }); + expect(s.maptile_street).toBe(url); + }); +}); diff --git a/front-end/utils/settings.js b/front-end/utils/settings.js index 3d7dd00..6223c4a 100644 --- a/front-end/utils/settings.js +++ b/front-end/utils/settings.js @@ -1,4 +1,21 @@ +// Keyless OSM raster basemap, used whenever a MapTiler style URL isn't +// configured — the map then renders without any API key (dev/demo). MapLibre +// accepts either a style URL string or a style object, so consumers don't care +// which one they get. +const osmFallbackStyle = { + version: 8, + sources: { + osm: { + type: "raster", + tiles: ["https://a.tile.openstreetmap.org/{z}/{x}/{y}.png"], + tileSize: 256, + attribution: "© OpenStreetMap contributors", + }, + }, + layers: [{ id: "osm", type: "raster", source: "osm" }], +}; + export const backend_url = process.env.NEXT_PUBLIC_DEVELOP_BACKEND_URL; -export const maptile_street = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_STREET; -export const maptile_satelite = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE; -export const maptile_dark = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_DARK; \ No newline at end of file +export const maptile_street = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_STREET || osmFallbackStyle; +export const maptile_satelite = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_SATELITE || osmFallbackStyle; +export const maptile_dark = process.env.NEXT_PUBLIC_DEVELOP_MAPTILE_DARK || osmFallbackStyle;