Skip to content

Console as an installable PWA on desktop: register the Service Worker for the app shell, and prepare a web_push delivery path #29155

Closed

Description

@hotlong

Context

Desktop users reach the Console in a browser, and the product decision is to serve them there rather than ship a native desktop client. The Console is already half a PWA:

  • ui/apps/console/src/main.tsx generates and injects a web app manifest (generatePWAManifest);
  • it wraps the app in MobileProvider with pwa.enabled: true.

But nothing registers a Service Worker. @object-ui/mobile ships serviceWorker.ts and serviceWorkerSource.ts, and no code path calls the registration. As a result:

ADR-0012 §6 already lists web_push among the push device vendors.

Ask

  1. App shell caching.
    • Register a Service Worker from the Console that precaches the app shell (the HTML entry and the hashed entry assets of the current build) and serves it when offline.
    • Hashed chunks are versioned by the build, so the worker must drop the previous build's cache on activation.
    • The worker must not cache API responses: data stays network-only, and session cookies are never stored by the worker.
  2. Update flow. When a new build is deployed, the next load picks it up without a hard refresh. Either the worker activates on next navigation, or the Console offers a "new version — reload" prompt. A stale shell must never pin users to an old build.
  3. web_push readiness.
  4. Install affordance. Verify that the Console is installable in Chrome and Edge on desktop: the manifest is valid, it has icons at the required sizes, and its start_url and scope are correct under /_console/. Branding from Setup applies to the installed app's name and icon (the manifest is already built from runtime branding).

Out of scope: a native desktop shell. If a concrete need appears later, the shape is a separate repository with a Tauri shell hosting the Console through the same window.ObjectStackNative bridge the mobile shell uses. The trigger needs are an installer or MDM distribution, tray residency, local file system or device access, or deep enterprise IM desktop integration. No card is opened for it until one of those needs is named.

