Skip to content

What was actually verified, and what was not โ€‹

  • Ran: 2026-08-27, headless chromium at 1440x1000.
  • Targets, both the operator's own running servers, neither restarted: the web app at http://localhost:5173 (step 1) and the admin + merchant portal at http://localhost:3001 (step 2), signed in as Agent Probe.
  • Harness and raw output: harness/. Captures: screens/.

Step 1: the signed-in web survey โ€‹

  • 12 of 14 routes rendered and were measured. The two that did not are findings, not gaps: #/active paints a blank page and #/waves redirects to #/. Full detail in design-survey-web.md.

Step 2: the empty-value grammar โ€‹

The result that matters: zero em dashes render on either portal. โ€‹

RouteRendered em dashes
/admin/system, /admin/config/services, /admin/merchants, /admin/venues, /admin/moderation, /admin/analytics/bigquery, /admin/financials0
/merchant/<id>/overview, /offers, /venues, /settings0
  • 11 routes, both portals, one signed-in session. The probe walked every visible element and read its own text nodes, so this counts what a person SEES, not what a grep finds.

The constant path is confirmed with real data, on the screen the survey named. โ€‹

  • screens/portal-services-cors.png: the Services & CORS "Prod URL" column renders seven NOT_SET hyphens where the survey's A9 recorded em dashes. That is the highest-density instance in the portal and it is now correct.

The COMPONENT path was NOT observed live, and the reason is measured rather than assumed. โ€‹

  • <NotSet /> and <Unavailable /> are data-dependent at every call site: a table cell with an empty field, a metric whose value is null. No such row exists in dev data today on the surfaces they were applied to.
  • Measured, not inferred: every visible row on /admin/venues carries an address and a source; /admin/merchants/all, /admin/merchants/applications and /admin/financials render no placeholder of any kind, before or after the change.
  • The only bare hyphen left on any admin page is the sidebar collapse indicator (expanded ? '-' : '+' in AdminShell.jsx), which is a paired glyph and deliberately untouched. It showed up as "Merchants-" and "Analytics-" in the probe, which is what it is supposed to be.
  • A load-window probe was also run (150ms through 5000ms after navigation) to catch a placeholder rendering before data arrives. It found none, because the loading surfaces render skeletons rather than placeholders.

So the component markup is covered by tests, not by a screenshot. โ€‹

  • Five render tests in EmptyValue.test.jsx assert the glyph by codepoint, the title, the sr-only phrase, the caller-supplied reason, and that NotSet does NOT claim the cursor: help affordance it has nothing to explain with.
  • The admin production build generates .sr-only (clip-path: inset(50%); position: absolute; 1px x 1px), so the label is hidden visually and available to assistive tech. Checked in the built CSS, because a Tailwind utility that is never scanned would have rendered the phrase visibly beside every dash on both portals.
  • This is the honest boundary: the sweep is verified live, the component's internals are verified by test. Seeding an empty field into shared dev data to force a render was not worth doing to other sessions.

The one instance the pass deliberately left is visible in the capture. โ€‹

  • screens/portal-merchant-overview.png shows the exact defect the survey described: "Unique visitors" renders a dash while "Draft", "Expired" and "Across venues" render zeros beside it, with nothing telling the reader which is which.
  • The probe confirms it is a bare U+2013 in a <dd>, not wrapped: Overview.jsx's own literal, which this pass did not touch (it is a #320 screen and another session had it open).
  • This is the single highest-value remaining application of step 2: two lines in one file, no visual change, and it gains the sentence that makes the dash mean "too quiet to report" rather than "nobody came".

