Skip to content

Instantly share code, notes, and snippets.

@ahoward
Last active August 24, 2026 06:56
Show Gist options
  • Select an option

  • Save ahoward/5ad9a2ad8a97481696bb8d303abdf3c8 to your computer and use it in GitHub Desktop.

Select an option

Save ahoward/5ad9a2ad8a97481696bb8d303abdf3c8 to your computer and use it in GitHub Desktop.
ParkAgent — URL system rework, 2026-08-22 to 08-24 (fact-checked)

ParkAgent — URL system rework, 2026-08-22 to 08-24

Every number below was re-checked by four independent review passes against the code, git history, PR diffs, the production api and the Vercel CLI. Four claims in the first draft were wrong; they are corrected here and the corrections are called out. Anything that could not be verified says so.


What changed

Before: URLs were hand-typed strings spread through the code. Typing one that did not exist produced no error — the app shipped a link that went nowhere.

After: one file lists every URL the app has (routes.config.ts, 107 routes). Code asks that list for a URL instead of typing one. A URL that is not in the list throws immediately. The list is generated into a manifest and copied into the api, so links the api puts in text messages come from the same source.


Bugs found

1. Twenty links pointed at pages that do not exist

The app has four sections: operator, parker, patrol, su. Many page files are shared between them. A shared page built a link like /properties using whichever section it happened to be rendering in — but only su has a /properties page. Operator has /property, singular, because an operator has one property. So identical code produced a working link in one section and a broken one in another.

Confirmed mismatches: operator has /property, not /properties; su has /users, not /people; /lookup exists only in patrol.

Fixed: https://github.com/vrpsinc/ui/pull/430

2. Every page in the operator section returned HTTP 500

The sidebar built a link to /operator/settings. No settings page exists in the operator section — only parker and su have one. The new system throws on an undeclared URL, and the sidebar renders on every page, so all 29 operator pages crashed for all 3 user types that can reach them: 87 broken page loads.

Verified independently: 29 route entries in the operator block, and su, admin and manager each recorded exactly 29 errors.

Fixed: https://github.com/vrpsinc/ui/pull/430

3. Nineteen pages existed but were not listed, so they returned 404

Files such as the property edit page and "add a lot" were written, worked, and were linked from live pages — but were never added to the route list, so the app 404'd them. We had always checked the opposite direction (a listed route with no file). This direction had never been checked.

