Skip to content

feat: make the app installable as a desktop app - #464

Merged
kNoAPP merged 2 commits into
developfrom
kNoAPP/446-feat-installable-desktop-app
Sep 26, 2026
Merged

kNoAPP merged 2 commits into
developfrom
kNoAPP/446-feat-installable-desktop-app

Conversation

@kNoAPP

@kNoAPP kNoAPP commented Sep 26, 2026 •

Copy link
Copy Markdown
Owner

Description

This PR delivers the whole desktop-install epic (#446 to #449) in one PR, as requested.

#446: make the app installable

  • Icons. public/icon-192.png, icon-512.png and a maskable icon-maskable-512.png. app/favicon.ico is now a 16/32/48 ICO. The export command is documented in public/brand/README.md, and it reproduces the committed files byte for byte.

    • Departure from the ticket: the maskable icon comes from app-icon.svg as specified. The standard icons and the favicon come from a new public/brand/app-tile.svg master instead: the same mark and background on a rounded tile, with the mark near full size. The OS shows standard icons uncropped, so a straight export of the full-bleed app-icon.svg would be a hard square with the mark at about 56% width. At taskbar and tab sizes that shrinks the mark to a few pixels.
  • Manifest. Adds "id": "/" and "launch_handler": { "client_mode": "focus-existing" }, plus the three icons; the .ico entry is gone. background_color and theme_color now match the real dark --bg and --surface; the old #0a0e14 matched neither. The name is unchanged: it was already "MeshCore Desktop" from chore: rename the app and repository from MeshCore-WebAgent to MeshCore-Desktop #452.

  • Title bar (departure from the ticket). Instead of per-scheme <meta name="theme-color" media=…> entries, the static theme-color stays the brand accent. The pre-paint theme script and ThemeProvider then repaint it to the header's --surface for the active theme. Two reasons:

    React re-adds its own copy of that meta at hydration, so ThemeProvider repaints every theme-color meta, not just the first.

#447: install entry points

  • Prompt capture and state. lib/pwa/install.ts captures beforeinstallprompt (calling preventDefault()) and clears it on appinstalled or once prompt() settles. The store gets runtime-only installPrompt, appInstalled and installGuideOpen. They are not preferences, so they are not in the preferences blob. reset() preserves installPrompt and appInstalled. It closes the guide, because it also zeroes the modal count under it.
  • Knowing the app is installed. There are three signals:
    • display-mode: standalone, including a change event for a tab Chrome moves into the app window without reloading.
    • appinstalled, in the tab that installs it.
    • A same-origin related_applications entry in the manifest, read with navigator.getInstalledRelatedApps(). This lets any later tab or reload know too, so the entry points don't come back after installing.
  • Prompt race. While testing I found that Chromium can fire beforeinstallprompt before hydration reaches the module that listens for it. I measured it: fired at 223 ms, after the load event, and the prompt was lost for that page load. A small inline <head> script now parks an early event on window, and the module adopts it when it loads. The name of that window property is in lib/pwa/config.ts, so the server layout never imports the store.
  • Entry points. They show whenever the app isn't installed. Each one calls prompt() when a prompt was captured, and opens the feat(ui): in-app guide for installing the desktop app #448 guide otherwise:
    • The connect screen: a quiet, non-dismissible row above the footer.
    • Settings → Display: an Install row.
    • The command palette: an "Install as a desktop app" action. Search only, since the empty-query launcher is for radio verbs. It is marked opensDialog, because it can open the guide.

#448: install guide

  • InstallGuideModal, built on ModalShell and keeping no state of its own, covers what you get, the steps for the detected browser, and how to uninstall.
    • Steps are for Chrome, Edge, or "use Chrome or Edge" for Firefox and Safari.
    • The uninstall text warns to leave "Also clear data" unchecked. Chrome's uninstall dialog offers that option, and it would wipe the tab's IndexedDB.
  • It is mounted outside the connected-only block, because the connect screen opens it too.
  • A Learn more link sits next to the connect and Settings entries while a prompt is available. The guide then also offers Install now.
  • The menu steps say "choose the Install option" rather than quoting a menu label. Chrome and Edge label that item with the app's name on installable sites, and this keeps the name out of the locale strings, as feat(pwa): make the app installable as a desktop app #446 asks.

#449: docs

  • The README gets an Install as a desktop app section after Browser Requirements, with steps that match the in-app guide.
  • It adds an Installable desktop app bullet, and the Fully client-side bullet now reads "runs on your computer, in a browser tab or the installed app".
  • Announcement: the PR title is a user-facing feat:, so the squash commit lands in CHANGELOG.md and the release notes as "make the app installable as a desktop app". The notes still need a check once that release is cut (the second half of docs: document and announce the installable desktop app #449).

Screenshots

Before After
before: connect screen after: connect screen with the install row above the footer
before: Settings Display card after: Settings Display card with the Desktop app row
Guide (Chrome, prompt available) Guide (Firefox)
install guide with Chrome steps and Install now install guide pointing to Chrome or Edge
Palette Icons: 512, 192, maskable 512, favicon 48/32/16
command palette with the install action the exported app icons and favicon frames

Verification

Tested in Chrome on Windows against the burner radio (KN0.APP, Heltec V4.3, over USB), in next dev and in the production export served from out/ on localhost:

  • Production export: the service worker is active, and all three manifest icons serve as image/png. Chrome fires beforeinstallprompt, which is Chromium's signal that the installability criteria are met, and the app captures it. out/index.html has a single accent theme-color meta.
  • Prompt capture: captured on repeated fresh loads after the early-capture fix. Before the fix, a load could lose it.
  • Survives connecting: the captured prompt stays through the connect flow's reset(). With the radio connected, Settings → Display shows Learn more and Install.
  • Firefox simulation: a Firefox user agent, with beforeinstallprompt swallowed. The connect row opens the guide with the "use Chrome or Edge" steps, with no Install now, and nothing is logged to the console.
  • Dismissed-prompt simulation: while connected, the palette action opens the guide (after the palette closes) with Chrome's menu steps.
  • After install: a synthetic appinstalled removes the entry points from the tab. A fresh load where getInstalledRelatedApps() is stubbed to report the app shows none. With the real API, the lookup returns [] without error, and the prompt still fires with related_applications in the manifest.
  • Title bar: the theme-color meta follows the theme toggle, #1a1d27 dark and #f4f6f9 light, in both dev and the production export.

Still needs a human: I didn't click Install in this Chrome profile, because it would install an app on the machine. Please confirm:

  • DevTools → Application → Manifest shows no errors.
  • After installing, a fresh browser tab hides the install entry points. This relies on getInstalledRelatedApps() matching the relative related_applications URL, which I couldn't check without installing.
  • The install dialog opens from each entry point.
  • The installed window uses the new icon and the matching title bar.
  • A second launch focuses the existing window.
  • USB and BLE connect from the installed window.

The Chrome and Edge menu wording and the es/de/fr strings are drafts for human review.

Issues

Type of Change

  • ❌ ! -- Breaking change (fix or feature that would cause existing
    functionality to change)
  • ✨ feat -- New feature (non-breaking change which adds functionality)
  • 🛠️ fix -- Bug fix (non-breaking change which fixes an issue)
  • 📝 docs -- Additional or updated documentation
  • 🎨 style -- Code style changes (formatting, missing semi colons, etc)
  • 🧹 refactor -- Code refactor
  • ⚡ perf -- Performance improvement
  • 🧪 test -- Tests (adding or updating tests)
  • 🏗️ build -- Build system or external dependencies changes
  • ✅ ci -- Build configuration change
  • 🗑️ chore -- Grunt tasks
  • ⏪ revert -- Revert previous changes

Chrome and Edge can now install the app: the manifest gains PNG icons
(192, 512 and a maskable 512) exported from new brand art, a stable
`id`, and a `focus-existing` launch handler so a second launch reuses
the open window instead of fighting it for the radio. The favicon is
now a 16/32/48 ICO. The title bar follows the active theme, while the
static `theme-color` keeps the accent for link-preview crawlers.

The app offers installation itself from the connect screen, Settings →
Display and the command palette. It keeps Chromium's install prompt
(captured before hydration, which it can beat) and falls back to an
install guide for Firefox, Safari and a dismissed prompt. Every entry
point disappears once the app is installed.

The README documents installing, updating and uninstalling.

Closes #446
Closes #447
Closes #448
Closes #449
@github-actions github-actions Bot added documentation Improvements or additions to documentation ui state config labels Sep 26, 2026
@kNoAPP
kNoAPP merged commit 588a986 into develop Sep 26, 2026
10 checks passed
@kNoAPP
kNoAPP deleted the kNoAPP/446-feat-installable-desktop-app branch September 26, 2026 23:25
@github-actions github-actions Bot mentioned this pull request Sep 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config documentation Improvements or additions to documentation state ui

Projects

None yet

1 participant