Commit graph

65 commits

Author SHA1 Message Date
4217e88338 v0.6.3: hide the paid city lots from users without a ParkSmarter account
All checks were successful
build-apk / build (push) Successful in 9m54s
The green lots are the map's only paid category ("City lots - Paid hourly or
permit"); paying for them goes through ParkSmarter, so they are no use to
someone browsing without an account. Every other category is free street
parking with a posted time limit and needs nothing.

Gated by category rather than by an id list. Two lots were named (off Oak St
and off N 3rd Ave) and then the beach ones, which together is every green lot
on the map — and an id list would silently break the next time the map is
regenerated from a new PDF, since ids are positional.

- Paid lots are filtered out of the overlay when signed out, so they are
  neither drawn nor tappable.
- "Park here" still detects them, so standing in one explains that it needs an
  account instead of reporting no parking nearby.
- The area screen guards too, in case one is reached with a stale nav param.
- Server carries an optional per-area requiresAccount override for a lot that
  turns out to take payment another way. Null means "use the category
  default", so an unset value can't be confused with an explicit false.

The new column needs a real migration: CREATE TABLE IF NOT EXISTS does not add
a column to a table that already exists, so an already-deployed server would
have kept the old schema. Covered by a test that builds the pre-migration
table and then opens it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 06:02:23 +00:00
6af3dad762 v0.6.2: show and manage on-phone sessions offline and signed out
All checks were successful
build-apk / build (push) Successful in 9m58s
A city-map timer started without signing in did not appear under Sessions.

The tab returned "Sign in to see your sessions" before rendering anything, so
a local session could never show; and even signed in it only ever listed
ParkSmarter's sessions. A local timer is the one kind that has no server copy,
which made it the one kind the screen could not display.

- Sessions now leads with "Tracking on this phone": the live countdown with
  +1 hour and End, working with no account and no network, because that is the
  only place the session exists.
- Recent local sessions are kept in a small on-device history (50 max). Without
  it a local session vanished the instant it ended — there is no server to ask.
  Paid ParkSmarter sessions are excluded so they don't appear twice.
- The ParkSmarter half is layered on top when signed in and can fail
  independently: offline it reports that and keeps the local half visible,
  rather than the whole tab going blank. It also no longer leaves an unhandled
  rejection when the fetch throws (it had try/finally but no catch).
- The card's second button follows the notification's rule: +1 hour for a local
  timer, Extend -> purchase screen for a bought session, since only one of those
  can honestly add time.

History is written in endActiveParking() before the record is dropped, which
also covers expiry — syncActiveParking() routes a lapsed session through the
same call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 05:39:14 +00:00
7fc92d24e4 v0.6.1: stop Anonymous Mode bouncing to sign-in on every foreground
All checks were successful
build-apk / build (push) Successful in 9m55s
Parking without an account showed "Your session expired — please sign in
again." repeatedly and kicked the user back to the login screen.

The 401 hook on the API client is global: it fires on every unauthorized
response whether or not the caller caught the error. Anonymous Mode has no
token, so any ParkSmarter call 401s, and two of them run automatically —
discoverPaidSession() on open and every foreground, and the map's
lastSessionLot() on every mount. Each one flipped the status to signedOut;
re-entering anonymous mode ran them again, hence "over and over".

Both call sites already caught their errors, which is why this hid: the
bounce came from the client hook, not from the catch.

- A 401 in Anonymous Mode is no longer treated as a session expiry. There is
  no session to expire, and bouncing to sign-in makes the mode pointless.
  This also covers the Sessions/Favorites/Scan tabs, which 401 the same way.
- authBus carries the mode to the non-React modules that need it.
- discoverPaidSession() and lastSessionLot() are skipped entirely when
  anonymous rather than fired and discarded.
