Skip to content

feat(update): update_check config block + dismissible Web UI update banner (Spec 079 US1)#805

Merged
Dumbris merged 8 commits into
mainfrom
feat/079-banner-config
Jul 3, 2026
Merged

feat(update): update_check config block + dismissible Web UI update banner (Spec 079 US1)#805
Dumbris merged 8 commits into
mainfrom
feat/079-banner-config

Conversation

@Dumbris

@Dumbris Dumbris commented Jul 3, 2026

Copy link
Copy Markdown
Member

Motivation

Spec 079 (specs/079-upgrade-nudge) US1: make version updates visible and controllable in the personal edition. This PR delivers the US1 remainder — a user-facing update banner in the Web UI plus a first-class update_check config block (FR-005, FR-012–FR-015) so update checking can be disabled or pointed at the RC channel from config, not just env vars. Roadmap: personal-edition polish initiative (upgrade-nudge task).

Implementation

  • update_check config block (internal/config): { "enabled": bool (default true), "channel": "stable"|"rc" } with nil-safe accessors and validation.
    • enabled=false gates both the background poll and manual CheckNow (/api/v1/info?refresh=true becomes a no-op — no network call), and GetVersionInfo() returns nil so /api/v1/info omits the update object entirely. Every surface (banner, sidebar badge, status/doctor annotations) goes quiet (FR-015).
    • channel:"rc" is the config-file equivalent of MCPPROXY_ALLOW_PRERELEASE_UPDATES; resolved per-check so a stable⇄rc switch applies immediately (FR-013).
    • Env precedence (FR-014): the spec leaves precedence open; documented decision is env wins as an operator override, and only in the widening/disabling direction — MCPPROXY_DISABLE_AUTO_UPDATE=true force-disables, MCPPROXY_ALLOW_PRERELEASE_UPDATES=true force-includes prereleases; env cannot re-enable config-disabled checking.
    • Hot-reload: DetectConfigChanges reports update_check; both reload paths (runtime ApplyConfig API path and lifecycle ReloadConfiguration disk path) re-gate the running checker. The background loop stays alive while disabled; re-enable/channel switch triggers a prompt re-check instead of waiting up to the 4h interval.
  • Web UI banner (FR-005): dismissible, non-modal UpdateBanner.vue on the Dashboard — "Update available: vX — you are running vY" with release-notes link. Dismissal persists the dismissed latest_version in localStorage (same version never re-nags; a newer release shows again). Manual "check for updates" reports checks-disabled instead of a misleading "you are running the latest version".
  • make swagger: config.UpdateCheckConfig surfaced in oas/.

Codex review outcome — applied

All findings applied in 411c69b2 (none rebutted):

  • Major: the Go tray's independent daily self-update check now reads update_check.enabled from the shared config file and skips the network check when disabled (FR-015 across all surfaces). Fails open on missing/unreadable config; MCPPROXY_DISABLE_AUTO_UPDATE still wins. Full FR-001a tray convergence (shared checker incl. channel) is a separate 079 work item — docs say exactly that.
  • Minor: tray treats an absent update object in /api/v1/info as "no update" (clears stale nudge after hot-reload disable instead of leaving it until restart).
  • Minor: checker SetConfig bumps a config generation and drops cached VersionInfo; in-flight checks from a stale generation are discarded, so a disable/channel-switch race can never publish wrong-channel or post-disable results.
  • Minor: UpdateBanner localStorage access wrapped in try/catch — degrades to session-only dismissal when storage is blocked.

Live verification (worktree, isolated instance)

  • Built with forced old version: go build -ldflags "-X main.version=v0.40.0 ..."mcpproxy --version = MCPProxy v0.40.0 (personal) darwin/arm64; make frontend-build regenerated web/frontend/dist.
  • Ran isolated on 127.0.0.1:18095: /api/v1/info returned the update object with the real latest release; Dashboard rendered the banner, dismiss persisted across reload, sidebar badge unchanged.
  • Set update_check.enabled=false and hot-reloaded: update object omitted from /api/v1/info, banner/badge disappeared, ?refresh=true no-op (no outbound check); re-enable triggered a prompt re-check.
  • Tests: go test -race ./internal/..., vitest (253 pass), golangci-lint v2 (.github/.golangci.yml), make build — all green.

