Skip to content

Merchant portal defects M1 to M5 โ€‹

  • Status: all five fixed and confirmed in a running browser, 2026-08-27.
  • Source: design-survey.md, section "What is wrong in the merchant portal?".
  • Scope: apps/admin/src/merchant only. apps/admin/src/shared was off limits (task 4 owned it that session), and nothing here needed it.
  • Not in scope: M6, the six thin merchant pages. It is a content question, not a defect.

What changed? โ€‹

ItemFixWhere
M1. Sub-metric icon breaks its rowDropped the Users icon from the "Unique visitors" <dt>Overview.jsx
M2. Recent Offers titles wrap to three and four linesAudience label and status badge moved to a second line, title takes the full row width and truncatesOverview.jsx, Overview.css
M3. Ad Views renders a 370px flat zero chartCompact empty state when the visible window has no non-zero dayOverview.jsx, Overview.css
M4. "Clicks ยท โ€“ CTR" is broken textCTR clause renders only when there are impressions to divide byOverview.jsx
M5. Merchant sidebar carries ~420px of empty navNav sizes to its content under a merchant-only .sidebar--merchant scopeMerchantShell.jsx, MerchantShell.css

Two new CSS files, colocated, rather than edits to the shared sheet. โ€‹

  • apps/admin/src/shared/styles/styles.css is the ADMIN portal's stylesheet as well, so a rule added there for a merchant panel is a rule the admin portal inherits. That is the design skill's own instruction for a screen-specific section, and it was also the session's hard boundary.
  • Every selector in both files is scoped to markup only the merchant portal renders: the new .offer-row--stacked and .overview-chart-empty classes, and .sidebar--merchant.

What did the browser actually show? โ€‹

  • Target: http://localhost:3001/merchant/m_KlAXPi6q4RvB/overview, the operator's own running stack. Nothing was restarted, rebuilt or killed; no stack of my own was stood up.
  • Signed in two ways: as Agent Probe (admin viewing a merchant, which is the shape the survey ran) and as the test merchant (their own login, whose sidebar footer is a different component in the same slot).
  • Viewport 1440x950, plus a 390x844 pass for the mobile drawer.
  • Harness: harness/m-check.mjs (before and after measurement), harness/m-populated.mjs (the non-empty branch), harness/m-sweep.mjs (siblings and breakpoints). Raw output in harness/m-before.json, m-after.json, m-populated.json, m-sweep.json.

Measured before and after, on the live page. โ€‹

ItemBeforeAfter
M1"Unique visitors" row 31.8px, siblings 20.8px20.8px, every sub-metric row equal
M2Title column 67.1px, titles over 3 and 4 linesTitle column 230.1px, every title 1 line
M3370px chart, flat line at zero, y-axis 0 to 1153.4px empty state, no chart, panel 261.4px
M4Clicks ยท โ€“ CTRClicks
M5407.9px between "Settings" and the footer40px

The populated path was exercised too, not just the empty one. โ€‹

  • An empty-state fix that hides real data is worse than the defect. The delivery response was intercepted in the browser and refilled with 30 dense days, which is why m-populated.json records the chart back at 586x260, the empty state absent, and the label reading Clicks ยท 6% CTR.
  • Nothing was written to shared dev data to do this. The interception happens in the browser context.

The siblings that share the touched classes were checked for regressions. โ€‹

  • The admin portal sidebar is untouched: merchantClass: false and flex: 1/1/0% on /admin/users, exactly as before.
  • The merchant Offers tab still renders the wide row: .offer-row without --stacked, titles 695px on one line, unchanged.
  • The merchant rail is correct collapsed (72px, 40px gap) and as a mobile drawer (390px, footer bottom at 582px inside an 844px viewport).

What did the survey get wrong or leave out? โ€‹

M2 is worse than the survey measured, and the cause is one property. โ€‹

  • The survey described a squeezed title column. The live number is 67px, which is narrower than the word "pastry".
  • The cause is flex-shrink: 0 on .offer-row__audience. The audience label holds its full width no matter how narrow the row gets, so every pixel of the squeeze lands on the title. The survey's "the subtitle truncates to Lantern A..." is the same property doing the same thing to the venue name.

M5 is 408px, not "roughly 420px". โ€‹

  • Close enough that the survey's framing stands. Recorded because the fix is verified against the real number.

M3's empty state has to key on the VISIBLE window, not the fetch. โ€‹

  • The panel fetches 30 days and the toggle slices 7 or 30 out of it. A merchant with views 20 days ago and none since would have got the chart on 30d and a flat zero line on 7d. The check runs on the sliced series, so both windows agree.

What was found and NOT fixed here? โ€‹

GrowthChart's empty message says "No signups in this window" whatever it is charting. โ€‹

  • apps/admin/src/components/GrowthChart.jsx:42 hardcodes the word, while the component takes metric and metricPlural props precisely so it can say the right one.
  • It is unreachable from the merchant Overview after this change (the empty state now intercepts first) but reachable from any other caller passing an empty array.
  • Not fixed because the file is outside apps/admin/src/merchant.

The merchant switcher reads "VIEWING Merchant" rather than the business name. โ€‹

  • Visible in harness/m-after-overview-viewport.png, where the page header on the same screen correctly reads "Round Trip Cafe 15890656".
  • MerchantSwitcher lives in apps/admin/src/shared/components/, so it was out of scope. Worth raising: a switcher that will not name what it switched to is the same failure as A1 and the Offer review venue id.

Built with VitePress