Skip to content

feat(webui): make the analytics dashboard the default landing page - #1044

Merged
github-actions[bot] merged 6 commits into
mainfrom
feat/analytics-default-landing
Aug 25, 2026
Merged

github-actions[bot] merged 6 commits into
mainfrom
feat/analytics-default-landing

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 25, 2026

Copy link
Copy Markdown
Member

Roadmap analytics-dashboard / analytics-default-landing (the epic's only remaining task).

  • / now opens the Dashboard on its Usage (analytics) panel; the tab order is Usage, Overview.
  • /usage and /overview added as deep-linkable routes rendering the same Dashboard component (meta.dashboardView picks the panel). Tab clicks router.replace to the matching path, so a panel survives a reload/shared link without remounting the view or piling up history entries. / keeps its dashboard route name, so every existing link and guard redirect still works.
  • Sidebar "Dashboard" entry stays highlighted on all three paths.
  • First run: with zero upstream servers the analytics panel would only ever show "no data", so it renders an "Add your first server" CTA (Add Server / Browse Registry / Go to Overview) instead of the chart grid — no conditional routing.
  • Roadmap task -> in_review, ROADMAP.md regenerated.

Tests: new frontend/tests/unit/dashboard-default-landing.spec.ts (route config, landing panel, deep links, URL rewrite on tab click, first-run CTA); dashboard-usage-switcher.spec.ts updated for the flipped default. Full vitest suite 671 passed, vue-tsc --noEmit clean. Verified live against an isolated instance with a Playwright sweep (landing panel, CTA, deep links + reload, panel with a server configured, no page exceptions).

The Web UI now lands on the Dashboard's Usage (analytics) panel instead of
the hub overview. `/usage` and `/overview` are added as deep-linkable routes
rendering the same Dashboard component, so each panel survives a reload or a
shared link and the tab switcher rewrites the URL (replace) without
remounting the view or piling up history entries.

First run (zero upstream servers) would show an empty chart grid, so the
usage panel renders an "add your first server" CTA instead — no conditional
routing, the landing page is the same for everyone.

Roadmap: analytics-default-landing -> in_review (ROADMAP.md regenerated).
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: c6b6481
Status: ✅  Deploy successful!
Preview URL: https://be1bddce.mcpproxy-docs.pages.dev
Branch Preview URL: https://feat-analytics-default-landi.mcpproxy-docs.pages.dev

View logs

…ing defects

Cross-model review of the default-landing change surfaced three genuine
defects in the promoted code path; all three are covered by regression tests
that fail without the fix.

- Dashboard tab switching lost the newest click. `selectView` compared the
  requested panel against `route`, which only updates once a navigation is
  confirmed, so a second click landing before `router.replace` resolved saw
  the pre-navigation route, issued no navigation of its own, and was then
  overwritten when the first navigation completed — leaving the panel the
  user clicked away from. Track the in-flight target so the newest click
  always navigates and the router cancels the superseded one.

- The first-run CTA fired on a failed server fetch. `fetchServers` swallows
  transport errors (recording them on `loading.error` and leaving `servers`
  untouched), making a failure indistinguishable from "zero servers
  configured" — a user with a full server list was told they had none. Gate
  the CTA on the absence of a fetch error as well.

- The usage panel could repaint with stale data. `reload()` has no request
  sequencing, yet the 30s auto-refresh, the window selector and the filter
  selects can all overlap; a slower earlier response landing last replaced
  the aggregate for the window the user had already moved to, and could also
  clear a spinner the newer request still owned. Sequence the requests so
  only the newest one writes state.

Also hold the chart bundle back until the server list has arrived. Mounting
UsageView first fired a usage aggregate request and flashed the "no usage
data yet" card on a fresh install, a moment before the CTA replaced it. Both
requests are issued together in onMounted, so this costs no extra round-trip.
…etch

Second cross-model review round on the hardening pass; both findings verified
against the code before fixing.

- The tab-click marker could be cleared by the wrong navigation. It was owned
  by panel value, so two navigations to the same panel (Overview → Usage →
  Overview) shared an identity and the older one's settlement released a
  marker the newer one still owned — after which the next click compared
  against the stale current route and was swallowed. Reachable because the
  app's router guard is async (it dynamic-imports the auth store and may await
  checkAuth), so an aborted navigation settles only when its guard resumes.
  Own the marker by a monotonic token instead, mirroring the reloadSeq guard.