- "Last lot" now says it needs an account instead of silently doing nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 04:19:12 +00:00
148e1635d3 v0.6.0: city parking-map overlay + local time tracking, no IPS API
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>
2026-08-13 03:28:18 +00:00
ad55559f55 v0.5.0: persistent parking notification — foreground service, End/Extend
All checks were successful
build-apk / build (push) Successful in 28m55s
The ongoing countdown now appears whenever a car is parked, paid or free,
and sticks around the way ntfy's does on GrapheneOS.

Why it wasn't showing at all on a paid park: StartSessionScreen carried its
own stricter copy of parseApiTime that demanded MM-DD-YYYY + AM/PM. The
dual-format fix from deb78bf only landed in sessionStatus.ts, so when the
API returned the other format the parser returned null and the notification
was simply never posted. There is now one parser (api/parseTime.ts), and the
failure logs instead of going silent.

Persistence: a plain notify() was never enough — Android 14+ lets the user
swipe an ongoing notification away, and nothing brought it back after a
reboot. BbpSessionService is a real foreground service (type specialUse) that
owns the notification, plus a BOOT_COMPLETED receiver to restore it; specialUse
is one of the types Android 14/15 still allow to start from BOOT_COMPLETED.
The service's life is exactly the session's life: it stops itself, removing
the notification, on End, at expiry, or when there's no session to show.

Buttons: paid sessions previously got no actions at all (only free check-ins
did). Both now get End and Extend/Pay. End stops tracking — honest about the
fact that ParkSmarter has no stop-session endpoint, so bought time keeps
running at the meter. Extend opens the purchase screen for that exact zone.

The active session (with its Zone) is persisted locally, so the countdown and
Extend survive reboot, offline, and Anonymous Mode instead of depending on a
round-trip. sessionStatus.ts + features/checkin collapse into one owner,
features/session/activeParking — one record, one notification.

Verified: :bbp-notify:compileDebugKotlin passes, and the merged app manifest
carries the service (specialUse + FGS subtype property), both receivers, and
the FOREGROUND_SERVICE/SPECIAL_USE/RECEIVE_BOOT_COMPLETED permissions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 20:10:54 +00:00
796674bca4 v0.4.1: check-in — one tap for a labeled zone, picker only when unlabeled
All checks were successful
build-apk / build (push) Successful in 32m30s
A zone labeled free_2h/3h/4h checks in straight at that limit (no prompt); an
unlabeled zone shows the 2h/3h/4h picker; pay-immediate zones show no check-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 22:10:42 +00:00
4a5660e7e1 v0.4.0: Anonymous Mode + zone-location mirror; free check-in on any zone
Some checks failed
build-apk / build (push) Failing after 1h56m52s
Anonymous Mode — "Park without signing in" on the login screen (with a popup of
what works vs needs a login). Anonymous users browse parking areas from our
mirror, see labels, and start free check-in timers; paying, sessions, and
account screens prompt to sign in. AuthContext gains an 'anonymous' status +
enterAnonymous/requireLogin.

Zone mirror — server gains a `zones` table + public GET /api/zones and admin
POST /api/zones/sync. Signed-in admins push the zones they pull (authed) from
ParkSmarter after each map search, so anonymous users can read areas without a
ParkSmarter login. Map/Scan read the mirror when anonymous.

