Commit Graph

4 Commits

Author SHA1 Message Date
Devin Foley 236588c753
fix(ui): stamp the service worker with a per-build id so deploys reach parked tabs (#12725)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The web UI ships a service worker (`ui/public/sw.js`) plus update
logic (`ui/src/lib/service-worker-updates.ts`) whose job is to keep
long-lived, parked SPA tabs on the freshly deployed bundle.
> - That reload-on-update path fires only on `controllerchange` — i.e.,
only when the browser installs a new `sw.js`.
> - But `sw.js` was a static public asset (`CACHE_NAME =
"paperclip-v2"`), copied verbatim and never varying per deploy, so a
normal deploy (new app bundle, unchanged `sw.js`) installed no new
worker and triggered no reload.
> - So a parked tab kept running the old bundle after a deploy until a
manual reload — the exact failure the update logic was written to
prevent.
> - This pull request makes `sw.js` change whenever the app bundle
changes, by stamping it with a per-build id at build time.
> - The benefit is that shipped UI fixes actually reach open tabs,
instead of waiting for each user to reload by hand.

## Linked Issues or Issue Description

No separate issue. Describing the bug in-PR using the bug-report fields:

**What happened?**
After a deploy that changes the app bundle but not `sw.js`, tabs left
open across the upgrade keep running the old bundle indefinitely. The
network-first service worker means a manual reload always recovers, but
nothing triggers that reload automatically. Concretely, the `2026.831.1`
onboarding fix did not reach tabs that were open on `2026.831.0`.

**Expected behavior**
When a new bundle is deployed, the existing update machinery
(`registration.update()` on visibility/interval, reload on
`controllerchange`) should bring parked tabs onto the new bundle without
a manual reload.

**Steps to reproduce**
1. Open the app and leave the tab open.
2. Deploy a build that changes the app bundle but not `sw.js` (the
common case — `sw.js` was static).
3. Observe the open tab keeps running the previous bundle; no new worker
installs, so no `controllerchange` and no reload.

**Paperclip version or commit**
Reproduced against `2026.831.1` and `master` before this change.

**Deployment mode**
Any web deployment that serves the built UI (local trusted quickstart,
managed, or self-hosted).

Related PRs (searched open + closed before opening this one):
- Refs #12198 (merged) — added the parked-tab
`update()`/`controllerchange` reload logic this PR completes by making
`sw.js` actually change per deploy.
- Refs #9951 (open) — an alternative "prompt to reload on new build"
approach to the same problem; this PR instead reuses the existing silent
auto-reload path. Reviewers may want to pick one.
- Refs #8112 (open) — serves `sw.js` with `no-cache`; complementary
(that keeps the worker script itself fresh; this makes the script vary
per build).

## What Changed

- `ui/public/sw.js`: derive `CACHE_NAME` from a `__PAPERCLIP_BUILD_ID__`
placeholder so the worker source varies per build.
- `ui/src/lib/vite-sw-build-id.ts`: new Vite build plugin that rewrites
the placeholder in the emitted `sw.js` with the entry chunk's content
hash (stable when the app is unchanged, new when it changes). Throws if
the placeholder is missing, so the worker can never silently stop
rotating.
- `ui/vite.config.ts`: register the plugin.
- `ui/src/lib/vite-sw-build-id.test.ts`: unit tests for the stamping
helper, the build-id derivation, and a contract test that `public/sw.js`
still carries the placeholder.

## Verification

- `vitest run ui/src/lib/vite-sw-build-id.test.ts` — 7 tests pass.
- `vite build` — the emitted `dist/sw.js` contains `BUILD_ID =
"index-<hash>"` matching the entry chunk `dist/assets/index-<hash>.js`,
and the `__PAPERCLIP_BUILD_ID__` placeholder is gone. A subsequent build
with unchanged app code produces the same id (no needless worker churn);
a build with changed code produces a new id.
- Dev (`vite serve`) leaves the literal placeholder in `sw.js`, where
HMR (not the worker) drives refreshes.

## Risks

- Low risk, build-time only. No runtime service-worker logic changes
beyond the cache name being build-specific; the activate handler already
deletes all caches, so a rotating name is inert there.
- If a future edit removes the placeholder, the build fails loudly
rather than silently shipping a non-rotating worker.

## Model Used

- Claude (Anthropic), model id `claude-fable-5` (Claude Fable 5), used
with tool use, shell commands, file editing, and test execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-09-03 13:08:19 -07:00
Devin Foley 01112c350c
fix(ui): always give respondWith a real Response in the sw fetch fallback (#11292)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - The UI registers a service worker (`ui/public/sw.js`) with a
network-first fetch handler whose cache is an offline fallback
> - The fallback hands `event.respondWith` the result of
`caches.match(...)`, which resolves `undefined` on a cache miss — and in
the navigation branch, `caches.match("/") || offlineResponse` never uses
the fallback because `caches.match` returns a promise, which is always
truthy
> - When the network fetch rejects (server restart, deploy, brief
outage) and the cache misses, the browser fails the request with
`Uncaught (in promise) TypeError: Failed to convert value to
'Response'`, so navigation breaks outright instead of degrading to the
offline page
> - This pull request awaits the cache lookups and guarantees a real
`Response` on every path
> - The benefit is that brief server unavailability degrades to the
offline fallback instead of a dead navigation

## Linked Issues or Issue Description

No existing issue. Description follows the bug template:

**What happened?**

Navigating while the server was briefly unavailable (mid-restart)
produced `The FetchEvent for "…" resulted in a network error response:
the promise was rejected.` and `sw.js:1 Uncaught (in promise) TypeError:
Failed to convert value to 'Response'.` The navigation failed instead of
showing the offline fallback.

**Expected behavior**

A failed navigation serves the cached app shell when present, otherwise
the "Offline" 503 response. A failed asset fetch serves its cache entry
when present, otherwise a proper network-error response. `respondWith`
always receives a real `Response`.

**Steps to reproduce**

1. Load the app so `sw.js` is active; ensure `/` is not in the service
worker cache (fresh cache version).
2. Restart or stop the backend.
3. Navigate to any page: the fetch rejects, `caches.match` misses, and
the browser logs the conversion TypeError with a failed navigation.

## What Changed

- `ui/public/sw.js`: the fetch fallback awaits `caches.match(...)` and
returns the "Offline" 503 for navigations and `Response.error()` for
assets when the cache misses.
- `ui/src/lib/sw-offline-fallback.test.ts`: evaluates the real `sw.js`
in a sandboxed scope and covers the three fallback paths; the two
miss-path tests fail against the previous code.

## Verification

- `pnpm vitest run src/lib/sw-offline-fallback.test.ts` in `ui/` — 3
tests pass.
- Verified both miss-path tests fail against the unmodified `sw.js`.

## Risks

Low risk. The change only affects the fetch-rejection path; successful
fetches and cache hits behave exactly as before. `Response.error()`
mirrors what the browser would produce for an unhandled failed no-cors
fetch.

## Model Used

- Claude (Anthropic), Claude Fable 5 (`claude-fable-5`), via Claude Code
CLI with tool use (code search, edit, test execution).

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [ ] All Paperclip CI gates are green
- [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
2026-08-12 11:23:18 -07:00
Dotta a57732f7dd fix(ui): switch service worker to network-first to prevent stale content
The cache-first strategy for static assets was causing pages to serve
stale content on refresh. Switched to network-first for all requests
with cache as offline-only fallback. Bumped cache version to clear
existing caches on activate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 09:08:39 -06:00
Dotta 0e387426fa feat(ui): add PWA configuration with service worker and enhanced manifest
Adds service worker with network-first navigation (SPA fallback) and
cache-first static assets. Enhances web manifest with start_url, scope,
id, description, orientation, and maskable icon entry.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:01:07 -06:00