BigBrainParking/server
Erik 148e1635d3
All checks were successful
build-apk / build (push) Successful in 10m38s
v0.6.0: city parking-map overlay + local time tracking, no IPS API
Adds the City of Sandpoint's printed "Downtown & Waterfront Public Parking"
map as a georeferenced overlay, and lets you track your time on any of its
areas without ever touching the ParkSmarter/IPS API.

Georeferencing (tools/citymap/)
- The PDF carries no geo metadata, so the page->WebMercator affine is
  recovered by fitting the drawing to OSM street centrelines.
- pdftocairo writes stroked street segments with per-path matrix()
  transforms in local coords while filled lots are absolute; both are
  handled. The five legend swatches share the real geometry's colours and
  are identified by stroke-width and position, then dropped.
- 49 areas, fitted to RMS 4.1 m (X) / 3.5 m (Y). On-street segments land a
  mean 4.0 m from the nearest OSM road. sp-039/040 sit further out because
  they are angled bays along the old rail corridor, on no named road at all.
- Sandpoint's grid jogs 38 m between N 2nd Ave and S 2nd Ave; the page shows
  the same jog at the fitted scale, which independently confirms the fit.

App
- Map tab: "City map" layer in the legend's colours, tappable.
- "Park here" pins the car from GPS and auto-detects the containing area
  (40 m snap). With no fix it asks you to tap the spot instead, so the pin
  never depends on GPS working.
- The pin lives in its own storage key, not inside the session: pinning the
  car without starting a timer must survive backing out of the screen.
- Durations cap at the posted limit — a 2-hour space is not offered a
  4-hour timer. Lots and no-limit spots get the long options.
- Reuses the existing foreground-service countdown. The second notification
  button reads "+1 hr" for a city area rather than "Extend": there is
  nothing to buy, so it edits the local timer and says so.
- Account -> Align city map: nudge/scale/rotate the whole overlay against a
  live GPS fix. Save-on-phone needs no admin token, since the person who can
  see the misalignment is the one standing on the street.

Server
- parking_areas + map_overlay tables, public read, admin replace-all. The
  areas come from one source document, so replacement is wholesale rather
  than an upsert.

Dropped geometryCenter from the geo module: on the real data it returns a
point in the water for the crescent City Beach lot and mid-block for
L-shaped runs. Nothing used it.

Tests: 8 geometry tests in app/, 5 area/overlay tests in server/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 03:28:18 +00:00
..
deploy server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
src v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
test v0.6.0: city parking-map overlay + local time tracking, no IPS API 2026-08-13 03:28:18 +00:00
.env.example server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
.gitignore server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
docker-compose.yml server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
Dockerfile server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
package-lock.json server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
package.json server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
README.md server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00
tsconfig.json server: zone-labels API (Fastify + SQLite) — Phase A 2026-07-24 18:09:03 +00:00

BigBrainParking zone-labels API

A tiny service that classifies parking zones so the app knows whether a space is free for a limit (free_2h / free_3h / free_4h) or a pay-immediately lot (pay_immediate). Public reads; writes require the admin password.

Lives in the monorepo but is not an npm workspace (keeps its native better-sqlite3 dep out of the app/CI install). Deploy it independently.

API

Method Path Auth Notes
GET /healthz liveness
GET /api/labels all labels { labels: [...] } (app bulk-caches)
GET /api/labels/:zoneId one label, 404 if none
PUT /api/labels/:zoneId admin upsert { kind, zoneName?, customerId?, note? }
DELETE /api/labels/:zoneId admin remove
GET /api/whoami admin { admin: true } — used by the app's "test password"

Auth: Authorization: Bearer <BBP_ADMIN_TOKEN> (timing-safe compare). Reads are public but rate-limited (~120/min/IP; writes ~20/min). Repeated bad tokens from an IP auto-block it for a cooldown; blocked_ips (DB) + BBP_BLOCKED_IPS (env) are a manual denylist. zoneId is the ParkSmarter ZoneId (e.g. 113165).

Develop

cd server
npm install
npm test          # node --test via tsx
BBP_ADMIN_TOKEN=dev-secret-please-change npm run dev

Deploy (Docker + nginx on the host the CNAME points to)

cd server
cp .env.example .env
sed -i "s#change-me-to-a-long-random-secret#$(openssl rand -base64 32)#" .env   # set the admin secret
docker compose up -d --build

# nginx + TLS (first time)
sudo cp deploy/bigbrainparking.mowden.top.conf /etc/nginx/sites-available/bigbrainparking.mowden.top
sudo ln -s ../sites-available/bigbrainparking.mowden.top /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d bigbrainparking.mowden.top

curl https://bigbrainparking.mowden.top/healthz         # {"ok":true}

The admin secret (from .env) is what you paste into the app under Settings → Admin. Rotate by editing .env and docker compose up -d.

Update

git pull && docker compose up -d --build