Also: the free "Check in" button is now always available with a 2h/3h/4h picker
(no longer gated on a prior label) — fixes "couldn't start a timer on a free
zone". CORS probe confirmed ParkSmarter allows any origin but only Content-Type,
so a future PWA can't auth to it — the mirror is what makes anonymous browsing
(and a PWA) possible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-03 21:38:32 +00:00
463facbe5a v0.3.0: local free check-in with actionable countdown notification (Phase C)
All checks were successful
build-apk / build (push) Successful in 40m22s
For a zone labeled free_2h/3h/4h, "Check in (free · Xh)" starts a local, API-free
countdown to the free limit — no ParkSmarter call. The bbp-notify module gains a
showCheckin() that posts the ticking chronometer with "End" / "Pay" buttons, a
BbpActionReceiver (declared in the module's new AndroidManifest.xml) that records
the tap in SharedPreferences and, for Pay, relaunches the app; consumePendingAction()
is drained on foreground by useCheckinSync() — End clears the check-in, Pay hands
off to StartSession for the stored zone. A pre-expiry alert reuses the existing
reminder lead-minutes. Starting a paid session supersedes an active check-in.

Also: MeterDetail shows the free/pay-immediate label and (admins) labeling chips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:23:58 +00:00
eeaf09ea0e app: zone-labels client + admin labeling UX (Phase B) + default map = downtown Sandpoint
- zoneLabels client (cached fetch wrapper) hitting bigbrainparking.mowden.top;
  adminStore keeps the admin password in secure-store.
- Admin screen (verify + save password) reachable from Account → Admin.
- MeterDetail shows a label badge and, for admins, 2h/3h/4h/Pay-now/Clear
  labeling controls that PUT/DELETE to the API.
- app.json extra.zoneLabelsApiUrl.
- Map default view is now downtown Sandpoint (48.2766,-116.5533 @ z14) instead
  of the continental-US fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:18:35 +00:00
c14081d85f server: zone-labels API (Fastify + SQLite) — Phase A
New server/ service classifying zones as free_2h/3h/4h street vs pay_immediate
lots. Public reads; writes require the admin bearer token (timing-safe compare).
@fastify/rate-limit (120/min global, 20/min writes), manual IP denylist +
auto-block on repeated auth failures. SQLite via better-sqlite3. Dockerfile +
compose (loopback-only, mem/cpu capped) + nginx block + README. 9 tests pass.

Deployed live at https://bigbrainparking.mowden.top (behind nginx + certbot).
Not an npm workspace — kept out of the app/CI install to avoid the native dep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 18:09:03 +00:00
2434879804 docs: RE report + correct the privacy writeup from live analysis
Adds OFFICIAL_APP_REVERSE_ENGINEERING.md — a technical teardown (methodology,
auth model, device-ID, GPS, Instabug) with mermaid diagrams and the mitmproxy
capture timeline.

Corrects OFFICIAL_APP_PRIVACY.md, which was wrong on two counts:
- IMEI: the app reads no IMEI; it sends the Android SSAID to /api/Device in a
  field misleadingly named IMEINumber.
- Trackers: Segment/Amplitude/Firebase-Analytics/Sentry/ad-ID are all ABSENT;
  the real telemetry SDK is Instabug (session replay, screenshots, network logs).
Also folds in the confirmed GPS auto-send (with the GrapheneOS caveat).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 19:11:26 +00:00
2bdb59b49a v0.2.6: fix kiosk-code scan resolution + add zone QR asset
All checks were successful
build-apk / build (push) Successful in 30m18s
ScanScreen.lookupCode tried getMetersByScannerCode first and let its thrown
error-envelope abort the whole lookup, so the getMetersByZoneName fallback never
ran; it also sent ZoneName in the scanned case (the API is case-sensitive — the
official app lowercases). Confirmed via live mitmproxy capture of the official
app: a scanned code resolves through ScannerCode, and ZoneName=dl (lowercase)
returns the zone.

Now tries ScannerCode -> ZoneName -> zonename(lowercased) -> TerminalSerNo, each
independent and non-fatal; only reports "Lookup failed" if every attempt errored.

Adds docs/zone-qr/ with the DL-Sandpoint QR (encodes ScannerCode "DL", works in
both BigBrainParking and the official app) and a how-to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 19:08:49 +00:00
deb78bf952 v0.2.5: parse live-session date format so the countdown actually posts
All checks were successful
build-apk / build (push) Successful in 36m28s
The active-session endpoint (/api/ParkingSession) returns EndTime as
"MM/DD/YYYY HH:MM:SS" (slashes, 24h, seconds), but parseApiTime only matched
the past-session "MM-DD-YYYY hh:mm AM/PM" form. Hermes' Date() can't parse
either, so live sessions parsed to null, got filtered out, and
refreshSessionStatus cleared the notification instead of posting it (log
showed "active sessions raw" but never "show native=").

parseApiTime now accepts both: - or / separators, optional seconds, optional
AM/PM (24h when absent). Added a "[STATUS] parsed N future-end session(s)" log.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:49:34 +00:00
2b973348bc v0.2.4: fix countdown notification not appearing + wrong time-remaining
All checks were successful
build-apk / build (push) Successful in 28m23s
Countdown notification "nothing appears" on session start:
- The small icon was the adaptive launcher mipmap (applicationInfo.icon),
  which Android 13+/GrapheneOS rejects as an invalid notification small icon
  and drops the post silently. Ship a proper white-on-transparent vector
  small icon (bbp_stat_parking) inside the module and use it.
- showCountdown now returns a diagnostic string (posted / notifications
  disabled / exception) instead of swallowing failures; JS logs the branch,
  permission result, and native outcome to the in-app diagnostics log.
- Log the raw active-session payload in refreshSessionStatus (element shape
  was previously unconfirmed).

Time remaining showed "7694 min":
- API's TimeRemaining is SECONDS, not minutes. Format it as hours+minutes
  in SessionDetail; annotate the type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:55:45 +00:00
9323bbd6c6 chore: untrack + gitignore local module gradle build output
All checks were successful
build-apk / build (push) Successful in 10m58s
Accidentally committed modules/bbp-notify/android/build/ from a local compile
check; remove it and ignore it going forward.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:05:09 -07:00
d0f31d5278 v0.2.3: true ticking countdown via a tiny native module (no deps)
Some checks failed
build-apk / build (push) Has been cancelled
Replaces the static status notification with a native Android chronometer that
counts DOWN to the session end — the system ticks it every second with zero app
CPU/battery, visible without opening the app.

- modules/bbp-notify: a ~50-line local Expo module using only NotificationCompat
  (setWhen + setUsesChronometer + setChronometerCountDown + ongoing). Zero
  third-party dependencies, no Firebase — fits the de-Googled ethos. Verified:
  autolinks + Kotlin compiles.
- sessionStatus.ts uses it when present, else falls back to the expo-notifications
  static expiry-time notification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:03:53 -07:00
b9bd6bd1c6 v0.2.2: active-session "time left" status notification (default on)
Some checks failed
build-apk / build (push) Has been cancelled
Starting a session posts an ongoing notification showing the expiry time + time
remaining, so you can glance at how long you have left without opening the app.
- Default ON; toggle in Account -> Notifications ("Active-session countdown").
- Kept fresh on app open / foreground (re-posts nearest-expiring active session,
  clears when none remain). Quiet channel, no banner/sound on refresh.
- Purely local (expo-notifications); no server/push.

Note: expo-notifications can't render a live per-second ticking countdown in the
background, so it shows the exact expiry time (always accurate) + remaining, and
refreshes on foreground. A true ticking chronometer would need notifee (native).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:46:39 -07:00
73079ca659 docs: show the big-brain-driver icon at the top of the README
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:35:48 -07:00
b1620ed19e v0.2.1: big-brain-driver app icon
All checks were successful
build-apk / build (push) Successful in 13m50s
New launcher icon: the big-brain guy driving with his brain filling the back seat.
- icon.png: full-frame artwork (1024x1024).
- adaptive-icon.png: car scaled into the central safe zone so the Android
  circle/squircle mask doesn't crop it; adaptive background set to white.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 20:33:15 -07:00
8a5408d193 docs: plain-language privacy analysis of the official ParkSmarter app
docs/OFFICIAL_APP_PRIVACY.md — readable-by-anyone comparison based on the official
app's manifest + decompiled bundle (v4.4.0): auto-sends GPS to IPS on map open;
bundles Segment/Amplitude/Firebase/Sentry + ad-ID + install-referrer; can send
IMEI; but NO background location. Contrasts with BigBrainParking (explicit-only
location, zero analytics). Confidence levels labeled. Linked from README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:49:16 -07:00
f10dbbc45c privacy: remove personal test coordinate from committed docs/tests
The Sandpoint lat/lng from .creds.json had been baked into the API-doc example and
the integration test. Doc now uses a rounded ~downtown point; the test reads the
coordinate from the gitignored .creds.json (rounded, non-personal fallback).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:35:33 -07:00
0a6ca53c04 v0.2.0: never auto-send GPS; "Last lot" uses last session; About page rework
All checks were successful
build-apk / build (push) Successful in 11m47s
Location privacy (hard invariant): the device GPS is sent to the API ONLY when
the user taps "My location" and searches.
- Map opens on your LAST session's parking lot (the meter's coordinate from
  history), not your GPS.