Docs updated

  • docs/features/version-updates.mdupdate_check reference, hot-reload, env-vs-config precedence, banner behavior
  • docs/configuration.md, docs/configuration/config-file.md — Update Check sections + complete-reference key
  • docs/configuration/environment-variables.md — auto-update table is core+tray, points at config equivalent
  • docs/api/rest-api.md/api/v1/info omits update when disabled; refresh=true no-op note
  • docs/cli/status-command.md — disabled case (no update object / annotation)
  • docs/prerelease-builds.md — RC opt-in via update_check.channel=rc

Out of scope

  • FR-001a tray convergence: the Go tray still runs its own release check (now gated on update_check.enabled, but env-only for channel); consuming the shared checker incl. channel is a separate 079 work item.
  • CLI/status annotation rendering changes beyond what falls out of the omitted update object.
  • Swift (macOS native) tray update UX.

🤖 Generated with Claude Code

Dumbris and others added 5 commits July 3, 2026 06:39
…nce (Spec 079 US1)

Add the update_check.{enabled,channel} config group (FR-012/FR-013):

- enabled (default true) gates BOTH the background poll and the manual
  CheckNow (/api/v1/info?refresh=true) path; when disabled no network
  check runs and GetVersionInfo returns nil so /api/v1/info omits the
  update object entirely — every surface (banner, badge, status/doctor
  annotation) goes quiet (FR-015).
- channel: "stable" (default; prereleases never offered) or "rc"
  (prereleases included), validated; the config-file equivalent of
  MCPPROXY_ALLOW_PRERELEASE_UPDATES.
- Precedence (FR-014): the existing env switches WIN over config —
  MCPPROXY_DISABLE_AUTO_UPDATE=true force-disables and
  MCPPROXY_ALLOW_PRERELEASE_UPDATES=true force-includes prereleases.
  The spec leaves precedence open; env-wins is the operator-override
  reading, documented in code and docs.
- Hot-reload: DetectConfigChanges reports "update_check"; ApplyConfig
  (API path) and ReloadConfiguration (disk path) both re-gate the
  running checker. The background loop now stays alive while
  config-disabled, and a re-enable/channel switch triggers a prompt
  re-check instead of waiting up to the 4h interval.
- make swagger: config.UpdateCheckConfig surfaced on config.Config.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ec 079 US1)

FR-005: non-modal alert banner on the dashboard when the checker reports
an update — "Update available: vX — you are running vY" with a
release-notes link and a dismiss (X) button. Dismissal persists the
dismissed latest_version in localStorage, so the same version never
re-nags across reloads while a newer release shows the banner again.
The existing sidebar badge + manual-check toast are unchanged.

When update_check.enabled=false the daemon omits the update object from
/api/v1/info, so the banner (and badge) are naturally absent; the manual
"check for updates" action now says checks are disabled instead of the
misleading "You are running the latest version".

Tests: 6 vitest cases (render, no-update, absent update object,
dismiss persists, stays dismissed on remount, newer version reappears).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… and the Web UI banner (Spec 079 US1)

- features/version-updates.md: update_check.{enabled,channel} reference,
  hot-reload behavior, explicit env-vs-config precedence (env wins, one
  direction only), config-based examples, dismissible per-version banner.
- configuration.md + configuration/config-file.md (served reference):
  new Update Check sections + complete-reference key.
- configuration/environment-variables.md: auto-update table is core+tray
  (not tray-only) and points at the config-file equivalent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e nudge/cache

Address Codex review findings on the Spec 079 US1 branch:

- Go tray self-update (major): the tray's independent daily release check
  (checkForUpdates) now reads update_check.enabled from the shared config
  file and skips the network check when disabled (FR-015: no check on any
  surface). Fails open on missing/unreadable config, matching the pre-079
  default; MCPPROXY_DISABLE_AUTO_UPDATE still wins (FR-014). Full FR-001a
  convergence (tray consuming the shared checker, incl. channel) remains a
  separate 079 work item — docs updated to say exactly that.
- Tray stale nudge (minor): checkUpdateFromAPI treats an absent update
  object in /api/v1/info (checker disabled via hot-reload) as "no update",
  clearing state and hiding the menu item instead of returning early and
  leaving a stale "New version available" entry until restart.
