feat(tray): render the 24h activity histogram inline at the top of the menu - #950
Conversation
…e menu The chart used to hide behind an "Activity (24h)" submenu — the overview was the only glance row that required a second navigation step. It now renders inline, directly under the summary line, so the day's shape is on screen the moment the tray icon is clicked. - GlanceSection builds the row eagerly but caches the chart item by its shaped bars (HistogramBar is Equatable), so the debounced closed-menu rebuilds do not re-render a SwiftUI chart nobody is looking at. - The row's KIND (loading placeholder / failure row / 132pt chart) is structural in updateInPlace — a kind flip resizes the menu, so it defers to menuDidClose like every other structural change. Within a kind, bars swap the hosted view in place and a failure refreshes its tooltip. - The submenu's "Not updating" marker is not ported: the inline row sits directly under the summary line, whose "not updating" segment already says it. - HistogramSubmenuDelegate is deleted; its loading/failed/chart decision (ActivityHistogram.state) and the chart factory seam are unchanged.
Deploying mcpproxy-docs with
|
| Latest commit: |
b4be59b
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://01c32d5d.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://feat-tray-inline-24h-histogr.mcpproxy-docs.pages.dev |
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 30830407136 --repo smart-mcp-proxy/mcpproxy-go
|
…round 1) The adversarial panel + opencode cross-model review caught a real regression: making the histogram kind structural meant a timeline that loads while the menu is open — the common open-just-after-launch case — returned false from updateInPlace on every tick, freezing the summary, row identities and client presence for the whole menu session. Main has a test pinning exactly this (testTheTimelineArrivingWhileTheMenuIsOpen- KeepsRowsUpdating), and it failed on the branch. - The histogram never vetoes the in-place update. Its two text kinds share one-line geometry and rewrite each other in place (a failing fetch replaces "loading…" instead of leaving it up); a text-to-chart flip keeps the built row on screen — the height cannot change under the cursor — and the next rebuild installs the chart, since menuWillOpen runs one before every display. - The chart cache is also keyed by the current time zone: the bars are UTC-keyed and survive a zone change, but the VoiceOver "busiest hour" label does not. - Stale "submenu" wording in the factory seam doc corrected.
|
Adversarial review round 1 (two independent lenses + opencode/gpt-5.6-sol cross-model; codex quota-blocked) — findings, all addressed in c3cb0ed:
Verified clean by the lifecycle lens on the current tip: cached-item re-add across rebuilds (single NSMenu, Deliberate accessibility decision: the old enabled "Activity (24h)" submenu anchor was keyboard-highlightable; the inline chart is a disabled view-backed item, skipped by arrow-key navigation (the chart data itself was equally keyboard-unreachable in the submenu — its only row was disabled). The full series summary stays on the host view for the VoiceOver cursor, and the summary text line above carries the headline numbers. Note: |
… prose The menu was over 600pt wide: failed rows put a 40-char error clause on the title line next to a 34-char label, and Needs-Attention rows carried untruncated core errors. The 24h chart also spent 132pt of height on a mostly-empty plot. Width discipline: the widest thing in the menu is now the chart block — the element designed to be looked at. - Failed rows are one fact per line: title is always `label ×N — age`; the error clause takes the muted second line (the failure mark already flags the row) and the displaced reason stays in the tooltip and the VoiceOver string. Pre-14.4, with no second line, the clause rejoins the title under a tighter 28-char budget. - A row GAINING its error line is structural (defers to close), exactly like a run gaining its reason; the identity-turnover and late-failure liveness tests pin the one-line fallback where the rewrite happens in place. - Budgets: label 34→30, second line 60→44 — both sized to the chart block width at the menu font. - Chart: 248×84 incl. legend (was 260×116), item 272×96 (was 288×132) — a shape to recognise, not a plot to read values off; the Web UI has the full-size version. - Needs-Attention rows truncate to the same 44-char budget, full text in the tooltip.
|
Round 2 — compact revision (owner feedback: menu too wide, chart too big; screenshot-verified live):
This is a deliberate compact revision of spec 090 FR-011a ("error joins the title") at the owner's request. Full suite: 787 tests, only the known environmental AppLifecycleTests failure (fails on main too). |
Round-2 verify notes: the 44-char truncation of Needs-Attention rows also truncated what VoiceOver reads (tooltips are not spoken), so the row now carries the full text as its accessibility label — the same FR-025 truncated-on-screen/spoken-in-full discipline as the glance rows. Three comments still describing the chart as 132 pt updated to its actual 96 pt.
…e day Owner feedback round 3: - Needs Attention is one row with a count and a warning icon; the per-server rows live in its submenu, a hover away. N servers no longer cost N rows of a menu that opens with a chart. The section stays absent entirely when nothing needs attention (as before). - A loaded-but-idle 24h axis renders as the muted sentence "No calls in the last 24h" instead of 24 empty bars — an all-zero chart reads as a broken widget, while the words say what the flat axis implied. Idle is a third text kind: it swaps with loading/failed in place (one-line geometry) and defers a flip to/from the chart to the next rebuild like the other text kinds.
|
Round 3 (owner feedback): Needs Attention collapses into a submenu — one row with count + warning icon, per-server detail on hover, absent entirely when nothing needs attention. An idle 24h axis renders words, not an empty chart: muted "No calls in the last 24h" (new third text kind — swaps in place with loading/failed, text↔chart flips defer to the next rebuild like before). Both screenshot-verified live on a dev build. Cross-model review (opencode) on the delta: no genuine defects — histogram state/cache transitions consistent, submenu ownership survives rebuilds, collapsed parent stays accessible. Suite: 787 tests, only the known environmental AppLifecycleTests failure. |
Owner-reported contradiction: the Recent list showed calls from days ago directly under a chart saying "No calls in the last 24h" — three windows (hour summary, 24h chart, unbounded recent list) in one block. The menu now speaks ONE frame, the 24 hours the histogram draws: - Header: "N calls in the last 24h" via new glanceCallsLast24h — the same poll+live-SSE reconciliation as the hour count (GH #934), summed over the histogram axis. A zero says nothing the idle row does not, so the segment appears only when there is something to count, and a summary with nothing to say gets no row (presence flip = structural). - Recent: rows filtered to the frame (GlancePresence.lookback — already 24h for clients); a retained record from days ago no longer appears. With nothing qualifying, the section is just "Open Activity…" — a header over an empty list explains nothing. - Clients placeholder: "No clients in the last 24h", naming the frame. - The hour-count machinery stays (callsThisHour + glanceCallsThisHour and their reconciliation tests are the tested contract the 24h getter shares its live-call state with). Header-consistency tests now pin the frame semantics: counts SURVIVE an hour rollover; only sliding off the 24h axis drops them.
…itic) Codex round 1 on the frame commit: Recent filtered on a raw 86,400 s cutoff while the header and chart use the hour-aligned 24-bar axis, so at e.g. 12:15 a call from yesterday 12:30 showed in Recent while the chart had already dropped its hour — recreating the contradiction one hour at a time. Recent now keeps a run only while its HOUR is still on the axis, the same rule ActivityHistogram.bars and callsInLast24Hours apply. Its other finding — the poll/SSE snapshot watermark race — is the pre-existing, deliberate GH-#934 trade-off (boundary = issue time; transient over-count self-corrects at the next 30 s poll), unchanged.
|
Round 4 (owner feedback): one 24h frame for every number in the menu. The reported contradiction — Recent rows from days ago under "No calls in the last 24h" — is gone:
Codex is back and served as critic: round 1 caught a real edge (Recent used a raw 86,400 s cutoff vs the hour-aligned axis — a call 23 h 40 m old could show in Recent after its hour slid off the chart); fixed in b4be59b with an edge test. Round 2: CLEAN, and it confirmed the poll/SSE snapshot watermark race it flagged is the pre-existing, deliberate GH-#934 trade-off on main, not worsened here. Screenshot-verified live. Suite: 789 tests, only the known environmental AppLifecycleTests failure. |
What
The 24h activity chart used to hide behind an "Activity (24h)" submenu — the overview was the only glance row that required a second navigation step. It now renders inline, directly under the summary line, so the day's shape is on screen the moment the tray icon is clicked.
Verified live on a dev build (screenshot in the session): summary → chart → Recent → Clients, no submenu.
How
GlanceSectionbuilds the row eagerly but caches the chart item by its shaped bars (HistogramBarisEquatable), so the debounced closed-menu rebuilds never re-render a SwiftUI chart nobody is looking at.rebuildMenureuses oneNSMenuviaremoveAllItems(), so re-adding the cached item is safe.updateInPlace: a kind flip resizes the menu, so it defers tomenuDidCloselike every other structural change (FR-023 discipline). Within a kind, new bars swap the hosted view in place (fixedchartItemSizeframe, no resize) and a failure row refreshes its tooltip.HistogramSubmenuDelegateis deleted. The pure loading/failed/chart decision (ActivityHistogram.state) and the injectable chart factory seam are unchanged.AppStateonly, no network request.Tests
Old submenu-structure tests are replaced by
GlanceInlineHistogramTests: the three row kinds, chart-wins-over-stale-failure, timeline-arrives-while-closed, the eager-build cache (factory called once for unchanged bars, same item instance), kind-flip-is-structural both directions, in-place view swap under an open menu, tooltip refresh, and the real-chart size/accessibility checks. Fullswift testsuite: 725+ tests, 0 failures.