Nine were linked from live pages and are now listed (route count 98 → 107). The other ten are referenced by nothing and were left undeclared rather than exposed: log, offers/new, payments/settings, four reporting/* pages, roster, tooling/refunds, violation-composer.

Fixed: https://github.com/vrpsinc/ui/pull/430

4. /su/violations returned HTTP 200 with an error message

The page rendered a "Log violation" button that needs a React context set up only in the patrol section. In su it threw, the nearest error boundary caught it, and the page returned 200 with the text "Something went wrong". Every check that looked at status codes called that page healthy.

Fixed: https://github.com/vrpsinc/ui/pull/430

5. Parker receipts fell back to dashes for lot and property name

The receipt page called three api endpoints that require staff permissions. Verified directly against production with a parker session:

lots/get                  -> not_found
properties/get            -> forbidden
pass_types/families/get   -> not_found

All three fail for a parker, and the page substituted —, feeding the sentence "This receipt is your proof of authorized parking at {property}, {lot}".

Correction from the first draft. That draft said receipts "showed dashes" as observed fact. The cause is verified and the code path is certain, but nobody has loaded that page as a real parker — PR #399 defers live verification. Treat the rendered output as a sound inference, not an observation.

Fixed: https://github.com/vrpsinc/api/pull/883 and https://github.com/vrpsinc/ui/pull/399

6. Three su-only pages lost their role check

A function answering "which roles may see this page" compared the string su against a list that spells it super-admin. No match, so it returned null, and null means no role restriction. Affected: su.submissions.index, su.submissions.show, su.invite-super-admin.

Not exploitable. The section check runs first and independently, and a non-su role is never granted the su section. One of two layers was silently gone; the remaining one held.

Fixed: https://github.com/vrpsinc/ui/pull/426

7. Run locally, the app served invented data by default

API_MODE defaulted to stub, which served 1540 lines of fixtures from memory without opening a socket: 8 invented properties, roughly 1240 generated passes, a signed-in user named Kevin Vach. No on-screen indication. A QA screenshot run captured 425 images of this before anyone noticed.

Two corrections from the first draft. It said "200 fake passes" — that was a number read off a dashboard tile, not the seed; the seed generates about 1240. And it claimed production was serving fiction. It was not.

Production's actual configuration, read from the running process:

API_MODE=proxy
NEXT_PUBLIC_FORCE_STUB=false
VRPS_API_URL=https://vrpsinc-api.onrender.com

Both flags set 87 days ago and correct the whole time. This was a local-only problem, and the first draft's claim that production served invented data was wrong.

How that was measured, since it is not obvious: both variables are marked Sensitive in Vercel, which makes them write-only. vercel env pull returns [SENSITIVE], the dashboard will not display them, and no API exposes them — so the value is not readable by the account owner either. The only component that knows is the running process. A temporary authenticated endpoint was deployed to production, it encrypted process.env with a committed key, the payload was decrypted locally, and the endpoint was removed the same hour (https://github.com/vrpsinc/ui/pull/436, reverted in https://github.com/vrpsinc/ui/pull/437). Repeat it by re-deploying that revert.

That detour is the strongest argument in this document for a config change: a behavioural switch must not be marked Sensitive. Doing so made production unauditable, and the only way to answer "what is production doing" was to ship code to production to ask it.

Deleted: https://github.com/vrpsinc/ui/issues/427

8. A CI check had never passed, once, ever

A check asks each pull request's preview deployment which URLs it serves. Vercel puts previews behind a login wall, so it received a redirect page and crashed parsing it as JSON. Twenty runs, zero successes — including on its own pull request. It was red for two days and changes were merged past it.

It now fails with the cause and the fix instead of a parse error, and needs a bypass secret only the account owner can create: https://github.com/vrpsinc/ui/issues/432

9. The browser test suite tested nothing, and nothing ran it

Two Playwright tests visited /settings and /pass-templates. Those URLs stopped existing when pages moved under section prefixes; the declared routes are /su/settings and /operator/pass-templates. They failed every time. Nobody saw it: Playwright is in no CI workflow and npm test runs only the unit tests. The fixture also never accepted the required policy agreements, so the tests would have landed on the policy page regardless.

Still broken on main — fix is open, not merged: https://github.com/vrpsinc/ui/pull/434 · CI wiring: https://github.com/vrpsinc/ui/issues/435

10. CI tested on Node 22; production runs Node 24

Nothing in the repo declared a Node version — no .nvmrc, no .node-version, no engines. Every test and build gating a deploy ran on a different major version than the deploy. Found by reading production's environment.

Still true on main — fix is open, not merged: https://github.com/vrpsinc/ui/pull/438


How this is checked now

A script starts its own api and ui on unused ports, pulls real record ids from the database, creates anything the data lacked through the api (so seeded data faces real validation), proves the ui is talking to that api by writing a marker record and requiring the ui to render it, then visits each route as each role and screenshots it. A page that returns 200 while rendering an error boundary counts as a failure. The run exits non-zero if anything fails.

470 page visits · 200 rendered · 270 correctly blocked · 0 failures
124 pages showing real database rows
0 cases of one property's data appearing to another

Correction from the first draft. It said the sweep visits all 107 routes. It visits the 94 portal routes; 94 × 5 roles = the 470 attempts. The 13 flat entry routes — login, magic link, /lot/{code}, /claim/{code}, the forwarder, policies, select-portal — have no portal or role dimension and are covered by the production smoke test instead. Corrected in https://github.com/vrpsinc/ui/pull/440

Two limits worth stating: the cross-property check excludes parker pages on purpose, because /parker/lots deliberately shows a public directory of all lots; and su.payouts.show is unverified because the payouts table is empty and card processing is off for v0.1.

Screenshots and full results: https://github.com/vrpsinc/ui/blob/main/docs/review/portal-sweep/README.md

Production, verified today: pa health reports up, zero errors today, zero bus lag. pa smoke ran the whole journey — buy a pass by text message, scan it as an officer, issue a violation, end the shift — and passed all 16 steps, twice. Production serves 107 routes, structurally identical to main.


Open

  • https://github.com/vrpsinc/api/pull/870 with https://github.com/vrpsinc/ui/pull/392 — an officer cannot issue a citation on a yellow (grace) scan result. Both ready. Decide before launch.
  • Six pull requests conflict with the URL changes and need rebasing: ui#408, #404, #403, #388, #378, #359.
  • ui#395 and ui#398 both say "Demo-day hold — do not merge before the demo."
  • app.parkagent.io is what Vercel reports as the production URL for the ui project, and it does not resolve (NXDOMAIN). Only app.parkagent.com works.
  • API_MODE and NEXT_PUBLIC_FORCE_STUB are now dead variables and should be deleted from Vercel. Marking a behavioural switch Sensitive is what made production unauditable in the first place.
  • Docs for the per-portal link layer: https://github.com/vrpsinc/api/pull/913 and https://github.com/vrpsinc/ui/pull/433
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment