All checks were successful
build-apk / build (push) Successful in 10m38s
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>
144 lines
7.9 KiB
Markdown
144 lines
7.9 KiB
Markdown
<p align="center">
|
||
<img src="app/assets/icon.png" alt="BigBrainParking — big-brain driver" width="220">
|
||
</p>
|
||
|
||
# BigBrainParking
|
||
|
||
An unofficial, de-Googled client for the ParkSmarter (IPS Group) parking system,
|
||
built to run on **GrapheneOS** with **UnifiedPush** notifications and distributed
|
||
via **Obtainium** — no Google Play Services, no Firebase, no app store.
|
||
|
||
## Install it (users)
|
||
|
||
BigBrainParking installs and updates through **Obtainium** — no Google Play, no account.
|
||
Works on GrapheneOS. On your phone (with Obtainium installed), tap:
|
||
|
||
**➕ [Add BigBrainParking to Obtainium](obtainium://add/https://git.mowden.top/hank/BigBrainParking)**
|
||
|
||
or follow **[INSTALL.md](INSTALL.md)** for the manual steps.
|
||
|
||
## Monorepo layout
|
||
|
||
```
|
||
bigbrainparking/
|
||
├── parksmarter-client/ # Zero-dep TypeScript API client (reverse-engineered, live-verified)
|
||
├── app/ # Expo / React Native app (BigBrainParking)
|
||
└── .gitea/workflows/ # CI: build signed APK -> publish release for Obtainium
|
||
```
|
||
|
||
- **`parksmarter-client`** — the API layer. All ~40 endpoints, the custom-header auth
|
||
model, and response models (most verified against production). Runs anywhere; the app
|
||
imports it directly. See its own README for the API details.
|
||
- **`app`** — the phone app. React Native (Expo prebuild), MapLibre maps, VisionCamera QR
|
||
scanning, expo-secure-store token storage, local session-expiry reminders, and
|
||
UnifiedPush wiring.
|
||
|
||
## Features
|
||
|
||
| Feature | Status | Notes |
|
||
| --- | --- | --- |
|
||
| Phone + password login | ✅ wired | tokens in OS keystore |
|
||
| Map of nearby meters (clickable, zoom, live GPS) | ✅ wired | MapLibre + OpenFreeMap tiles (no key) |
|
||
| City parking map overlay (2h/3h/4h/no-limit/lots) | ✅ wired | the city's printed map, georeferenced — **never touches the IPS API** |
|
||
| "Park here" pin + time tracking on any city area | ✅ wired | GPS or hand-placed pin, auto-detects the area, local countdown |
|
||
| Last-lot + My-location search | ✅ wired | opens on your last session's lot; GPS sent only via explicit "My location" |
|
||
| QR kiosk scan → meter lookup | ✅ wired | on-device VisionCamera |
|
||
| Save / share kiosks | ✅ wired | local (no server favorites API exists) |
|
||
| Active / past sessions | ✅ wired | list views + tap for full receipt |
|
||
| Start a paid session | ✅ works | confirmed live end-to-end (real $0.10 DL-zone charge); declines surfaced |
|
||
| Ongoing parking countdown | ✅ wired | foreground service; ticks down, **End** / **Extend** buttons, survives reboot |
|
||
| Session-expiry reminders | ✅ wired | **local** on-device notifications — no server, no push |
|
||
| UnifiedPush (ntfy) | ⚪ optional | not needed for reminders; stub for future server-initiated msgs |
|
||
|
||
## The city parking map
|
||
|
||
The **City map** layer on the Map tab is the City of Sandpoint's printed *Downtown &
|
||
Waterfront Public Parking* map, georeferenced and drawn in the same colours as the legend:
|
||
2-hour free, 3-hour, 4-hour, no time limit, and the paid city lots. 49 areas in all.
|
||
|
||
**None of it touches ParkSmarter.** The areas live in the local database (bundled with the
|
||
app, refreshed from the zone-labels server, cached on-device), the countdown is the phone's
|
||
own clock, and the notification is the same foreground service every other session uses. So
|
||
tracking your time on a city spot works with no account, no signal, no payment, and in
|
||
Anonymous Mode. Two ways to start:
|
||
|
||
- **Park here** — pins your car from GPS and works out which area you're in. No GPS fix
|
||
(garage, indoors, radio off)? It asks you to tap the spot instead and pins that. The pin
|
||
stays on the map until you end the session, because "where did I leave the car" is half
|
||
the point.
|
||
- **Tap a coloured segment** — pick the block directly, no pin needed.
|
||
|
||
Either way you choose how long to track, capped at the posted limit (a 2-hour space won't
|
||
offer to run a 4-hour timer — that's just scheduling a ticket). The ongoing notification's
|
||
second button reads **+1 hr** here rather than *Extend*: there is nothing to buy, so it
|
||
edits the local timer and says so.
|
||
|
||
The georeference was fitted to OpenStreetMap street centrelines and lands within ~4 m
|
||
(see [`tools/citymap/`](tools/citymap/) to regenerate it from a new edition of the PDF).
|
||
Because a few metres is the difference between two sides of a street, **Account → Align city
|
||
map** lets you nudge the whole overlay against a live GPS fix and save it — on the phone, or
|
||
published to the server for every device if you hold the admin token.
|
||
|
||
## Privacy
|
||
|
||
BigBrainParking sends your location to ParkSmarter **only** when you explicitly tap "My
|
||
location" and search; it opens on your last parking lot instead of your GPS, and ships **no**
|
||
analytics/tracking (no Segment/Amplitude/Firebase/Sentry, no ad-ID, no Google services).
|
||
For a plain-language comparison with the official app — which auto-sends your GPS on map
|
||
open and bundles that tracking stack — see
|
||
**[docs/OFFICIAL_APP_PRIVACY.md](docs/OFFICIAL_APP_PRIVACY.md)**. What reaches the API and
|
||
what never does is also spelled out in the app's **About** page.
|
||
|
||
## Build & run (dev)
|
||
|
||
Requires Node 20, JDK 17, Android SDK, and a GrapheneOS device (or any Android device)
|
||
with USB debugging.
|
||
|
||
```bash
|
||
npm install # installs both workspaces
|
||
npm run build --workspace parksmarter-client # compile the client
|
||
cd app
|
||
npx expo prebuild --platform android # generate native project
|
||
npx expo run:android # build + install a dev client
|
||
```
|
||
|
||
The app talks to prod (`apiv2.parksmarter.com`) by default — change `extra.psEnvironment`
|
||
in `app/app.json` to point elsewhere.
|
||
|
||
## Notifications on GrapheneOS
|
||
|
||
Everything here is **entirely on-device** — no server, no push, no FCM, no Play Services —
|
||
so it works fully offline. UnifiedPush (ntfy) is wired only as an optional, no-op stub for
|
||
any *future* server-initiated messages; nothing time-based needs it.
|
||
|
||
**The ongoing parking countdown.** Whenever a session is active — a paid one you bought or
|
||
a free check-in — a persistent notification shows the time left and ticks down, with
|
||
**End** and **Extend** buttons. It is held up by a real **foreground service**
|
||
(`modules/bbp-notify`, type `specialUse`), which is what makes it stick on GrapheneOS the
|
||
way ntfy's does: it survives the app being killed, can't be swiped away, and a
|
||
`BOOT_COMPLETED` receiver brings it back after a reboot. The service's life is exactly the
|
||
session's life — it stops itself, removing the notification, on **End**, when the meter
|
||
runs out, or whenever there's no session to show.
|
||
|
||
The countdown itself costs no battery: the end time is handed to Android as a
|
||
[chronometer](https://developer.android.com/reference/android/app/Notification.Builder#setChronometerCountDown(boolean)),
|
||
and the system redraws the ticking text with the app closed and no timer of its own.
|
||
|
||
- **End** stops tracking and clears the notification. On a *free check-in* that genuinely
|
||
ends it. On a *paid* session it only stops the display — ParkSmarter has no stop-session
|
||
endpoint, so time you already bought keeps running at the meter either way.
|
||
- **Extend** (labeled **Pay** on a free check-in) opens the purchase screen for that exact
|
||
zone. The active session is stored locally *with its zone*, so this works offline, in
|
||
Anonymous Mode, and after a reboot. Extending doesn't end anything until the purchase
|
||
actually goes through.
|
||
|
||
**Expiry reminders** fire a configurable lead time (default 15 min) before the end, via
|
||
`AlarmManager`. Configure them in **Account → Notifications**, where a **"Send a test
|
||
reminder"** button lets you confirm they fire on your phone; the same screen has a toggle
|
||
for the ongoing countdown.
|
||
|
||
## Distribution via Obtainium (self-hosted)
|
||
|
||
Tag a release and CI builds a signed APK and publishes it to this repo's releases; your
|
||
phone's Obtainium tracks the repo and offers updates. See
|
||
[`docs/DISTRIBUTION.md`](docs/DISTRIBUTION.md) for the full server + CI + keystore setup.
|