- Checker races (minor): SetConfig now bumps a config generation and drops
  the cached VersionInfo on any effective change; check() captures the
  generation and updateVersionInfo discards results from a stale generation
  or while disabled. An in-flight check can no longer publish/announce after
  disable, and a channel switch or re-enable never briefly serves
  wrong-channel cached info (FR-013/FR-015).
- UpdateBanner (minor): localStorage reads/writes wrapped in try/catch,
  degrading to session-only dismissal when storage is blocked (precedent:
  stores/system.ts), so blocked storage cannot break Dashboard setup.

Tests: new unit tests for the tray config gate, the stale-nudge clear
(httptest core stub), the checker generation/disable races, and a
blocked-localStorage banner spec. go test -race, vitest (253 pass),
golangci-lint v2, and make build all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- rest-api.md: /api/v1/info omits the update object when checking is
  disabled (update_check.enabled=false or MCPPROXY_DISABLE_AUTO_UPDATE);
  note refresh=true no-ops while disabled and point at the update_check
  config block.
- cli/status-command.md: document the disabled case (no update object,
  version shown without annotation).
- prerelease-builds.md: RC opt-in now also possible via
  update_check.channel=rc for the core checker (Go tray self-update check
  stays env-only until FR-001a).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 3, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: fe1d102
Status: ✅  Deploy successful!
Preview URL: https://42b98f4f.mcpproxy-docs.pages.dev
Branch Preview URL: https://feat-079-banner-config.mcpproxy-docs.pages.dev

View logs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Jul 3, 2026

Copy link
Copy Markdown

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

Codecov Report

❌ Patch coverage is 84.04255% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/runtime/runtime.go 0.00% 7 Missing and 1 partial ⚠️
internal/updatecheck/checker.go 90.76% 4 Missing and 2 partials ⚠️
internal/runtime/lifecycle.go 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: feat/079-banner-config

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 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 28641746072 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

…bled

Codex final-review findings on PR #805 (Spec 079 US1):

- Major: ServerAdapter.GetConfigPath() hardcoded ~/.mcpproxy/mcp_config.json,
  ignoring MCPPROXY_TRAY_CONFIG_PATH — the very path the tray launches core
  with as --config (buildCoreArgs). So the update_check gate read the wrong
  file under a custom config path and failed open, letting the tray-owned
  daily GitHub check run despite update_check.enabled=false. Resolve the env
  override first, matching what core actually uses.

- Minor: check() captured cfgGen but not the enabled flag, so a disable racing
  after the caller's outer Enabled() gate (loop tick / re-enable goroutine)
  still issued a GitHub request. The generation guard dropped the result, but
  the request fired — violating FR-015 "no network check on any surface".
  Re-read enabled under the same lock as gen and bail before checkFunc.

Both covered by new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Dumbris

Dumbris commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Codex final review — verdict: REQUEST_CHANGES → resolved

Ran a read-only Codex (gpt-5.5, high effort) review over the full diff vs origin/main, including the merge+OpenAPI-regen commit. Verified each finding against the code and fixed both. Pushed as 9fc56305.

OpenAPI regen integrity (focus a): clean. origin/main is fully merged (equals the merge base — main hasn't advanced). config.UpdateCheckConfig survives in both oas/swagger.yaml (#/components/schemas/config.UpdateCheckConfig, referenced from the config schema) and oas/docs.go; nothing main added was dropped.

Findings (both confirmed real, both fixed)

  1. Major — tray update-check gate ignored the --config override. ServerAdapter.GetConfigPath() hardcoded ~/.mcpproxy/mcp_config.json, but the tray launches core with MCPPROXY_TRAY_CONFIG_PATH as --config (buildCoreArgs). So under a custom config path the new updateCheckEnabledByConfig gate read the wrong file and failed open — the tray-owned daily GitHub check still ran despite update_check.enabled:false (FR-015 violation). Fixed: GetConfigPath() now resolves the env override first, matching what core actually uses.

  2. Minor — check() could fire a network request after a racing disable. It captured cfgGen but not the enabled flag, so a disable landing after the caller's outer Enabled() gate (loop tick / re-enable goroutine) still hit GitHub. The generation guard dropped the result, but the request fired. Fixed: re-read enabled under the same lock as gen and bail before checkFunc.

Both covered by new tests (TestServerAdapter_GetConfigPath_*, TestChecker_CheckSkipsNetworkWhenDisabled).

Verification

  • go test -race ./internal/updatecheck/... ./internal/config/... ./cmd/mcpproxy-tray/... ./internal/tray/... — pass
  • frontend vitest tests/unit/update-banner.spec.ts — 7/7 pass
  • golangci-lint v2 (.github/.golangci.yml) on changed packages — 0 issues
  • go build ./cmd/mcpproxy + ./cmd/mcpproxy-tray — OK

Other reviewed areas (config tri-state + nil-safe accessors + channel validation, SetConfig generation-guard, env-narrows precedence, RWMutex non-reentrancy via enabledLocked, hot-reload wiring across SetVersion/ApplyConfig/ReloadConfiguration incl. UpdateCheck=nil→defaults, banner per-version localStorage dismissal with guarded storage) checked out with no further issues.

Net verdict after fixes: APPROVE.

The tray must interact with the core only over the socket/REST API and
hold no state (CLAUDE.md). The previous update-check gate violated this by
calling config.LoadFromFile on the core's mcp_config.json.

Replace updateCheckEnabledByConfig with fetchCoreUpdateInfo, which asks the
core via GET /api/v1/info (the same endpoint checkUpdateFromAPI uses). The
core omits the update object when update_check.enabled=false, making its
config the single source of truth:

- core reports an update object -> run the legacy GitHub self-update flow
- core omits the update object   -> skip (checking disabled / no update)
- core unreachable               -> skip this tick (do not fall open to a
  network check the operator may have disabled); the 24h ticker retries

checkUpdateFromAPI now shares fetchCoreUpdateInfo. File-based gate tests are
replaced with API-based ones asserting the network path (injected
selfUpdateFunc) runs only when the core reports an update.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Dumbris

Dumbris commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

Architecture correction: tray self-update gate now uses the core API, not the config file

Per maintainer review — the tray must interact with the core only over the socket/REST API and hold no state (CLAUDE.md: "the tray holds no state; it reads/writes core config via REST + SSE"). The previous update-check gate violated this by calling config.LoadFromFile on the core's mcp_config.json.

What changed (internal/tray/tray.go):

  • Removed updateCheckEnabledByConfig() (the file-reading gate) and its file-based tests.
  • Added fetchCoreUpdateInfo(), which asks the core via GET /api/v1/info — the same endpoint (and address/transport resolution) that checkUpdateFromAPI already uses. checkUpdateFromAPI now shares it, so there's a single fetch path.
  • checkForUpdates() now gates the legacy GitHub self-update on the core's answer:
    • core reports an update object → run the legacy self-update flow (its own GitHub asset resolution stays for now; full FR-001a convergence is out of scope).
    • core omits the update object → update checking is disabled (update_check.enabled=false, Spec 079 FR-015) or no update exists → skip.
    • core unreachableskip this tick rather than fall open to a network check the operator may have disabled; the 24h ticker retries. Env kill-switch (MCPPROXY_DISABLE_AUTO_UPDATE) semantics unchanged and still checked first.

The core's update_check config is now the single source of truth, without the tray ever touching the file.

Tests replaced with API-based ones (core stub via httptest + injected selfUpdateFunc seam): core-reports-update → self-update runs; core-omits-update → skipped, no network call; core-unreachable → skipped; not-connected → skipped. The core-side TestChecker_CheckSkipsNetworkWhenDisabled is untouched.

Pre-existing violation flagged (not fixed here — out of scope): the OAuth login path at internal/tray/tray.go:1759 still calls config.LoadFromFile(a.server.GetConfigPath()) to resolve server credentials. That is itself a tray-reads-config-file violation predating this PR; the GetConfigPath env-resolution fix (cmd/mcpproxy-tray/internal/api/adapter.go) is retained because it fixes a real bug on that path. Leaving it for a separate change.

@Dumbris Dumbris merged commit ed42d46 into main Jul 3, 2026
37 checks passed
@Dumbris Dumbris deleted the feat/079-banner-config branch July 3, 2026 06:20
Dumbris added a commit that referenced this pull request Jul 3, 2026
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Jul 3, 2026
…y debt (#806)

Found during the PR #805 architecture review: the tray must use only
the socket/REST API, but the OAuth login path still parses
mcp_config.json directly (pre-existing). Swift tray audited clean.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Dumbris added a commit that referenced this pull request Jul 3, 2026
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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