- "Last" -> "Last lot": centers on the last session's lot and searches there
  (~couple-mile view) — no GPS.
- "Search this area" falls back to the tracked viewport center, never the GPS fix.

About page: lead with a BigBrainParking open-source blurb + repo link (was showing
ParkSmarter's own About text first); privacy wording updated to match; drop the
now-unused getAbout fetch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:29:13 -07:00
70047b3964 docs: add ParkSmarter API usage guide + endpoint reference
- docs/PARKSMARTER_API.md: narrative guides (bootstrap/auth, finding parking,
  estimates, initiating a transaction, sessions/receipts, account) + per-endpoint
  reference + a behaviors/gotchas table (200-on-error, stale-token empty body,
  multi-estimate fallback, free windows, flat-rate zones, no free check-in).
- client README: link the guide; promote PastSession/ParkingReceipt to CONFIRMED
  (ActiveSession still pending — no active session at capture time).
- Main README: mark "Start a paid session" as working (confirmed live end-to-end).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 19:25:54 -07:00
8a2a87a1a9 v0.1.9: About page — plain-English "what's sent to IPS" + privacy blurb
All checks were successful
build-apk / build (push) Successful in 16m49s
Documents exactly what reaches ParkSmarter's API (sign-in, account, a single
map-search coordinate, session-start details) and what never does: device GPS is
used only on-device to position the map and cached locally — never attached to
sign-in/sessions/vehicles/payments; no background location, no IMEI/device-id/ad
tracking, no Google services. Audited against the source: the only endpoint that
sends a coordinate is getMetersByLocation, called solely from map searches with
the map's centered point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:48:33 -07:00
26b6a3ea82 CI: pre-install NDK+CMake with retry (SDK mirror served a corrupt CMake zip)
All checks were successful
build-apk / build (push) Successful in 10m37s
v0.1.7 build failed on ':react-native-screens:configureCMake' —
ZipException "Archive is not a ZIP archive" from a truncated cmake;3.22.1
download. Install ndk+cmake up front and retry up to 3x, clearing the partial
dir between attempts so a bad download self-heals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:42:56 -07:00
4cb5478737 v0.1.8: tap a session for full detail + receipt
- Sessions list items are now tappable -> SessionDetailScreen.
- Past sessions fetch the full receipt (GET /api/ParkingReceipt): vehicle,
  start/end, time purchased, parking/fee/total, payment (MASTERCARD ••1234),
  auth code, meter, city, transaction id. Adds an "Email me this receipt" action.