Acceptance

  • Playwright test: after one load, going offline and reloading shows the Console shell, not a browser error, and data calls fail visibly as offline.
  • Deploying a new build is picked up within one navigation (test with two builds, or a mocked worker update).
  • Unit tests cover the worker's cache-versioning, the API-bypass rule, and the push / notificationclick handlers (the click opens the route; a route that escapes /_console/ is refused).
  • A Lighthouse or Chrome "installable" check passes on the served Console.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:ui · pm:queue · 2026-11-02T08:30Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim: PM loop round 11 · 2026-11-02T09:15Z
    Session: session_01QsR2jQhk3S1XeTLgXSkEMt (card thread, os-dev); dispatched by seat session session_01QsR2jR7ZFkpqmm4U8S1f9y
    Account: zhuangjianguo
    Branch: claude/project-thread-detgab (platform-assigned to the card thread; it can push only this branch)
    Worktree: objectstack-issue-29155
    Domain: domain:ui
    Seat: domain:ui#4
    File surface: ui/packages/mobile/src/serviceWorker.ts, serviceWorkerSource.ts, their tests and README, the registration in ui/apps/console/src/main.tsx, ui/apps/console/vite.config.ts (worker emit and build id only), ui/apps/console/public/manifest.json if needed, a Playwright offline/update spec under ui/e2e/, and a changeset (stop on breach; explain in the report)
    Container & model: L, mode:thread, model: default tier (dispatch-gates --tier: no path-derived mandate), effort: default
    Clause-②: no
    Metadata-protocol: none
    Responsibility: desktop users run the Console as an installed browser app, but nothing registers a Service Worker, so a cold start offline is a browser error page and there is no worker for Web Push to reach | @object-ui/mobile already ships the registration and a worker generator that no code path calls | every desktop user of the installed Console
    Thread-read: 6572618088
    Premise: holds (at origin/main, no file in ui/apps/console/src calls registerServiceWorker)
    Serial constraints cleared: PR #29140 (#29039) touches main.tsx and @object-ui/mobile and is in the merge queue; merge main after it lands


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T09:23Z · the Clause-② line is corrected; the branch, session and file surface are unchanged.

    Clause-②: yes (widening)
    Metadata-protocol: none

    Reading (the seat's, from the dev's stop-and-tell before any push): @object-ui/mobile is published, and its surface widens.

    • ServiceWorkerSourceOptions gains the optional members strategy ('offline-first' | 'app-shell'), build and notifications.
    • New exports: the type ServiceWorkerStrategy, the constant SERVICE_WORKER_CACHE_URLS_MESSAGE and the function cacheLoadedResources(worker).
    • The default output of getServiceWorkerSource() stays byte-identical, so every existing caller gets the same worker.

    That is a widening, and the changeset stays patch. The review owed is the lane seat's own TIER_DEFAULT record. The app-shell strategy must keep API requests network-only (the brief's ruling), and that is what the review will check.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T09:45Z · the file surface widens by two files; the branch, session and declaration lines are unchanged.

    Clause-②: yes (widening)
    Metadata-protocol: none

    Measured by the dev on a default-base build served the way the CLI serves it, in real Chromium with CDP Page.getInstallabilityErrors:

    • The Console's CSP (consoleContentSecurityPolicy in packages/cli/src/utils/console.ts) sets default-src 'self' and no manifest-src. So the branded blob: manifest that main.tsx injects is refused, and the served Console is not installable on main today (no-manifest, manifest-parsing-or-network-error).
    • With manifest-src 'self' blob: added, there are no installability errors, and the manifest carries the branded name, start_url and scope under /_console/.

    Ruling: option A. Add the one directive at the producer, and keep Setup branding on the installed app. The widening is narrow: a blob: manifest can only be minted by a script that has already passed script-src.

    File surface (added):

    • packages/cli/src/utils/console.ts: the manifest-src 'self' blob: directive, plus its comment, which today says default-src covers the manifest.
    • packages/cli/test/console.content-security-policy.test.ts (or wherever the CSP is pinned): a pin of the new directive.
    • The changeset adds @objectstack/cli: patch.

    The PR body names the CSP change as a security-header widening, with the measurement above.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T10:25Z · the file surface widens by one config line; the branch, session and declaration lines are unchanged.

    Clause-②: yes (widening)
    Metadata-protocol: none

    Measured by the dev at head f74b1548 (after main merged #29140): vite.config.ts imports the Service Worker generator from source, ../../packages/mobile/src/serviceWorkerSource.ts, which is a dependency-free file. turbo run type-check --filter=@object-ui/console is red with TS6307, because the composite tsconfig.node.json does not list that file. The vite build itself is green.

    Ruling: option A. Add that one path to the include of ui/apps/console/tsconfig.node.json, beside the existing ../../scripts/vite-*.ts entries. This is the same pattern the config already uses for source files it imports with an explicit .ts. Option B, importing from the @object-ui/mobile entry, is refused: it would load the mobile dist barrel, with its React components, into Node for the config, and the CI step "Build the Console for E2E" runs vite build directly, so it would depend on mobile's dist being built first.

    File surface (added): ui/apps/console/tsconfig.node.json, the one include entry only.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T10:57Z · the file surface widens by three cache-input entries in the root turbo.json; the branch, session and declaration lines are unchanged.

    Clause-②: yes (widening)
    Metadata-protocol: none

    Measured by the dev at 7ba4d977: pnpm --dir ui exec vitest run scripts/ is red, with 40 cases across turbo-build-inputs, turbo-test-inputs and turbo-type-check-inputs. The cause is the one behind 6574807547. The console's build, test and type-check programs now read ui/packages/mobile/src/serviceWorkerSource.ts from outside the package, and turbo does not hash that file. A change to the generator would then replay a stale cache. The gate's own fix text asks for the path in those three tasks' inputs.

    Ruling: add "$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts" once each to the build, test and type-check task inputs in the root turbo.json. Make no other change to that file. This follows the pattern the file already uses for $TURBO_ROOT$/ui/scripts/vite-*.ts. Importing the package entry instead stays refused (6574807547). check-governed-merges --test on turbo.json, ui/apps/console/tsconfig.node.json and packages/cli/src/utils/console.ts reads NOT governed, 0 of 3.

    File surface (added): turbo.json, those three inputs entries only.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Claim amended · 2026-11-02T11:26Z · the file surface widens by one paths-filter row in .github/workflows/ci.yml. This makes the PR governed: landing tier H (a human merge). The branch, session and declaration lines are unchanged.

    Clause-②: yes (widening)
    Metadata-protocol: none

    Measured by the dev at e4e73277, which carries the turbo.json entries of 6575431847: pnpm check:ci-filter-parity exits 1. Every $TURBO_ROOT$ build and test input must also be a row in ci.yml's paths-filter core: list. ui/packages/mobile/src/serviceWorkerSource.ts is not one yet. Adding the row turns the check green: 138 of 138 self-test assertions pass, and all 30 build inputs and 9 test inputs are covered.

    Option E was tried and refused on measurement. It would give @object-ui/mobile a subpath export and have vite.config.ts import it, which needs no root inputs. The root ui vitest config loads the console's vite.config.ts, so mobile's dist must exist wherever vitest or the console build runs. Test UI's "Run the ui tests" (16 shards) and Live E2E UI's "Build the Console for the live lane" build no @object-ui dist. Every shard would abort at startup, and fixing that means editing ci.yml job steps. Dropping the build-time emit would leave the card undone.

    Ruling: add one row, - 'ui/packages/mobile/src/serviceWorkerSource.ts', with a short comment, beside the existing ui build-input rows of the core: filter. That is a filter row only: no job, step or trigger change. Amendments 6574807547 (tsconfig.node.json include) and 6575431847 (turbo.json inputs) stand.

    File surface (added): .github/workflows/ci.yml, that one core: filter row only.

    Landing: check-governed-merges --pr will read GOVERNED (.github/workflows/**). The seat posts its review as the Tier H record, with the 维护者速读 pinned and needs-user-decision. The PR merges by the maintainer's hand, or by an authorized approval and then the seat. The seat does not flip it ready, enqueue it or arm auto-merge before that.


    Generated by Claude Code

  8. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y
    os-dev-report

    {
      "issue": 29155,
      "status": "done",
      "branch": "claude/project-thread-detgab",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/29326",
      "head": "b5658de9d5d4778a2531dcaf245cdaf4faa15fc3",
      "session": "session_01QsR2jQhk3S1XeTLgXSkEMt",
      "premise_still_valid": true,
      "summary": "The Console's production build emits a build-versioned app-shell service-worker.js (new 'app-shell' strategy in @object-ui/mobile's generator; default 'offline-first' output byte-identical to main) and registers it under /_console/, never in the native shell, where an earlier one is unregistered. /api/ and out-of-scope requests are network-only; navigations network-first with the cached entry offline; hashed assets cache-first; activation deletes this scope's older build caches. push/notificationclick handlers open only in-scope in-app routes; no subscription. The CLI console CSP gains manifest-src 'self' blob: (seat ruling A), making the served Console installable.",
      "tests": "@object-ui/mobile vitest 115/115 (25 new); @objectstack/cli CSP suite 50/50; console tsc + vite build green; turbo type-check mobile+console 72/72; Playwright chromium 66 passed / 2 skipped pre-existing at e4e73277, incl. 2 new console-service-worker cases (offline reload fromServiceWorker + auth-server-unreachable + API TypeError + no /api/ cache; one-navigation update with only the -next cache left). Ablations, each restored byte-identical: API bypass removed -> 2 red; click-route scope check removed -> 2 red; activation deleting all caches -> 1 red. Installability CDP: before manifest-parsing-or-network-error + no-manifest, after none (installability-check.png).",
      "gates": "dispatch-gates --ran: 98 derived, 98 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0 (65 at e4e73277, 33 ci.yml-row families at b0ef42a0, ci-filter-parity/type-check-debt/sdui-manifest/ui vitest scripts/ 169 files 4903 tests re-run at b5658de9 after merging main a19759ac). PR-body gates with GITHUB_EVENT_PATH: check-changeset-no-major --base origin/main 0, check-adr-0087-registration 0, check-closing-keyword-parity 0, check-partof-closing-keyword 0. check-governed-merges --pr 29326: exit 3 GOVERNED, tier H, one hit .github/workflows/ci.yml, 1119 lines. CI check-runs: pending at report time.",
      "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
      "line_budget": "+1090/-29 across 14 files, under 3000",
      "files_changed": [
        ".changeset/29155-console-desktop-pwa.md",
        ".github/workflows/ci.yml",
        "packages/cli/src/utils/console.content-security-policy.test.ts",
        "packages/cli/src/utils/console.ts",
        "turbo.json",
        "ui/apps/console/src/main.tsx",
        "ui/apps/console/tsconfig.node.json",
        "ui/apps/console/vite.config.ts",
        "ui/e2e/console-service-worker.spec.ts",
        "ui/packages/mobile/README.md",
        "ui/packages/mobile/src/__tests__/serviceWorkerSource.test.ts",
        "ui/packages/mobile/src/index.ts",
        "ui/packages/mobile/src/serviceWorker.ts",
        "ui/packages/mobile/src/serviceWorkerSource.ts"
      ],
      "deviations": [
        "Clause-② amended by the seat to yes (widening); Metadata-protocol none; both copied verbatim.",
        "Surface amendments granted by the seat: CLI CSP + pin (6573999866), tsconfig.node.json include (6574807547), turbo.json three inputs (6575431847), one ci.yml core: filter row (6575983994). Option E (subpath export) measured failing and dropped.",
        "The e2e serves dist itself with an injected base tag instead of vite preview (no base tag there, app leaves the worker scope).",
        "PR is Tier H (ci.yml): left draft, not flipped or enqueued; landing is the seat's after the user decides."
      ],
      "mcp_calls": "1 — mcp__github__pull_request_read (get #29326); no write tools",
      "api_writes": "9 — git push x7 to claude/project-thread-detgab; pr_create x1 via fleet-write relay (#29326); os-dev-report comment x1 via post-stamped on #29155",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  9. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

    Seat review · 2026-11-02T12:58Z · PR #29326 at 55e3e5e5: contract review (CONTRACT_REVIEW_TIER, director seat) PASS 6577669391, the seat's TIER_DEFAULT PASS 6577635035, and ACCEPT → PR #29326 comment 6577690271. Tier H (ci.yml): needs-user-decision is on the PR, and review is requested from os-zhuang and hotlong. The PR stays a draft until an authorized approval or a maintainer's merge. The director's should-fix is filed as #29361.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions