feat: an agency roster of two or more clients opens as a dense table (#691 phase 3) - #696
Open
hyoshi wants to merge 1 commit into
Open
feat: an agency roster of two or more clients opens as a dense table (#691 phase 3)#696hyoshi wants to merge 1 commit into
hyoshi wants to merge 1 commit into
Conversation
Phase 3 of #691. A card grid answers "how is this client doing?" one client at a time. Past a handful of clients the question becomes "which of these do I open first?", and a grid answers that badly: the figures sit in a different place on every card, so comparing two of them is a search rather than a glance. A table puts one metric in one column, which is the whole of what it is for. WHO GETS IT. Two or more VISIBLE clients. A single-workspace OSS install opens a client detail directly and never sees a roster; a roster down to one visible client keeps the cards, because a one-row table is a worse card. The cards remain as a toggle and the choice is remembered per browser — it is a preference about reading, not a fact about the data, so it lives in localStorage and never on the wire. A remembered "table" does not resurrect one for a roster that has since shrunk. What the table decides, and what it refuses to ---------------------------------------------- Status order is the default and it is an argument: needs-action, then watch, then clear, biggest spender first inside each group. The ranking is reports_triage.js own — a row the alert list calls urgent cannot sort as healthy, because the table never forms a second opinion about health or about any figure. Every number comes from the same aggregateClientKpis the cards use. A figure mureo will not state is "—" and sorts to the END in both directions. Unknown is not a small number, and letting it float to the top of an ascending sort would put the least informative rows where the most urgent belong. The totals row sums spend and conversions and takes CPA as the WEIGHTED average — total spend over total conversions — not the mean of the per-client CPAs, which would let a client spending 12,000 pull the roster figure as hard as one spending 128,000. A client whose figures are withheld is left out of the sums entirely rather than counted as zero. CTR has no roster-level value worth stating, so it is "—". The CPA-vs-target column ------------------------ It is in the approved design and it is NOT rendered, because nothing on the reports wire carries a target CPA: no field on the summary, and STRATEGY.md guardrails reach no web payload. Checked before building rather than assumed. A column of "—" the whole way down would advertise a comparison mureo cannot make, so the column is omitted when no client has a target. The renderer ships and is tested against a feed that has one, so the day a target reaches the summary the column appears with no further work. This is the same shape phase 4 has with #690: the view is ready and the datum is the dependency. One filter, both views ---------------------- applyReportsHealthFilter now iterates every roster row rather than the card grid children, so one implementation hides both views. It applies the client-name search too, and that is deliberate rather than lazy: `hidden` on these rows has exactly one owner. Two functions both writing it would each undo the other depending on which ran last, which is the bug shape #665 is about. The filter and the search compose; neither overrides. Test changes ------------ reports_filter_interaction.test.js now switches to the cards explicitly. That file is about the GRID filtering, and this change made the table the default for a roster its size — so it says which view it means rather than relying on a default that is no longer its own. Same assertions. Two bugs the new tests found before review did: - "No client matches" never appeared. The count of still-visible rows included the card grid items, which are built either way and carry no name hook, so an emptied search never looked empty. - the totals row had no class, so nothing could address it without a descendant selector the harness does not support. And two expectations I had written wrong: a stale client withholds its spend, so it sorts LAST rather than by value, and it is out of the totals. The code was right and the test was not. Refs #691
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Phase 3 of #691: an agency roster of two or more visible clients opens as a dense, status-sorted table. Mockup screen 1b is the reference.
A card grid answers "how is this client doing?" one client at a time. Past a handful of clients the question becomes "which of these do I open first?", and a grid answers that badly — the figures sit in a different place on every card, so comparing two is a search rather than a glance.
Single-workspace (OSS) installs are untouched: they open a client's detail directly and never see a roster.
The table
reports_triage.js's own; the table forms no second opinion about health or about any figure, and every number comes from the sameaggregateClientKpisthe cards use.aria-sortis set.The totals row
Spend and conversions are sums. CPA is the weighted average — total spend over total conversions — not the mean of the per-client CPAs, which would let a client spending ¥12,000 pull the roster figure as hard as one spending ¥128,000. A client whose figures are withheld is left out of the sums entirely rather than counted as zero. CTR is "—": averaging CTRs across clients with different impression volumes states a number nobody measured.
The toggle, remembered
The cards stay as a toggle and the choice persists per browser (
localStorage, like the alert list's dismissals) — it is a preference about reading, not a fact about the data, so it never goes on the wire. A remembered "table" does not resurrect one for a roster that has since shrunk to a single client.One filter, both views
applyReportsHealthFilternow iterates every roster row rather than the card grid's children, so one implementation hides both views. It applies the client-name search too, and that is deliberate rather than lazy:hiddenon these rows has exactly one owner. Two functions both writing it would each undo the other depending on which ran last — the bug shape #665 is about. The filter and search compose; neither overrides.Covered with the cascade resolver: a hidden row is asserted to compute
display: noneagainst the realapp.css, not merely to carry the attribute.It is in the approved design. Nothing on the reports wire carries a target CPA: there is no field on the summary payload, and STRATEGY.md's guardrails reach no web payload. I checked before building rather than assuming:
Your spec said "don't show the bar for a client with no target in STRATEGY — don't invent one". Applied to reality that means every client, so a rendered column would be "—" the whole way down: a column advertising a comparison mureo cannot make.
So the column is omitted when no client has a target, and the renderer for it ships and is tested against a feed that has one. The day a target reaches the summary the column appears with no further work. This is the same shape phase 4 has with #690 — the view is ready and the datum is the dependency.
If you want the column visible now, the data has to land first. Worth its own issue, parallel to #690.
New module
dashboard_reports_table.js(592 lines).dashboard_reports.jswas already at 769, so the table would have pushed it past the 800 line ceiling from #687; the toolbar and view-switching live with the table they drive rather than in the index. Load order isstate → report → overview → cards → triage → table → index, and every file stays under 800.Test changes
tests/js/reports_filter_interaction.test.jsnow switches to the cards explicitly. That file is about the grid's filtering, and this change made the table the default for a roster its size — so it states which view it means rather than relying on a default that is no longer its own. Assertions unchanged.tests/js/reports_roster_table.test.jsis new: 26 tests over the realapp.htmland realapp.css— who gets the table, status ordering, header sorting and direction flip, null-sorts-last, the toggle and its persistence, the filter and search reaching table rows and composing, the totals arithmetic, and both branches of the target column.Two bugs the new tests found before review did:
And two expectations I wrote wrong: a stale client withholds its spend, so it sorts last rather than by value, and it is out of the totals. The code was right and my test was not; both are corrected with the reasoning in place.
One thing worth noting for future UI work: the harness's
click()only dispatchesaddEventListenerhandlers, so a control wired with.onclickis untestable by click. I usedaddEventListenerthroughout (with a one-time guard, since the toolbar is static markup and re-binding per draw would stack handlers).dashboard_reports_triage.js:115still uses.onclick— pre-existing, not touched here.Verification
node --test tests/js/*.test.js→tests 422, pass 422, fail 0(396 → 422)pytest tests/ -q→14 failed, 9898 passed, 8 skipped in 433.90s— all 14 enumerated by name, identical to the baseline set onmain. Zero new.ruff check mureo/ tests/→ All checks passedblack --check mureo/ tests/→ 743 files unchangedmypy mureo/ --ignore-missing-imports→ Success: no issues found in 359 source filesfont-sizevalues added (the 3 remaining are pre-existing relativeem)i18n.jsongains 14 keys in both locales.Agency multi-client capture review is yours.
Refs #691