Step 5, first three items: the header subtitle, the Overview empty value, the prose sweep โ€‹

  • Ran: 2026-08-27, headless chromium at 1440x1000, on http://localhost:3001, the operator's own stack, nothing restarted or rebuilt. Vite HMR carried the source edits.
  • Post-login URL, every run: http://localhost:3001/admin, and every route additionally recorded onLoginScreen: false. So none of the zeros below is the rate-limiter wearing a clean pass.
  • Harness: harness/verify-step5-jobs.mjs. Raw output: harness/step5-verify.json. Captures: screens/j-a-*.png (admin) and screens/j-m-*.png (merchant).
  • 15 routes, 10 admin and 5 merchant, one signed-in session per run.

PageHeader carries the subtitle in title on 13 of 15 routes, including all 5 merchant ones. โ€‹

  • The header bar measures 1130x68 on every one of the 15, unchanged, so the flush sidebar alignment the CSS comment protects is intact.
  • The route the decision was about still clips, and now carries the text: /admin/analytics/bigquery/export at 452 of 513px, title present and equal to the rendered subtitle. That is the mitigation working. It is not a fix for truncation, and the durable answer is still shorter copy.
  • Every other route measures clientWidth == scrollWidth, so nothing else clips at 1440px, which matches the original 11-route measurement.

The 2 routes without a title are the ones that pass a NODE, not a string, and this is the guard behaving correctly. โ€‹

RouteWhat its subtitle isClipped
/admin/systemA fragment: Infrastructure metrics and service status plus a conditional Cached (Ns ago) badge (SystemHealth.jsx:231)no, 367/367
/admin/financialsSame shape (Financials.jsx:494)no, 323/323
  • title takes a string. Passing a React element would have rendered an object stringification into the tooltip, so the component declines rather than lying.
  • Neither clips, so the gap costs nothing today. Covering them needs a decision that was not made: either a second subtitleTitle prop or reading the text back off a ref after mount. That is a fork, not an oversight, so it is left for whoever owns step 5's remaining items.

Merchant Overview's "Unique visitors" now says what its dash means. โ€‹

  • Read live off the running page: glyph U+2013, cursor: help, and both the title and the sr-only phrase reading "Too quiet to report, not zero: counts from fewer than 3 unique visitors stay hidden to protect anonymity".
  • The floor is interpolated from K_ANON_DISPLAY_FLOOR, which the file already imports, rather than typed as a literal, so it cannot drift from the server's actual gate.
  • Zero visual change, confirmed against screens/portal-merchant-overview.png (before) and screens/j-m-overview.png (after). The card still reads Lanterns 30d: 11 with a dash beside Unique visitors, next to Draft 0, Expired 0 and Across venues 1. The difference is only reachable, which is the point.
  • Nothing else on that screen was touched. statValue still serves its other seven call sites with the bare literal, deliberately: one generic reason attached to all of them would flatten meanings that genuinely differ.

Zero banned long dashes render on any of the 15 routes. โ€‹

  • The probe walks every visible element, reads its own text nodes, and also scans every title attribute, since job 1 deliberately puts copy there. It counts what a person sees, not what a grep finds.
  • The first run found 2 that a source grep could never have caught, both on /admin/client-sdk, in the category descriptions for "User Profiles" and "Onboarding". They render from apps/admin/src/generated/client-sdk.json, which is gitignored and therefore invisible to the ratchet linter, and they are authored in generate-client-sdk-docs.mjs's CATEGORY_DESCRIPTIONS map. Fixed at the authored source and regenerated; the second run reads 0.
  • This is the argument for driving the browser rather than trusting the linter. A gitignored build artifact is out of every static gate's scope and still lands in a user's face.

What blocked, and what it cost โ€‹

  • The portal sign-in rate limit ate one run. /auth/admin/signin and /auth/merchant/signin share one 10-per-15-minutes-per-IP bucket, and the operator's own portal session spends from it too. The second pass silently landed on the login screen and reported zeros for every route, which reads exactly like a working page with no findings.
  • The tell: AFTER_LOGIN http://localhost:3001/ instead of /admin. Every harness in this folder logs the post-login URL for that reason. Budget one sign-in per run and reuse the session.

Built with VitePress