Skip to content

feat(update): show "new version available" modal with one-click update [ENG-1686]#408

Open
robinnewhouse wants to merge 7 commits intomainfrom
eng-1686-update-available-toast
Open

feat(update): show "new version available" modal with one-click update [ENG-1686]#408
robinnewhouse wants to merge 7 commits intomainfrom
eng-1686-update-available-toast

Conversation

@robinnewhouse
Copy link
Copy Markdown
Collaborator

@robinnewhouse robinnewhouse commented Apr 24, 2026

Adds a modal that pops up in the web UI when a newer Kanban version is available on npm, with a one-click "Update Now" button that runs the right install command for the user's package manager. Closes ENG-1686.

Why

Kanban is meant to run all the time, so users naturally fall behind on versions. There was already a backend startup auto-update check (runAutoUpdateCheck) plus a kanban update CLI command (runOnDemandUpdate), but neither surfaced anything in the UI — a user with the browser tab open all day had no signal that a new version had shipped, and the --update flag is invisible to non-CLI users.

This adds the missing UI piece: when the existing startup check finds a newer version, the next page render shows a centered modal that mirrors the linked design (icon, title, current/latest version, copy-able install command, Later / Update Now). Update Now goes through the existing runOnDemandUpdate, so behavior matches kanban update exactly — same package-manager detection, same fallbacks.

Change

Backend

  • src/update/update.ts: extend runAutoUpdateCheck to record a PendingUpdateNotification (current/latest version, update timing, user-facing install command) when a newer version is detected. Add buildUserFacingInstallCommand so the modal shows pnpm add -g …, npx kanban, etc. — not the internal cache-refresh script.
  • src/core/api-contract.ts: add runtimeUpdateStatusResponseSchema and runtimeRunUpdateResponseSchema.
  • src/trpc/runtime-api.ts + src/trpc/app-router.ts: expose runtime.getUpdateStatus (query) and runtime.runUpdateNow (mutation).
  • src/server/runtime-server.ts + src/cli.ts: thread getUpdateStatus and runUpdateNow as deps. runUpdateNow delegates to the existing runOnDemandUpdate so users get the same install behavior as kanban update.

Frontend

  • web-ui/src/runtime/runtime-config-query.ts: fetchRuntimeUpdateStatus and runRuntimeUpdateNow tRPC client helpers.
  • web-ui/src/hooks/use-update-notification.ts: polls the runtime once on mount, retries after 10s (the server populates the pending notification asynchronously after startup), then falls back to an hourly poll. Returns { availableUpdate, dismiss } and is session-scoped — dismissing won't resurface the same prompt until the next page load.
  • web-ui/src/components/update-available-dialog.tsx: centered modal with the install command in a copy-able code block. Phases: idle / running (spinner on Update Now, Later disabled) / success (green check + "Restart Kanban to use the new version" + Close) / error (inline red alert, retryable).
  • web-ui/src/App.tsx: wire the hook and render the modal alongside the other top-level dialogs.

Tests

  • test/runtime/update/auto-update.test.ts: extend the getPendingUpdateNotification suite to assert installCommand for both startup (npm install -g …) and shutdown (npx kanban) timing paths.

How to test

  1. Temporarily seed a pending notification at the top of src/update/update.ts:
    let pendingUpdateNotification: PendingUpdateNotification | null = {
        currentVersion: "0.1.64",
        latestVersion: "0.1.99",
        updateTiming: "startup",
        installCommand: "npm install -g kanban@latest",
    };
  2. npm run dev:full, open the Vite dev URL.
  3. Modal appears within ~1s. Click the copy icon — command copies. Click Update Now — spinner shows, npm install -g kanban@latest runs in the dev:full terminal, modal flips to a green success state. Click Close.
  4. Revert the seeded value (it is null on this branch).

In production, the modal only fires when the existing startup auto-update check actually finds a newer version on npm, so end users see it organically.

Notes

  • Update behavior is unchanged from kanban update. We didn't add a parallel install path; the modal just calls into runOnDemandUpdate.
  • The hook only polls hourly after the initial check + 10s retry. The runtime's runAutoUpdateCheck only runs on startup, so polling more often wouldn't produce different results.
  • Later dismisses the modal for the session. The next kanban launch will show it again if the underlying version still mismatches.

Screenshots

Screenshot 2026-04-24 at 3 26 36 PM Screenshot 2026-04-24 at 3 26 37 PM

…e (ENG-1686)

Detect newer Kanban versions on startup and surface a modal in the web UI
that lets the user run the appropriate install command for their package
manager (npm, pnpm, yarn, bun, npx) without leaving the browser.

Backend
- src/update/update.ts: track PendingUpdateNotification (current/latest
  version, update timing, user-facing install command) when the existing
  startup auto-update check detects a newer version. Add
  buildUserFacingInstallCommand to derive the command per package manager.
