|
| 1 | +# Group D — Packaging, Installer, Branding Fix & Auto-update |
| 2 | + |
| 3 | +**Date:** 2026-06-26 |
| 4 | +**Status:** Design approved, ready for planning |
| 5 | +**Backlog:** App-shell lifecycle, Group D (#6 installer splash + the Windows |
| 6 | +toast/exe branding fix). The last group before the download web page. |
| 7 | + |
| 8 | +## Goal |
| 9 | + |
| 10 | +Stand up the Windows packaging pipeline (none exists yet) and ship a free, |
| 11 | +zero-cost distributable: |
| 12 | + |
| 13 | +1. **Package** the Electron app into a branded, per-user **NSIS one-click installer**. |
| 14 | +2. **Fix the Windows toast & exe branding** so the app reads "Vitreous" (+ icon), |
| 15 | + not the raw AUMID / "electron.exe" — finishing decision #6 and completing the |
| 16 | + Group-A startup-entry branding. |
| 17 | +3. **Auto-update** via `electron-updater` + GitHub Releases, designed so an update |
| 18 | + can never interrupt a locked focus session. |
| 19 | + |
| 20 | +## Distribution & cost decisions (locked) |
| 21 | + |
| 22 | +- **Zero spend.** Every piece is free: electron-builder + NSIS (open-source), |
| 23 | + GitHub Releases (host), GitHub Pages (download page, next milestone). |
| 24 | +- **Unsigned, but signing-ready.** No paid certificate. The installer/app are |
| 25 | + unsigned → Windows SmartScreen shows a one-time "More info → Run anyway" until |
| 26 | + reputation builds; documented on the download page. The pipeline is structured so |
| 27 | + a signer can be added later with no rework. |
| 28 | +- **Free signing is a post-release follow-up, not part of Group D.** [SignPath |
| 29 | + Foundation](https://signpath.org/) gives free OV signing for qualifying OSS, but |
| 30 | + requires an existing released build + a download page first (so it cannot sign the |
| 31 | + first build). Pursue it after the download-page milestone. Publisher would then |
| 32 | + read "SignPath Foundation". |
| 33 | +- **No Microsoft Store / MSIX.** Store policy rejects apps that require elevation |
| 34 | + for *any* functionality — Vitreous requires it (hosts-file write via the elevated |
| 35 | + scheduled task, HKLM browser policies). MSIX virtualization would also break the |
| 36 | + enforcement core (elevated helpers, hosts edits, process kills). Direct download is |
| 37 | + the only viable channel for a system-level blocker. |
| 38 | + |
| 39 | +## Section 1 — Packaging pipeline (electron-builder + NSIS) |
| 40 | + |
| 41 | +**Tooling.** Add `electron-builder` (devDependency) layered on the existing |
| 42 | +`electron-vite build`. Config in a new **`electron-builder.yml`**. New scripts: |
| 43 | +- `"dist": "electron-vite build && electron-builder"` — full installer build. |
| 44 | +- `"dist:dir": "electron-vite build && electron-builder --dir"` — unpacked build for |
| 45 | + fast local testing (no installer). |
| 46 | + |
| 47 | +**Identity.** `appId: com.vitreous.app` (matches `setAppUserModelId` and the toast |
| 48 | +AUMID). `productName: Vitreous` → binary is **`Vitreous.exe`** with "Vitreous" |
| 49 | +version metadata. |
| 50 | + |
| 51 | +**Target.** `nsis`, **x64** only (arm64 deferrable). Output dir `release/`. |
| 52 | + |
| 53 | +**Installer.** `oneClick: true`, `perMachine: false` → double-click installs per-user |
| 54 | +to `%LOCALAPPDATA%\Programs\Vitreous`, **no UAC for install**, auto-launches on |
| 55 | +finish. Branding: `assets/logo/icon.ico` for `installerIcon` / `uninstallerIcon` / |
| 56 | +`installerHeaderIcon`. NSIS auto-generates the uninstaller (also satisfies a future |
| 57 | +SignPath requirement). |
| 58 | + |
| 59 | +**Package contents.** `files` includes built `out/**` plus the runtime assets not |
| 60 | +under `out/`: **`assets/**`** and **`extension/**`**. **`asarUnpack`** both so the |
| 61 | +path-sensitive files live on real disk (not inside `app.asar`): |
| 62 | +- `assets/**` — toast icon (`icon-300.png`) + bundled sounds (need real paths). |
| 63 | +- `extension/**` — the Firefox `.xpi` (Firefox installs from a `file://` path). |
| 64 | + |
| 65 | +No change needed to `scheduler-task.js`/`taskPaths()` — it already resolves |
| 66 | +`Vitreous.exe` when `app.isPackaged`. |
| 67 | + |
| 68 | +## Section 2 — Toast & exe branding fix (decision #6) |
| 69 | + |
| 70 | +1. **Exe + Task Manager name** — free from `productName: Vitreous`. The Group-A |
| 71 | + startup entry and Task Manager → "Startup apps" now read **Vitreous** (resolving |
| 72 | + the "electron.exe" artifact seen during Group-A dev testing). |
| 73 | +2. **Start-Menu shortcut carries the AUMID** — Windows resolves a desktop app's toast |
| 74 | + name/icon from an installed shortcut whose `AppUserModelID` matches the running |
| 75 | + app. electron-builder NSIS stamps the shortcut AUMID from `appId` |
| 76 | + (`com.vitreous.app`), matching `setAppUserModelId('com.vitreous.app')`. With the |
| 77 | + already-shipped `registerToastAppId` HKCU registration, toasts attribute correctly |
| 78 | + in the **packaged** app. |
| 79 | +3. **Toast `IconUri` → real on-disk file.** `registerToastAppId` is currently passed |
| 80 | + `join(app.getAppPath(), 'assets','logo','icon-300.png')`, which resolves *inside* |
| 81 | + `app.asar` when packaged and won't render. Fix: a pure helper |
| 82 | + `unpackedAssetPath(appPath, rel)` returning the `app.asar.unpacked` twin when the |
| 83 | + path is inside an asar (exists because Section 1 unpacks `assets/**`), else the |
| 84 | + plain join. `index.js` passes its result to `registerToastAppId`. Unit-tested; |
| 85 | + rendering verified in the packaged smoke test. |
| 86 | + |
| 87 | +## Section 3 — Auto-update (lock-aware) |
| 88 | + |
| 89 | +**Mechanism.** Add `electron-updater` (dependency) + electron-builder GitHub publish |
| 90 | +provider: `publish: { provider: github, owner: subhasisbiswal012, repo: Vitreous }`. |
| 91 | +Build emits `latest.yml` + installer; publishing a GitHub Release makes it the feed. |
| 92 | +Unsigned auto-update works now; SignPath signing later just makes the publisher-match |
| 93 | +check pass — no rework. |
| 94 | + |
| 95 | +**New module `src/main/updater.js`.** On app ready, **only when `app.isPackaged`** (a |
| 96 | +clean no-op in dev — never errors without `app-update.yml`): |
| 97 | +- `autoDownload = true`, `autoInstallOnAppQuit = true`. |
| 98 | +- `checkForUpdates()` ~15s after launch, then every 6h. |
| 99 | +- On `update-downloaded`: fire a toast ("A new version of Vitreous is ready") and |
| 100 | + broadcast status to the renderer. |
| 101 | + |
| 102 | +**Lock-safety (falls out naturally).** We never call `quitAndInstall()` mid-session. |
| 103 | +With `autoInstallOnAppQuit = true`, the update installs **only when the app actually |
| 104 | +quits** — and quitting is already hard-blocked during any locked session |
| 105 | +(`shouldPreventQuit` in `before-quit`). So an update can never interrupt or end a |
| 106 | +focus session. The one explicit path, a "Restart to update now" button, is gated by |
| 107 | +the same `shouldPreventQuit`: locked → disabled with a note ("Vitreous will update |
| 108 | +when your session ends"); unlocked → set `isQuitting = true` then `quitAndInstall()`. |
| 109 | + |
| 110 | +**UI (minimal).** IPC `update:getStatus` + `update:restartNow` on the bridge. The |
| 111 | +**About page** gains an update line — "Vitreous is up to date" / "Update ready — |
| 112 | +restart to apply" with a lock-aware Restart button — plus the one-time toast on |
| 113 | +download complete. No intrusive modal. |
| 114 | + |
| 115 | +## Section 4 — Testing & packaging-gated verification |
| 116 | + |
| 117 | +**Unit-testable (pure, `node:test`):** |
| 118 | +- `unpackedAssetPath(appPath, rel)` — asar → asar.unpacked mapping when packaged, |
| 119 | + plain join in dev. |
| 120 | +- A small test asserting `electron-builder.yml` parses and carries the critical keys |
| 121 | + (`appId`, `productName`, `nsis.oneClick`, `asarUnpack` covering `assets` + |
| 122 | + `extension`) so a careless edit can't silently break unpacking. |
| 123 | +- Auto-update's lock gate reuses the already-tested `shouldPreventQuit`; `updater.js` |
| 124 | + itself is electron-coupled and not unit-tested. |
| 125 | + |
| 126 | +**Build-verified:** `npm run dist` produces `release/Vitreous Setup <version>.exe` |
| 127 | +(+ `latest.yml`) with no errors. |
| 128 | + |
| 129 | +**Human-gated packaged smoke test** (needs the real installed app): |
| 130 | +1. One-click install → no UAC, lands in `%LOCALAPPDATA%\Programs\Vitreous`, |
| 131 | + auto-launches into the splash. |
| 132 | +2. Branding: session-start toast reads **Vitreous** + icon; Task Manager → Startup |
| 133 | + apps shows **Vitreous** after enabling run-at-startup; the real **hidden-to-tray |
| 134 | + login launch** is finally verifiable (the Group-A item not testable in dev). |
| 135 | +3. Firefox force-install works from the unpacked `.xpi`; bundled chime plays. |
| 136 | +4. Core enforcement intact in the packaged app: hosts blocking, the one-UAC scheduled |
| 137 | + task, app-kill, lockdown. |
| 138 | +5. Auto-update E2E once a second release exists (v0.1.0 → v0.1.1). |
| 139 | + |
| 140 | +## Out of scope |
| 141 | + |
| 142 | +- Code signing (free SignPath follow-up after the download page). |
| 143 | +- arm64 / 32-bit targets. |
| 144 | +- The download web page itself (next milestone; this group makes its artifacts). |
| 145 | +- macOS/Linux packaging (Windows-first). |
| 146 | + |
| 147 | +## Files touched (anticipated) |
| 148 | + |
| 149 | +- `electron-builder.yml` (new) |
| 150 | +- `package.json` (scripts, electron-builder devDep, electron-updater dep) |
| 151 | +- `src/main/paths.js` (new) — `unpackedAssetPath` pure helper |
| 152 | +- `src/main/updater.js` (new) — electron-updater wrapper, lock-aware |
| 153 | +- `src/main/index.js` — use `unpackedAssetPath` for the toast icon; init updater |
| 154 | +- `src/main/ipc.js` — `update:getStatus` / `update:restartNow` handlers |
| 155 | +- `src/preload/index.js` — `getUpdateStatus` / `restartToUpdate` bridge |
| 156 | +- `src/renderer/pages/About.jsx` — update line + lock-aware Restart button |
| 157 | +- `test/paths.test.js`, `test/electron-builder-config.test.js` (new) |
| 158 | +- `.gitignore` — ignore `release/` |
0 commit comments