Run log: does the localhost default actually work? โ
- Date: 2026-08-30
- Issue:
#1002 - Branch:
feat/admin-and-merchant-portals, commit1d527b30 - Harness:
harness/probe-1002.mjs,harness/compare-errors-tab.mjs,harness/probe-query-run.mjs, headless Playwright chromium
What server was this run against? โ
My own admin portal at http://localhost:3013, stood up for this run and stopped after it. โ
- Started from
apps/adminwith the two flipped origins in its environment, exactly the values.env.local.examplenow ships:VENUE_API_ORIGIN=http://localhost:8080ANALYTICS_API_ORIGIN=http://localhost:8082
- Pointed at the venues-api and analytics-api that were ALREADY running (pids 25799 and 10036), both started by the operator from this checkout with
node --env-file=../../../.env.local src/index.js, so both serve current branch code. - The operator's stack was not touched. Her portal on
3001, web on5173, docs on5174, site on5175, Storybook on6006and the APIs on8080to8086were left running, unrestarted, unrebound.
The proxy target was confirmed at the network layer before any UI claim. โ
- Same request through both portals, reading response headers rather than status:
| Portal | GET /api/analytics/admin/bq-schema | Verdict |
|---|---|---|
Mine, :3013 | 401, x-powered-by: Express only | LOCAL analytics-api |
Hers, :3001 | 401, plus x-cloud-trace-context and server: Google Frontend | DEPLOYED Cloud Run |
GET /api/venues/admin/...gives the identical split, so the venues origin flips the same way.
What did the run show? โ
System Health uptime: 9 endpoints, not 4. That is the canonical proof. โ
- Capture:
screens/01-system-health-uptime.png - Rendered rows: 9. Payload summary:
{"total":9,"healthy":9,"degraded":0,"down":0}. Avg response 1923ms. - The nine: Main App, Admin Portal, Venue API, Docs API, Analytics API, Lanterns API, Auth API, Merchants API, Assistant API.
- The A/B is on one origin, one session, one browser. Restarting the same portal on
:3013withANALYTICS_API_ORIGINpointed back at Cloud Run gave 4 endpoints in the payload (screens/08-errors-tab-deployed-analytics.png). Nothing else changed between the two reads, so the endpoint count is the origin and nothing else.
System Health errors: the one thing that degrades locally, and it names its own cause. โ
- Capture:
screens/02-system-health-errors.png - Server Errors renders: "Cloud Logging returned 403. Reading Cloud Logging needs roles/logging.viewer on the runtime service account."
- That is a handled state, not a crash:
getServerErrorsMetricsinservices/api/analytics/src/services/systemHealth.service.jsreturnsnot_availableon a 403 by design, and its own comment predicts exactly this. - The local service authenticates as
firebase-adminsdk-fbsvc@lantern-app-dev.iam.gserviceaccount.com(fromGOOGLE_APPLICATION_CREDENTIALSin.env.local), which does not holdroles/logging.viewer. - Against the deployed origin the panel does not appear at all, because the Server Errors section is branch code the deployed service predates. So this run cannot compare local against deployed for the same code, only local against nothing.
- Not verified here: whether the DEPLOYED runtime service account holds
roles/logging.viewer.gcloudis not on the PATH in this session, so the IAM check could not be run.
Every BigQuery-backed surface worked against the local service. โ
| Surface | Route | What came back |
|---|---|---|
| Analytics configurations | /admin/analytics/config | 19 table rows of live BigQuery metadata, analytics.events partitioned on timestamp, 90 day expiry, clustered event_name, user_id, partition filter REQUIRED (capture) |
| BigQuery export status | /admin/analytics/bigquery/export | bq-export-status?limit=30 200 (capture) |
| Query Console, schema | /admin/analytics/query-console | 9 datasets, 25 tables (capture) |
| Query Console, a real job | same | SELECT COUNT(*) FROM analytics.event_counts_daily returned 403 rows, job 0 ms, 0 B, cache miss (capture) |
| Venue Activity | /admin/venues/activity | 22,636 total venues, 17 lanterns lit, 11 waves sent, 6 active users, top-venues chart drawn (capture) |
- The Query Console row is the one that matters for the permission question: creating a BigQuery job is the privilege most likely to be missing locally, and it is not missing.
- This clears the block recorded on 2026-08-28 in
docs/engineering/testing/runs/analytics-config-and-reports/README.md, where scenarios 1 to 4 were blocked because the portal read the deployed service. The retention table renders from live metadata on the local origin.
Venues rendered, but the admin portal barely uses venues-api. โ
- Capture:
screens/07-venues-list.png, 200 venues listed. - Those reads come from client Firestore. In
apps/adminthe only calls to venues-api arecreateVenueandupdateVenue(apps/admin/src/shared/lib/venuesApi.js), both writes. - Deliberately not exercised: driving one would write to shared dev Firestore while the operator is reviewing the venues surfaces. The origin flip is proven for venues at the proxy layer instead (table above), and the local service account's Firestore access is already demonstrated by the local auth-api answering 200 on every admin profile call in this run.
- The bigger consumer of
VENUE_API_ORIGINisapps/web, whose Vite config proxies the same variable.
What could not be exercised, and why โ
The schema viewer and the Report Builder are not routed in the app on this branch. โ
apps/admin/src/admin/analytics/schema/SchemaViewer.jsxand.../reports/ReportBuilder.jsxare imported by nothing except their own stories and tests.- So there is no URL to visit and no browser pass to run on them yet. The nearest live equivalents are covered above: the Query Console renders the same
bq-schemapayload the schema viewer will consume, and it ran a real query job.
What else the run surfaced โ
DOCS_API_ORIGIN is in the Vite proxy but in neither .env.local nor .env.local.example. โ
- With it unset there is no
/api/docsproxy entry, sodocsApicalls the Cloud Run URL straight from the browser, which is the exact failure the proxy comment inapps/admin/vite.config.mjsdescribes. - In this run it showed as CORS failures on
docs-api-...run.app. That part is an artifact of my port:ALLOWED_ORIGINSinpackages/shared/services/index.jslistslocalhost:3001and3002, not3013. Her portal is allowlisted; a throwaway port is not. - Left alone as out of scope for #1002, worth a follow-up.
Two sign-in attempts were spent, and the first one failed for a reason worth writing down. โ
AGENT_PROBE_PASSWORDis refused by the portal with "Incorrect email or password".AGENT_PROBE_PORTAL_PASSWORDis the one the admin tier accepts.- Every later run reused a Playwright persistent profile, so they cost zero further attempts.
The App Check debug-token exchange 403s on a local dev build, harmlessly. โ
- Known behaviour: the dev build mints an unregistered token. Every API call still returned 200 because the local services run with
APP_CHECK_DISABLED=truefrom.env.local.
Who applied the operator's own .env.local, and why not me โ
APPLIED 2026-08-30 by the coordinator, on her explicit permission. Both lines now read http://localhost:8080 and http://localhost:8082 in her file. โ
This lane deliberately did not touch it, because the edit is not inert. โ
- The task expected this edit to be inert until the next dev-server restart. It is not. Vite watches the env files and restarts the server when one changes:
node_modules/vite/dist/node/chunks/node.jsaddsgetEnvFilesForMode(...)to the watcher, andhandleHMRUpdatecallsrestartServerWithUrlson a match. - Editing
.env.localwould therefore have restarted her admin portal on3001and her other Vite servers mid-review, which the run's hard constraint forbids. - So this lane left the file alone and verified the flip on a separate portal instead. She was then asked, said yes knowing her servers would restart, and the coordinator applied it.
The gitignore hole a backup of .env.local fell through โ
.gitignore named exact env filenames, so ANY copy of one was untracked and visible, not ignored. โ
- Found 2026-08-30 when applying the two lines to her
.env.local: the backup.env.local.bak-2026-08-30showed up as??ingit status, sitting in the repo holding every real secret, onegit add -Aaway from a commit. It was moved to~/.lantern-env-backups/at mode 600. - The old block covered
.env,.env.local,.env.development,.env.production,.env.*.local,functions/.env.*andservices/bots/discord/.env.*. A suffix defeats every one of them:.env.local.bak-...,.env.local~,.env.local.save,.env.local.orig,.env.local.20260830.
The fix is six patterns, and their PLACEMENT is the load-bearing part. โ
.env*.bak*
.env*.backup*
.env.local.*
.env*~
.env*.orig
.env*.save.env.local.*also matches.env.local.example, which is TRACKED. It survives because!.env*.examplesits below these lines, and git's last matching pattern wins. A new pattern added below that negation would delete the example files from git, so the block carries that warning in a comment.
Verified in both directions, plus a live one. โ
| Check | Result |
|---|---|
git check-ignore -v on .env.local.bak-2026-08-30, .env.bak, .env.production.backup, .env.local.save, .env.local.orig, .env.local~, .env.local.20260830, apps/web/.env.local.bak-1 | all ignored, each naming the pattern that caught it |
git check-ignore -v on .env.local.example, .env.development.example, .env.production.example, .env.development, services/bots/discord/.env.example | none ignored |
git check-ignore -v --no-index .env.local.example | .gitignore:70:!.env*.example, the negation is what saves it |
Wrote a real .env.local.bak-test-1002 and a real .env.local~, ran git status --short | neither appeared; both deleted after |
git ls-files --error-unmatch .env.local.example | still tracked |
- One footnote for whoever repeats this: plain
git check-ignore -von.env.local.exampleprints empty fields, because it consults the index and skips tracked paths. Add--no-indexto see the deciding pattern.
Is there a fourth place these two values are written? โ
No. Searched for assignment form, not mentions. โ
grep -rnE "^\s*(export\s+)?(VENUE|ANALYTICS)_API_ORIGIN\s*[:=]"across the repo returns.env.local.exampleand nothing else, apart from thebrowser-testskill's throwawayANALYTICS_API_ORIGIN=http://localhost:8093 npx vite --port 3012example, which is correct as an isolated-stack invocation rather than a default..env.development(tracked, public) carries no*_API_ORIGINkeys. No workflow,wrangler.toml, or CI file sets either one.
bootstrap-env.mjs is clean, and a new contributor inherits the fix. โ
- It creates
.env.localwithcopyFileSync(ENV_EXAMPLE, filePath)(line 311), so the example IS the template. - Both keys are in
PUBLIC_VARS, which only nags when a value is missing or placeholder-shaped.http://localhost:8080is neither, so no nag. HINTShas no entry for either key, so there is no stale Cloud Run hint text to correct.- One latent inconsistency, not fixed here and with no live effect: the nag at line 634 tests
postState[k].startsWith('your-'), while the file also defines a stricterisPlaceholderValue()(line 155) written precisely becausestartsWithmisses an embedded form like/home/youruser/.... None of the sevenPUBLIC_VARScurrently carry such a value, so nothing is being missed today.