Dashboard design survey โ
- Status: survey complete, nothing decided, nothing built.
- Ran: 2026-08-24, against the running local stack.
- Input:
brainstorm.md, which holds her raw words. - Evidence:
screens/(one full-page capture per route),harness/(the Playwright driver and the measurement output).
What is this? โ
A catalogue of design defects across the admin portal, the merchant portal and the web app, with options rather than verdicts. โ
- It is a survey, not a fix. No UI code changed. Her words when the block was rescoped: "I don't want just real numbers, we need to scaffold the actual design correctly."
- Every item carries two or three options so she can rule, and the build follows the ruling.
- Items she named are marked HERS. They are inputs, not findings, and they are not re-argued.
What was actually driven, and where. โ
| Surface | URL | Coverage |
|---|---|---|
| Admin portal | http://localhost:3001/admin/* | 24 routes, all rendered, signed in as Agent Probe |
| Merchant portal | http://localhost:3001/merchant/m_KlAXPi6q4RvB/* | all 7 tabs |
| Web app | http://localhost:5173/#/* | public routes only, see What could not be seen |
- Viewport 1440x950 for both portals, 390x844 for the web app.
- Measurements come from
getBoundingClientRectandgetComputedStyleon the live page, not from reading CSS.
Where does the count stand? โ
| Group | Items |
|---|---|
| Hers, already named | 5 |
| Cross-cutting primitives | 7 |
| Admin portal, per screen | 9 |
| Merchant portal | 6 |
| Web app | 3 |
| The three redesigns | 3 sections, not counted as line items |
| Total line items | 30 |
What did she already name? โ
Five items, verbatim from the brainstorm, each now measured.
HERS 1. The Reporting Period banner is the wrong shape. โ
- Where:
VenueActivityDashboard.jsx:213andVenueActivityDashboard.css:13. - Measured on the live page: the bar is 1034px wide and holds 341px of content (a 136px label plus a 205px toggle). On Ad Delivery the same bar is 1034px holding 297px, leaving a 703px dead span between the label and the control. That is 68% of the bar carrying nothing.
- Cause:
display: flexon a block-level div withjustify-content: space-between, so the box fills its parent and then pushes the two children apart. - Her call: dynamic width based on the content.
- This collides with rule 4 of the
designskill, which says size content to its content and nameswidth: fit-contentas the idiom.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
A. width: fit-content, drop space-between, join with gap | The bar shrinks to label plus toggle, roughly 360px, and sits left | Both dashboards once the primitive is shared (see X1) | The bar stops reading as a full-width section divider, which some may miss |
| B. Keep the full-width box, move the toggle next to the label | Same box, content clustered left, dead space moves to the right edge | Same | Still a 1034px box carrying 341px, so it only half answers her |
C. Drop the bar entirely, move the toggle into the PageHeader actions slot | One control row instead of two, and the header already has an actions slot | Every analytics screen, plus the header primitive's crowding | The header is already tight, see A3 on subtitle truncation |
Rendered, not argued. All four shapes are in screens/mockup-period-bar.png, built against the real theme.css, the real admin styles.css and the real VenueActivityDashboard.css, at the true 1034px content width.
- A shrinks the bar to about 360px and reads as a control rather than a divider.
- B keeps the box and moves the emptiness to the right, which answers the clustering but not her "dynamic width" call.
- C paid a visible price in the render. At the real 1130px header width, adding the range toggle to the actions slot truncated the subtitle to
...social engagement across the pla.... That is the header-crowding risk actually happening, not a prediction.
HERS 2. This is cross-cutting, not one screen. โ
- Her framing: "there are also some design choices that need to get fixed across the board".
- Confirmed and quantified. It gets its own section: Cross-cutting primitives. The seven items there are the answer to this one.
HERS 3. Merchant Overview spacing. โ
- Where:
Overview.jsx:165, grid atstyles.css:4618. - Her words: "simply even just the spacing here is fucked".
- Measured: all four cards render exactly 208px tall (the grid stretches them), but the gap between the last sub-metric row and the card's bottom padding is 1px on Active Offers, 19px on Lanterns, 30px on Venues and 30px on Waves. The boxes are equal; the content inside them is not, so three of four cards carry a visible empty band.
- The cause is
grid-template-columns: repeat(4, 1fr)with defaultalign-items: normal, which stretches every card to the tallest, combined with one card having two sub-rows and three having one.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Give every card the same number of sub-rows | Active Offers keeps Draft and Expired; the other three each gain a real second metric | Needs three new metrics, which is item HERS 4 | Inventing a filler metric is worse than the gap |
B. align-items: start so cards size to their own content | Cards become genuinely different heights, no dead band | Every .role-cards-grid in both portals | A ragged bottom edge across the row, which may read as broken rather than intentional |
| C. Pin the sub-metric block to the card's bottom edge | Cards stay 208px; the dead space moves above the metrics instead of below | Only the card component | The gap still exists, it just moves; cosmetic rather than structural |
HERS 4. The merchant dashboard needs more data. โ
- Her words: "we honestly need to add more data to this". What data is her call, so this is options only.
- Context that survived the live run: the whole Ad Delivery panel reads zero across impressions, clicks, CTR, claims and redemptions, and the Ad Views chart is a flat line at zero over 30 days. The merchant has 3 active offers and 1 venue, so the zeros are real, not broken.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Foot traffic, which is what the partnership promises | Lanterns lit per day at their venues, unique visitors, busiest hours, repeat rate | Needs venue-scoped aggregation the merchant is allowed to see | K-anonymity: a quiet venue produces suppressed counts, and the page fills with the empty glyph |
| B. Offer performance over time | Per-offer claims, redemption rate, time to redeem, which offer wins | Mostly exists already; it is disaggregation, not new data | Still zero until ad delivery actually runs |
| C. Comparative context | Their venue against the neighbourhood or category average | New aggregate endpoints, and a privacy read on cross-merchant comparison | Leaks other merchants' performance if the cohort is small |
HERS 5. Merchants need a report creator. โ
- Her words: "we need to build a report creator so merchants can actually pull data they want".
- This is its own project, not a line item here. Scoped, not designed, as instructed.
- What it plausibly overlaps:
#320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants, which is where merchant-facing aggregate analytics already lives. It has unmet Phase 1 and Phase 2 dependencies. - What already exists to build on: the admin BigQuery workspace at
/admin/analytics/bigquery, which already has saved queries, a result table and a meta strip. A merchant report creator is that idea with a safe query surface instead of raw SQL.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Pick from a fixed catalogue of reports | Merchant chooses a prepared report and a date range, exports CSV | Small: a list, a range control, an export endpoint | Not really a creator; she may want composition |
| B. Guided builder: pick metric, pick dimension, pick range | Real composition inside a bounded metric set | A metric registry, a query compiler, and a k-anonymity gate on every cell | The privacy gate is the hard part, not the UI |
| C. Scheduled digests instead of an interactive builder | Weekly or monthly report to the merchant, no UI to build | Email or in-portal delivery, and the same metric registry | Answers "pull data they want" with "we push what we chose" |
What is wrong in the shared primitives? โ
Her item 2 in detail. These are the ones that will keep coming back if they get patched per screen.
X1. The period bar is duplicated as two separately-named components with byte-identical CSS. โ
- Where:
VenueActivityDashboard.css:13andAdDeliveryDashboard.css:12. .venue-activity__period-barand.ad-delivery__period-bardeclare the same eight properties in the same order. So do the two__period-labelrules.- They have already drifted where it shows: the label reads Reporting period on one screen and Reporting window on the other, and the period sets differ (24h / 7d / 30d / 90d against 7d / 30d / 90d).
- Fixing HERS 1 in one file leaves the other wrong. This is the concrete case for a shared primitive.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
A. One <RangeBar> primitive with the periods as a prop | Both screens import it; the fix lands once | Two dashboards today, every future one after | Needs a decision on the canonical label and period set |
B. Promote the CSS to styles.css under one class, keep two components | Cheapest deduplication | Only the two CSS files | The label and period-set drift survives untouched |
C. Fold the range control into <PageTabs> or <PageHeader> | No bar at all, the range lives in existing chrome | The primitives every admin page uses | Largest blast radius of the three |
X2. Three different glyphs mean "no value", and the largest group is a live em dash. โ
- Counted across
apps/admin/src, excluding tests: 39 em dash (U+2014) literals, 6 en dash (U+2013), 18 bare hyphen (|| '-'). Sixty-three empty-value renderings, three vocabularies. - The em dash ones are a live rule violation. AGENTS.md forbids the em dash in anything we write, UI copy included.
lint:emdashratchets from a grandfathered baseline, so these pass CI while rendering on screen. - Fifteen files carry the em dash version, including
SystemSection.jsx:86, which is a shared component, andMerchantsAll.jsx:163. - The en dash version is
Overview.jsx:144, whose comment explains it deliberately: an unknown count must not render as 0 because the server's k-anonymity guard suppressed it. That reasoning is sound and worth keeping whatever glyph wins. - The hyphen version is
OfferReviewQueue.jsx:117and 17 siblings. - The visible cost: on merchant Overview one card row reads
Unique visitors โwhile the row beside it readsAcross venues 1. A dash and a zero in the same four-card row, meaning different things, with nothing telling the reader which.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
A. One <EmptyValue> component, one glyph, everywhere | 63 sites collapse to one decision; the em dash backlog burns down with it | Wide but purely mechanical per site | The design skill's own warning: an em dash sweep already deleted a span whose only content WAS the placeholder. Review each replacement |
| B. Two components, because there are two meanings | <NotYet> for not-loaded and <Suppressed> for privacy-withheld, visually distinct | Same sites, but each needs a judgement call | Slower, and someone has to classify 63 sites |
| C. Replace the glyph with a word | "None", "Not yet", "Hidden" instead of punctuation | Same sites; column widths grow | Words in a tabular-nums column break the number alignment |
- Note for whoever builds this: option A is exactly the shape the
designskill calls out as "a mechanical sweep is not mechanical", with 57 replacements needing 5 human corrections. Budget for the review pass.
X3. PageHeader truncates the subtitle mid-word at a normal window size. โ
- Where:
styles.css:3552,white-space: nowrapplusflex-shrink: 999. - Seen live at 1440px on Ad Delivery: the subtitle renders
Sponsored placement delivery: fills vs verified impressions, clicks, and the claim funnel per placement, merand stops. The rest is unreachable. - The truncation is deliberate, and the comment explains why: the subtitle must give way so a long title does not shove the action buttons off the bar. The design is sound; the copy outgrew it.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Cap subtitle copy at a length that fits, treat overflow as an authoring bug | Nothing structural changes; a lint or a review catches long ones | Copy on every admin page | No mechanism today, so it decays |
| B. Let the header grow to two lines when the subtitle would truncate | Full copy always readable | The 68px header height, which is flush with the sidebar header by design | Breaks the flush alignment the comment protects |
| C. Move the explanatory sentence out of the header into the page body | Header carries the title only; the explanation sits where there is room | Every page using a long subtitle | Adds a block above the content on every screen |
X4. Section subtitles explain our implementation to the reader. โ
- Venue Activity, Top Venues: "Bars share the BQ trend palette so this slot reads as the same chart family across analytics screens." That is a note to ourselves about palette reuse, rendering as user-facing copy.
- Ad Delivery, Daily Delivery by Placement: "The hero rail is the contended slot; the fill-to-view gap per surface is the honest-delivery signal pacing we will meter."
- Ad Delivery empty state: "No ad delivery recorded in the 30d window. The rollup lands daily via the aggregate-ad-delivery-daily job; a freshly provisioned table stays empty until its first run." It names an internal job to whoever is reading.
- This matters more than it looks, because the merchant portal shares this component family. Copy written for us can reach a merchant.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Rewrite each as what the reader gets from the panel | Three strings | Small | Someone has to write them, and the rationale is genuinely useful to us |
| B. Split the field: a reader subtitle plus a dev note behind a hover or a docs link | Keeps the reasoning, hides it from the default read | The chart-section component | Another affordance to design |
| C. Move the reasoning to the component's code comment or its story | Reader copy only in the UI, rationale where rationale belongs | Three strings and three comments | Nothing to hold it there next time |
X5. Two stat idioms stack on one page. โ
- On merchant Overview, four
.role-card--metriccards sit directly above.overview-delivery-stats, a flat 984px flex row of four bare number-and-label pairs with no card, no border and no colour identity. - Same page, same kind of information, two different visual languages, 100px apart.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Promote the delivery stats to role-cards | One idiom on the page | Merchant Overview, and the admin Ad Delivery screen if it follows | Eight cards in a column is a lot of chrome |
| B. Demote the top row to the flat idiom inside one card | Also one idiom, less chrome | Same | Loses the per-metric colour identity the role-cards carry |
| C. Keep both, but make the difference mean something | Cards for standing state, flat strip for a time-windowed reading | Needs the rule written down or it drifts | An unwritten rule is not a rule |
X6. .role-cards-grid is a hard 4-up that jumps straight to 2-up. โ
styles.css:4618:repeat(4, 1fr)down torepeat(2, 1fr)at 1200px, with nothing between.- At 1440px the merchant cards are 247px wide. Below 1200px they double in width overnight. A sibling class 2300 lines later,
.role-cards-grid--3col, already uses theauto-fit/minmaxidiom, so the codebase holds both answers. - The
designskill warns thatminmax(min(300px, 100%), 1fr)collapses in an already-squeezed track, so any change here gets measured rather than assumed.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
A. Adopt the --3col auto-fit idiom on the base class | Smooth reflow, no jump | Every role-cards grid in both portals | Card count per row stops being predictable, which breaks layouts that assume 4 |
| B. Add a 3-up step between 1200 and 1440 | Smaller change, keeps counts predictable | Same grids | Still stepped, just less coarse |
| C. Leave it | Zero risk | Nothing | The jump stays, and 1200 to 1440 is a common laptop range |
X7. Clicking a merchant row opens a new browser tab. โ
- Where:
MerchantsAll.jsx:119,window.open(..., '_blank', 'noopener,noreferrer'). - Nothing in the row signals it. The row is a
role="button"with a chevron, which is the affordance for in-place navigation everywhere else in the portal. noopenermeans there is no way back, so the admin accumulates tabs.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Navigate in place, with the merchant switcher's "Back to Admin" as the exit | Matches every other row in the portal | One call site | Loses side-by-side comparison of two merchants |
| B. Keep the new tab, but signal it with an external-link icon | Honest about what it does | One call site plus an icon | Still spawns tabs |
| C. Open a detail panel in place, like the Users page does | Matches the pattern the design skill names as the anchor | Larger: needs the panel | Duplicates what the merchant portal already shows |
What is wrong in the admin portal? โ
A1. Top Venues charts raw venue IDs on the Y axis. โ
- Where: Venue Activity, Top Venues. Live capture shows axis labels
v9jQLgR...pncGwand00N4CsJ...Bhl8w. - The chart's whole job is ranking venues by lanterns lit, and it does not say which venues.
- Options: resolve the id to
venue.nameclient-side from data already fetched; have the ranking endpoint return the name alongside the id; or fall back to a truncated name with the id on hover. The first two are the real choice, and which one depends on whether the BigQuery ranking already joins venue metadata.
A2. Three of five analytics dashboards are scaffolds. โ
- User Engagement, Platform & Device and Conversion Funnels each render only "This dashboard section is scaffolded and ready for implementation."
- They are full tabs in
DashboardTabs.jsx, indistinguishable from the two real ones until you click. - Options: mark them in the tab bar so the click is informed; hide them until they are built; or keep them and treat the placeholder as a roadmap surface with what is coming and when.
A3. The two analytics dashboards use different names and different period sets for the same control. โ
- Covered mechanically in X1. Named separately because the copy decision (which word, which periods) is hers and does not follow from the refactor.
A4. Ad Delivery shows a zeroed dashboard rather than an empty state. โ
- Four cards read 0, 0, 0, 0 with sub-rows reading
0,โ,0,โ. The chart area carries a paragraph of explanation. The pills read0 CELLSand30D. - A dashboard of zeros is indistinguishable from a broken dashboard.
- Options: an explicit pre-launch empty state for the whole page when there is no delivery at all; keep the cards but grey them and say why once, at the top; or leave the zeros and fix only the mixed dash-and-zero vocabulary via X2.
A5. The Dashboard is a card grid of links. โ
- Her redesign, so it gets its own section: The Dashboard.
A6. Every Dashboard card carries an "Available" badge, and every one says Available. โ
- A status field with one value carries no information, and it occupies the visually loudest position on each card (top right, green).
- Folded into the Dashboard redesign, noted here so it is not lost if the redesign is deferred.
A7. Merchant status renders the raw enum. โ
- The merchants list shows
pending_setupas a badge, snake_case and untranslated. - Options: a label map to human copy; a shared
<StatusBadge>that owns both the label and the tone; or leave the value and fix only the casing.
A8. Feature Tracker and Billing render error and loading states as their steady state. โ
- Feature Tracker: "GitHub token not configured. Try Again". Billing: "Loading billing data..." and "Refreshing..." both present at once.
- Both are real states, not defects in themselves. The design question is whether a screen whose steady state is an error should look like a configuration prompt rather than a failed page.
A9. The em dash renders live on Services & CORS. โ
- Seven em dashes on
/admin/config/services, between service name and port, fromConfigServicesCors.jsx:33. - Same root as X2 and fixed by the same decision. Called out because this one is a rule violation visible on screen today, not a consistency preference.
What is wrong in the merchant portal? โ
M1. The "Unique visitors" row is the only sub-metric with an icon, and the icon breaks the row. โ
- Where:
Overview.jsx:216,<Users size={11} aria-hidden="true" /> Unique visitorsinside a<dt>. - Measured: that row renders 32px tall against 21px for every sibling row, because the
dtisdisplay: blockand the svg sits at the same top as the text rather than inline with it. An 11px difference in one row of one card. - No other sub-metric row in either portal carries an icon.
- Options: drop the icon so the row matches its siblings; make the
dtan inline-flex with a baseline-aligned icon and give every sub-metric row one; or keep it as a deliberate accent and fix only the alignment.
M2. Recent Offers titles wrap to three and four lines. โ
- The offer title column is squeezed by a placement chip and a status chip on the same row, so "Half price cold brew" renders over three lines and "Free pastry with any coffee" over four, while the subtitle truncates to
Lantern A.... - Options: stack the chips below the title and give the title the full width; truncate the title to one line with the full text on hover; or move the chips into a second row of the card.
M3. The Ad Views panel renders a 370px chart of a flat zero line. โ
- Y axis runs 0 to 1, x axis spans Jul 26 to Aug 24, and the series is flat at zero.
- Options: replace the chart with a compact empty state until there is at least one non-zero day; keep the chart but collapse it to a short strip while empty; or leave it, on the argument that the axis dates prove the window is real.
M4. "Clicks ยท โ CTR" renders as broken text. โ
- The label composes a static word, a separator, the CTR value and a unit. When CTR is empty the result reads
Clicks ยท โ CTR, which parses as neither a label nor a value. - Options: hide the CTR clause entirely when there is no value; render
Clicksalone and put CTR in its own stat; or keep the clause and use the empty-value component from X2 so at least the glyph is consistent.
M5. The merchant sidebar carries roughly 420px of empty nav between Settings and the switcher. โ
- Seven items in a full-height rail. The admin sidebar fills its height; the merchant one does not.
- Options: let the nav size to its content and float the switcher up under it; keep the switcher pinned to the bottom and accept the gap as breathing room; or use the space for the merchant's own context (venue name, plan, quick stats).
M6. Offers, Venues, Notes, Photos, Account and Settings are all thin. โ
- Measured body text: Notes 50 characters, Venues 170, Photos 171, Account 266, Settings 268, Offers 605.
- Not a defect on its own, and each may be correct. Recorded because HERS 4 asks what data the merchant portal should carry, and the answer is not only about Overview.
What is wrong in the web app? โ
W1. The Frens empty state uses an emoji where the system uses Lucide icons. โ
- Where:
FrensList.jsx:309,icon: '\u{1F91D}'. - The
designskill's rule 6 is Lucide icons only in UI chrome, emoji only in user-generated content. An empty state is chrome. - Two more sites use text glyphs the same way:
AdminSignup.jsx:877andProfileSettings.jsx:742both render checkmark and warning characters inline instead ofCheckandAlertTriangle. - Options: swap all three for the Lucide equivalents; keep the emoji only in the empty-state illustration slot and write that exception down; or leave the checkmarks in prose lists and fix only the Frens icon, on the argument that a checkmark inside a sentence is punctuation rather than an icon.
W2. The three profile routes render one identical signed-out screen. โ
#/profile,#/profile/privacyand#/profile/safetyeach render exactly the same 97 characters: "Sign In Required. Please sign in to view your profile."- A user following a direct link to the safety page lands on a generic profile prompt and, after signing in, on the profile root rather than where they were going.
- Options: keep the shared gate but name the destination and return to it after sign-in; give each sub-route its own signed-out message; or leave it, on the argument that deep links to these routes are rare.
W3. Only the public screens were reachable, so this section is not a clean bill. โ
- The public routes render; every signed-in surface needs a phone plus OTP session that this run did not establish.
- Recorded as a coverage gap rather than a finding. See What could not be seen.
The Dashboard โ
Her words: "Having cards to links doesn't do anyone anygood." She marked it NEEDS BRAINSTORM, so this brings three genuinely different shapes rather than one idea with variants.
What is there now. โ
- Where:
/admin/dashboard. - Eleven cards in a 3-up grid. Each card is an icon, a green "Available" badge, a title, a two-line description and a link chip.
- Every card duplicates a sidebar entry: Documentation Editor, Storybook, Analytics Dashboard, Merchant Management, User Management, System Health, Billing, Task Tracker and more. The only card without a direct sidebar twin is Invite User.
- So the page is a second copy of the navigation, wearing more chrome, below the first copy. It carries no number, no state and no recency.
- What she wants instead, in her words: links to releases, news, what we are, and similar.
Shape 1: The situation board. โ
- The question it answers: what is happening right now, and does anything need me?
- What is on it: live counts that already exist elsewhere in the portal (venues live now, lanterns lit today, offers awaiting review, open moderation cases, merchant applications pending), each a number that is also the link to the screen that resolves it.
- Why it fits: it turns the duplicated navigation into navigation that earned its place, because a zero means do not click and a five means click now.
- What it costs: every number needs a source, and several of the current ones are placeholders or zero. It would launch mostly empty.
- What it does not do: it says nothing about releases, news or what we are, which is half of what she asked for.
Shape 2: The changelog front page. โ
- The question it answers: what changed, and what are we?
- What is on it: the latest release notes, recent merged work, the current version banner, and a short standing block on what Lantern is and where the plan lives. The repo already holds all of it, in
docs/changelogs/dev/and the launch plan. - Why it fits: it is closest to her literal words, and it makes the Dashboard the page you land on to orient rather than to navigate.
- What it costs: needs a source of truth the page can read. The docs API already serves markdown, so this is plumbing rather than invention.
- What it does not do: nothing operational. An admin who lands here to act still goes to the sidebar.
Shape 3: Split it, and let the landing page be a reading page. โ
- The question it answers: both of the above, by refusing to choose.
- What is on it: a narrow left rail of live counts (shape 1, compressed to one line each) beside a wide reading column of releases and news (shape 2).
- Why it fits: the operational glance and the orientation read are genuinely different jobs, and a 1440px window has room for both.
- What it costs: the most work of the three, and it needs both sources.
- The risk worth naming: a split page can end up doing neither job well, which is close to what the current card grid already is.
What is common to all three, and is not optional. โ
- The "Available" badge goes. A status with one value is decoration.
- The 1:1 card-per-sidebar-entry mapping goes. That duplication is the substance of her complaint, and any shape that keeps it has not answered her.
Ad Network โ
Her words: "It needs its own dedicated considerations." She wants it first class, like Venues.
What "first class" means structurally. โ
- Today:
dashboards/ads, a tab inside Analytics, one of five, reached in two clicks. - Venues: a plain top-level item in the
Platformsection, one click, no parent. - So first class means a top-level
Platformitem beside Venues and Merchants, not a tab.
What it would contain, which is the real question. โ
- Today the tab holds one thing: the Ad Delivery dashboard, which is a reporting view. A top-level surface implies more than reporting.
- The pieces that plausibly belong to it, each currently living somewhere else or nowhere:
- Delivery reporting, which is the current tab, moved.
- Placements, the inventory. The chart already segments by Hero, Inline, Feed and Unknown, so the concept exists in the data with no surface that owns it.
- Offer review, currently under Merchants at
merchants/offer-review. It is the gate an offer passes to enter the network, so it may belong here rather than under Merchants. This overlaps her third redesign and the two should be decided together. - Pacing and fill rules, which the Ad Delivery copy already gestures at ("the fill-to-view gap per surface is the honest-delivery signal pacing we will meter") with no surface behind it.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Promote the existing dashboard to a top-level item, nothing else changes | One nav edit, delivers the structure she asked for today | AdminShell.jsx nav and one route | A top-level item holding a single reporting tab reads as thin |
| B. Promote it as a section with tabs: Delivery, Placements, Review | Matches how Merchants and Analytics already work, and gives Offer review a better home | Nav, routes, and moving Offer review out of Merchants | Offer review's redesign gets entangled with the move |
| C. Wait, and decide it together with Offer review and the pacing work | One coherent design instead of a nav change followed by three more | Nothing yet | Leaves it a tab under Analytics in the meantime, which is what she flagged |
- Recommendation, hers to overrule: B. It gives Offer review the home its redesign is going to want anyway, and the placement concept already exists in the data with nowhere to live. A is cheap but she will be back here.
What it costs that a nav move does not show. โ
Platformcurrently holds Analytics, Venues, Merchants. Adding Ad Network makes four, which is fine.- But Offer review moving out of Merchants changes what Merchants means: it becomes accounts and applications only, which is arguably cleaner.
Offer review โ
Her words: "REALLY needs a redesign."
What is there now. โ
- Where:
OfferReviewQueue.jsx, 148 lines, at/admin/merchants/offer-review. - A
.users-tablewith five columns: Title, Description, Venue, Placement, and an unlabelled actions column holding Approve and Reject buttons. - Venue renders as a raw id in a
monocell (line 117), the same defect as A1 on the Top Venues chart. - Description is truncated into a 320px cell via an inline
maxWidth. - Empty values render as a bare hyphen, the third glyph from X2.
- The queue was empty during this run, so the populated layout is read from source rather than seen. That is a real limit on this section and is listed in What could not be seen.
Why a table is the wrong shape for this specific job. โ
- The decision needs the thing, not a row about the thing. Approving an offer means judging copy, placement and how it will look to a user. A truncated description in a 320px cell cannot support that.
- The repo already owns the right primitive and it is not wired here. The merchant offer form has a phone preview rail,
PhonePreview.jsx, which renders an offer as the user will see it. The reviewer decides without it; the author gets it. - The actions are irreversible and sit at the end of a row. Approve and Reject are two adjacent small buttons in the last column, with no confirmation and no reason capture on reject.
| Option | What it does | What it affects | Risks |
|---|---|---|---|
| A. Keep the queue as a list, add a detail panel | Click a row, a panel opens with the full offer, the phone preview and the actions. This is the Users page pattern the design skill names as the anchor | One screen, reusing an existing pattern and an existing preview component | The phone preview is a locked artifact: it may be given different props, but its markup and layout must not be touched |
| B. Review one offer at a time, queue style | No table at all. One offer, full width, preview beside it, Approve or Reject, then the next one | One screen, a genuinely new layout | Loses the overview: no sense of how many, or of comparing two similar offers |
| C. Cards in a grid, each card a small preview | Scannable and visual, decide without a second click | One screen | A card large enough to judge copy is large enough that four fill the screen, so it degrades to option B with worse density |
- Recommendation, hers to overrule: A. It reuses a pattern the portal already has, it gets the preview in front of the reviewer, and it keeps the queue overview. B is the better pure-review experience and the worse queue-management one.
Two things any of the three should carry. โ
- Reject needs a reason. There is no capture today, so a merchant learns their offer was rejected and nothing else.
- The venue id becomes a venue name, the same fix as A1.
What could not be seen โ
Recorded plainly rather than papered over.
The Offer review queue was empty. โ
- "No offers awaiting review" on the live stack, so the populated table was read from source, not seen.
- Populating it would mean writing an offer into shared dev data, which this survey does not do. If she wants the populated layout captured, that is a decision for her, and it is one seed offer through the merchant path.
The web app's signed-in surfaces. โ
- Landing, login and signup render. Every screen behind a session needs a phone plus OTP login that this run did not establish.
- The web app is therefore represented by three items found on public screens only, and should not be read as clean.
The admin portal needed a workaround to reach at all. โ
- The auth-api on port 8084 returns 500 on admin sign-in,
Failed to determine service account, so no fresh admin login works on the local stack. - The survey harness routes
/api/authto a scratch auth-api on 8184 started from the repo's own.env.local. Her running services were not restarted, killed or reconfigured. - Worth knowing because anyone else picking this up will hit the same wall.
Only one merchant was driven. โ
m_KlAXPi6q4RvB, "Round Trip Cafe", with 3 active offers, 1 venue and zero ad delivery. A merchant with real delivery numbers would likely surface layout problems the zeros hide.
What would I fix first, and why? โ
Not a decision, a recommendation. All three are cheap relative to what they unblock.
| Rank | Item | Why first |
|---|---|---|
| 1 | X1, the duplicated period bar | HERS 1 cannot be fixed correctly without it. Fix the bar in one file and the other screen silently keeps the defect, which is exactly the per-screen patching her item 2 was warning about. It is also the smallest of the three |
| 2 | X2, the empty-value glyph | It is the only item here that is a live rule violation rather than a preference: 39 em dashes rendering in the UI, passing CI on a grandfathered ratchet. It also touches both portals, so every later screen inherits the decision |
| 3 | A1 and the Offer review venue id | The same defect in two places, and both make a screen fail at its stated job. A ranking chart that will not name what it ranks, and a review queue that will not name the venue being reviewed |
- Deliberately not in the top three: HERS 3, the merchant card spacing. It is real and measured, but option A depends on HERS 4, which is a question for her. Fixing the spacing before she rules on the data would mean doing it twice.
What is related? โ
brainstorm.md, her raw input, including the nav rearrange that is already decided and the one open question on where Billing goes.README.md, the project.#320: Phase 3: Venue analytics, aggregate interest and vibe insights for merchants, which is where HERS 5 most plausibly lands.docs/engineering/guides/ADMIN_PAGE_PATTERNS.md, the pattern reference any of this work follows.