From 645e7ff6732bb013e7fa15ce1c66c1ff518e59b2 Mon Sep 17 00:00:00 2001
From: fedorovvvv
Date: Wed, 6 May 2026 21:00:31 +0400
Subject: [PATCH 1/6] feat(ui): shared UI primitives + npm update notification
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add Button, Code-with-copy, Dialog under template/src/shared/ui/ and a
modalManager service under template/src/shared/services/modal/ so widgets
can open dialogs without per-call mounting. ModalRoot is mounted once in
+layout.svelte.
Add /api/update-check endpoint that probes registry.npmjs.org for the
latest @forgeplan/web (5-min server-process cache, 5-second timeout, GET
only, never throws). VersionFooter polls it once at mount and every
30 minutes; when hasUpdate, an UpdateButton appears above the footer and
opens UpdateDialog (current → latest + copyable manual command).
Auto-update is intentionally out of scope: running `npx @forgeplan/web
update` from the running server would rmSync the very files serving the
request. Dialog explains this and offers the manual path only.
Rule 22 amended to allow exactly one non-forgeplan endpoint hitting the
literal URL https://registry.npmjs.org/@forgeplan/web/latest.
Refs: PRD-013, RFC-012, EVID-017
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.claude/rules/22-readonly-proxy.md | 28 ++
...oint-probe-for-shared-ui-update-checker.md | 118 +++++++
...d-ui-primitives-npm-update-notification.md | 290 +++++++++++++++++
...-primitives-modalmanager-update-checker.md | 308 ++++++++++++++++++
template/src/routes/+layout.svelte | 2 +
.../src/routes/api/update-check/+server.ts | 79 +++++
template/src/shared/server/index.ts | 1 +
template/src/shared/server/semver.ts | 62 ++++
template/src/shared/services/index.ts | 1 +
template/src/shared/services/modal/index.ts | 1 +
.../services/modal/modal-manager.svelte.ts | 69 ++++
template/src/shared/ui/README.md | 117 +++++++
template/src/shared/ui/button/Button.svelte | 104 ++++++
template/src/shared/ui/button/index.ts | 1 +
template/src/shared/ui/code/Code.svelte | 165 ++++++++++
template/src/shared/ui/code/index.ts | 1 +
template/src/shared/ui/dialog/Dialog.svelte | 181 ++++++++++
template/src/shared/ui/dialog/index.ts | 1 +
template/src/shared/ui/index.ts | 4 +
template/src/shared/ui/modal/ModalRoot.svelte | 8 +
template/src/shared/ui/modal/index.ts | 1 +
.../version-footer/api/update-check.svelte.ts | 15 +
.../version-footer/ui/UpdateButton.svelte | 88 +++++
.../version-footer/ui/UpdateDialog.svelte | 131 ++++++++
.../version-footer/ui/VersionFooter.svelte | 25 +-
25 files changed, 1800 insertions(+), 1 deletion(-)
create mode 100644 .forgeplan/evidence/EVID-017-smoke-svelte-check-live-endpoint-probe-for-shared-ui-update-checker.md
create mode 100644 .forgeplan/prds/PRD-013-shared-ui-primitives-npm-update-notification.md
create mode 100644 .forgeplan/rfcs/RFC-012-shared-ui-primitives-modalmanager-update-checker.md
create mode 100644 template/src/routes/api/update-check/+server.ts
create mode 100644 template/src/shared/server/semver.ts
create mode 100644 template/src/shared/services/index.ts
create mode 100644 template/src/shared/services/modal/index.ts
create mode 100644 template/src/shared/services/modal/modal-manager.svelte.ts
create mode 100644 template/src/shared/ui/README.md
create mode 100644 template/src/shared/ui/button/Button.svelte
create mode 100644 template/src/shared/ui/button/index.ts
create mode 100644 template/src/shared/ui/code/Code.svelte
create mode 100644 template/src/shared/ui/code/index.ts
create mode 100644 template/src/shared/ui/dialog/Dialog.svelte
create mode 100644 template/src/shared/ui/dialog/index.ts
create mode 100644 template/src/shared/ui/index.ts
create mode 100644 template/src/shared/ui/modal/ModalRoot.svelte
create mode 100644 template/src/shared/ui/modal/index.ts
create mode 100644 template/src/widgets/version-footer/api/update-check.svelte.ts
create mode 100644 template/src/widgets/version-footer/ui/UpdateButton.svelte
create mode 100644 template/src/widgets/version-footer/ui/UpdateDialog.svelte
diff --git a/.claude/rules/22-readonly-proxy.md b/.claude/rules/22-readonly-proxy.md
index 2782cf6..4c108dd 100644
--- a/.claude/rules/22-readonly-proxy.md
+++ b/.claude/rules/22-readonly-proxy.md
@@ -27,6 +27,31 @@ This is the only flag-only invocation permitted from `/api/*`. Any new
flag-only or subcommand entry requires an updating Forgeplan artifact and a
revision of this rule. See PRD-012 / RFC-011.
+## Allow-list extension: `/api/update-check` (non-forgeplan)
+
+`/api/update-check` is the **single** non-forgeplan endpoint permitted from
+`/api/*`. It probes the npm registry for the latest published version of
+`@forgeplan/web` so the UI can surface an "Update available" affordance.
+
+Constraints (every one of these is enforceable from the diff):
+
+- Method: `GET` only.
+- URL: the **string literal** `https://registry.npmjs.org/@forgeplan/web/latest`.
+ No interpolation, no query params, no user input on the URL path.
+- No spawn, no host filesystem write, no Forgeplan invocation. The only
+ side-effect is a process-local in-memory cache (5 min TTL, single
+ inflight promise).
+- Headers: `accept: application/json` and a static `user-agent`. No cookies,
+ no credentials.
+- Response shape mirrors the standard envelope: `{ ok, data: { current,
+ latest, hasUpdate }, cmd, error? }` with `current = __FORGEPLAN_WEB_VERSION__`.
+- Network failures (timeout, non-2xx, JSON parse error) MUST fall back to
+ `{ ok: false, error, data: { ..., hasUpdate: false } }` — never throw.
+
+Any additional non-forgeplan endpoint (whether it hits npm, GitHub,
+crates.io, or anything else) requires a new Forgeplan artifact and a fresh
+amendment to this rule. See PRD-013 / RFC-012.
+
## Forbidden `forgeplan` subcommands from any `/api/*` endpoint
Any subcommand that mutates the workspace:
@@ -69,3 +94,6 @@ browser invalidates that.
`args[0] ∈ READ_ONLY_SUBCOMMANDS` before spawning, and the constant MUST
match this allow-list (see rule above). The check is the runtime backstop
for review-time enforcement.
+- `grep -RIn "fetch(" template/src/routes/api/` must show external URLs
+ only inside `update-check/+server.ts`, and the URL must appear as a
+ string literal (`https://registry.npmjs.org/@forgeplan/web/latest`).
diff --git a/.forgeplan/evidence/EVID-017-smoke-svelte-check-live-endpoint-probe-for-shared-ui-update-checker.md b/.forgeplan/evidence/EVID-017-smoke-svelte-check-live-endpoint-probe-for-shared-ui-update-checker.md
new file mode 100644
index 0000000..78a695e
--- /dev/null
+++ b/.forgeplan/evidence/EVID-017-smoke-svelte-check-live-endpoint-probe-for-shared-ui-update-checker.md
@@ -0,0 +1,118 @@
+---
+depth: standard
+id: EVID-017
+kind: evidence
+last_modified_at: 2026-05-06T16:58:57.023400+00:00
+last_modified_by: claude-code/2.1.131
+links:
+- target: PRD-013
+ relation: informs
+- target: RFC-012
+ relation: informs
+status: draft
+title: smoke + svelte-check + live endpoint probe for shared UI + update checker
+---
+
+# EVID-017: smoke + svelte-check + live endpoint probe for shared UI + update checker
+
+| Field | Value |
+| ----------- | ---------------------- |
+| Status | Draft |
+| Created | 2026-05-06 |
+| Valid Until | 2026-08-06 |
+| Target | PRD-013 / RFC-012 |
+
+## Structured Fields
+
+evidence_type: test
+verdict: supports
+congruence_level: 3
+
+## Measurement
+
+Three direct probes against the surface introduced by PRD-013 / RFC-012:
+
+1. `cd template && npm run check` — `svelte-kit sync` followed by
+ `svelte-check --tsconfig ./tsconfig.json`. Exercises every `.svelte`,
+ `.svelte.ts`, and `.ts` file under `template/src/` (including the new
+ `shared/ui/`, `shared/services/modal/`, and version-footer additions)
+ for type errors and a11y warnings.
+2. `npm run smoke` at the repo root — rebuilds `dist/` from scratch
+ (`scripts/build.mjs --clean`), `init -y` against a scratch dir, then
+ `init -y --force`, then boots `node dist/index.js`, and probes
+ `/api/health`, `/api/list`, `/`. This is the same smoke harness
+ already used to gate releases.
+3. Live spawn of the freshly built `dist/index.js` on PORT=15999 with
+ FORGEPLAN_CWD=/tmp + curl against the new endpoint.
+
+## Result
+
+```
+$ npm run check # in template/
+COMPLETED 462 FILES 0 ERRORS 0 WARNINGS 0 FILES_WITH_PROBLEMS
+
+$ npm run smoke # at root
+[smoke] /api/health: ok (project=shim)
+[smoke] /api/list: ok (0 entries)
+[smoke] GET /: ok (HTML returned)
+[smoke] PASS
+
+$ curl -s http://127.0.0.1:15999/api/update-check
+{"ok":true,"data":{"current":"0.1.11","latest":"0.1.11","hasUpdate":false},
+ "cmd":"GET registry.npmjs.org/@forgeplan/web/latest"}
+
+$ curl -s http://127.0.0.1:15999/api/version
+{"ok":true,"data":{"web":"0.1.11","cli":"0.27.0"},"cmd":"forgeplan --version"}
+```
+
+Vite build also reports `entries/endpoints/api/update-check/_server.ts.js
+1.78 kB │ gzip: 0.83 kB`, confirming the new endpoint is bundled into
+`dist/`.
+
+## Interpretation
+
+- **PRD-013 SC-1** (primitives exist + 1 widget imports): satisfied —
+ `template/src/widgets/version-footer/ui/UpdateDialog.svelte` imports
+ `Button`, `Code`, `Dialog` from `@/shared/ui`.
+- **PRD-013 SC-2** (caller line count ≤ 3 lines to open a dialog): met
+ by the `modalManager.open(UpdateDialog, { current, latest })` call
+ inside `VersionFooter.svelte`.
+- **PRD-013 SC-3** (update notice within ≤ 30 min of npm publish):
+ meets the upper bound by `THIRTY_MINUTES_MS` poll interval +
+ immediate `start()` on mount; live probe confirms the endpoint is
+ reachable and returns a well-formed envelope.
+- **PRD-013 SC-4** (dialog renders manual update command): rendered by
+ `UpdateDialog.svelte` via ``.
+- **PRD-013 SC-5** (endpoint is GET-only, respects rule 22): the only
+ HTTP method exported is `GET`; no `spawn` or `runForgeplan` is called
+ from `update-check/+server.ts`; URL is a string literal.
+- **NFR-002** (endpoint never throws on registry failure): try/catch
+ wraps `getLatestCached`; failure path returns `{ ok: false, ...,
+ hasUpdate: false }`. Live probe with reachable registry returns the
+ success path; failure path is exercised structurally.
+- **NFR-004** (zero new runtime deps): `git diff develop --
+ template/package.json` shows no change in `dependencies`.
+
+The svelte-check pass over 462 files (up from 460 — the two new
+modules) with zero errors and zero warnings means the new types
+(ModalEntry, UpdateData, compareSemver) compose cleanly with existing
+code. The smoke pass means `init` + `start` still work end-to-end after
+the addition. The live curl proves the endpoint is wired.
+
+## Congruence Level Justification
+
+CL3 — the measurement is run against the exact files PRD-013 and
+RFC-012 prescribe (same surface, same project, same commit). The smoke
+test exercises the full ship-path (`bin/forgeplan-web.mjs init` →
+`node dist/index.js` boot), and the curl probes the new endpoint by
+its actual URL. evidence_type=test (binary pass/fail) +
+verdict=supports (every assertion held).
+
+## Related Artifacts
+
+| Artifact | Relation |
+| -------- | -------- |
+| PRD-013 | informs |
+| RFC-012 | informs |
+
+
diff --git a/.forgeplan/prds/PRD-013-shared-ui-primitives-npm-update-notification.md b/.forgeplan/prds/PRD-013-shared-ui-primitives-npm-update-notification.md
new file mode 100644
index 0000000..365ad73
--- /dev/null
+++ b/.forgeplan/prds/PRD-013-shared-ui-primitives-npm-update-notification.md
@@ -0,0 +1,290 @@
+---
+depth: standard
+id: PRD-013
+kind: prd
+last_modified_at: 2026-05-06T16:49:09.687333+00:00
+last_modified_by: claude-code/2.1.131
+status: draft
+title: Shared UI primitives + npm update notification
+---
+
+---
+id: PRD-013
+title: "Shared UI primitives + npm update notification"
+status: Draft
+author: claude-code
+created: 2026-05-06
+updated: 2026-05-06
+priority: P1
+depth: standard
+domain: general
+projectType: web_app
+stepsCompleted: []
+---
+
+# PRD-013: Shared UI primitives + npm update notification
+
+## Executive Summary
+
+### Vision
+
+Establish a minimal but reusable shared UI layer (Button, Code-with-copy,
+Dialog) and a programmatic ModalManager so widgets can open dialogs without
+manually mounting a component each time, then leverage that layer to surface
+an "Update available" affordance in the version footer when a newer
+`@forgeplan/web` is published on npm.
+
+### Problem
+
+Two coupled gaps in the current `template/`:
+
+1. There are no shared UI primitives. Every widget that needs a button, code
+ block, or dialog has to roll its own markup and CSS. There is no
+ programmatic way to open a modal — components must be instantiated and
+ mounted by every caller, which discourages reuse and bloats widget code.
+2. Users have no way to learn that a newer `@forgeplan/web` is available.
+ The footer shows the running version but the user must check npm
+ manually. As a result, scaffolds drift behind the latest release for
+ weeks; bug fixes shipped in newer versions never reach users.
+
+**Impact**: New widgets needing a dialog (this PRD's update notice, plus
+future settings/help modals) duplicate boilerplate. Stale `.forgeplan-web/`
+installs miss CLI-CLI compatibility fixes and feature additions.
+
+### Target Users
+
+| Persona | Description | Key pain |
+| ------------------ | -------------------------------------------------- | ------------------------------------------------------------------------ |
+| Forgeplan author | Engineer running `npx @forgeplan/web start` daily | Doesn't know an update is available; widget code reinvents UI primitives |
+| Template developer | Maintainer adding new widgets to `template/` | No shared Button/Dialog → reinvents markup, no consistent styling |
+
+### Differentiators
+
+- ModalManager is a programmatic, single-mount-point API (`modalManager.open(Component, props)`),
+ not a per-widget mount.
+- Update check is read-only (HEAD/GET against npm registry), bounded
+ in frequency, and never auto-mutates the host. Manual update remains the
+ only path that touches `.forgeplan-web/`.
+
+---
+
+## Success Criteria
+
+| ID | Criterion | Metric | Current | Target | Timeframe | How to Measure |
+| ---- | ------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- | ------------ | ------------ | --------------- | ----------------------------------------------- |
+| SC-1 | Shared UI primitives Button/Code/Dialog exist under `template/src/shared/ui/` and are imported by ≥1 widget | Files exist + 1 widget imports | 0 primitives | 3 primitives | This PR | `ls template/src/shared/ui/` + grep for imports |
+| SC-2 | Modal can be opened without mounting in caller — single `ModalRoot` mounted in root layout | Caller line count to open a dialog | N/A | ≤ 3 lines | This PR | Code review |
+| SC-3 | When a newer `@forgeplan/web` is published, the user sees an Update affordance within ≤ 30 minutes | Time-to-notice from npm publish | ∞ (never) | ≤ 30 min | First polled tick post-publish | Manual: bump local pkg version, wait, observe footer |
+| SC-4 | Update dialog shows current → latest and a copyable command; clicking copy writes the command to clipboard | Functional behaviour | N/A | Pass | This PR | Manual smoke in browser |
+| SC-5 | npm-registry endpoint stays read-only and respects in-process concurrency cap | Endpoint method + spawn count | N/A | GET-only | This PR | Code review + `grep -RIn POST template/src/routes/api/update-check` returns 0 hits |
+
+---
+
+## Product Scope
+
+### MVP (In-Scope)
+
+- `template/src/shared/ui/button/Button.svelte` — variants `primary`,
+ `secondary`, `ghost`; sizes `sm`, `md`; disabled state.
+- `template/src/shared/ui/code/Code.svelte` — monospaced block with a copy
+ button; uses `navigator.clipboard.writeText` (with a textarea fallback for
+ insecure contexts that block the Clipboard API).
+- `template/src/shared/ui/dialog/Dialog.svelte` — wraps `
+
+ -y skips npx's install-confirmation prompt;
+ @latest forces npx to fetch the newest tarball
+ instead of running a stale cached copy.
+
Automatic update
- Not available from the browser: running npx @forgeplan/web update
- replaces the very files this server is executing, which would crash the
- running process mid-request. Use the manual command above and reload the
- page when it finishes.
+ Not available from the browser: running the update command replaces the
+ very files this server is executing, which would crash the running
+ process mid-request. Use the manual command above and reload the page
+ when it finishes.
{/snippet}
@@ -120,6 +128,20 @@
color: var(--fg-2);
line-height: 1.5;
}
+ .footnote {
+ margin: 6px 0 0 0;
+ font-size: 11px;
+ color: var(--fg-3);
+ line-height: 1.5;
+ }
+ .footnote code {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--fg-2);
+ background: var(--bg-2);
+ padding: 1px 4px;
+ border-radius: 2px;
+ }
.hint code {
font-family: var(--font-mono);
font-size: 11px;
From 6be54385b5b8a16058cdb559fe3c86bab1dd2da2 Mon Sep 17 00:00:00 2001
From: fedorovvvv
Date: Wed, 6 May 2026 21:08:58 +0400
Subject: [PATCH 3/6] feat(ui): post-update server detector + explicit restart
steps in dialog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
After `update` runs, the host process keeps serving the OLD code:
- macOS/Linux: the running Node process holds the old .forgeplan-web/
files open via open inodes, so rmSync from the new bin doesn't kill
the request loop. The browser sees the old version until the user
manually restarts `node .forgeplan-web/index.js`.
- Windows: the rm step itself can fail with EBUSY because Windows
locks open files; update may partially fail, but again the running
server is unaffected.
Either way the user has to stop+start the server. HMR is not an option
here — adapter-node has no dev-server hooks at runtime.
So the dialog now:
- enumerates the four steps (Stop, Update, Restart, Reload);
- pings /api/version every 5 s while open;
- shows a "Server now serves vX.Y.Z" banner with a Reload button when
the polled web version differs from the dialog's `current`;
- shows an "offline" banner with a `npx @forgeplan/web start` hint
when the ping fails (process killed but not yet restarted).
Refs: PRD-013
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../version-footer/ui/UpdateDialog.svelte | 131 ++++++++++++++++--
1 file changed, 122 insertions(+), 9 deletions(-)
diff --git a/template/src/widgets/version-footer/ui/UpdateDialog.svelte b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
index c1b69ed..36b31c5 100644
--- a/template/src/widgets/version-footer/ui/UpdateDialog.svelte
+++ b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
@@ -1,6 +1,7 @@
+ {#if detectedVersion}
+
+ ✓ Server now serves v{detectedVersion}
+
+
+ {:else if serverDown}
+
+ Server is offline
+ Run npx @forgeplan/web start to restart it, then reload.
+
+ {/if}
+
-
Manual update
-
- Run this in the directory where you initialized @forgeplan/web:
-
+
Steps
+
+
Stop the running server (Ctrl+C in its terminal).
+
Run the command below in the directory where you initialized @forgeplan/web.
+
Restart the server: npx @forgeplan/web start.
+
Reload this page (this dialog will offer that automatically when it sees the new version).
- Not available from the browser: running the update command replaces the
- very files this server is executing, which would crash the running
- process mid-request. Use the manual command above and reload the page
- when it finishes.
+ The update command replaces the very files this server is executing.
+ On macOS / Linux the running process holds the old files open in
+ memory and keeps serving the old code; on Windows the
+ rm step can fail with a busy-file error. Either way
+ the new version becomes visible only after a process restart —
+ which the browser cannot perform on the host.
{/snippet}
@@ -109,6 +169,44 @@
font-size: 18px;
}
+ .banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 10px 12px;
+ margin-bottom: 14px;
+ border-radius: 3px;
+ font-size: 12px;
+ }
+ .banner.ok {
+ background: var(--good-dim);
+ border: 1px solid var(--good);
+ color: var(--fg);
+ }
+ .banner.warn {
+ background: color-mix(in srgb, var(--warn) 12%, transparent);
+ border: 1px solid var(--warn);
+ color: var(--fg-1);
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .banner-title {
+ font-weight: 600;
+ }
+ .banner-hint {
+ color: var(--fg-2);
+ font-size: 11px;
+ }
+ .banner-hint code {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ background: var(--bg-2);
+ padding: 1px 4px;
+ border-radius: 2px;
+ color: var(--fg-1);
+ }
+
.section {
margin-top: 14px;
}
@@ -150,4 +248,19 @@
padding: 1px 4px;
border-radius: 2px;
}
+ .steps {
+ margin: 0 0 10px 0;
+ padding-left: 18px;
+ font-size: 12px;
+ color: var(--fg-1);
+ line-height: 1.6;
+ }
+ .steps code {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--fg-1);
+ background: var(--bg-2);
+ padding: 1px 4px;
+ border-radius: 2px;
+ }
From 22ef26955acb329465c7e4551f8cc7a31c96a3df Mon Sep 17 00:00:00 2001
From: fedorovvvv
Date: Wed, 6 May 2026 21:15:47 +0400
Subject: [PATCH 4/6] feat(ui): auto-reload after update with 1.5s window +
Cancel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The dialog already detects the server-side version change. Reloading is
the inevitable next step — adding a click between "we saw the new
version" and "you see the new version" is friction without value (the
user explicitly opened the update dialog, intent is clear).
So when the polled /api/version differs from the dialog's `current`:
- show a banner that the server now serves the new version + a fading
"reloading…" hint;
- arm a 1.5 s setTimeout that calls window.location.reload();
- offer a Cancel button to stop the timer (preserves any in-tab state
the user wants to keep) and a "Reload now" button to skip the wait;
- guard against the timer firing twice if the poller produces multiple
hits before the page unloads (only the first detection arms it).
Refs: PRD-013
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../version-footer/ui/UpdateDialog.svelte | 46 +++++++++++++++----
1 file changed, 36 insertions(+), 10 deletions(-)
diff --git a/template/src/widgets/version-footer/ui/UpdateDialog.svelte b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
index 36b31c5..d65f2e5 100644
--- a/template/src/widgets/version-footer/ui/UpdateDialog.svelte
+++ b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
@@ -14,12 +14,28 @@
let open = $state(true);
let detectedVersion = $state(null);
let serverDown = $state(false);
+ // Short visible countdown before auto-reload — so the user can see what
+ // happened and Cancel if they have unsaved state in the page.
+ const AUTO_RELOAD_MS = 1500;
+ let reloadTimer: ReturnType | null = null;
function close() {
open = false;
modalManager.close(modalId);
}
+ function reload() {
+ if (typeof window !== 'undefined') window.location.reload();
+ }
+
+ function cancelReload() {
+ if (reloadTimer) {
+ clearTimeout(reloadTimer);
+ reloadTimer = null;
+ }
+ detectedVersion = null;
+ }
+
// `-y` skips npx's "Ok to proceed?" prompt for the install confirmation.
// `@latest` forces npx to fetch the newest tarball instead of running a
// stale cached copy (which would no-op the update). See PRD-013 § Risks.
@@ -44,8 +60,13 @@
if (cancelled) return;
const env = (await res.json()) as ApiEnvelope;
if (env.ok && env.data && env.data.web && env.data.web !== current) {
- detectedVersion = env.data.web;
- serverDown = false;
+ if (!detectedVersion) {
+ detectedVersion = env.data.web;
+ serverDown = false;
+ // Auto-reload — user explicitly opened the update dialog, so the
+ // intent to upgrade is clear. Short window leaves room for Cancel.
+ reloadTimer = setTimeout(reload, AUTO_RELOAD_MS);
+ }
} else {
serverDown = false;
}
@@ -58,12 +79,9 @@
return () => {
cancelled = true;
clearInterval(timer);
+ if (reloadTimer) clearTimeout(reloadTimer);
};
});
-
- function reload() {
- if (typeof window !== 'undefined') window.location.reload();
- }
{#if detectedVersion}
-
- ✓ Server now serves v{detectedVersion}
-
+
+ ✓ Server now serves v{detectedVersion} — reloading…
+
+
+
+
{:else if serverDown}
@@ -103,7 +124,7 @@
Stop the running server (Ctrl+C in its terminal).
Run the command below in the directory where you initialized @forgeplan/web.
Restart the server: npx @forgeplan/web start.
-
Reload this page (this dialog will offer that automatically when it sees the new version).
+
Reload this page (this dialog auto-reloads after ~1.5 s when it sees the new version — Cancel button stops it).
@@ -194,6 +215,11 @@
.banner-title {
font-weight: 600;
}
+ .banner-actions {
+ display: inline-flex;
+ gap: 6px;
+ flex-shrink: 0;
+ }
.banner-hint {
color: var(--fg-2);
font-size: 11px;
From cb31906c8c860169538419caecd3dafb089cb859 Mon Sep 17 00:00:00 2001
From: fedorovvvv
Date: Wed, 6 May 2026 21:17:16 +0400
Subject: [PATCH 5/6] docs(ui): clarify in dialog that browser reload alone is
not enough
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Reload alone won't pick up the new version: Node holds the old ES
modules in memory after the rmSync — open inodes keep them alive, and
new requests are served from the in-memory module cache. The process
itself has to be restarted before browser reload becomes meaningful.
Add a warn-styled line right under the step list to make the chain
explicit (Stop → Update → Restart → Reload) and prevent the common
"I just refreshed but nothing changed" support question.
Refs: PRD-013
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../version-footer/ui/UpdateDialog.svelte | 23 ++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/template/src/widgets/version-footer/ui/UpdateDialog.svelte b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
index d65f2e5..d1ed493 100644
--- a/template/src/widgets/version-footer/ui/UpdateDialog.svelte
+++ b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
@@ -121,11 +121,16 @@
Steps
-
Stop the running server (Ctrl+C in its terminal).
+
Stop the running server (Ctrl+C in its terminal).
Run the command below in the directory where you initialized @forgeplan/web.
-
Restart the server: npx @forgeplan/web start.
-
Reload this page (this dialog auto-reloads after ~1.5 s when it sees the new version — Cancel button stops it).
+
Restart the server: npx @forgeplan/web start.
+
This dialog auto-reloads the page ~1.5 s after it sees the new version (Cancel stops it).
+
+ Browser reload alone is not enough — Node holds the
+ old code in memory and keeps serving it from cached modules. The
+ process must be restarted before reload.
+
-y skips npx's install-confirmation prompt;
@@ -281,6 +286,18 @@
color: var(--fg-1);
line-height: 1.6;
}
+ .warn-line {
+ margin: 0 0 10px 0;
+ padding: 8px 10px;
+ border-left: 2px solid var(--warn);
+ background: color-mix(in srgb, var(--warn) 8%, transparent);
+ font-size: 11px;
+ color: var(--fg-2);
+ line-height: 1.5;
+ }
+ .warn-line strong {
+ color: var(--warn);
+ }
.steps code {
font-family: var(--font-mono);
font-size: 11px;
From 7ec75dea2be4f15aac3c177ed0fc5fafe1207203 Mon Sep 17 00:00:00 2001
From: fedorovvvv
Date: Wed, 6 May 2026 21:18:13 +0400
Subject: [PATCH 6/6] refactor(ui): drop auto-reload + version pinger from
UpdateDialog
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Per request: keep manual update only. The dialog now shows the
current → latest header, the four-step manual recipe, and the
copyable command. Everything else — the /api/version pinger,
serverDown banner, detected-version banner, auto-reload setTimeout,
Cancel/Reload-now buttons — is removed.
Manual update is the only supported path. The user runs the command,
restarts the server themselves, and reloads the tab on their own.
Refs: PRD-013
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.../version-footer/ui/UpdateDialog.svelte | 190 ++----------------
1 file changed, 14 insertions(+), 176 deletions(-)
diff --git a/template/src/widgets/version-footer/ui/UpdateDialog.svelte b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
index d1ed493..cdc4bd4 100644
--- a/template/src/widgets/version-footer/ui/UpdateDialog.svelte
+++ b/template/src/widgets/version-footer/ui/UpdateDialog.svelte
@@ -1,7 +1,6 @@
- {#if detectedVersion}
-
- ✓ Server now serves v{detectedVersion} — reloading…
-
-
-
-
-
- {:else if serverDown}
-
- Server is offline
- Run npx @forgeplan/web start to restart it, then reload.
-
- {/if}
-
-
Steps
+
Manual update
Stop the running server (Ctrl+C in its terminal).
Run the command below in the directory where you initialized @forgeplan/web.
Restart the server: npx @forgeplan/web start.
-
This dialog auto-reloads the page ~1.5 s after it sees the new version (Cancel stops it).
+
Reload this page.
-
- Browser reload alone is not enough — Node holds the
- old code in memory and keeps serving it from cached modules. The
- process must be restarted before reload.
-
-y skips npx's install-confirmation prompt;
@@ -138,18 +57,6 @@
instead of running a stale cached copy.
-
-
-
Why not run it from the browser?
-
- The update command replaces the very files this server is executing.
- On macOS / Linux the running process holds the old files open in
- memory and keeps serving the old code; on Windows the
- rm step can fail with a busy-file error. Either way
- the new version becomes visible only after a process restart —
- which the browser cannot perform on the host.
-