- src/core/api-contract.ts: add runtimeUpdateStatusResponseSchema and
  runtimeRunUpdateResponseSchema.
- src/trpc/runtime-api.ts + src/trpc/app-router.ts: expose runtime.getUpdateStatus
  query and runtime.runUpdateNow mutation.
- src/server/runtime-server.ts + src/cli.ts: thread getUpdateStatus and
  runUpdateNow as deps. runUpdateNow delegates to the existing
  runOnDemandUpdate so users get the same install behavior as `kanban update`.

Frontend
- web-ui/src/runtime/runtime-config-query.ts: add fetchRuntimeUpdateStatus
  and runRuntimeUpdateNow tRPC client helpers.
- web-ui/src/hooks/use-update-notification.ts: new hook that polls the
  runtime once on mount, retries after 10s (the server populates the
  pending notification asynchronously), then falls back to an hourly
  poll. Returns the available update info plus a session-scoped dismiss().
- web-ui/src/components/update-available-dialog.tsx: new centered modal
  with the install command in a copy-able code block, a Later/Update Now
  button row, and idle/running/success/error phases. Update Now calls
  runtime.runUpdateNow and shows the result inline; Later dismisses for
  the session.
- web-ui/src/App.tsx: wire the hook and render the modal.

Tests
- test/runtime/update/auto-update.test.ts: extend the
  getPendingUpdateNotification suite to assert installCommand for the
  startup (npm install -g) and shutdown (npx) timing paths.
@linear
Copy link
Copy Markdown

linear Bot commented Apr 24, 2026

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 24, 2026

Greptile Summary

This PR adds a "New version available" modal to the web UI that surfaces the existing backend startup auto-update check (runAutoUpdateCheck) as a visible prompt. When a newer version is detected, a PendingUpdateNotification is stored server-side; the frontend polls for it on mount (with a 10-second retry and hourly fallback) and renders a dialog with a copyable install command and a one-click "Update Now" button that delegates to runOnDemandUpdate. All three issues from the previous review round (notification not cleared post-update, hook not self-correcting to null, wrong install command for pnpm dlx/yarn dlx/bunx users) have been resolved.

Confidence Score: 5/5

Safe to merge — all three previously-flagged bugs have been correctly addressed and the new code is well-tested.

No P0/P1 findings remain. The three issues from the prior review round (notification not cleared post-update, hook not self-correcting, wrong command for dlx/bunx users) are all fixed. Backend state management is correct (module-level singleton guarded by clear function), frontend polling is clean (cancelled flag + ref-based dismiss), and the dialog's allowClose guard properly prevents escape during an in-flight update.

No files require special attention.

Important Files Changed

Filename Overview
src/update/update.ts Adds PendingUpdateNotification, buildUserFacingInstallCommand (with updateTiming-aware dlx/bunx handling), getPendingUpdateNotification, and clearPendingUpdateNotification; sets the notification in runAutoUpdateCheck before spawning the actual update.
src/cli.ts Wires getUpdateStatus and runUpdateNow into the runtime server deps; runUpdateNow correctly calls clearPendingUpdateNotification() on updated/already_up_to_date/cache_refreshed to prevent modal re-appearance after a successful update.
web-ui/src/hooks/use-update-notification.ts Polling hook with mount check, 10s early retry, hourly interval; correctly self-corrects to null when a subsequent poll returns updateAvailable: false; dismiss is session-scoped via a ref.
web-ui/src/components/update-available-dialog.tsx Clean four-phase (idle/running/success/error) dialog; allowClose guard prevents escape while updating; error state is retryable; success shows correct restart guidance.
test/runtime/update/auto-update.test.ts New getPendingUpdateNotification suite covers startup/shutdown timing paths for npm, npx, pnpm dlx, yarn dlx, and bunx, plus the no-update and clearPendingUpdateNotification afterEach cleanup cases.

Sequence Diagram

sequenceDiagram
    participant S as Server (startup)
    participant ST as update.ts
    participant API as tRPC runtime API
    participant H as useUpdateNotification
    participant D as UpdateAvailableDialog

    S->>ST: runAutoUpdateCheck()
    ST->>ST: detect newer version
    ST->>ST: set pendingUpdateNotification

    H->>API: fetchRuntimeUpdateStatus() [on mount]
    API->>ST: getPendingUpdateNotification()
    ST-->>API: PendingUpdateNotification
    API-->>H: { updateAvailable: true, ... }
    H->>D: render dialog

    D->>API: runRuntimeUpdateNow() [Update Now click]
    API->>ST: runOnDemandUpdate()
    ST-->>API: { status: updated }
    API->>ST: clearPendingUpdateNotification()
    API-->>D: success response
    D->>D: phase → success

    H->>API: fetchRuntimeUpdateStatus() [hourly poll]
    API->>ST: getPendingUpdateNotification() → null
    API-->>H: { updateAvailable: false }
    H->>H: setAvailableUpdate(null)