- Active sessions show zone/space/start/ends/time-remaining.
- Confirm PastSession + ParkingReceipt types from a live DL-zone session (were
  UNCONFIRMED); fix past-session card to show Zone/Description, not blank.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 18:38:39 -07:00
42a96b0cff v0.1.7: collapse flat-rate zones to one option; add DL capture script
Some checks failed
build-apk / build (push) Failing after 8m10s
- Start-session: when the single-estimate fallback finds the same price at the
  min and max duration (a flat-rate zone like DL), show ONE option — "$X flat,
  parked until <end>" — instead of a wall of identical-price rungs. No reason to
  offer shorter durations when the price doesn't change.
- capture-dl.mjs: dumps the DL zone's live requests+responses (tokens redacted)
  to ~/Downloads/dl-capture/ for a durable record.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:58:49 -07:00
65fbd86644 v0.1.6: validate the session on open, not mid-map
All checks were successful
build-apk / build (push) Successful in 38m15s
A stale/expired token doesn't 401 — /api/User returns an empty body, so
getUserDetail() resolved to undefined without throwing and the launch probe set
signedIn anyway. The dead session then only surfaced when the map/kiosk calls
ran. Now the launch check requires real user data (PersonalPhone/Email); if it's
missing it clears the token and shows the login screen at open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 10:46:04 -07:00
7212badd2d v0.1.5: detect free windows in the estimate fallback + add API tests
All checks were successful
build-apk / build (push) Successful in 49m41s
App:
- When the single-estimate fallback sees MaxTime 0 (or an all-$0.00 ladder),
  show "Parking is currently free" instead of a bogus $0.00 purchase ladder.
  Free-detection extracted to tested client helpers (isFreeEstimate/ladderAllFree).

