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>
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>
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>
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>
- .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>
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>
- 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>
- 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>