Wire desktop updates through a generic feed
electron-builder now publishes latest-linux.yml / latest.yml and embeds app-update.yml plus package-type, so electron-updater runs pacman -U or dpkg -i through pkexec for packages and updates the per-user NSIS install without elevation. The app checks only when asked (menu item), asks before downloading and before installing, and J621_UPDATE_URL overrides the feed for tests or forks. deploy/push_desktop.sh builds and publishes the artifacts to deploy/data/desktop, which the frontend nginx mounts read-only at /desktop/. Verified detection and up-to-date handling against a local feed with the packaged Arch build.
This commit is contained in:
@@ -136,6 +136,18 @@ Push multi-arch images to the Gitea registry:
|
|||||||
They tag `:latest` and `:<commit-sha>` and expect `docker login
|
They tag `:latest` and `:<commit-sha>` and expect `docker login
|
||||||
gitea.rainbow-herring.ts.net` to succeed.
|
gitea.rainbow-herring.ts.net` to succeed.
|
||||||
|
|
||||||
|
Push the desktop builds and their update metadata to the frontend's feed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./push_desktop.sh # Linux packages (deb + pacman)
|
||||||
|
./push_desktop.sh --win # also cross-build the NSIS installer (wine)
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts land in `deploy/data/desktop/`, which the frontend nginx mounts
|
||||||
|
read-only and serves at `/desktop/`. The desktop app's "Check for updates…"
|
||||||
|
menu item reads `latest-linux.yml` / `latest.yml` from there (see
|
||||||
|
`desktop/README.md`). Backend-only composes have no frontend, so no feed.
|
||||||
|
|
||||||
## Scheduled jobs
|
## Scheduled jobs
|
||||||
|
|
||||||
Compose files with a backend also run a **`scheduler`** service — the same
|
Compose files with a backend also run a **`scheduler`** service — the same
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: deploy/J621-Frontend
|
dockerfile: deploy/J621-Frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Desktop update feed (deploy/push_desktop.sh): latest*.yml + installers.
|
||||||
|
- ./data/desktop:/usr/share/nginx/html/desktop:ro
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.29-alpine
|
image: nginx:1.29-alpine
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: deploy/J621-Frontend
|
dockerfile: deploy/J621-Frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Desktop update feed (deploy/push_desktop.sh): latest*.yml + installers.
|
||||||
|
- ./data/desktop:/usr/share/nginx/html/desktop:ro
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.29-alpine
|
image: nginx:1.29-alpine
|
||||||
|
|||||||
@@ -100,6 +100,9 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: deploy/J621-Frontend
|
dockerfile: deploy/J621-Frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Desktop update feed (deploy/push_desktop.sh): latest*.yml + installers.
|
||||||
|
- ./data/desktop:/usr/share/nginx/html/desktop:ro
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.29-alpine
|
image: nginx:1.29-alpine
|
||||||
|
|||||||
@@ -99,6 +99,9 @@ services:
|
|||||||
context: ..
|
context: ..
|
||||||
dockerfile: deploy/J621-Frontend
|
dockerfile: deploy/J621-Frontend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
# Desktop update feed (deploy/push_desktop.sh): latest*.yml + installers.
|
||||||
|
- ./data/desktop:/usr/share/nginx/html/desktop:ro
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.29-alpine
|
image: nginx:1.29-alpine
|
||||||
|
|||||||
Executable
+62
@@ -0,0 +1,62 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Build the J621 desktop packages and publish them to the frontend's update
|
||||||
|
# feed (deploy/data/desktop, mounted read-only into the frontend nginx and
|
||||||
|
# served at /desktop/). electron-updater reads latest-linux.yml / latest.yml
|
||||||
|
# from there; the app's feed URL comes from desktop/electron-builder.yml.
|
||||||
|
#
|
||||||
|
# Usage: ./push_desktop.sh [--win] [--no-build]
|
||||||
|
# --win also cross-build the Windows NSIS installer (needs wine)
|
||||||
|
# --no-build publish what is already in desktop/release/
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
BUILD=1
|
||||||
|
WIN=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--win) WIN=1 ;;
|
||||||
|
--no-build) BUILD=0 ;;
|
||||||
|
*)
|
||||||
|
echo "usage: $0 [--win] [--no-build]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$BUILD" = 1 ]; then
|
||||||
|
echo "==> Building Linux packages (deb + pacman) ..."
|
||||||
|
npm --prefix desktop run dist:linux
|
||||||
|
if [ "$WIN" = 1 ]; then
|
||||||
|
echo "==> Cross-building the Windows installer (wine) ..."
|
||||||
|
npm --prefix desktop run dist:win
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
FEED=deploy/data/desktop
|
||||||
|
mkdir -p "$FEED"
|
||||||
|
|
||||||
|
shopt -s nullglob
|
||||||
|
deb=(desktop/release/*.deb)
|
||||||
|
zst=(desktop/release/*.pkg.tar.zst)
|
||||||
|
linux_meta=(desktop/release/latest-linux.yml)
|
||||||
|
win_exe=("desktop/release/J621 Setup "*.exe)
|
||||||
|
win_meta=(desktop/release/latest.yml)
|
||||||
|
blockmaps=(desktop/release/*.blockmap)
|
||||||
|
|
||||||
|
if [ "${#deb[@]}" -eq 0 ] && [ "${#zst[@]}" -eq 0 ]; then
|
||||||
|
echo "No artifacts in desktop/release/ — run without --no-build first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Replace the previous release's metadata before copying the new artifacts.
|
||||||
|
rm -f "$FEED"/latest-linux.yml "$FEED"/latest.yml
|
||||||
|
cp -f "${deb[@]}" "${zst[@]}" "${linux_meta[@]}" "$FEED"/ 2>/dev/null || true
|
||||||
|
if [ "${#win_exe[@]}" -gt 0 ]; then
|
||||||
|
cp -f "${win_exe[@]}" "${win_meta[@]}" "${blockmaps[@]}" "$FEED"/ 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Published to $FEED:"
|
||||||
|
ls -1sh "$FEED" | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
echo "Served read-only by the frontend nginx at /desktop/ (no restart needed):"
|
||||||
|
echo " https://<frontend-host>/desktop/latest-linux.yml"
|
||||||
+23
-1
@@ -55,12 +55,34 @@ It installs to `/opt/J621` with a `/usr/bin/j621-desktop` symlink and a
|
|||||||
rights are needed to install or update it — but it is unsigned, so SmartScreen
|
rights are needed to install or update it — but it is unsigned, so SmartScreen
|
||||||
will warn, and it has not been smoke-tested on real Windows.
|
will warn, and it has not been smoke-tested on real Windows.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
The app checks only when asked (**J621 → Check for updates…** in the menu):
|
||||||
|
Linux packages install through pacman/dpkg, which needs administrator rights,
|
||||||
|
and the Windows build is unsigned, so nothing installs silently. The check
|
||||||
|
reads `latest-linux.yml` / `latest.yml` from the feed configured in
|
||||||
|
`electron-builder.yml` (`publish.url`, baked into `app-update.yml`); set
|
||||||
|
`J621_UPDATE_URL` to point a build at another feed (the smoke test uses this).
|
||||||
|
|
||||||
|
Publishing a release:
|
||||||
|
|
||||||
|
1. Bump `version` in `desktop/package.json` — that is what the updater compares.
|
||||||
|
2. `./deploy/push_desktop.sh --win` builds deb/pacman/NSIS and copies the
|
||||||
|
artifacts plus both channel files into `deploy/data/desktop/`, which the
|
||||||
|
frontend nginx serves read-only at `/desktop/`.
|
||||||
|
3. Existing installs find the new version on their next manual check.
|
||||||
|
|
||||||
|
`package-type` in the app resources tells electron-updater whether to run
|
||||||
|
`pacman -U` or `dpkg -i` (both via pkexec/sudo); the per-user NSIS install
|
||||||
|
updates without elevation.
|
||||||
|
|
||||||
## Smoke test
|
## Smoke test
|
||||||
|
|
||||||
`npm run smoke` builds everything, runs Electron headless through `xvfb-run`
|
`npm run smoke` builds everything, runs Electron headless through `xvfb-run`
|
||||||
and checks that the bundled SPA loads over `app://j621`: the SPA fallback, an
|
and checks that the bundled SPA loads over `app://j621`: the SPA fallback, an
|
||||||
asset fetch, `localStorage`, OPFS, WebCodecs and the preload bridge. It exits
|
asset fetch, `localStorage`, OPFS, WebCodecs and the preload bridge. It exits
|
||||||
non-zero on failure.
|
non-zero on failure. With `J621_UPDATE_URL` set it also checks that the feed
|
||||||
|
reports the expected version.
|
||||||
|
|
||||||
## Backend CORS
|
## Backend CORS
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ appId: io.j621.desktop
|
|||||||
productName: J621
|
productName: J621
|
||||||
copyright: Copyright (c) 2026 JakeBreath — Jake Labs Non-Commercial Software Licence
|
copyright: Copyright (c) 2026 JakeBreath — Jake Labs Non-Commercial Software Licence
|
||||||
|
|
||||||
|
# Update feed served by the frontend nginx (deploy/data/desktop, published
|
||||||
|
# with deploy/push_desktop.sh). Baked into resources/app-update.yml; override
|
||||||
|
# at runtime with J621_UPDATE_URL for a fork or a test feed.
|
||||||
|
publish:
|
||||||
|
provider: generic
|
||||||
|
url: https://j621.rainbow-herring.ts.net/desktop
|
||||||
|
|
||||||
directories:
|
directories:
|
||||||
output: release
|
output: release
|
||||||
buildResources: build
|
buildResources: build
|
||||||
|
|||||||
Generated
+51
-12
@@ -7,7 +7,10 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "j621-desktop",
|
"name": "j621-desktop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "UNLICENSED",
|
"license": "LicenseRef-Jake-Labs-Non-Commercial",
|
||||||
|
"dependencies": {
|
||||||
|
"electron-updater": "^6.8.9"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^24.13.6",
|
"@types/node": "^24.13.6",
|
||||||
"electron": "^44.4.3",
|
"electron": "^44.4.3",
|
||||||
@@ -798,7 +801,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/asn1js": {
|
"node_modules/asn1js": {
|
||||||
@@ -954,7 +956,6 @@
|
|||||||
"version": "9.7.0",
|
"version": "9.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.7.0.tgz",
|
||||||
"integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
|
"integrity": "sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
@@ -1213,7 +1214,6 @@
|
|||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -1523,6 +1523,34 @@
|
|||||||
"mime": "^2.5.2"
|
"mime": "^2.5.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/electron-updater": {
|
||||||
|
"version": "6.8.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.8.9.tgz",
|
||||||
|
"integrity": "sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"builder-util-runtime": "9.7.0",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"lazy-val": "^1.0.5",
|
||||||
|
"lodash.escaperegexp": "^4.1.2",
|
||||||
|
"lodash.isequal": "^4.5.0",
|
||||||
|
"semver": "~7.7.3",
|
||||||
|
"tiny-typed-emitter": "^2.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/electron-updater/node_modules/semver": {
|
||||||
|
"version": "7.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"bin": {
|
||||||
|
"semver": "bin/semver.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/electron-winstaller": {
|
"node_modules/electron-winstaller": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/electron-winstaller/-/electron-winstaller-5.4.0.tgz",
|
||||||
@@ -1811,7 +1839,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
|
||||||
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
"integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -2037,7 +2064,6 @@
|
|||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/has-flag": {
|
"node_modules/has-flag": {
|
||||||
@@ -2259,7 +2285,6 @@
|
|||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||||
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
"integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -2317,7 +2342,6 @@
|
|||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
|
||||||
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
"integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
@@ -2340,7 +2364,6 @@
|
|||||||
"version": "1.0.5",
|
"version": "1.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz",
|
||||||
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
"integrity": "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
@@ -2350,6 +2373,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.escaperegexp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isequal": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
|
||||||
|
"deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz",
|
||||||
@@ -2510,7 +2546,6 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-abi": {
|
"node_modules/node-abi": {
|
||||||
@@ -3073,7 +3108,6 @@
|
|||||||
"version": "1.6.1",
|
"version": "1.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz",
|
||||||
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
"integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=11.0.0"
|
"node": ">=11.0.0"
|
||||||
@@ -3336,6 +3370,12 @@
|
|||||||
"semver": "bin/semver"
|
"semver": "bin/semver"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-typed-emitter": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyglobby": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.17",
|
"version": "0.2.17",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||||
@@ -3440,7 +3480,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
|
|||||||
@@ -28,5 +28,8 @@
|
|||||||
"electron": "^44.4.3",
|
"electron": "^44.4.3",
|
||||||
"electron-builder": "^26.15.3",
|
"electron-builder": "^26.15.3",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"electron-updater": "^6.8.9"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+142
-1
@@ -12,7 +12,8 @@
|
|||||||
* the same way it does in a browser. The only desktop-specific bits are the
|
* the same way it does in a browser. The only desktop-specific bits are the
|
||||||
* origin, external-link handling and (later) updates.
|
* origin, external-link handling and (later) updates.
|
||||||
*/
|
*/
|
||||||
import { app, BrowserWindow, ipcMain, Menu, protocol, shell } from "electron";
|
import { app, BrowserWindow, dialog, ipcMain, Menu, protocol, shell } from "electron";
|
||||||
|
import { autoUpdater } from "electron-updater";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { readFile, stat, writeFile } from "node:fs/promises";
|
import { readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
@@ -187,6 +188,124 @@ function isDownloadNavigation(url: URL): boolean {
|
|||||||
return DOWNLOAD_EXTENSIONS.has(path.extname(url.pathname).toLowerCase());
|
return DOWNLOAD_EXTENSIONS.has(path.extname(url.pathname).toLowerCase());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates are manual by design: Linux packages install through pacman/dpkg
|
||||||
|
* (pkexec/sudo) and the Windows build is unsigned, so the app asks before
|
||||||
|
* downloading and again before installing. The feed comes from `publish` in
|
||||||
|
* electron-builder.yml and can be overridden with J621_UPDATE_URL.
|
||||||
|
*/
|
||||||
|
type UpdateEvent =
|
||||||
|
| { state: "available"; version: string }
|
||||||
|
| { state: "not-available" }
|
||||||
|
| { state: "downloaded"; version: string }
|
||||||
|
| { state: "error"; message: string };
|
||||||
|
|
||||||
|
let updateReporter: ((event: UpdateEvent) => void) | null = null;
|
||||||
|
let updateCheckRunning = false;
|
||||||
|
|
||||||
|
function setUpUpdates(win: BrowserWindow): void {
|
||||||
|
const override = process.env.J621_UPDATE_URL?.trim();
|
||||||
|
if (override) autoUpdater.setFeedURL({ provider: "generic", url: override });
|
||||||
|
if (!app.isPackaged) autoUpdater.forceDevUpdateConfig = true;
|
||||||
|
autoUpdater.autoDownload = false;
|
||||||
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
|
||||||
|
autoUpdater.on("error", (error) => {
|
||||||
|
updateReporter?.({ state: "error", message: error.message });
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-not-available", () => {
|
||||||
|
if (SMOKE) return updateReporter?.({ state: "not-available" });
|
||||||
|
void dialog.showMessageBox(win, {
|
||||||
|
type: "info",
|
||||||
|
title: "Updates",
|
||||||
|
message: `J621 ${app.getVersion()} is up to date.`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-available", (info) => {
|
||||||
|
if (SMOKE) {
|
||||||
|
return updateReporter?.({ state: "available", version: info.version });
|
||||||
|
}
|
||||||
|
void (async () => {
|
||||||
|
const { response } = await dialog.showMessageBox(win, {
|
||||||
|
type: "info",
|
||||||
|
title: "Updates",
|
||||||
|
message: `J621 ${info.version} is available.`,
|
||||||
|
detail:
|
||||||
|
"Download it now? Installing it later needs administrator rights " +
|
||||||
|
"on Linux (pacman/dpkg); the Windows installer runs without them.",
|
||||||
|
buttons: ["Download", "Later"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
if (response !== 0) return;
|
||||||
|
try {
|
||||||
|
await autoUpdater.downloadUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
await dialog.showMessageBox(win, {
|
||||||
|
type: "error",
|
||||||
|
title: "Update failed",
|
||||||
|
message: "The update could not be downloaded.",
|
||||||
|
detail: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("download-progress", (progress) => {
|
||||||
|
win.setProgressBar(Math.min(progress.percent / 100, 1));
|
||||||
|
});
|
||||||
|
|
||||||
|
autoUpdater.on("update-downloaded", (info) => {
|
||||||
|
win.setProgressBar(-1);
|
||||||
|
if (SMOKE) {
|
||||||
|
return updateReporter?.({ state: "downloaded", version: info.version });
|
||||||
|
}
|
||||||
|
void (async () => {
|
||||||
|
const { response } = await dialog.showMessageBox(win, {
|
||||||
|
type: "info",
|
||||||
|
title: "Updates",
|
||||||
|
message: `J621 ${info.version} is ready to install.`,
|
||||||
|
detail:
|
||||||
|
process.platform === "linux"
|
||||||
|
? "Installing asks for administrator rights and then restarts J621."
|
||||||
|
: "J621 will restart to finish installing.",
|
||||||
|
buttons: ["Restart and install", "Later"],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
if (response === 0) {
|
||||||
|
autoUpdater.quitAndInstall(false, true);
|
||||||
|
} else {
|
||||||
|
// Linux packages elevate, so never install silently on quit there.
|
||||||
|
autoUpdater.autoInstallOnAppQuit = process.platform !== "linux";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkForUpdates(win: BrowserWindow): Promise<void> {
|
||||||
|
if (updateCheckRunning) return;
|
||||||
|
updateCheckRunning = true;
|
||||||
|
try {
|
||||||
|
await autoUpdater.checkForUpdates();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
updateReporter?.({ state: "error", message });
|
||||||
|
if (!SMOKE) {
|
||||||
|
await dialog.showMessageBox(win, {
|
||||||
|
type: "error",
|
||||||
|
title: "Updates",
|
||||||
|
message: "Could not check for updates.",
|
||||||
|
detail: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
updateCheckRunning = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
function buildMenu(win: BrowserWindow): void {
|
function buildMenu(win: BrowserWindow): void {
|
||||||
@@ -202,6 +321,10 @@ function buildMenu(win: BrowserWindow): void {
|
|||||||
label: "Open backend in browser",
|
label: "Open backend in browser",
|
||||||
click: () => win.webContents.send("j621:open-backend"),
|
click: () => win.webContents.send("j621:open-backend"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Check for updates…",
|
||||||
|
click: () => void checkForUpdates(win),
|
||||||
|
},
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{ role: "quit" },
|
{ role: "quit" },
|
||||||
],
|
],
|
||||||
@@ -277,6 +400,7 @@ function createWindow(): BrowserWindow {
|
|||||||
});
|
});
|
||||||
|
|
||||||
buildMenu(win);
|
buildMenu(win);
|
||||||
|
setUpUpdates(win);
|
||||||
void win.loadURL(DEV_SERVER || `${APP_ORIGIN}/`);
|
void win.loadURL(DEV_SERVER || `${APP_ORIGIN}/`);
|
||||||
if (SMOKE) runSmokeTest(win);
|
if (SMOKE) runSmokeTest(win);
|
||||||
return win;
|
return win;
|
||||||
@@ -350,6 +474,23 @@ function runSmokeTest(win: BrowserWindow): void {
|
|||||||
app.exit(1);
|
app.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (process.env.J621_UPDATE_URL) {
|
||||||
|
const update = await new Promise<UpdateEvent>((resolve) => {
|
||||||
|
updateReporter = resolve;
|
||||||
|
const timer = setTimeout(
|
||||||
|
() => resolve({ state: "error", message: "update check timed out" }),
|
||||||
|
60_000,
|
||||||
|
);
|
||||||
|
void checkForUpdates(win).finally(() => clearTimeout(timer));
|
||||||
|
});
|
||||||
|
updateReporter = null;
|
||||||
|
console.log("[smoke] update:", JSON.stringify(update));
|
||||||
|
if (update.state === "error") {
|
||||||
|
console.error("[smoke] FAILED: update check");
|
||||||
|
app.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
console.log("[smoke] OK");
|
console.log("[smoke] OK");
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user