Client + tests (node:test, zero deps):
- src/estimates.ts: pure isFreeEstimate/ladderAllFree helpers (exported).
- test/estimates + test/http (mocked fetch): headers, query serialization,
  error->ParkSmarterApiError, rolling Auth_Token refresh, 401 clears token.
- test/integration: live checks against the Sandpoint (DSB) zones; targets the
  `test` instance by default and SKIPS when it's unreachable (non-prod is
  IP-restricted -> 403). Verified 3/3 green against prodv2 read-only.
- CI runs `npm test` as a build gate.

Note: dev/stage/test instances return 403 "Web App - Unavailable" from the
public internet (IP-restricted). API schema is otherwise unchanged vs Jul 6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 20:42:58 -07:00
f8d9e2970c v0.1.4: fix silent failure on zones that reject ParkingEstimateMulti
All checks were successful
build-apk / build (push) Successful in 14m27s
Some zones (e.g. short-term "DL" spaces at the downtown lot) return
Response.Status:Error "Unable to process your request" for ParkingEstimateMulti
even though the single-duration ParkingEstimate works. The screen swallowed that
error -> empty duration list, disabled Pay button, no message ("nothing happens").

- Fall back to a single-estimate ladder across the zone's Min/Max time so these
  zones are purchasable (verified live: flat ~$0.10 up to ~33 min).
- Surface estimate errors with a message + Try again, instead of a blank screen.
- Log the estimate fallback/failure to the on-device diagnostics log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 16:31:22 -07:00
30f32bc494 CI: force IPv4 on ALL build JVMs via JAVA_TOOL_OPTIONS
All checks were successful
build-apk / build (push) Successful in 16m15s
The refactor dropped the command-line -Djava.net.preferIPv4Stack from gradlew, so
artifact-transform/worker JVMs reverted to IPv6 and couldn't reach dl.google.com
("Network is unreachable"). JAVA_TOOL_OPTIONS is inherited by every spawned JVM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 12:47:36 -07:00
626e46a55f v0.1.3: version bump for a clean CI trigger on the cached runner
Some checks failed
build-apk / build (push) Failing after 21m0s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 12:22:19 -07:00
ba3ae8b3e6 CI: dedicated cached runner for this repo (deploy script + volume caches)
- scripts/deploy-runner.sh: compose-managed BigBrainParking Forgejo runner
  (adapted from the Camp Scan one), with valid_volumes for bigbrainparking-gradle
  and bigbrainparking-npm. No android-sdk volume — the RN build image ships it.