- The first-run gate depended on which of two responses landed last. onMounted
  issued fetchServers() twice, unsequenced, and the gate read the store's
  shared loading.error live. Drop the duplicate request, and snapshot the
  outcome of the Dashboard's own fetch: loading.error is also written by
  silent background refreshes and is never cleared on success, so a live read
  let one transient failure suppress the CTA for the rest of the session.

The same-panel marker case is fixed by construction rather than by test —
vue-router's abort ordering could not be staged faithfully from the component
harness, and a test encoding the wrong model would be worse than none. The
burst test asserts the end state only; the other two findings have regression
tests that fail without their fix.
…oaded

Third cross-model review round; both findings verified before fixing, and they
share one root cause: the Dashboard was inferring "did the server list load"
from `loading.error`, a field it does not own.

- An unrelated failure suppressed the first-run CTA. App.vue also calls
  fetchServers() on mount, concurrently with the Dashboard's own call, and
  silent background refreshes write the same shared field. If that other
  request failed first, the Dashboard snapshotted a failure that had nothing
  to do with its own successful fetch and hid the CTA. Removing the Dashboard's
  duplicate request last round narrowed the window but could not close it —
  the racing caller is in another component.

- A successful refresh could never restore the CTA. The snapshot was taken once
  from the initial request, so an initial failure followed by a background
  refresh that succeeded and confirmed zero servers left the CTA gone for the
  rest of the session.

Add a `loaded` flag to the servers store, set only when a list actually arrives
successfully, and gate the CTA on that. It is monotonic and owned by the store,
so a concurrent or later failure elsewhere cannot clear it and a later success
does set it. The chart mount keeps a separate local flag for "our initial fetch
has settled, successfully or not", so a failed fetch still falls through to the
usage panel instead of spinning forever.
@github-actions

github-actions Bot commented Aug 25, 2026 •

Copy link
Copy Markdown
Contributor

📦 Build Artifacts

