Merchant and venue dashboards โ
- Status: survey done 2026-08-24, awaiting her rulings on most of it. The admin nav rearrange and the financials consolidation are BUILT and live-verified, 2026-08-27.
- Issue:
#320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants - Launch plan:
ALPHA.mdrows "Merchant-side dashboards" and "Venue and activity dashboards", both P1, both In progress.
What is this? โ
Two dashboard surfaces that share one issue. โ
- The merchant side is what a partner merchant sees about their own offers and foot traffic. It exists because the partnership is what promises them data.
- The venue and activity side is period activity, wave accept rates and offer delivery in the admin portal, which the plan calls how we watch the experiment while it runs.
What is the current state? โ
The design survey is done and it is the thing to read next. โ
design-survey.md, 30 items across the admin portal, the merchant portal and the web app, each with its file and line and two or three options.- Her five named items are marked HERS and measured. The Dashboard, Ad Network and Offer review each have their own section, since she called all three redesigns.
brainstorm.mdholds her raw words, including the nav rearrange that is already decided.- Evidence:
screens/captures every route off the running stack,harness/is the Playwright driver that produced them. - Nothing is decided. The build follows her rulings on the options.
The two plan rows still need untangling before anyone works from them. โ
- One issue covers both plan rows. Either split
#320into two, or merge the two rows. - This is not academic: a dashboard auto-refresh was already built twice in one day because the two were treated as separate work.
- Only
README.mdexists in this project so far.
What was built on 2026-08-27? โ
The admin sidebar rearrange she decided on 2026-08-24, plus a consolidation she added mid-session: every financial surface moved onto one page, and that page renamed from Billing to Financials.
The sidebar now reads Main, Platform, Operations, Development, Settings. โ
- System Health left
OperationsforDevelopment, which is where a developer surface belongs. Developmentmoved down to sit directly aboveSettings.- Nothing sits below
DevelopmentorSettings, which the move made true rather than something needing its own work. - Billing stayed in
Operations, under its new name. - Nav ids double as routes, so every id, label and icon was carried across unchanged and no route moved.
The Costs tab moved from System Health into Financials, as its own tab. โ
- It sits between
VendorsandSettings, rendered byInfraCosts.jsx. - It owns its own fetches rather than reading the page's
billingData. The provider breakdown comes from thegetSystemHealthcallable, whosegetBillingHealthmaps the BigQuery attribution pipeline into the legacy per-provider shape those cards expect; the other tabs read that same pipeline through analytics-api in its native shape. The OTP numbers come from the auth-api admin route, which no other tab touches. Collapsing the two request paths would mean reshaping the payloads, which is a redesign rather than a move. - It renders outside the page's loading and error branches, so a slow or failed analytics-api fetch cannot blank it, and the page Refresh button drives it through a refresh token.
The last two cost blocks left System Health for that tab. โ
- They overlapped.
Usage & Cost Insighton the Overview showed cost per user and cost per venue;Usage to Cost Correlationon the Usage tab showed cost per user again, plus venues per dollar. - The union of the distinct metrics now lives once on the Costs tab. Current Monthly Cost was not carried over, because it is the Cost Summary card at the top of that same tab.
- Both inputs ride in on the
getSystemHealthpayload the tab already fetches, so this needed no new request.
Billing became Financials, route and all. โ
- Her call, 2026-08-27: "Billing" reads as what you charge customers, and this page is what we spend. It also leaves the name free for real merchant billing, which the merchant profile already uses at
MyMerchantProfile.jsx("Billing & Plan"). /admin/billingbecame/admin/financials, the nav id and label followed, and the folder and page component were renamed. The old-URL map now sends/billingto/admin/financials.- Swapped, not migrated, per her standing call.
/admin/billingno longer resolves and falls through to the admin dashboard. There is no compatibility redirect for it by design. - The vendor section components (
GitHubBilling,GcpBilling, and the rest) keep their names. They render vendor billing data, which is still what that is.
What was decided about the Dashboard? โ
PM DECISION 2026-08-27, made under her autonomy grant and NOT her word, so it is hers to overturn: Shape 3, the split reading page. The admin Dashboard is now a dominant reading column (what we are, plus the release stream out of docs/changelogs/dev/) with a narrow 240px status rail of live counts beside it. The eleven-card grid, its "Available" badges and its duplicated sidebar entries are gone.
- Why 3 rather than 1 or 2: her own words name both halves at different times. She asked for releases and news, which is the reading column; in the Alerts conversation she said alerts "could potentially be on the dashboard as well", and her alerts are threshold and health state, which is the rail. Shape 1 or 2 answers half of what she has asked for.
- The reasoning, the rail's sources, the not-optional list, the live run and what it is reversible into are all in
dashboard-redesign.md. - No alerting was built. The rail is shaped to take a threshold row; the backend does not exist and is separate work.
- The first cut of the reading column bundled its two source documents into the build, which on Cloudflare Pages published them as static assets served before any authentication. They are fetched through the docs API at runtime now, the column unchanged. That and five smaller review findings are in
review-remediation.md.
What did the work turn up that the brainstorm did not predict? โ
Three dead links, all the same bug. โ
AdminShell's back-compat effect that rewrites a #/... hash into a real route runs on MOUNT ONLY. Clicked mid-session, such a link sets a hash fragment and never navigates. Verified in the browser: clicking one leaves the URL at /admin/system#/admin/users with the page unchanged.
- The Costs tab's "GCP / Firebase Detailed Costs" section, whose whole body was a button linking to
#/billing. Removed: self-referential once inside Financials, and already inert. - The Overview footer's two quick links. The in-page one is now a plain page hash the tab router already listens for; the cross-page one is a real path.
The System Health Overview's two cost cards pointed at a tab that was leaving. โ
They called onNavigate('billing') for a tab that no longer exists, which would have silently done nothing. They now navigate to /admin/financials#costs.
- RULED 2026-08-27: both removed. Her words: "Cost spikes are fine but financials stay out of system health in general." Cost is not a health signal. The Four Golden Signals, USE and RED all measure whether the system is working now, while cost is FinOps: a different audience on a different clock. The dead plumbing went with the cards (the
DollarSignimport, theonOpenCostshandler, thebillinglocal and the cost tooltip). - SIDE QUEST, hers, same message: a dedicated alerts page, so anomaly signals are centralized rather than scattered across the surfaces they came from. A cost spike would live there rather than on System Health. Not scoped, not started.
The em dash ratchet keys on file path, so a folder rename reads as 86 new violations. โ
- Renaming
billing/tofinancials/reset 15 grandfathered files to a zero allowance. Fixed by remapping those baseline paths verbatim, counts unchanged, so the ratchet still fails on anything genuinely added. - Deliberately not a
--update-baselinere-snapshot, which would also grandfatherharness/admin-report.json, whose escaping fix she reverted herself in31988cf0. - That file still fails
npm run lint:emdash, and it did before this branch. It is the one thing standing between this branch and a green em dash gate, and it is hers to call, since the fix for it was reverted on purpose.
The em dash gate was red for a reason no one had named, and it is now codified. โ
- The failing file was
harness/admin-report.json, a Playwright capture of what the admin portal renders. The portal's Services and CORS page used an em dash as its empty-value placeholder, so the capture recorded seven of them faithfully and the gate failed on the EVIDENCE rather than the source. - Three sessions chased the capture instead of the source. One escaped the characters as
\u2014; that fix was reverted in31988cf0; the gate stayed red. - Fixed at the source instead: six
|| '\u2014'placeholders across the admin portal became|| '-', matching the eighteen sites that already did. Generated capture output underdocs/projects/<project>/harness/*.jsonis now excluded from the linter, and both traps are written intolinter-organization.mdso the next session reads them instead of rediscovering them. npm run lint:emdashpasses, and the baseline was re-snapshot to lock in the win.
The Uptime tab was measuring almost nothing, and three of its four checks could never pass. โ
Found by reading the tab in the running portal after she said it was missing endpoints. It checked four hardcoded URLs in systemHealth.service.js.
- None of the seven Cloud Run services were covered, so the tab could report "operational" while saying nothing at all about the backend. The API half now derives from
packages/shared/services, which rule 8 already makes every new service join, so a service is monitored the day it is registered. admin.ourlantern.appandstorybook.ourlantern.appdo not resolve (curl returns 000). Their dev hosts answer 200. Two of the four checks were probing hostnames that were never provisioned, and reporting DOWN for it. The frontend checks now use the same prod-or-dev fallback the registry gives the APIs.- The Discord bot has no HTTP listener, anywhere in
services/bots/discord. It is a gateway client, so the probe of its Railway/healthcould never pass and reported the bot DOWN whether or not it was running. Removed rather than left red, because a check that cannot succeed teaches everyone to ignore the tab. It is now unmonitored, and monitoring it for real needs either an HTTP health surface on the bot or a gateway-side check. - Cloud Run cold starts would have painted the tab amber. Measured 5.3s for venue-api and 3.7s for auth-api against 0.3s warm, versus an existing 3s degraded bar. API checks now carry an 8s allowance; frontends keep 3s.
- Nine endpoints now, two frontends plus seven APIs, up from four of which three were broken. The tests derive their counts from the registry too, so a new service cannot silently go unmonitored and cannot break the suite either.
The admin portal reads the DEPLOYED analytics-api, not the local one. โ
Worth knowing before anyone debugs a System Health change again, because the obvious diagnosis is wrong. The chain is SystemHealth.jsx -> getSystemHealth() in firebase.js -> getSystemHealthApi() -> VITE_ANALYTICS_API_URL=/api -> the Vite proxy's /api/analytics -> ANALYTICS_API_ORIGIN, which .env.local points at https://analytics-api-531553779372.us-central1.run.app.
- So a local edit to systemHealth.service.js can never show in the portal, restart or no restart. The data comes from deployed dev, which runs whatever last merged. There is also a 5-minute server-side cache on
admin:systemHealthbehind that. - Live-verified anyway, on separate ports, per the
browser-testskill's branch-only-endpoint path: the branch's analytics-api on8093and an admin portal on3012withANALYTICS_API_ORIGINpointed at it. Her stack on3001and8080to8086was never touched, and both isolated servers were stopped afterwards. Result: nine endpoints, all200, nine healthy, average802mswarm. Screenshot:harness/uptime-branch-verified.png. - That run is also what set the cold-start threshold. An 8s bar flagged assistant-api at 9.2s on every cold call, so the measured range (2.6s to 9.2s cold, 0.3s to 0.5s warm) put the bar at 12s: above the worst cold start with headroom, so tripping it means genuinely slow rather than merely cold.
Metric cards stretched when a row was sparse. โ
Three grids on System Health forced role-cards-grid--2col, which is repeat(2, 1fr), so two cards rendered 509px wide against the standard 247px. Cloud Storage, Content and Bandwidth now use the default 4-up grid. Verified by measurement: every grid on the Usage, Firebase and Cloudflare tabs now renders 247px per card at 1, 2, 3 and 4 cards.
- Left alone deliberately:
bq-role-cards-gridalready caps atminmax(280px, 360px)withjustify-content: flex-start, and--3colholds three donut charts, which are a deliberate full-width row rather than sparse metric cards.
The Errors tab was reading a collection nothing has ever written to. โ
She asked whether "No errors logged" was correct after remembering a cloud error during billing aggregation. It was not correct, and it never could have been.
getClientErrorsMetricsreads theclientErrorsFirestore collection. Live dev holds zero documents in it, and nothing inapps/,services/,packages/,firestore.rulesor the Cloud Functions writes to it. The panel said "No errors logged" unconditionally, and its empty state promised "errors are automatically captured when users encounter issues", which was never true.- Fixed by reading the store that already has everything. Every Cloud Run service, Cloud Function and Scheduler job already writes failures to Cloud Logging, so
getServerErrorsMetricsquerieslogging.googleapis.comforseverity>=ERRORrather than adding a second error store nobody would remember to instrument. Auth isGoogleAuth, which resolves the metadata server when deployed and ADC locally, so it is verifiable off-GCP. - Needs
roles/logging.vieweron the deployed analytics-api service account. Without it the Logging API answers 403 and the section degrades to not_available with that explanation, rather than failing the health payload. - The client half is kept and its empty state now says what it means: "nothing currently writes to the clientErrors collection, so treat this as not instrumented rather than none happened."
It immediately surfaced a real bug: the nightly aggregation job has 404ed for days. โ
34 server errors in 7 days, 4 in the last 24 hours, across 5 sources, on the tab that reported none. The largest group is 20 errors from analytics-daily-aggregation, a Cloud Scheduler job hitting POST /analytics/scheduled/daily-aggregation and getting URL_ERROR-ERROR_NOT_FOUND ... 404, four retries a night at 02:00 UTC.
- The code predicted it.
services/api/analytics/src/routes/scheduled.jssays the route was retired in the #616 cleanup and that "theanalytics-daily-aggregationCloud Scheduler job must be deleted alongside this (deploy note in the PR), or it will 404 nightly". The job was never deleted. - HERS to action: deleting a Cloud Scheduler job is an infrastructure change on dev GCP, not a code change, and
gcloudis not executable from an agent shell here. The route is correctly gone; only the orphaned job remains. - Also surfaced: 7
cloud_run_revision, 4gce_instance, 2bigquery_resourceand 1bigquery_projecterror, none of which anything was reporting.
SIDE QUEST, hers 2026-08-27: upstream platform status. โ
Monitor whether the platforms Lantern depends on are themselves down, so a GCP or Cloudflare outage shows up rather than being inferred from our own failures. Distinct from the uptime check, which only proves OUR endpoints answer. Not scoped, not started.
Infrastructure actions taken 2026-08-27, on her explicit permission. โ
Her words: "I give you permission for gcloud deletion and role grant."
- DELETED the
analytics-daily-aggregationCloud Scheduler job. Verified first that the route is genuinely gone (scheduled.jsexposes onlyaggregate-event-counts-daily,aggregate-ad-delivery-dailyandpseudonymize-user-ids), backed up the full job definition before deleting, and confirmed afterwards that the other 13 jobs are untouched.gcloudis not on PATH here; it lives at~/google-cloud-sdk/bin/gcloud. - The
roles/logging.viewergrant was NOT needed, so it was not made. analytics-api runs as531553779372-compute@developer.gserviceaccount.com, which already holdsroles/editor, and that role includeslogging.logEntries.list(verified against the live role definition, 11979 permissions). Adding the binding would have been redundant IAM.
Yes, there is a resolved-or-not system, and GCP is already running it. โ
Her question: "Is there any way we implement a system on if an error is resolved or not?"
Google Cloud Error Reporting is already enabled on lantern-app-dev and already tracks exactly this. It groups errors by signature and carries resolutionStatus (OPEN, ACKNOWLEDGED, RESOLVED, MUTED), firstSeenTime, lastSeenTime, count and affectedServices. Nothing has ever surfaced it. Five groups are currently OPEN, all application exceptions:
| Service | Error | First seen | Last seen |
|---|---|---|---|
| merchants-api | ZodError | 2026-04-30 | 2026-08-08 |
| auth-api | Error: failed with status code 500 | 2026-01-30 | 2026-08-03 |
| auth-api | invalid documentPath resource path | 2026-08-03 | 2026-08-03 |
| analytics-api | ERR_MODULE_NOT_FOUND: @lantern/shared | 2026-02-10 | 2026-08-01 |
| assistant-api | ZodError | 2026-07-31 | 2026-07-31 |
- It is complementary to the Cloud Logging feed already built, not a replacement. Error Reporting only picks up things that look like application exceptions, which is why it shows 5 groups where Logging showed 34 entries: the Scheduler 404s are not exceptions, so they never reach it. Logging is the catch-all with no state; Error Reporting is the stateful subset.
- The recommendation is to surface Error Reporting groups as the primary Errors view (grouped, with status and first/last seen and a recurrence count) and keep the raw Logging feed underneath as the catch-all.
resolutionStatusis writable viaPUT v1beta1/{group.name}, so a Resolve control in the portal is possible rather than deep-linking to the GCP console. - A resolved group that recurs is the signal that matters, and it comes free:
lastSeenTimemoving after a group was marked RESOLVED means the fix did not hold.
Client errors: nothing is captured at all. โ
Her question: "Are we capturing any client errors?" Checked across both apps.
- No
window.onerror, nounhandledrejectionhandler, no Sentry, nocaptureExceptionanywhere inapps/web/srcorapps/admin/src. - The admin portal has one
ErrorBoundary, and itscomponentDidCatchonly callsconsole.error. It never sends anywhere. - The web PWA, the app users actually run, has no ErrorBoundary at all.
- The
clientErrorscollection the old panel read holds zero documents and has no writer, which closes the loop on why it always said "No errors logged". - So a user hitting a crash produces nothing anyone can see. Scoped on her ask as its own project:
application-monitoring, planning task 4, against the existing#164: Infra: Application Monitoring & Observability. The uptime and server-error work above belongs to that project too; it is recorded here because it came out of this task's nav rearrange.
How was it verified? โ
Headless Playwright against her running local stack (admin on 3001, APIs on 8080 to 8086), driven by the Agent Probe test admin. Screenshots in harness/ prefixed final-.
- Routes loaded:
/admin/financialsand its#overview,#reports,#vendors/gcp,#settingsand#costshashes;/admin/system;/admin/users;/admin/analytics;/admin/feature-tracker;/admin/moderation;/admin/config/overview;/admin/api;/admin/client-sdk; plus the old/billing(redirects correctly) and/admin/billing, which fell through to the Dashboard. That fall-through was recorded here as intended and it was not: the branch review caught it as a dead route and it now redirects to/admin/financials, seereview-remediation.md. - Every System Health tab and every Financials tab was clicked and rendered with no page errors.
- The Costs tab renders live data: total $298.64, five provider cards, cost per user $8.53, cost per venue $0.01, venues per dollar 76, and OTP at 10/2000 sends, 8 verified, $0.28 estimated spend.
- The repointed System Health cost card lands on
/admin/financials#costs. npm run test -w apps/adminon the touched suites: 32 passed.
What was fixed on 2026-08-28? โ
footer-pin-988.md, why the merchant sidebar footer rose up (#988) and the computed styles that proved it. The cause was the previous day's M5 fix, and the answer reverses M5's chosen option because the operator's own words pick the other one.merchant-defect-survey-0828.md, the goal-two sweep: six defects fixed, seven reported with a reason for each, and the two places a fix wanted to reach intoapps/admin/src/shared.../../engineering/testing/runs/merchant-portal-defects-2026-08-28/README.md, fourteen scenarios, all filled in, including the three that could have said the diagnosis was wrong.- Decided 2026-08-28, leave it: the merchant rail now has 407.9px between "Settings" and the pinned footer, which is M5 returning by design. She chose the empty space over filling it, so the survey's third option is declined and survey item M5 closes rather than staying open.
What does the design still need? โ
- Her verdict, 2026-08-28: the merchant design needs a considerable overhaul on every page except the offer creation and builder flow.
merchant-design-brief.mdis the evidence for it: all seven routes measured at 1440x950 with their screenshots, seven observations, and the four questions a decision would need to answer. It proposes no layouts, because the shape is hers to name.- The headline number: content fill runs from 7.8% on Settings to 84.4% on Overview, and five of the seven pages leave more than half the window empty below their last block.
- Three directions are drawn and waiting on her, 2026-08-30:
directions/index.htmlputs them side by side, and each one is its own file indirections/. They open straight from disk: no server, no CDN, no network call, tokens and the Inter face inlined from the repo. A. Fewer doors, seven routes become three. B. Sized to content, the container follows the content. C. Data led, every page carries the data it is about. Each names its own tradeoff, and none of them is picked. - They are mockups, not app code. Nothing is being built against this until she names the shape.
What else is related? โ
- Venue Activity period stats come from BigQuery
analytics.eventsviagetPlatformOverviewBqandgetVenueRankingsBq, not from Firestore. An earlier "the zeros are real" diagnosis was wrong; the Firestore queries had three field-name bugs.