v0.1.7: collapse flat-rate zones to one option; add DL capture script
Some checks failed
build-apk / build (push) Failing after 8m10s
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>
This commit is contained in:
parent
65fbd86644
commit
42a96b0cff
3 changed files with 132 additions and 37 deletions
|
|
@ -3,14 +3,14 @@
|
|||
"name": "BigBrainParking",
|
||||
"slug": "bigbrainparking",
|
||||
"scheme": "bigbrainparking",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"orientation": "portrait",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"icon": "./assets/icon.png",
|
||||
"android": {
|
||||
"package": "top.mowden.bigbrainparking",
|
||||
"versionCode": 6,
|
||||
"versionCode": 7,
|
||||
"edgeToEdgeEnabled": true,
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
|
|
|
|||
|
|
@ -45,9 +45,27 @@ const money = (v?: string | number) => `$${Number(v ?? 0).toFixed(2)}`;
|
|||
export interface SingleLadderResult {
|
||||
/** MaxTime 0 / all-$0.00 => currently free; show the free banner, not a ladder. */
|
||||
free: boolean;
|
||||
/** Flat-rate zone: one price for any duration, so we offer a single max option. */
|
||||
flat: boolean;
|
||||
ladder: ParkingDetail[];
|
||||
}
|
||||
|
||||
/** Price a single duration; returns a rung (Minutes overridden to the request) or null. */
|
||||
async function singleRung(
|
||||
base: { zoneId: number; spaceId: number; customerId: number; vehicleId: number },
|
||||
minutes: number,
|
||||
): Promise<ParkingDetail | null> {
|
||||
try {
|
||||
const r = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: minutes, creditCardId: 0 });
|
||||
const p = r.ParkingDetail;
|
||||
if (!p || (r as any)?.Response?.Status === 'Error') return null;
|
||||
// The single endpoint often echoes Minutes:0; trust the requested duration.
|
||||
return { ...p, Minutes: minutes } as ParkingDetail;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildSingleLadder(base: {
|
||||
zoneId: number;
|
||||
spaceId: number;
|
||||
|
|
@ -60,32 +78,31 @@ export async function buildSingleLadder(base: {
|
|||
}
|
||||
// MaxTime 0 means no paid time is available right now — this is a free window,
|
||||
// not a purchasable zone. Don't build a bogus $0.00 ladder.
|
||||
if (isFreeEstimate(probe)) return { free: true, ladder: [] };
|
||||
if (isFreeEstimate(probe)) return { free: true, flat: false, ladder: [] };
|
||||
|
||||
const min = Math.max(5, Number(probe.MinTime) || 5);
|
||||
const max = Math.max(min, Number(probe.MaxTime) || min);
|
||||
|
||||
// Price both ends first. If they match, it's a flat-rate zone — one price no
|
||||
// matter how long you stay — so there's no reason to pick a shorter duration;
|
||||
// offer a single option for the whole window (park to MaxTime).
|
||||
const [minRung, maxRung] = await Promise.all([singleRung(base, min), singleRung(base, max)]);
|
||||
if (maxRung && ladderAllFree([maxRung])) return { free: true, flat: false, ladder: [] };
|
||||
if (minRung && maxRung && Number(minRung.ParkingCost) === Number(maxRung.ParkingCost)) {
|
||||
return { free: false, flat: true, ladder: [maxRung] };
|
||||
}
|
||||
|
||||
// Graded rate: fill in intermediate durations between the two ends.
|
||||
const span = max - min;
|
||||
const step = span > 180 ? 30 : span > 60 ? 15 : span > 20 ? 10 : 5;
|
||||
const durs = new Set<number>();
|
||||
for (let d = min; d < max; d += step) durs.add(d);
|
||||
durs.add(max);
|
||||
const rungs = await Promise.all(
|
||||
[...durs].map(async (d) => {
|
||||
try {
|
||||
const r = await ps.getParkingEstimateSingle({ ...base, durationInMinutes: d, creditCardId: 0 });
|
||||
const p = r.ParkingDetail;
|
||||
if (!p || (r as any)?.Response?.Status === 'Error') return null;
|
||||
// The single endpoint often echoes Minutes:0; trust the requested duration.
|
||||
return { ...p, Minutes: d } as ParkingDetail;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const ladder = rungs.filter((r): r is ParkingDetail => r != null);
|
||||
// If every rung came back $0.00, it's effectively a free window too.
|
||||
if (ladderAllFree(ladder)) return { free: true, ladder: [] };
|
||||
return { free: false, ladder };
|
||||
const mids = new Set<number>();
|
||||
for (let d = min + step; d < max; d += step) mids.add(d);
|
||||
const midRungs = await Promise.all([...mids].map((d) => singleRung(base, d)));
|
||||
const ladder = [minRung, ...midRungs, maxRung]
|
||||
.filter((r): r is ParkingDetail => r != null)
|
||||
.sort((a, b) => Number(a.Minutes) - Number(b.Minutes));
|
||||
if (ladderAllFree(ladder)) return { free: true, flat: false, ladder: [] };
|
||||
return { free: false, flat: false, ladder };
|
||||
}
|
||||
|
||||
/** Parse the API's "MM-DD-YYYY hh:mm AM" end-time string into a Date for reminders. */
|
||||
|
|
@ -124,6 +141,8 @@ export function StartSessionScreen() {
|
|||
const [ladderError, setLadderError] = useState<string | null>(null);
|
||||
// Free detected from the estimate (MaxTime 0 / $0.00) rather than the policy.
|
||||
const [estimatedFree, setEstimatedFree] = useState(false);
|
||||
// Flat-rate zone: one price for the whole window (a single option, not a ladder).
|
||||
const [flatRate, setFlatRate] = useState(false);
|
||||
|
||||
// Load the account's vehicles + cards and pick the defaults.
|
||||
useEffect(() => {
|
||||
|
|
@ -147,6 +166,7 @@ export function StartSessionScreen() {
|
|||
setLoading(true);
|
||||
setLadderError(null);
|
||||
setEstimatedFree(false);
|
||||
setFlatRate(false);
|
||||
const base = {
|
||||
zoneId: zone.ZoneId!,
|
||||
spaceId: space.SpaceId!,
|
||||
|
|
@ -175,6 +195,7 @@ export function StartSessionScreen() {
|
|||
setEstimatedFree(true);
|
||||
return;
|
||||
}
|
||||
setFlatRate(fb.flat);
|
||||
details = fb.ladder;
|
||||
}
|
||||
if (!details.length) {
|
||||
|
|
@ -382,7 +403,15 @@ export function StartSessionScreen() {
|
|||
))}
|
||||
</View>
|
||||
|
||||
<Text style={[styles.label, { color: colors.subtext }]}>Duration</Text>
|
||||
<Text style={[styles.label, { color: colors.subtext }]}>
|
||||
{flatRate ? 'Flat rate' : 'Duration'}
|
||||
</Text>
|
||||
{flatRate ? (
|
||||
<Text style={{ color: colors.text, marginBottom: 4 }}>
|
||||
{money(selected?.ParkingCost)} flat — one price for the whole window
|
||||
{selected?.EndTime ? `, parked until ${selected.EndTime}` : ''}.
|
||||
</Text>
|
||||
) : (
|
||||
<FlatList
|
||||
horizontal
|
||||
data={ladder}
|
||||
|
|
@ -396,6 +425,7 @@ export function StartSessionScreen() {
|
|||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text style={[styles.label, { color: colors.subtext }]}>Card</Text>
|
||||
<View style={styles.chipRow}>
|
||||
|
|
|
|||
65
parksmarter-client/capture-dl.mjs
Normal file
65
parksmarter-client/capture-dl.mjs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* capture-dl.mjs — record the live requests + responses for the Sandpoint "DL"
|
||||
* zone (ZoneID 113165 / SpaceID 329522 / CustomerID 217) so we have a durable
|
||||
* record of its odd flat-rate behavior.
|
||||
*
|
||||
* Writes one JSON file per call (request line with tokens redacted by the client
|
||||
* + the full parsed response) to ~/Downloads/dl-capture/ plus a _summary.json.
|
||||
*
|
||||
* Usage: node capture-dl.mjs (needs ./.creds.json)
|
||||
*/
|
||||
import { readFileSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { ParkSmarterClient } from './dist/index.js';
|
||||
|
||||
const c = JSON.parse(readFileSync('./.creds.json', 'utf8'));
|
||||
const OUT = join(homedir(), 'Downloads', 'dl-capture');
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const reqLog = [];
|
||||
const ps = new ParkSmarterClient({
|
||||
environment: 'prodv2',
|
||||
timeoutMs: 20000,
|
||||
logRequests: true, // the client redacts Auth_Token/Application_Token/SessionId
|
||||
logSink: (line) => reqLog.push(line),
|
||||
});
|
||||
|
||||
await ps.loginWithPhone({ phoneNumber: c.phoneNumber, password: c.password });
|
||||
const me = await ps.getUserDetail();
|
||||
const vehicleId = me?.VehicleDetails?.[0]?.VehicleID;
|
||||
const base = { zoneId: 113165, spaceId: 329522, customerId: 217, vehicleId };
|
||||
|
||||
const save = (name, obj) => writeFileSync(join(OUT, `${name}.json`), JSON.stringify(obj, null, 2) + '\n');
|
||||
const summary = { zone: base, note: 'Sandpoint DL zone — flat $0.10, MaxTime = minutes until the paid-window boundary; ParkingEstimateMulti errors here.', calls: [] };
|
||||
|
||||
async function cap(name, fn) {
|
||||
const from = reqLog.length;
|
||||
let response = null, error = null;
|
||||
try { response = await fn(); } catch (e) { error = `${e.name} ${e.status ?? ''} ${e.message}`; }
|
||||
const entry = { name, capturedField: 'request+response', request: reqLog.slice(from), response, error };
|
||||
save(name, entry);
|
||||
summary.calls.push({ name, ok: !error, error });
|
||||
console.log(` ${name}: ${error ? 'ERR ' + error : 'ok'}`);
|
||||
}
|
||||
|
||||
console.log(`Capturing DL zone -> ${OUT}`);
|
||||
// The zone/space/policy record (search for DL by name).
|
||||
await cap('00-zone-DL', async () => {
|
||||
const byName = await ps.getMetersByZoneName('DL');
|
||||
const z = (byName?.Zones || []).find((z) => (z.ZoneId ?? z.ZoneID) == 113165) ?? byName?.Zones?.[0];
|
||||
return z ?? { note: 'DL zone not found by name lookup' };
|
||||
});
|
||||
// Single-estimate at a spread of durations (shows the flat $0.10 + moving MaxTime).
|
||||
for (const d of [5, 15, 30, 60, 120, 240, 480]) {
|
||||
await cap(`10-estimate-single-${String(d).padStart(3, '0')}min`, () =>
|
||||
ps.getParkingEstimateSingle({ ...base, durationInMinutes: d, creditCardId: 0 }),
|
||||
);
|
||||
}
|
||||
// The multi ladder (errors for this zone) and the item-based estimate.
|
||||
await cap('20-estimate-multi', () => ps.getParkingEstimateMulti(base));
|
||||
await cap('21-estimate-items', () => ps.getParkingEstimateItems(base));
|
||||
|
||||
summary.capturedAtLocalNote = 'timestamps are inside each request/response line';
|
||||
save('_summary', summary);
|
||||
console.log(`Done. ${summary.calls.length + 1} files in ${OUT}`);
|
||||
Loading…
Add table
Add a link
Reference in a new issue