Workflow Run: View Run
Branch: feat/analytics-default-landing

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (29 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (23 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 32808978101 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

Fourth cross-model review round. Both findings verified, and they converge on
one root cause: `servers` has three independent writers that never cancel each
other — a fetch from any component (App.vue and Dashboard.vue each issue one on
mount), a silent background refresh, and the Spec 047 SSE full-list payload.

- A stale response could resurrect an old list. Every successful fetch applied
  its payload unconditionally, so if the user added a server and the resulting
  refresh returned before an older in-flight request, that older response
  overwrote the list — dropping the new server and bringing the first-run CTA
  back. The same race lets a fetch clobber a list just delivered by SSE.

- An SSE-delivered list did not count as loaded. The Spec 047 fast path applies
  an authoritative full list but left `loaded` false, so the usage panel kept
  spinning while a list was already in hand, and an empty SSE list could never
  raise the first-run CTA.

Route all three writers through one applyServerList() that takes a ticket in
issue order, drops anything older than the last applied list, and marks the
store loaded. The Dashboard's chart gate now also clears on `loaded`, so a list
arriving by SSE before the HTTP fetch settles ends the spinner immediately.
Fifth cross-model review round, one finding, verified before fixing.

`deleteServer` removed the server from local state directly without advancing
the list ticket introduced last round. A GET issued before the delete landed
therefore still satisfied the sequencing check and re-applied the pre-deletion
list, resurrecting the server the user had just deleted — and, on the last
server, keeping the first-run CTA away until another authoritative list
arrived.

Route the removal through applyServerList() with a fresh ticket: the post-delete
list is authoritative as of that moment, so anything already in flight is now
correctly treated as stale. `addServer` needed no change — it awaits
fetchServers(), which takes its own newer ticket.
@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot enabled auto-merge (squash) August 25, 2026 11:03
@github-actions
github-actions Bot merged commit 48892c0 into main Aug 25, 2026
39 checks passed
github-actions Bot pushed a commit that referenced this pull request Aug 25, 2026
…up, and "what needs me" on the landing page (#1049)

* fix(webui): give first run a way forward (F3, F19) and keep "what needs me" on the landing page (F4)

Web UI UX audit (docs/qa/ux-audit-webui-2026-08.md), first-run and onboarding.

F3 — Servers page, zero servers. The page a new user lands on after closing
the wizard said "No servers found / No servers available" and offered nothing
to click, above four zero-valued stat tiles, four zero-count filter pills and
a search box with nothing to search. It now renders the AgentTokens-style
empty state: a headline, one line of why upstream servers matter, Add Server
(primary), Browse Registry, and a quiet "Import from your AI client configs"
link into the wizard's Servers step. The stat/filter/search chrome is hidden
until a server exists, and "you have nothing yet" is now a separate state from
"nothing matched your filter" — the two need different answers.

F4 residual (after #1044) — #1044 routed /usage, made the analytics panel the
default landing page and demoted the hub diagram, which is most of the fix.
What it did not carry across is the other half the finding asked for: the
landing page still answered "what happened" and not "what needs me". The
servers-needing-attention and tools-pending-approval banners were inside the
Overview panel, so a fresh landing could render charts while a server was down
and an unreviewed tool was waiting, and say neither. They move above the panel
switcher: visible on whichever panel is active, silent when there is nothing
to act on.

F19 — onboarding wizard, Servers step. It was entirely "pick which servers
from your existing AI clients to import", so a user with no prior MCP config
hit a dead end. Adds a nothing-to-import branch (browse the registry / add a
server manually). Import & quarantine becomes the single primary and the
unreviewed path becomes a link that says what it skips, "Import without
review" — previously two similar primaries with the safer one rendered weaker.
The Docker-isolation/quarantine panel is expanded by default and moved out of
the sticky footer into the step body, where it can be open without squeezing
the list; the import list is capped and scrolls in place instead of clipping
its last row; and every step gets a Back.

* fix(webui): initialise the wizard when it is mounted already-open, and keep every step exitable

Cross-model review (opencode/gpt-sol) on the first-run changes.

P1 — the Servers page's import link sets the store flag and then routes to the
Dashboard, which is the component that mounts the wizard. The wizard therefore
came into existence with `show` already true and never saw a transition to
watch: it rendered, but the open sequence never ran. The onMounted fallback
that existed for this case kicked off the fetches but never chose a tab and
never consumed the request — so the wizard opened on Clients regardless of what
the opener asked for, and left the request behind to hijack the next plain
open. One `immediate` watcher now owns opening; the fallback is gone. The
requested tab is read before the first await, so it cannot outlive its own open
or be consumed twice, and it applies immediately so the wizard never paints the
wrong step while the fetches are in flight.

P2 — `goToRegistry` did not await `dismiss()`. `dismiss()` awaits its
mark-skipped calls before emitting `close`, so routing first tore down the
Dashboard that owns the open flag and the emit could land with nothing left to
clear it, springing the wizard back open on return.

P2 — the Servers page fetches on neither mount nor route: App.vue and the
Dashboard do. That leaves a window where `loading` is false and the list is
still unknown, in which the filter/search empty state claimed "no servers match
your search criteria" about a list nobody had fetched. Adds an explicit pending
state, gated so servers already in hand are never held behind it.

Also restores Close to the import action footer, which the review asked about
and the live run then proved: a step whose only exits are "import" strands the
user, and the committed e2e sweep dismisses an auto-opened wizard through
exactly that control — on a fresh instance the wizard can land on this step.
The security disclosure gains `group` so its arrow reflects the now-open state.

* fix(webui): abandon superseded wizard opens, and stop a failed refresh from hiding the server list

Round 2 of the cross-model review (opencode/gpt-sol).

P1 — nothing cancels the wizard's open sequence. A close, or a close followed
by a reopen, while its five fetches were still in flight let the abandoned run
finish and stamp its tab over the newer state — and call startPolling() on a
wizard that was no longer open, leaving a 5s poll running until unmount. Each
run now takes a generation token and checks it still owns the wizard before it
writes; closing and unmounting both retire whatever is in flight.

P2 — `loading.error` was write-only. Silent background refreshes set it and no
success ever cleared it, so a single transient blip replaced the Servers page
with a full-page "Failed to load servers" for the rest of the session, hiding a
list we were still holding. A list that arrives now clears the error, and the
blocking error state is reserved for the case with no fallback: when servers
are already in hand, a failed refresh degrades to a non-blocking notice above
the grid that says the data may be stale and offers Retry.

* fix(webui): sequence the wizard's config read and the server-list failure path

Round 3 of the cross-model review (opencode/gpt-sol).

The open sequence's config read is the one fetch whose result the user can also
edit: it drives the quarantine and Docker-isolation checkboxes, and this branch
expands that panel by default, so toggling one the moment the step appears is
now the expected path rather than a corner. A read left over from a superseded
open landing afterwards would silently flip the checkbox back to the old server
value. It now carries the open's generation token and does not commit if it has
been superseded. The other fetches in the sequence write read-only display
state that either run populates identically, so they are left alone.

Server-list failures now take the same ticket as successes. Only the list
itself was sequenced, so an older request failing after a newer one had already
delivered a list would raise a stale error over fresh data — the mirror image
of the stale-list problem the sequencing exists to prevent.

* fix(webui): let a failed server fetch advance the sequencing mark

Round 4 of the cross-model review (opencode/gpt-sol).

Only a delivered list advanced the mark, so a newer request that failed left it
behind — and an older list arriving afterwards still passed the check, applying
stale servers over the newer outcome and clearing its error on the way. Both
outcomes are news about the same list, so both now claim the ticket.
Dumbris added a commit that referenced this pull request Aug 26, 2026
The document read as an open P0-P3 backlog, but seven PRs merged on 2026-08-25
(#1044, #1048, #1049, #1051, #1052, #1053, #1054) closed it. Anyone opening this
file against current main was being handed 36 stale defects.

Added a status banner and a Resolution section that separates three things
honestly:

- Re-checked live on v0.61.0 against a running isolated core with real /mcp
  traffic: F1, F4, F5, F7, F8, F12, F13, F20, F24, F26, F33 confirmed fixed,
  each with the observed evidence rather than a PR reference.
- Still open: F6 is half fixed. Focus enters the Add Server dialog and submit is
  in a sticky footer, but Escape still does not close it - AddServerModal.vue has
  no key handler and JsonViewer.vue is the only component under components/ that
  handles Escape at all. F28's "key called optional" half is fixed; its
  three-surfaces state is not.
- Not re-checked: named explicitly rather than implied, so the next reader knows
  which closures rest on a PR body and which on an observation.

Five defects found on main while re-checking are recorded separately, since they
are not regressions of anything in the audit. The first is security-relevant:
quarantining a server through the config file leaves its tools in the search
index, so retrieve_tools still hands the agent their names and descriptions -
which is exactly what quarantine exists to prevent - before the call is refused.
Runtime.QuarantineServer purges the index; the reload path detects the same
transition (quarantined_changed: true) and does not.

Also corrects the record on #1053: its body names F1-F16, which is the tray
audit's numbering mis-pasted. #1055 is the tray PR; #1053 touches no Swift file.
Dumbris added a commit that referenced this pull request Aug 26, 2026
The document read as an open P0-P3 backlog, but eight PRs merged on 2026-08-25
(#1044, #1048, #1049, #1050, #1051, #1052, #1053, #1054) closed it. Anyone
opening this file against current main was being handed 36 stale defects.

Added a status banner and a Resolution section that separates four things:

- Re-checked live on v0.61.0 against a running isolated core with real /mcp
  traffic: F1, F4, F5, F6, F7, F8, F12, F13, F20, F24, F26, F33 confirmed fixed,
  each with the observed evidence rather than a PR reference.
- Not re-checked: named explicitly rather than implied, so the next reader knows
  which closures rest on a PR body and which on an observation. F28 is called
  out as partial - its "key called optional" half is fixed, its
  three-surfaces-at-once state is not.
- Corrections to the audit itself: F23 mis-diagnosed a per-request structural
  estimate as a stale aggregation, and F31's "Manual badge has no tooltip" half
  was wrong even at audit time (spec-088 trust-mode badge, already titled).
- Five defects found on main while re-checking, recorded separately since they
  are not regressions of anything in the audit. The first is security-relevant:
  quarantining a server through the config file leaves its tools in the search
  index, so retrieve_tools still hands the agent their names and descriptions -
  which is exactly what quarantine exists to prevent - before the call is
  refused. Runtime.QuarantineServer purges the index; the reload path detects
  the same transition (quarantined_changed: true) and does not.

F6 carries a methodology note. Re-checking it produced a false negative: Escape
driven through the browser-automation layer never reaches the page's document
keydown listener, so the dialog looked stuck open. Dispatched in-page it closes
at once, and #1050's useModalA11y is wired into all five <dialog :open> modals.
The sweep assertion this document proposes must dispatch in-page, or it tests
the automation layer instead of the app.

Also corrects the record on #1053: its body names F1-F16, which is the tray
audit's numbering mis-pasted. #1055 is the tray PR; #1053 touches no Swift file.
Dumbris added a commit that referenced this pull request Aug 26, 2026
Verified against v0.60.0 rather than taken on trust:

- The tray Settings tab is named "Raw", not "Raw JSON"
  (native/macos/MCPProxy/MCPProxy/Views/SettingsView.swift:38 at v0.60.0). The
  §1.2 inventory, the §2 parity matrix and F6's body all said "Raw JSON".
- The Web UI had no /usage route at v0.60.0 - the router's path list runs
  /, /servers, /repositories, /search, /settings, /secrets, /sessions, /tools,
  /activity, /security, /tokens with no usage or overview entry. Usage was a
  Dashboard tab; /usage became a route later, in #1044. The parity matrix's
  "Usage report | ⚠ savings % on Dashboard | ✅ /usage | inconsistent" overstated
  the Web UI side of that comparison.

Also noted in §6 that three §1.1 menu labels have changed since the walk as part
of the F9 fix: Open MCPProxy Window, Open Web UI in Browser, Launch at Login.
Dumbris added a commit that referenced this pull request Aug 26, 2026
* docs(qa): Web UI UX audit — ranked findings (ux-audit epic)

Heuristic + live-instance audit of every routed Web UI view in light/dark,
first-run and populated states, at 1440/820/390px. 36 findings ranked P0-P3
with view, evidence, heuristic violated, suggested fix and effort.

Audited against standard heuristics plus the house activity-UI rules
(grouping, failure-only status, intent reason, histogram/log parity).

* docs(qa): record what the Web UI audit's backlog actually resolved to

The document read as an open P0-P3 backlog, but eight PRs merged on 2026-08-25
(#1044, #1048, #1049, #1050, #1051, #1052, #1053, #1054) closed it. Anyone
opening this file against current main was being handed 36 stale defects.

Added a status banner and a Resolution section that separates four things:

- Re-checked live on v0.61.0 against a running isolated core with real /mcp
  traffic: F1, F4, F5, F6, F7, F8, F12, F13, F20, F24, F26, F33 confirmed fixed,
  each with the observed evidence rather than a PR reference.
- Not re-checked: named explicitly rather than implied, so the next reader knows
  which closures rest on a PR body and which on an observation. F28 is called
  out as partial - its "key called optional" half is fixed, its
  three-surfaces-at-once state is not.
- Corrections to the audit itself: F23 mis-diagnosed a per-request structural
  estimate as a stale aggregation, and F31's "Manual badge has no tooltip" half
  was wrong even at audit time (spec-088 trust-mode badge, already titled).
- Five defects found on main while re-checking, recorded separately since they
  are not regressions of anything in the audit. The first is security-relevant:
  quarantining a server through the config file leaves its tools in the search
  index, so retrieve_tools still hands the agent their names and descriptions -
  which is exactly what quarantine exists to prevent - before the call is
  refused. Runtime.QuarantineServer purges the index; the reload path detects
  the same transition (quarantined_changed: true) and does not.

F6 carries a methodology note. Re-checking it produced a false negative: Escape
driven through the browser-automation layer never reaches the page's document
keydown listener, so the dialog looked stuck open. Dispatched in-page it closes
at once, and #1050's useModalA11y is wired into all five <dialog :open> modals.
The sweep assertion this document proposes must dispatch in-page, or it tests
the automation layer instead of the app.

Also corrects the record on #1053: its body names F1-F16, which is the tray
audit's numbering mis-pasted. #1055 is the tray PR; #1053 touches no Swift file.
Dumbris added a commit that referenced this pull request Aug 27, 2026
* docs(qa): macOS tray UX + settings-parity audit

Read-only sweep of the running v0.60.0 tray (accessibility-tree menu walk)
plus a static comparison of the tray's native surfaces and settings
catalogue against the Web UI.

Parity matrix (feature surfaces, settings fields, per-server actions) and
16 ranked findings: dead menu-bar health badge, unlabelled status item,
silent action failures, unreachable Agent Tokens view, and three settings
fields that drifted out of the tray catalogue.

* docs(qa): mark the tray audit resolved, and say what was not re-verified

The document read as an open backlog, but #1055 ("fix(tray): close the 16
findings of the macOS tray UX + settings-parity audit", merged 2026-08-25) fixed
all 16, with #1056 as a catalogue follow-up. The header's "no version skew, so
every finding below is reproducible from source" is no longer true against main.

- Header scopes that claim to the time of the walk and carries a status banner.
- New §6 Resolution records the two fixes whose obvious implementation is wrong:
  F1's severity dot is an attributed-title glyph, not a composite image (the
  status image stays isTemplate and a template image renders monochrome), and F6
  added a parity gate rather than just the three missing keys.
- Verified on main rather than taken on trust: Menu/TrayIcon.swift is deleted,
  TokensView is reachable from Views/MainWindow.swift, MCPProxyApp.swift sets
  isTemplate + accessibilityDescription + toolTip + attributedTitle glyph, and
  SettingsCatalog.swift carries deep_scan / instructions /
  code_execution_max_parallel.

Note for anyone tracing this later: #1053's body claims to fix these 16 tray
findings, but #1053 touches no Swift file — it is the Web UI cluster its own
title names. #1055 is the tray PR.

The caveat is stated rather than hidden: this pass could not re-walk the live
tray, because macOS Accessibility was revoked on the reviewing machine
(check_accessibility -> trusted:false), so the resolution is read from the #1055
diff, not from a second walk.

* docs(qa): correct two §1 claims that were wrong at the time of the walk

Verified against v0.60.0 rather than taken on trust:

- The tray Settings tab is named "Raw", not "Raw JSON"
  (native/macos/MCPProxy/MCPProxy/Views/SettingsView.swift:38 at v0.60.0). The
  §1.2 inventory, the §2 parity matrix and F6's body all said "Raw JSON".
- The Web UI had no /usage route at v0.60.0 - the router's path list runs
  /, /servers, /repositories, /search, /settings, /secrets, /sessions, /tools,
  /activity, /security, /tokens with no usage or overview entry. Usage was a
  Dashboard tab; /usage became a route later, in #1044. The parity matrix's
  "Usage report | ⚠ savings % on Dashboard | ✅ /usage | inconsistent" overstated
  the Web UI side of that comparison.

Also noted in §6 that three §1.1 menu labels have changed since the walk as part
of the F9 fix: Open MCPProxy Window, Open Web UI in Browser, Launch at Login.

* docs(qa): re-walk the live tray and record that the findings still hold

Replaces the "could not re-walk" caveat with the walk itself. macOS
Accessibility was granted, so mcpproxy-ui-test works now; the walk reproduces
this audit almost verbatim, because the installed tray is still v0.60.0 with
"Update 0.61.0 — ready to restart?" sitting in the menu.

Reproduced live: F2 (empty status-bar title, no description, no tooltip), F5 (no
Agent Tokens entry), F7 (ElevenLabs "Stop" vs cloudflare-graphql "Disable"; and
memory showing status Disabled above action Start), F8 (all three quarantined
servers offer only Disable/Stop, Restart, View Logs), F11 (research and deploy
still offered at 0 tools), F12 (streamable-http beside http and sse), F15 (29
flat entries), F16 (no tool search), and the pre-#1055 menu labels.

One drift worth noting: the Clients block now reads "No clients in the last 24h"
rather than listing an idle client - the empty state H5 exists to avoid.

The consequence for the resolution section: #1055's fixes are real in the diff
but remain unverified against a running tray. That needs the pending 0.61.0
restart or the dev bundle-swap, neither of which this pass did - restarting the
tray kills the core it manages.
Dumbris added a commit that referenced this pull request Aug 29, 2026
…inish the ux-audit sweep (#1085)

* chore(roadmap): truth-sync ux-audit and upgrade-nudge

Two epics claimed a state the repo contradicts.

upgrade-nudge was `done` with all four tasks done, but Spec 079 FR-002 —
the human-readable "N releases / M weeks behind" delta required on status,
doctor, the startup log, the Web UI and both trays — was never built. The
four shipped tasks are US-slices and the FR belonged to none of them; spec
079 has only spec.md, so no checkbox surface could catch the gap. It
survived only as TODO(spec-079/FR-002) comments in
internal/updatecheck/checker.go and cmd/mcpproxy/status_cmd.go, which is
how an outside contributor found it (#1081). Epic back to in_progress with
an explicit upgrade-nudge-delta task.

ux-audit was `todo` with both sweeps todo, but both shipped in August
(#1046 Web UI, 36 findings; #1043 macOS tray, 16 findings) along with ten
PRs closing every finding and the five defects the re-check pass raised
(#1061/#1064/#1065, all closed). Recorded the sweeps and fix waves as done
and added the two items that really are open: folding the audit's F6 modal
Escape/focus and F1 cross-view total assertions into e2e/web-ui-sweep, and
verifying the tray fixes on a running tray (the 2026-08-26 re-walk found
the installed tray still on v0.60.0, so #1055 remains unobserved).

Also cleared the two --check-github errors, neither introduced here:

- tpa-db and mcp-2026-upgrade each linked a docs(specs) PR in `pr:`. That
  field is implementation evidence, so a spec merge read as "epic done".
  Both entries' own notes already said implementation had not started;
  moved the refs into the notes and left the statuses honest at todo.
- analytics-default-landing (#1044) and schema-deferred (#1063, tasks.md
  89/89) were genuinely finished and are now done.

* feat(079): "N releases / M weeks behind" upgrade delta (FR-002)

Closes the last outstanding slice of Spec 079. FR-002 requires the
current version, the latest version AND a human-readable delta on
`mcpproxy status`, `doctor`, the startup log, the Web UI and both trays;
what shipped was latest-version-only, and the deferral survived as
TODO(spec-079/FR-002) comments that an outside contributor found (#1081).

The blocker was data, not rendering: the checker fetched only
/releases/latest, so it had the offered release's publish date but not the
running one's, and no tag sequence to count.

internal/updatecheck/delta.go computes the delta as a pure, network-free,
clock-free function, so every spec rule is a table row. Fetching is a
best-effort enrichment hung off the existing check — never a second check
pipeline (FR-001):

- one page of /releases (ETag-conditional, so a repeat costs no rate-limit
  budget), plus a by-tag lookup only when the running build predates that
  page and its date is therefore unknown;
- both requests share one context deadline. A per-request timeout bounds
  each hop and nothing bounds the total, and CheckNow runs synchronously
  inside GET /api/v1/info?refresh=true, so an unbounded delta would stall
  a request a user is waiting on;
- counting is by semver, never by list position: GitHub orders the list by
  created_at, and the live data has v0.53.0-rc.9 sitting ahead of the
  later-published rc.10.

Three degradation rules matter more than the feature. A delta miss must not
set CheckError (statusVersionSuffix returns "" whenever it is non-empty, so
that would DELETE the nudge instead of enriching it), must not touch the
FR-018 backoff counters, and must leave every surface rendering exactly what
it rendered before. There is a regression test per rule.

The clause is rendered once in Go (FormatBehindSummary) and printed verbatim
by every surface, with the raw numbers alongside for machine consumers. That
is deliberate: GuidanceLine is already hand-ported into UpdateBanner.vue with
a "keep the two in sync" comment, and FR-002 asks for consistent framing
across five surfaces.

Fields are additive per FR-021 and had to be threaded through six
hand-maintained mirrors — VersionInfo, the InfoResponseUpdate DTO and its
copier, internal/contracts (→ make swagger), the contracts.ts generator,
status_cmd's map decoding, and the Swift tray's Codable, which turned out
never to have decoded the US2 fields either.

Two adjacent bugs fixed because the delta sits on the same code:

- GetLatestReleaseIncludingPrereleases returned releases[0] from a
  created_at-ordered list, so an RC user could be offered a release older
  than one already published. Now max-by-semver.
- GetReleaseByTag interpolated an unvalidated tag into the request path and
  deferred validation to callers by comment only. The delta feeds it the
  ldflags-stamped version, so it now validates and escapes.

Both trays render the delta in the update item's tooltip rather than its
title: Spec 092 FR-010 fixes the title's exact shape, and a menu row reading
"Update v0.62.0 — ready to restart? — 8 releases / ~14 weeks behind" stops
being scannable.

Known bound, accepted deliberately: the scan reads one 100-release page, so a
build older than that window reports a lower bound ("59+ releases") and a
semver-in-range release created long ago can be missed without setting that
flag. Spec 079's own assumption (L159) allows an approximation where an exact
count is unavailable, and the figure is motivational, not load-bearing.

Verified: unit + integration tests against an httptest GitHub (delta,
saturation, tag fallback, 304 reuse, budget, malformed tags), `go test -race`
across internal and cmd, golangci-lint v2 clean, 962 frontend unit tests, and
a live version-stamped instance confirming the degradation path renders the
pre-delta output when the check fails.

* test(ux-audit): finish the sweep's regression net, and fix what it caught

The August Web UI audit closes by asking for four assertions to be folded
into the committed sweep. Three were already there (F9 contrast, F30
accessible names + aria-live, F14 responsive); this adds the two that were
not, and then fixes the defects they immediately exposed on main.

F6 — the Add Server modal takes focus, traps Tab, closes on Escape and
restores focus to its trigger. These `<dialog :open>` modals are opened by
the `open` attribute rather than showModal(), so none of that comes from the
browser; it all comes from useModalA11y, whose own comment delegates the
real-browser half of its coverage to "the Playwright sweep" — a promise the
sweep did not keep. Escape is dispatched IN-PAGE, never via
page.keyboard.press: re-checking F6 during the audit produced a false
negative for exactly that reason, because a synthetic key never reaches the
document listener under test.

F1 — Activity and Usage must report the same 24h numbers as the API. The
invariant is not naive equality: Usage folds `blocked` into its error tile
while Activity splits them, and /activity/usage is served from a short read
cache while /activity/summary counts live, so the assertion polls for
convergence and encodes the real relationship.

Running the completed sweep turned up three defects already on main, none of
them regressions of the audit's 36:

1. The LANDING PAGE failed AA in both themes (3.09-4.23:1) and two of its
   controls had no accessible name. Usage.vue uses the raw `opacity-*`
   utility for muted text; #1054 fixed that failure mode for the
   text-base-content ramp and .stat-* but never covered raw opacity, and
   nothing caught it because the audit walked `/` before #1044 made Usage
   the landing page. Moved onto the AA-checked --tone-muted; both selects
   got an aria-label.

2. The Activity status pill clips at 390px for any status outside the known
   four. An unmapped status falls through to a ghost pill carrying the raw
   value, and `tool_auto_approved` measured 145px inside a 93px cell (the
   table is table-fixed below 640px). The pill now truncates against the
   cell and keeps the full value in its title, rather than the column being
   sized to today's longest label — which would hold only until the next
   status is added.

The phone check that finally caught #2 had been passing vacuously: it
measures rows.first(), and a `success` row — ~90% of traffic — renders an
sr-only label of zero width. It only became a real assertion once the new F1
check generated an error row and put one first. That coupling is worth
knowing about: the sweep shares one instance, so one check's traffic is the
next check's data, and a newly-failing neighbour after adding a check is more
likely a real find than a flake. Both were separated from the change under
test by running the same sweep against clean HEAD.

Sweep: 25 passed / 3 failed on clean HEAD, 30 passed / 0 failed now.

Roadmap: ux-audit was still carrying both sweeps as `todo` although they and
every finding shipped in August; recorded here along with what the completed
net found.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants