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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
- .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>
- 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>
- 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>
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>
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>
- 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>
- 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>
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>
- 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>
- 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>
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>
The native MapLibre camera re-applies its last setCamera() stop on the search
re-render (confirmed via trace: revert to prior target with userInteraction=false).
Track the live viewport and, before searching the current area, setCamera() to the
current center (0ms) so the re-applied stop is a no-op instead of a jump-back.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- ROOT CAUSE: MapLibre Camera re-applies its last setCamera() stop on every
re-render, so a search (setZones) snapped the map back to the last programmatic
target. Memoize the Camera + UserLocation elements so search re-renders never
touch them; only the marker ShapeSource updates. (Confirmed via [CAM] trace.)
- Login: brain-car logo + full theming so text is readable in light/dark
- Meter policies: negative Rate is a sentinel (RateType No Parking=-1 / Free=-3),
not a price; show DisplayString + colored type dot, real $/hr only when positive.
Fix zone rate "$ 4.00" formatting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MeterDetail now themed (fixes unreadable black headings in dark mode)
- Rate no longer double-'$'; MaxTime 0 -> "Unlimited"; policies show a clean
title + time window and hide sentinel (<=0) rates
- Temporary [CAM] logging on the map to trace the persistent recenter-on-search
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
MarkerView (React overlays repositioned per frame) caused the pins to float and
the camera to snap back to the user on pan/zoom/tap. Meters are now a ShapeSource
+ CircleLayer (GPU-drawn, coordinate-anchored); tapping a circle opens its parking
flow via the source's native onPress. No React overlays = no floating, no camera thrash.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Markers are now compact colored pins (no long label bubbles); tapping one opens
the meter/parking flow for that zone
- UserLocation renderMode "native" -> "normal": the native location component was
tracking the camera back to the user on re-render / marker tap. Normal mode draws
the dot in the JS layer with no camera coupling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Client: server returns 200 + Status:"Error" on bad credentials; loginWith* now
throw LoginError with the server message instead of faking a signed-in state
- Map: uncontrolled camera positioned once on load + explicit actions only, so
Search/marker-tap/re-renders no longer snap back to the user's location
- Markers: always-dark high-contrast bubble + zone-colored dot (some zones report
a white BackgroundColor -> was white-on-white)
- Dark mode now switches map tiles to CARTO dark-matter
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Client: onUnauthorized hook; 401 clears the token and (in-app) bounces to sign-in
- Map: "My location" uses the native UserLocation fix + flyTo; safe-area insets on
the floating status bar / controls
- Enable edgeToEdgeEnabled (Android 15 forces it) so headers/back-arrow sit correctly
- App icon: brain-car launcher + adaptive icon
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- "Search this area" now searches the map's current center (getCenter), so it
works with no GPS fix — pan anywhere and search. Fixes "search here does nothing".
- "My location" recenters the camera on the GPS fix and searches there.
- Status bar shows real feedback (N meters / none found / errors) instead of silent.
- Add bottom tab icons via @expo/vector-icons (map/qr/star/time).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The '@/' import alias needs this plugin; debug builds hid the gap because
they bundle lazily via Metro. Release (standalone) bundling now succeeds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- About screen surfaces getAbout() content; tapping the title 7x reveals
assets/joelangel.png (placeholder committed; replace with the real image)
- Correct UnifiedPush package to react-native-unifiedpush-connector (lazy-required)
- Fix expo-notifications NotificationBehavior (shouldShowAlert)
- Reachable via an ⓘ header button on the main tabs
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reverse-engineered ParkSmarter API client (TypeScript, live-verified) plus a
de-Googled Expo/React Native app for GrapheneOS: MapLibre meter map with GPS,
VisionCamera QR kiosk scanning with save/share, local session-expiry reminders,
UnifiedPush wiring, and Gitea CI to publish signed APKs for Obtainium.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>