Advertise installation as web app #37

Closed
opened 2026-07-27 14:15:16 +00:00 by Grandiras · 4 comments
Owner

Maybe we can put a small, uninterrupting notice to install as a web app. Or add an action for that into the main hamburger menu.

Maybe we can put a small, uninterrupting notice to install as a web app. Or add an action for that into the main hamburger menu.
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/122); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-37/run-122.md) — updating every ~5 minutes while the task runs.
Collaborator

⚠️ Claude run failed (API Error: Connection closed mid-response. The response above may be incomplete.) — see the action log for details.
📜 Full session transcript

⚠️ Claude run failed (API Error: Connection closed mid-response. The response above may be incomplete.) — see the action log for details. 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-37/run-122.md)
Collaborator

🤖 On it — follow along in the action run; the reply will link the full session transcript.

📜 Live transcript — updating every ~5 minutes while the task runs.

🤖 On it — follow along in [the action run](https://git.grandiras.net/Grandiras/claude-bot/actions/runs/123); the reply will link the full session transcript. 📜 [Live transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-37/run-123.md) — updating every ~5 minutes while the task runs.
Collaborator

Pushed to claude/issue-37.

Advertise installation as a web app

Went with the hamburger-menu action, plus the smallest possible notice: a dot on the hamburger until the menu has been opened once.

The catch was that nothing could be installed to begin with — there was no manifest and no service worker, so no browser ever offered an install. So this is the whole path, not just the button:

Making it installable

  • wwwroot/manifest.webmanifest + wwwroot/icons/ — PNGs generated from two SVG siblings of BrandMark.razor: icon.svg (rounded, full-bleed) for 192/512, and icon-maskable.svg (square edge, content in the inner 80% safe zone) for the maskable icon and the apple-touch-icon, since iOS reads none of the manifest.
  • wwwroot/service-worker.js caches nothing but an offline page, and a comment says to keep it that way — this is Blazor Server, so a cached shell could only ever show a page the server has moved on from. It exists because installability needs a worker with a fetch handler, and the handler has to be real (Chrome detects and skips no-op ones). It answers navigations only, falling back to offline.html — which matters in a standalone window that has no address bar to recover from. That page is self-contained and ships both languages, picking one from the ponente-locale cookie.

The offer

  • A row in the nav menu (Install app / Opens in its own window), above sign-out, in the brand-surface style the menu already uses. Chrome's own mini-infobar is suppressed, so the menu is the only place it shows up.
  • A dot on the hamburger while the offer is new; opening the menu spends it for good (ponente-install-seen). No banner, no toast.
  • js/pwa-install.js is a classic script in <head>, not a module: beforeinstallprompt fires once and early, usually before a circuit exists, so a listener attached from OnAfterRenderAsync would miss it on most loads. It parks the event; NavHeader asks for the state and gets called back when it changes.
  • iOS fires no install event and installs from the share sheet, so it gets its own state and the row shows instructions instead of prompting.
  • One thing that fell out of it: a theme pinned in the menu now also reaches the installed window's title bar. The theme-color tags follow the OS by design, so prefs-interop.js appends an overriding tag when the theme is pinned — otherwise Dark-on-a-light-system left a white bar over a dark header.

Strings are in both locales, NavHeader already inherits LocalizedComponentBase, and CLAUDE.md / README.md record the constraints above.

Verified (no container engine in CI, so I installed the .NET 10 SDK, Postgres and Chromium and ran a live server):

  • dotnet build clean, dotnet test 46/46 (including the en/de key-parity test).
  • New tests/e2e/install.spec.js — 7 tests: manifest shape and every icon reachable, service worker registering at root scope, nothing advertised when the browser hasn't offered, the dot + row appearing via the callback into the circuit, the prompt actually being invoked, the nudge not returning after a reload, translation, and the iOS share-sheet branch under an iPhone user agent. All pass. It dispatches the install event the way Chrome would, because a headless run can't provoke a real one.
  • Regression run of board / i18n / mobile / both entrance specs: 66/67. The one failure (mobile.spec.js → "card tags stay labels") needs a pre-existing tagged card on the board and can't pass against the empty database I built here — unrelated to this change, and worth a re-run on the real test stack.
  • Checked the menu in light and dark by screenshot, and confirmed the assets serve with Content-Type: application/manifest+json and Cache-Control: no-cache + ETag (right for a worker and manifest — updates roll out immediately).

Not verifiable from here: a real Android install prompt and a real iOS home-screen add. Both depend on the browser's own judgement rather than on app code, but the icons and the iOS tags are worth eyeballing on a device once this is deployed.


⏱ 21m 14s · 100 turns · tokens: 117.6k in / 57.1k out (+8634.0k cache-read)
📜 Full session transcript
→ Opened PR: #41

Pushed to `claude/issue-37`. ## Advertise installation as a web app Went with the hamburger-menu action, plus the smallest possible notice: a dot on the hamburger until the menu has been opened once. The catch was that **nothing could be installed to begin with** — there was no manifest and no service worker, so no browser ever offered an install. So this is the whole path, not just the button: **Making it installable** - `wwwroot/manifest.webmanifest` + `wwwroot/icons/` — PNGs generated from two SVG siblings of `BrandMark.razor`: `icon.svg` (rounded, full-bleed) for 192/512, and `icon-maskable.svg` (square edge, content in the inner 80% safe zone) for the maskable icon and the apple-touch-icon, since iOS reads none of the manifest. - `wwwroot/service-worker.js` **caches nothing but an offline page**, and a comment says to keep it that way — this is Blazor *Server*, so a cached shell could only ever show a page the server has moved on from. It exists because installability needs a worker with a fetch handler, and the handler has to be real (Chrome detects and skips no-op ones). It answers navigations only, falling back to `offline.html` — which matters in a standalone window that has no address bar to recover from. That page is self-contained and ships both languages, picking one from the `ponente-locale` cookie. **The offer** - A row in the nav menu (*Install app / Opens in its own window*), above sign-out, in the brand-surface style the menu already uses. Chrome's own mini-infobar is suppressed, so the menu is the only place it shows up. - A dot on the hamburger while the offer is new; opening the menu spends it for good (`ponente-install-seen`). No banner, no toast. - `js/pwa-install.js` is a **classic script in `<head>`**, not a module: `beforeinstallprompt` fires once and early, usually before a circuit exists, so a listener attached from `OnAfterRenderAsync` would miss it on most loads. It parks the event; `NavHeader` asks for the state and gets called back when it changes. - **iOS** fires no install event and installs from the share sheet, so it gets its own state and the row shows instructions instead of prompting. - One thing that fell out of it: a theme pinned in the menu now also reaches the installed window's title bar. The `theme-color` tags follow the OS by design, so `prefs-interop.js` appends an overriding tag when the theme is pinned — otherwise Dark-on-a-light-system left a white bar over a dark header. Strings are in both locales, `NavHeader` already inherits `LocalizedComponentBase`, and `CLAUDE.md` / `README.md` record the constraints above. **Verified** (no container engine in CI, so I installed the .NET 10 SDK, Postgres and Chromium and ran a live server): - `dotnet build` clean, `dotnet test` 46/46 (including the en/de key-parity test). - New `tests/e2e/install.spec.js` — 7 tests: manifest shape and every icon reachable, service worker registering at root scope, nothing advertised when the browser hasn't offered, the dot + row appearing via the callback into the circuit, the prompt actually being invoked, the nudge not returning after a reload, translation, and the iOS share-sheet branch under an iPhone user agent. All pass. It dispatches the install event the way Chrome would, because a headless run can't provoke a real one. - Regression run of `board` / `i18n` / `mobile` / both entrance specs: 66/67. The one failure (`mobile.spec.js` → "card tags stay labels") needs a pre-existing tagged card on the board and can't pass against the empty database I built here — unrelated to this change, and worth a re-run on the real test stack. - Checked the menu in light and dark by screenshot, and confirmed the assets serve with `Content-Type: application/manifest+json` and `Cache-Control: no-cache` + ETag (right for a worker and manifest — updates roll out immediately). Not verifiable from here: a real Android install prompt and a real iOS home-screen add. Both depend on the browser's own judgement rather than on app code, but the icons and the iOS tags are worth eyeballing on a device once this is deployed. --- ⏱ 21m 14s · 100 turns · tokens: 117.6k in / 57.1k out (+8634.0k cache-read) 📜 [Full session transcript](https://git.grandiras.net/Grandiras/claude-bot/src/branch/claude-logs/transcripts/Grandiras/Ponente/issue-37/run-123.md) → Opened PR: https://git.grandiras.net/Grandiras/Ponente/pulls/41
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Grandiras/Ponente#37
No description provided.