Loading

Reviews (4): Last reviewed commit: "test(runtime): use update-aware api help..." | Re-trigger Greptile

Comment thread src/cli.ts
Comment thread web-ui/src/hooks/use-update-notification.ts
…-correct hook

Address Greptile review feedback on #408:

- src/cli.ts: when runUpdateNow finishes with updated / already_up_to_date /
  cache_refreshed, call clearPendingUpdateNotification() so the modal does
  not reappear on page reload after the user has already applied the update.
  The pending notification is a one-shot signal recorded at startup; it is
  not meant to persist past resolution.
- web-ui/src/hooks/use-update-notification.ts: when a poll returns
  updateAvailable: false, reset availableUpdate to null so the hook self-
  corrects without requiring a page reload (e.g., when another session
  applied the update and the runtime cleared its pending notification).
- web-ui/src/hooks/use-update-notification.test.tsx: cover the happy path,
  up-to-date null state, the new self-correction path, dismiss(), and
  rejected query (5 tests).
@robinnewhouse
Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Both points addressed in b95daeff:

P1 — clearPendingUpdateNotification() after success. src/cli.ts now calls it on updated / already_up_to_date / cache_refreshed, so the modal won't re-fire on page reload after a successful update.

P2 — hook self-correction. use-update-notification.ts now resets availableUpdate to null when a later poll returns updateAvailable: false, instead of holding stale state.

Added web-ui/src/hooks/use-update-notification.test.tsx (5 cases) covering the happy path, up-to-date, the new self-correction, dismiss(), and rejected queries. Local checks: tsc --noEmit clean across root + web-ui, biome clean, all 498 web-ui + 108 relevant root tests passing.

@robinnewhouse
Copy link
Copy Markdown
Collaborator Author

@greptileai

Comment thread src/update/update.ts Outdated
…nly for actual updates

Address Greptile review feedback on #408:

- src/update/update.ts: buildUserFacingInstallCommand now branches on
  installation.updateTiming. PNPM/YARN/BUN with updateTiming "shutdown"
  (transient pnpm dlx / yarn dlx / bunx invocations) yield re-run commands
  (pnpm dlx kanban / yarn dlx kanban / bunx kanban) instead of
  steering the user toward an unintended global install.
- web-ui/src/components/update-available-dialog.tsx: success state now
  only shows "Updated to Kanban X. Restart Kanban" when status is
  literally "updated". For already_up_to_date and cache_refreshed it
  falls back to the result.message (e.g. "Kanban is already up to date
  (1.0.0)."), avoiding a misleading restart prompt when nothing was
  installed.
- test/runtime/update/auto-update.test.ts: add three new cases asserting
  installCommand for pnpm dlx, yarn dlx, and bunx transient runs.
@robinnewhouse
Copy link
Copy Markdown
Collaborator Author

Both points addressed in b7d286d7:

P1 — wrong install command for pnpm dlx / yarn dlx / bunx. buildUserFacingInstallCommand now takes updateTiming and branches: shutdown-timing PNPM/YARN/BUN return pnpm dlx kanban / yarn dlx kanban / bunx kanban (re-run the launcher) instead of pnpm add -g … etc. (which would change the user's workflow into a global install).

P2 — misleading restart message on already_up_to_date. The dialog success state now only shows "Updated to Kanban X. Restart Kanban …" when status === "updated". For already_up_to_date and cache_refreshed it falls back to result.message (e.g. "Kanban is already up to date (1.0.0).").

Added 3 tests in test/runtime/update/auto-update.test.ts covering pnpm-dlx, yarn-dlx, and bunx shutdown-timing paths. 31 update tests + 111 trpc/runtime tests + 498 web-ui tests all pass; tsc + biome clean.

@robinnewhouse
Copy link
Copy Markdown
Collaborator Author

@greptileai

@robinnewhouse robinnewhouse changed the title feat(update): show "new version available" modal with one-click update feat(update): show "new version available" modal with one-click update [ENG-1686] Apr 24, 2026
Comment thread web-ui/src/App.tsx Outdated
import type { BoardData } from "@/types";

export default function App(): ReactElement {
const { availableUpdate, dismiss: dismissUpdateNotification } = useUpdateNotification();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Generally, I think we should start to debloat the App.tsx component. We could extract all of these to a Provider or something, that takes care of fetching the hook and conditionally rendering the UpdateAvailableDialog.

WDYT?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Always in favor of de-bloating. Let me see what I can do.

Comment thread src/server/runtime-server.ts Outdated
Comment on lines +74 to +75
getUpdateStatus?: () => RuntimeUpdateStatusResponse;
runUpdateNow?: () => Promise<RuntimeRunUpdateResponse>;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why can these be undefined?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for catching. fixed.

@robinnewhouse
Copy link
Copy Markdown
Collaborator Author

@greptileai

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