- Workflow mounts those caches (GRADLE_USER_HOME + npm cache) and enables the
  Gradle build cache (org.gradle.caching + --build-cache), so repeat builds skip
  the dep re-download and recompile that dominate a cold ~35 min build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:05:35 -07:00
208cddee33 CI: cap Kotlin daemon heap (the real memory hog) + container swap off
Some checks are pending
build-apk / build (push) Waiting to run
Server report (20GB/4-core box) showed 3 Kotlin daemons at -Xmx5376m each + clang
swarm ~9GB thrashing swap. Cap kotlin.daemon.jvmargs and org.gradle.jvmargs to
1536m in gradle.properties, workers.max=2, no parallel. Container: --cpus=2
--memory=8g with swap disabled (--memory-swap==--memory) so it OOM-fails instead
of thrashing disk. Build now uses ~5GB.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:40:39 -07:00
0b8713790b CI: cap build resources so it can't starve the host
Container hard limits: --cpus=2 --memory=6g (Docker-enforced regardless of Gradle).
Gradle: -Xmx3g heap, --max-workers=2, no parallel project builds. Adjust the
--cpus/--memory to the server's size.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:37:20 -07:00
870da6f432 v0.1.2: theme Favorites screen (dark mode); versionCode 2
Some checks failed
build-apk / build (push) Failing after 1h1m11s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:31:48 -07:00
5d269bf2a3 Fix release signing in CI: anchor plugin regex to buildTypes release block
All checks were successful
build-apk / build (push) Successful in 35m3s
The signing plugin added a release{} inside signingConfigs{}, so the debug-line
replacement matched that block instead of the release BUILD TYPE — CI produced
debug-signed APKs (would break Obtainium updates vs the release-signed v0.1.0).
Anchor on `buildTypes { ... release {`. Verified locally: release buildType now
uses the release key when BBP_UPLOAD_* is set. Also unify keystore path via
github.workspace in both steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:49:37 -07:00
7970678b90 CI: force IPv4 for Gradle (runner container lacks IPv6 -> dl.google.com unreachable)
Some checks failed
build-apk / build (push) Failing after 53m53s
react-native-edge-to-edge's buildscript needs com.android.tools.build:gradle:8.1.1
from Google Maven; the runner's container has no IPv6 route so the AAAA lookup was
"Network is unreachable". Pass -Djava.net.preferIPv4Stack=true to Gradle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 15:34:47 -07:00
3d1505d2e7 CI: move workflow to .forgejo/workflows; self-contained Forgejo release; bump 0.1.1
Some checks failed
build-apk / build (push) Failing after 10m45s
- .forgejo/workflows/build-apk.yml (Forgejo's location): on v* tag, build signed
  APK using the Actions secrets and publish via the Forgejo API (curl + auto token),
  no external action. Best-effort NDK install for the RN version.
- Remove the old .gitea workflow to avoid double runs.
- Bump app version to 0.1.1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:18:06 -07:00
02e2c750ea Notifications: local-only session reminders; drop the FCM-bridge framing
- Session-expiry reminders are purely on-device (AlarmManager); added an Android
  notification channel and a "Send a test reminder" button (Account > Notifications)
  to verify delivery without a real session
- Strip all FCM/bridge talk from unifiedPush.ts + README; UnifiedPush(ntfy) is now
  an optional no-op stub for hypothetical future server-initiated messages only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:14:34 -07:00
0bc9aa97ba docs: make the Obtainium add link a real clickable link
Forgejo keeps obtainium:// URLs in markdown links (verified via the render API) but
not as bare autolinks — so use [text](obtainium://…) syntax in INSTALL.md and README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:02:28 -07:00
a8ba1fcae4 Release signing fix + Obtainium install guide + publish script
Some checks failed
build-apk / build (push) Failing after 5m9s
- Fix withReleaseSigning: RN template's unconditional debug signingConfig was
  overriding the release key. Now uses the release key when BBP_UPLOAD_* is set
  (verified: APK signed CN=BigBrainParking), else debug.
- INSTALL.md: dead-simple Obtainium setup (one-tap obtainium:// link + manual
  Forgejo-source steps). README links it.
- scripts/publish-release.sh: create a Forgejo release + upload the signed APK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:27:19 -07:00
a5bfec894e Fix "$652.89/hr" on Variable-rate meters (display only)
Waterfront zones (BS/WB/RV/PB/CB) use RateType "Variable" with a placeholder
Rate 652.89 ("Variable Rate - See signs"), not an hourly price. Only render $/hr
for the real "Hour" rate type; show "Variable — see signs" otherwise, and derive
the headline Rate from the current policy instead of the raw zone Rate ("NA"/652.89).
Actual quoting is unaffected — the estimate ladder returns sane per-duration prices.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:17:46 -07:00
d1473f9776 Scanner: parse processQR URLs, manual Zone ID entry, zone-name fallback
Kiosk QR decoded to a bare https://www.parksmarter.com/home/processQR (no zone) —
even the official app rejects code-less URL QRs. Now: parse processQR?code= URLs,
detect bare app-link QRs and prompt for the printed Zone ID, and add manual Zone ID
entry. Lookups try getMetersByScannerCode then getMetersByZoneName (DSB13 maps to
zone 113150). Fixes scanning real stickers, which previously passed the URL to the API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:38:38 -07:00
1c597ec471 Detect declined sessions (200 + Status:Error); theme Sessions screen
- startParkingSession: server returns HTTP 200 even on a declined charge — now
  detect Response.Status:"Error" and throw the gateway reason (e.g. "DECLINED")
  so the app shows the failure instead of a false "Parked!"
- Confirmed the session request format is correct (server processed it); expand
  StartParkingSessionResponse with the real fields (OriginalErrorMessage, etc.)
- Theme SessionsScreen (was black-on-black in dark mode)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:25:14 -07:00
7ec7ae9200 Add on-device debug logging: file capture + share (for real-session testing)
- fileLogger: append redacted API requests/responses + app events to an on-device
  log file (persists across restarts); the client's logSink now writes to it
- Diagnostics screen (Account > Diagnostics): toggle recording, Share log file
  (expo-sharing), clear. StartSession logs the session attempt/result explicitly.
- Lets a real parking session be captured while away from a computer, then shared.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:59:41 -07:00
80812d25cf StartSession: current-policy awareness (Free / No Parking / paid)
Read the CurrentSlot policy: when the meter's current time-of-day window is Free
or No Parking, show that status instead of a purchase flow (matches the official
app). Confirmed via API: DSB3 9AM-5PM is RateType "Hour" $4/hr with no free grace;
the "2 hours free" is a city grace period outside the ParkSmarter payment system.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:48:35 -07:00
f044a9a511 Wire up start-session flow (variable-rate aware) + fix About dark mode
- New StartSession screen off MeterDetail: pick vehicle -> duration (from the live
  multi-estimate ladder, exact cost per option) -> card; detects a free prefix and
  shows "Free until X, then charged"; explicit Pay $X confirmation before charging
  via postStartParkingSession; schedules the local expiry reminder on success
- About screen themed (was black-on-dark in dark mode)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:39:19 -07:00
9a5c9d445f Add Notifications preferences (session-expiry reminder lead time)
- New Notifications screen (Account > Notifications): enable/disable expiry
  reminders + a 5-minute-increment stepper (5–60 min), default 15 min
- localReminders.scheduleExpiryReminder now reads the persisted lead time + enabled
  flag, so it's ready to fire once the start-session flow is wired

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:33:17 -07:00
a566f41d66 Remove [CAM] debug logging; default HTTP request logging off
Camera recenter-on-search confirmed fixed (view-lock). Strip the temporary trace
logs; keep viewport tracking (needed for the fix). HTTP request logging now defaults
off — still toggleable in Account > Log requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 11:27:14 -07:00