From 863925f4c8cb0d81c11d687cad43b23a62c948a6 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 18:49:41 +0800 Subject: [PATCH 01/21] fix ci --- .github/workflows/build-android.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 7aea630..7d67613 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -34,8 +34,11 @@ jobs: - name: Install required Android SDK packages run: sdkmanager "platforms;android-36" "build-tools;36.0.0" + - name: ci init + run: npm config set legacy-peer-deps true + - name: Install root dependencies (electerm + build tooling) - run: npm ci + run: npm i - name: Install Capacitor dependencies run: npm --prefix build/android install From 54c7cb563c5fd5f104074d069923a9feb6a9fd2c Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 19:17:18 +0800 Subject: [PATCH 02/21] fix(ci): install typescript so capacitor.config.ts can be parsed by cap --- build/android/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/android/package.json b/build/android/package.json index 57a6733..d1e8875 100644 --- a/build/android/package.json +++ b/build/android/package.json @@ -14,6 +14,7 @@ "@capacitor/android": "^8.4.2", "@capacitor/cli": "^8.4.2", "@capacitor/core": "^8.4.2", - "@capawesome/capacitor-nodejs": "^0.1.0" + "@capawesome/capacitor-nodejs": "^0.1.0", + "typescript": "^5.7.0" } } From b00f9ba2b6408ad5f407d1b2e9b2828e27e4cd14 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 19:22:48 +0800 Subject: [PATCH 03/21] fix(ci): append nodejsMobileVersion on its own line in variables.gradle --- .github/workflows/build-android.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 7d67613..1ad2f8c 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -54,10 +54,12 @@ jobs: - name: Ensure nodejs-mobile gradle variable is defined # @capawesome/capacitor-nodejs expects $nodejsMobileVersion to be set. + # Append on its own line (variables.gradle has no trailing newline, so a + # plain `echo >>` would concatenate onto the closing `}` and break Gradle). run: | cd build/android/android grep -q "nodejsMobileVersion" variables.gradle || \ - echo "nodejsMobileVersion = '18.20.4-capawesome.1'" >> variables.gradle + printf '\nnodejsMobileVersion = "18.20.4-capawesome.1"\n' >> variables.gradle - name: Apply icon / splash overlay run: cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ From 600bb4e42283585bd9ee0c5c39f3617494519894 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 19:31:04 +0800 Subject: [PATCH 04/21] fix(ci): set nodejsMobileVersion on ext extension in variables.gradle --- .github/workflows/build-android.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 1ad2f8c..c07000f 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -59,7 +59,7 @@ jobs: run: | cd build/android/android grep -q "nodejsMobileVersion" variables.gradle || \ - printf '\nnodejsMobileVersion = "18.20.4-capawesome.1"\n' >> variables.gradle + printf '\next.nodejsMobileVersion = "18.20.4-capawesome.1"\n' >> variables.gradle - name: Apply icon / splash overlay run: cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ From 35292c8ae811fcbe91d6af33bff0bf5d3e5d2359 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 19:38:47 +0800 Subject: [PATCH 05/21] fix(ci): drop template's conflicting splash.png and ic_launcher_background after overlay --- .github/workflows/build-android.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index c07000f..e205808 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -62,7 +62,14 @@ jobs: printf '\next.nodejsMobileVersion = "18.20.4-capawesome.1"\n' >> variables.gradle - name: Apply icon / splash overlay - run: cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ + run: | + cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ + # Capacitor's default template ships its own drawable/splash.png and + # values/ic_launcher_background.xml. These collide with our overlay's + # splash.xml / colors-electerm.xml (same resource name, different file), + # so drop the template's copies after overlaying ours. + rm -f build/android/android/app/src/main/res/drawable/splash.png + rm -f build/android/android/app/src/main/res/values/ic_launcher_background.xml - name: Build debug APK run: cd build/android/android && chmod +x gradlew && ./gradlew assembleDebug From 6b4f492ee322c9429cad5d4df1e8c9e801a62122 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Thu, 16 Jul 2026 19:43:48 +0800 Subject: [PATCH 06/21] fix(ci): generate ephemeral keystore in app/ dir so gradle finds it --- .github/workflows/build-android.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index e205808..6e85c08 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -80,13 +80,17 @@ jobs: KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD || 'android' }} run: | cd build/android/android + # Gradle resolves the injected store.file relative to the :app module + # dir (build/android/android/app), so generate the keystore there and + # pass the same absolute path to gradle. + KEYSTORE="$PWD/app/release.keystore" keytool -genkey -v \ - -keystore release.keystore -alias electerm \ + -keystore "$KEYSTORE" -alias electerm \ -keyalg RSA -keysize 2048 -validity 10000 \ -storepass "$KEYSTORE_PASSWORD" -keypass "$KEY_PASSWORD" \ -dname "CN=electerm" ./gradlew assembleRelease \ - -Pandroid.injected.signing.store.file=release.keystore \ + -Pandroid.injected.signing.store.file="$KEYSTORE" \ -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \ -Pandroid.injected.signing.key.alias=electerm \ -Pandroid.injected.signing.key.password="$KEY_PASSWORD" From 445333608a2d53cdac8f3ed434ce305648287009 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 11:03:43 +0800 Subject: [PATCH 07/21] fix build --- build/android/build.mjs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/build/android/build.mjs b/build/android/build.mjs index 2c57576..406bac5 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -215,6 +215,37 @@ class Stmt { return shimPath } +// esbuild plugin: rewrite path-to-regexp v8 Unicode property-escape regexes +// so they run on the on-device Node 18 build (which lacks \p{...} support +// inside character classes due to its stripped ICU data). +// +// path-to-regexp v8 defines three regexes that use \p{ID_Start} and +// \p{ID_Continue} — Unicode property escapes that require full ICU support. +// We replace them with ASCII-equivalent character classes; route parameter +// names are always ASCII in practice, so the behaviour is identical. +const patchPathToRegexpPlugin = { + name: 'patch-path-to-regexp', + setup (build) { + build.onLoad({ filter: /path-to-regexp/ }, async (args) => { + let src = await fs.promises.readFile(args.path, 'utf8') + src = src + .replace( + '/^[$_\\p{ID_Start}]$/u', + '/^[$_a-zA-Z]$/' + ) + .replace( + '/^[$\\u200c\\u200d\\p{ID_Continue}]$/u', + '/^[$\\u200c\\u200da-zA-Z0-9_]$/' + ) + .replace( + '/^[$_\\p{ID_Start}][$\\u200c\\u200d\\p{ID_Continue}]*$/u', + '/^[$_a-zA-Z][$\\u200c\\u200da-zA-Z0-9_]*$/' + ) + return { contents: src, loader: 'js' } + }) + } +} + async function bundleBackend (shimPath) { console.log('[android] bundling backend (esbuild)…') await esbuild.build({ @@ -244,6 +275,7 @@ async function bundleBackend (shimPath) { banner: { js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" }, + plugins: [patchPathToRegexpPlugin], // keep node built-ins external; everything else is bundled logLevel: 'info' }) @@ -308,6 +340,7 @@ await import('./app.bundle.mjs') ) } +// -------------------------------------------------------------------------- // -------------------------------------------------------------------------- async function main () { fs.rmSync(WWW, { recursive: true, force: true }) From 9cfd08289abae7cb2781fa734daa936c8e195917 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 11:35:14 +0800 Subject: [PATCH 08/21] fix --- package-lock.json | 589 ++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 276 insertions(+), 315 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5107c31..ee9c373 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "dotenv": "16.3.1", "electerm-sync": "2.0.0", "esbuild": "^0.28.1", - "express": "5.2.1", + "express": "4.22.2", "express-jwt": "^8.5.1", "express-ws": "5.0.2", "fast-deep-equal": "3.1.3", @@ -2995,13 +2995,13 @@ ] }, "node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "license": "MIT", "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" + "mime-types": "~2.1.34", + "negotiator": "0.6.3" }, "engines": { "node": ">= 0.6" @@ -3185,6 +3185,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -3506,42 +3512,56 @@ "license": "MIT" }, "node_modules/body-parser": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", - "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { - "bytes": "^3.1.2", - "content-type": "^2.0.0", - "debug": "^4.4.3", - "http-errors": "^2.0.1", - "iconv-lite": "^0.7.2", - "on-finished": "^2.4.1", - "qs": "^6.15.2", - "raw-body": "^3.0.2", - "type-is": "^2.1.0" + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/body-parser/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">=0.10.0" } }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -3984,16 +4004,15 @@ } }, "node_modules/content-disposition": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", - "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "safe-buffer": "5.2.1" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "engines": { + "node": ">= 0.6" } }, "node_modules/content-type": { @@ -4022,13 +4041,10 @@ } }, "node_modules/cookie-signature": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", - "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", - "license": "MIT", - "engines": { - "node": ">=6.6.0" - } + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" }, "node_modules/cpu-features": { "version": "0.0.10", @@ -4247,6 +4263,16 @@ "node": ">=6" } }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -5384,42 +5410,45 @@ "license": "MIT" }, "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" }, "engines": { - "node": ">= 18" + "node": ">= 0.10.0" }, "funding": { "type": "opencollective", @@ -5451,35 +5480,6 @@ "ms": "^2.1.1" } }, - "node_modules/express-http-proxy/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/express-http-proxy/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/express-jwt": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-8.5.1.tgz", @@ -5515,6 +5515,21 @@ "express": "^4.0.0 || ^5.0.0-alpha.1" } }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5601,26 +5616,38 @@ "license": "MIT" }, "node_modules/finalhandler": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", - "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", "license": "MIT", "dependencies": { - "debug": "^4.4.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "on-finished": "^2.4.1", - "parseurl": "^1.3.3", - "statuses": "^2.0.1" + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/find-free-port": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/find-free-port/-/find-free-port-2.0.0.tgz", @@ -5741,27 +5768,6 @@ "node": ">= 6" } }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -5772,12 +5778,12 @@ } }, "node_modules/fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", - "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/fs.realpath": { @@ -7708,26 +7714,32 @@ } }, "node_modules/media-typer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", - "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "license": "MIT", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" } }, "node_modules/merge-descriptors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", - "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", "license": "MIT", - "engines": { - "node": ">=18" - }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -8204,29 +8216,37 @@ "miller-rabin": "bin/miller-rabin" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", - "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "mime-db": "^1.54.0" + "mime-db": "1.52.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/minimatch": { @@ -8335,49 +8355,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/multer/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/multer/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/nan": { "version": "2.28.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.28.0.tgz", @@ -8411,9 +8388,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -8630,6 +8607,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -8927,14 +8905,10 @@ } }, "node_modules/path-to-regexp": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", - "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" }, "node_modules/picocolors": { "version": "1.1.1", @@ -9356,31 +9330,39 @@ } }, "node_modules/range-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", - "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "license": "MIT", "engines": { "node": ">= 0.6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", + "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/react": { @@ -9802,28 +9784,6 @@ "dev": true, "license": "MIT" }, - "node_modules/router": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", - "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.4.0", - "depd": "^2.0.0", - "is-promise": "^4.0.0", - "parseurl": "^1.3.3", - "path-to-regexp": "^8.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/router/node_modules/is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", - "license": "MIT" - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9967,31 +9927,44 @@ } }, "node_modules/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", - "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", "license": "MIT", "dependencies": { - "debug": "^4.4.3", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "fresh": "^2.0.0", - "http-errors": "^2.0.1", - "mime-types": "^3.0.2", - "ms": "^2.1.3", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "statuses": "^2.0.2" + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/serialport": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/serialport/-/serialport-13.0.0.tgz", @@ -10038,22 +10011,18 @@ } }, "node_modules/serve-static": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", - "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", "license": "MIT", "dependencies": { - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "parseurl": "^1.3.3", - "send": "^1.2.0" + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" }, "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.8.0" } }, "node_modules/set-function-length": { @@ -11051,34 +11020,16 @@ } }, "node_modules/type-is": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", - "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "license": "MIT", "dependencies": { - "content-type": "^2.0.0", - "media-typer": "^1.1.0", - "mime-types": "^3.0.0" - }, - "engines": { - "node": ">= 18" + "media-typer": "0.3.0", + "mime-types": "~2.1.24" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/type-is/node_modules/content-type": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", - "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", - "license": "MIT", "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "node": ">= 0.6" } }, "node_modules/typed-array-buffer": { @@ -11339,6 +11290,15 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -11646,6 +11606,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, "node_modules/ws": { diff --git a/package.json b/package.json index ac3f227..1070773 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "electerm-sync": "2.0.0", "esbuild": "^0.28.1", "sql.js": "^1.12.0", - "express": "5.2.1", + "express": "4.22.2", "express-jwt": "^8.5.1", "express-ws": "5.0.2", "fast-deep-equal": "3.1.3", From ce69b7086fffd45cc85372c4a8cf376ce2ea07c7 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 15:15:08 +0800 Subject: [PATCH 09/21] fix 1 --- .github/workflows/build-android.yml | 73 +++++++++++++++++++++++++++-- build/android/build.mjs | 64 ++++++++++++++++++++----- build/android/capacitor.config.ts | 10 ++++ src/app/server/server.js | 6 +++ 4 files changed, 139 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 6e85c08..8ff3e30 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -71,6 +71,45 @@ jobs: rm -f build/android/android/app/src/main/res/drawable/splash.png rm -f build/android/android/app/src/main/res/values/ic_launcher_background.xml + - name: Permit cleartext to the local Node.js backend in the manifest + # The on-device Node.js backend serves the UI + API over plain http on + # 127.0.0.1:5577. The app targets SDK 36, which blocks cleartext by + # default. res-overlay/AndroidManifest.xml sets usesCleartextTraffic + + # networkSecurityConfig, but the overlay step above copies it into res/ + # (where AndroidManifest.xml is ignored) instead of merging it, so the + # attributes never reach the compiled manifest. Inject them directly + # into the merged main manifest so http://127.0.0.1:5577 is reachable. + run: | + MF=build/android/android/app/src/main/AndroidManifest.xml + grep -q 'usesCleartextTraffic' "$MF" || \ + sed -i 's|# \n#' "$MF" + grep -n 'usesCleartextTraffic\|networkSecurityConfig' "$MF" + + - name: Configure per-ABI APK splits + # Produce one APK per architecture (arm64-v8a, armeabi-v7a, x86_64) + # plus a universal build, instead of a single ~160MB universal blob. + # The second `android { }` block is cumulative with Capacitor's + # template app/build.gradle; output renaming is done post-build in + # shell to avoid AGP-version-specific output-name APIs. + run: | + GRADLE=build/android/android/app/build.gradle + grep -q 'splits' "$GRADLE" || printf '%s\n' \ + '' \ + '// electerm: split per ABI (injected by CI)' \ + 'android {' \ + ' splits {' \ + ' abi {' \ + ' enable true' \ + ' reset()' \ + " include 'arm64-v8a', 'armeabi-v7a', 'x86_64'" \ + ' universalApk true' \ + ' }' \ + ' }' \ + '}' >> "$GRADLE" + tail -n 15 "$GRADLE" + - name: Build debug APK run: cd build/android/android && chmod +x gradlew && ./gradlew assembleDebug @@ -95,12 +134,40 @@ jobs: -Pandroid.injected.signing.key.alias=electerm \ -Pandroid.injected.signing.key.password="$KEY_PASSWORD" + - name: Rename APKs to electerm-android-{arch}-{version}.apk + # Version comes from package.json. Each per-ABI APK is renamed to + # electerm-android-{arch}-{version}.apk (release) / + # electerm-android-{arch}-{version}-debug.apk (debug). The {arch} is + # parsed from gradle's per-ABI output filename; falls back to + # "universal" for the universal build (or if splits are off). + run: | + VERSION=$(node -p "require('./package.json').version") + APKROOT=build/android/android/app/build/outputs/apk + mkdir -p "$APKROOT/named" + for variant in debug release; do + dir="$APKROOT/$variant" + [ -d "$dir" ] || continue + for apk in "$dir"/*.apk; do + [ -f "$apk" ] || continue + name=$(basename "$apk") + arch=$(printf '%s' "$name" | grep -oE 'arm64-v8a|armeabi-v7a|x86_64|universal' | head -1) + [ -z "$arch" ] && arch=universal + if [ "$variant" = release ]; then + out="electerm-android-$arch-$VERSION.apk" + else + out="electerm-android-$arch-$VERSION-debug.apk" + fi + cp "$apk" "$APKROOT/named/$out" + echo " $variant/$name -> named/$out" + done + done + echo "=== named APKs ===" + ls -la "$APKROOT/named/" + - name: Upload APKs uses: actions/upload-artifact@v4 with: name: electerm-android-apks - path: | - build/android/android/app/build/outputs/apk/debug/*.apk - build/android/android/app/build/outputs/apk/release/*.apk + path: build/android/android/app/build/outputs/apk/named/*.apk if-no-files-found: error retention-days: 30 diff --git a/build/android/build.mjs b/build/android/build.mjs index 406bac5..bae250e 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -121,17 +121,46 @@ function writeLoadingPage () {
Starting engine…
@@ -269,11 +298,15 @@ async function bundleBackend (shimPath) { 'node-bash', 'font-list' ], - // Some bundled CJS deps (e.g. dotenv) do `require('fs')`. In an ESM bundle - // esbuild's `__require` shim throws "Dynamic require not supported" unless a - // real `require` exists — provide one via createRequire. + // Some bundled CJS deps (e.g. sql.js's initSqlJs) reference __dirname / + // __filename, which don't exist in an ESM bundle. Define them from + // import.meta.url. NOTE: do NOT `import { dirname } from "path"` here — + // the bundle already imports `dirname` at top level, which would collide + // ("Identifier 'dirname' has already been declared"). Alias fileURLToPath + // to a private name for the same reason, and derive __dirname from a + // directory URL. banner: { - js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" + js: "import { createRequire } from 'module'; import { fileURLToPath as __etu } from 'url'; const require = createRequire(import.meta.url); const __filename = __etu(import.meta.url); const __dirname = __etu(new URL('.', import.meta.url));" }, plugins: [patchPathToRegexpPlugin], // keep node built-ins external; everything else is bundled @@ -296,6 +329,14 @@ import { fileURLToPath } from 'node:url' const __d = fileURLToPath(new URL('.', import.meta.url)) +// The embedded Node.js engine starts with cwd "/" (Android filesystem root), +// not the nodejs-project directory. electerm's runtime-constants.js reads +// "package.json" via resolve(process.cwd(), 'package.json'), so without +// chdir it tries to open "/package.json" -> ENOENT -> uncaught exception -> +// the Node process exits and the app crashes (SIGSEGV during teardown). +// Switch cwd to the project directory before loading the backend bundle. +process.chdir(__d) + // Runtime configuration for the on-device electerm server. process.env.NODE_ENV = 'production' process.env.HOST = '127.0.0.1' @@ -305,7 +346,8 @@ process.env.PORT = '5577' process.env.SERVER_SECRET = 'electerm-android-local-dev-secret' // No real pty on Android -> disable the local terminal feature. process.env.DISABLE_LOCAL_TERMINAL = '1' -// Tell the server where it was deployed (cwd on device is the node project dir). +// Tell the server where the pug views live (cwd is now the node project dir, +// set above via process.chdir(__d)). process.env.VIEW_FOLDER = resolve(__d, 'views') // Stable, app-private user-data directory. diff --git a/build/android/capacitor.config.ts b/build/android/capacitor.config.ts index f0916c8..e923b98 100644 --- a/build/android/capacitor.config.ts +++ b/build/android/capacitor.config.ts @@ -15,6 +15,16 @@ const config: CapacitorConfig = { appId: 'org.electerm.electerm', appName: 'electerm', webDir: 'www', + // The loading page is served from the Capacitor local server and then + // redirects (top-level navigation) to the on-device Node.js backend at + // http://127.0.0.1:5577. Using the http scheme (instead of the default + // https) avoids mixed-content blocking of that http backend. + server: { + androidScheme: 'http', + // Keep navigation to the backend host in-app (don't hand it to the system + // browser, which can't reach the app's private loopback port anyway). + allowNavigation: ['127.0.0.1'] + }, plugins: { Nodejs: { // directory (relative to webDir) that holds the Node.js project diff --git a/src/app/server/server.js b/src/app/server/server.js index 26c8d4d..fa1fff2 100644 --- a/src/app/server/server.js +++ b/src/app/server/server.js @@ -1,4 +1,5 @@ import express from 'express' +import pug from 'pug' import { wsRoutes } from '../routes/ws.js' import { httpRoutes } from '../routes/http.js' import { applyExtensions } from '../lib/extensions.js' @@ -28,6 +29,11 @@ export async function createApp () { ':method :url :status :res[content-length] - :response-time ms' )) app.set('view engine', 'pug') + // Register the pug engine explicitly so Express uses the bundled pug + // directly instead of lazily `require('pug')` at render time. The lazy + // require breaks bundled builds (esbuild can't see the dynamic string + // require, so "pug" is missing at runtime -> GET / hangs forever). + app.engine('pug', pug.__express) app.set( 'views', process.env.VIEW_FOLDER || From 188eea7b554b3b1796e7b5c17b3154fcbc8703b7 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 16:03:11 +0800 Subject: [PATCH 10/21] Fix build --- build/android/build.mjs | 31 +++++++++++++++++++++++++++++++ build/android/package.json | 5 +++-- src/app/server/session.js | 29 +++++++++++++++++++++++------ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/build/android/build.mjs b/build/android/build.mjs index bae250e..9e9cb13 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -382,9 +382,35 @@ await import('./app.bundle.mjs') ) } +// -------------------------------------------------------------------------- +// 5. Overlay electerm icons/splash into the native Android project +// -------------------------------------------------------------------------- +// `cap sync` regenerates android/app/src/main/res from Capacitor's default +// templates, which replaces the electerm launcher icon with the generic +// Capacitor icon. Applying the overlay *after* every sync keeps the correct +// branding in place. This function is a no-op when the android project has +// not been created yet (e.g. during a pure `npm run build` before `cap add`). +function applyResOverlay () { + const overlayDir = path.resolve(__dirname, 'res-overlay') + const resDir = path.resolve(__dirname, 'android', 'app', 'src', 'main', 'res') + if (!fs.existsSync(resDir)) { + console.log('[android] native project not found, skipping res-overlay (run cap add android + cap sync first)') + return + } + console.log('[android] applying res-overlay →', resDir) + copyDir(overlayDir, resDir) +} + // -------------------------------------------------------------------------- // -------------------------------------------------------------------------- async function main () { + // --overlay-only: just re-apply the res-overlay after `cap sync` without + // rebuilding the entire www bundle. Used by the `sync` npm script. + if (process.argv.includes('--overlay-only')) { + applyResOverlay() + return + } + fs.rmSync(WWW, { recursive: true, force: true }) fs.mkdirSync(NODEJS_DIR, { recursive: true }) @@ -397,6 +423,11 @@ async function main () { writeNodeEntry() copyEnv() + // Apply icons after building www (no-op if native project doesn't exist yet). + // The `sync` and `android` npm scripts re-run `node build.mjs --overlay-only` + // after `cap sync` to restore the icons that cap sync resets. + applyResOverlay() + console.log('[android] web + node project ready at', WWW) } diff --git a/build/android/package.json b/build/android/package.json index d1e8875..5c240f6 100644 --- a/build/android/package.json +++ b/build/android/package.json @@ -7,8 +7,9 @@ "scripts": { "assets": "python3 scripts/gen-assets.py", "build": "node build.mjs", - "sync": "cap sync android", - "android": "npm run build && cap add android && cap sync android" + "overlay": "node build.mjs --overlay-only", + "sync": "cap sync android && node build.mjs --overlay-only", + "android": "npm run build && cap add android; cap sync android && node build.mjs --overlay-only" }, "devDependencies": { "@capacitor/android": "^8.4.2", diff --git a/src/app/server/session.js b/src/app/server/session.js index b936b79..3739e29 100644 --- a/src/app/server/session.js +++ b/src/app/server/session.js @@ -1,6 +1,25 @@ -function getModulePath (type) { - return `./session-${type}.js` +// Static imports so bundlers (esbuild) can discover and include all session +// modules. Dynamic import() with a computed path (e.g. `./session-${type}.js`) +// is opaque to bundlers — the files are never included in the bundle and the +// import fails at runtime. A plain dispatch table is the standard fix. +import * as sessionSsh from './session-ssh.js' +import * as sessionTelnet from './session-telnet.js' +import * as sessionSerial from './session-serial.js' +import * as sessionLocal from './session-local.js' +import * as sessionRdp from './session-rdp.js' +import * as sessionVnc from './session-vnc.js' +import * as sessionSpice from './session-spice.js' + +const sessionModules = { + ssh: sessionSsh, + telnet: sessionTelnet, + serial: sessionSerial, + local: sessionLocal, + rdp: sessionRdp, + vnc: sessionVnc, + spice: sessionSpice } + function getType (initOptions) { const type = initOptions.termType || initOptions.type const tail = [ @@ -19,14 +38,12 @@ function getType (initOptions) { export const terminal = async function (initOptions, ws) { const type = getType(initOptions) console.log('type', type) - const modulePath = getModulePath(type) - const { terminal } = await import(modulePath) + const { terminal } = sessionModules[type] return terminal(initOptions, ws) } export const testConnection = async (initOptions, ws) => { const type = getType(initOptions) - const modulePath = getModulePath(type) - const { testConnection } = await import(modulePath) + const { testConnection } = sessionModules[type] return testConnection(initOptions, ws) } From 05fb1de09ba599086e4b7376cd8e4fe6c67c92fb Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 16:14:00 +0800 Subject: [PATCH 11/21] fix 3 --- build/android/build.mjs | 20 ++- docs/android-release-and-play-store.md | 176 +++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 docs/android-release-and-play-store.md diff --git a/build/android/build.mjs b/build/android/build.mjs index 9e9cb13..7314f6f 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -252,6 +252,24 @@ class Stmt { // \p{ID_Continue} — Unicode property escapes that require full ICU support. // We replace them with ASCII-equivalent character classes; route parameter // names are always ASCII in practice, so the behaviour is identical. +// esbuild plugin: mark all .node native-addon files as external. +// Before the session.js refactor the dynamic `import(\`./session-${type}.js\`)` +// was opaque to esbuild, so it never traversed into session-ssh.js and its +// dependencies (ssh2 → cpu-features → cpufeatures.node, sshcrypto.node). +// Now that session.js uses static imports esbuild sees those .node files and +// errors because it has no loader for them. Marking them external is correct: +// the native binaries are not present on Android anyway and the libraries that +// use them have pure-JS fallbacks guarded by try/catch. +const nativeNodePlugin = { + name: 'native-node-files', + setup (build) { + build.onResolve({ filter: /\.node$/ }, (args) => ({ + path: args.path, + external: true + })) + } +} + const patchPathToRegexpPlugin = { name: 'patch-path-to-regexp', setup (build) { @@ -308,7 +326,7 @@ async function bundleBackend (shimPath) { banner: { js: "import { createRequire } from 'module'; import { fileURLToPath as __etu } from 'url'; const require = createRequire(import.meta.url); const __filename = __etu(import.meta.url); const __dirname = __etu(new URL('.', import.meta.url));" }, - plugins: [patchPathToRegexpPlugin], + plugins: [nativeNodePlugin, patchPathToRegexpPlugin], // keep node built-ins external; everything else is bundled logLevel: 'info' }) diff --git a/docs/android-release-and-play-store.md b/docs/android-release-and-play-store.md new file mode 100644 index 0000000..0255edd --- /dev/null +++ b/docs/android-release-and-play-store.md @@ -0,0 +1,176 @@ +# Android release signing and Google Play publishing + +This document covers two separate distribution paths for `electerm-android`: + +1. GitHub Releases / direct APK sideloading +2. Google Play Store publishing + +They share the same app code, but they do **not** share the same release process. + +## 1. Keep the release keystore consistent + +Android requires every installable APK to be signed. + +For GitHub Releases and direct sideloading, the signing key should be **stable** across builds. If you sign one release with one key and a later release with another key, Android treats them as different signers and the later APK will not update the installed app in place. + +### What the current repo does + +The CI workflow currently generates an **ephemeral keystore** during the release job and uses it to sign the release APK. That is enough for a one-off artifact download, but it is not good for long-term updates because the key changes every run. + +### What to do instead + +Use one persistent keystore for all GitHub Release APKs. + +Recommended setup: + +1. Generate a release keystore once. +2. Store the keystore file securely outside the repo. +3. Store the keystore password, key password, and alias in GitHub Secrets. +4. Make the Android build workflow load those secrets and sign release APKs with that same keystore every time. + +### Suggested secret names + +- `ANDROID_RELEASE_KEYSTORE_BASE64` +- `ANDROID_RELEASE_KEYSTORE_PASSWORD` +- `ANDROID_RELEASE_KEY_PASSWORD` +- `ANDROID_RELEASE_KEY_ALIAS` + +Base64-encoding the keystore is often the easiest way to store it in GitHub Secrets. + +### Example signing flow + +At release time: + +1. Decode the keystore secret into a temporary file in the workflow runner. +2. Pass the keystore path and passwords to Gradle. +3. Build the release APK. +4. Publish the resulting APK to GitHub Releases. + +### Operational rules + +- Never commit the keystore into git. +- Back up the keystore offline. +- Keep the alias and passwords documented somewhere safe. +- If the keystore is lost, app update continuity is lost for sideloaded releases. + +## 2. Can users install from GitHub Releases? + +Yes. + +Android users can download a signed APK from GitHub Releases and install it manually, as long as the device allows sideloading from unknown sources. + +For this repo, that is enough for direct distribution. + +What Google Play adds is: + +- managed app updates +- store listing and discovery +- Play App Signing +- policy and review handling + +Google Play is **not required** if you only want direct APK distribution. + +## 3. Google Play publishing guide + +Google Play now expects new apps to use Android App Bundles (`.aab`) for production publishing. Play Console also uses Play App Signing. + +### High-level steps + +1. Create a Google Play Developer account. +2. Create the app in Play Console. +3. Complete the store listing and policy declarations. +4. Enable Play App Signing. +5. Build and upload an Android App Bundle. +6. Pass review and publish to production or a testing track. + +### Practical checklist for this repo + +#### A. Set up package and versioning + +- Keep the app package name stable. +- Increment `versionCode` for every Play upload. +- Increment `versionName` as desired for human-readable releases. + +The repo already has a version in [`package.json`](/E:/dev/electerm-android/package.json). Make sure the Android Gradle config uses a monotonically increasing `versionCode` for each upload. + +#### B. Build an App Bundle, not just an APK + +For Google Play, the preferred production artifact is an `.aab`. + +If the current Android build only produces APKs, add or adjust the Gradle release task so the CI can build: + +- `bundleRelease` for Google Play +- `assembleRelease` for direct APK distribution + +You can keep both: + +- APK for GitHub Releases and manual testing +- AAB for Play Console submission + +#### C. Enable Play App Signing + +In Play Console, configure Play App Signing during app setup or on the first release. + +With Play App Signing: + +- Google stores the app signing key +- you upload with an upload key +- Google re-signs the distributed app + +This is the standard model for Play distribution. + +#### D. Upload the first release + +For the first production or testing release: + +1. Create the app entry. +2. Complete identity, content, privacy, and policy forms. +3. Upload the signed AAB. +4. Review generated device coverage and warnings. +5. Publish to internal testing, closed testing, or production. + +#### E. Prepare testing tracks + +Before production, use one of these: + +- Internal testing +- Closed testing +- Internal app sharing + +These tracks let you verify the build on real devices before broader release. + +### Important Play Store notes + +- Play Console enforces `versionCode` limits. +- Play requires current target API levels. +- The store review process can reject builds for policy or content issues. +- If you change signing keys after launch, you need to follow Play’s key management flow. + +## 4. Recommended distribution strategy for this project + +For this repo, the most practical split is: + +- GitHub Releases: signed APK, manually installed by users +- Google Play: signed AAB, uploaded through Play Console + +That gives you: + +- a direct-download channel for power users +- an official store channel for broader distribution + +## 5. Repo action items + +If you want to make GitHub Releases reliable for updates, update the CI workflow to: + +- read a persistent release keystore from secrets +- stop generating a new keystore every run +- sign all release APKs with the same key + +If you want Play Store support, add a bundle release path and upload the `.aab` to Play Console. + +## References + +- [Create and set up your app](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en) +- [Use Play App Signing](https://support.google.com/googleplay/android-developer/answer/9842756?hl=en) +- [Sign and upload an APK](https://support.google.com/googleplay/android-developer/answer/16761055?hl=en) +- [Select key for existing package name](https://support.google.com/googleplay/android-developer/answer/16762143?hl=en) From faa4a49ffd62e3aae2b5c1a52d5b1ac2ea0e5ce4 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 17:19:39 +0800 Subject: [PATCH 12/21] fix build --- build/android/build.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/build/android/build.mjs b/build/android/build.mjs index 7314f6f..3197fc2 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -387,6 +387,18 @@ const userDataDir = (() => { })() process.env.DB_PATH = userDataDir +// Android does not set a meaningful HOME directory. The Node.js runtime +// (and os.homedir()) falls back to /data, which the app process cannot +// access, causing "EACCES: permission denied" when electerm tries to +// enumerate SSH keys from ~/.ssh. Point HOME at the writable user-data +// directory so that: +// - os.homedir() returns a path the app can read/write +// - SSH keys stored in /.ssh are found automatically +// - The .ssh dir is created once on first launch +const sshDir = resolve(userDataDir, '.ssh') +mkdirSync(sshDir, { recursive: true }) +process.env.HOME = userDataDir + await import('./app.bundle.mjs') ` fs.writeFileSync(path.resolve(NODEJS_DIR, 'index.js'), entry) From 93b3f520bf0d18ef6db7a91f05e5ba34ea8a0419 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 17:30:57 +0800 Subject: [PATCH 13/21] Fix build with key store --- .github/workflows/build-android.yml | 23 +++++------- docs/android-release-and-play-store.md | 50 ++++++++++++++++++-------- 2 files changed, 45 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 8ff3e30..0df316a 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -113,26 +113,21 @@ jobs: - name: Build debug APK run: cd build/android/android && chmod +x gradlew && ./gradlew assembleDebug - - name: Build release APK (signed with ephemeral keystore) + - name: Build release APK (signed with persistent keystore) env: - KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD || 'android' }} - KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD || 'android' }} + ANDROID_RELEASE_KEYSTORE_BASE64: ${{ secrets.ANDROID_RELEASE_KEYSTORE_BASE64 }} + ANDROID_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEYSTORE_PASSWORD }} + ANDROID_RELEASE_KEY_PASSWORD: ${{ secrets.ANDROID_RELEASE_KEY_PASSWORD }} + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.ANDROID_RELEASE_KEY_ALIAS }} run: | cd build/android/android - # Gradle resolves the injected store.file relative to the :app module - # dir (build/android/android/app), so generate the keystore there and - # pass the same absolute path to gradle. KEYSTORE="$PWD/app/release.keystore" - keytool -genkey -v \ - -keystore "$KEYSTORE" -alias electerm \ - -keyalg RSA -keysize 2048 -validity 10000 \ - -storepass "$KEYSTORE_PASSWORD" -keypass "$KEY_PASSWORD" \ - -dname "CN=electerm" + echo "$ANDROID_RELEASE_KEYSTORE_BASE64" | base64 -d > "$KEYSTORE" ./gradlew assembleRelease \ -Pandroid.injected.signing.store.file="$KEYSTORE" \ - -Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \ - -Pandroid.injected.signing.key.alias=electerm \ - -Pandroid.injected.signing.key.password="$KEY_PASSWORD" + -Pandroid.injected.signing.store.password="$ANDROID_RELEASE_KEYSTORE_PASSWORD" \ + -Pandroid.injected.signing.key.alias="$ANDROID_RELEASE_KEY_ALIAS" \ + -Pandroid.injected.signing.key.password="$ANDROID_RELEASE_KEY_PASSWORD" - name: Rename APKs to electerm-android-{arch}-{version}.apk # Version comes from package.json. Each per-ABI APK is renamed to diff --git a/docs/android-release-and-play-store.md b/docs/android-release-and-play-store.md index 0255edd..0e14a25 100644 --- a/docs/android-release-and-play-store.md +++ b/docs/android-release-and-play-store.md @@ -15,7 +15,7 @@ For GitHub Releases and direct sideloading, the signing key should be **stable** ### What the current repo does -The CI workflow currently generates an **ephemeral keystore** during the release job and uses it to sign the release APK. That is enough for a one-off artifact download, but it is not good for long-term updates because the key changes every run. +The CI workflow decodes a **persistent keystore** from GitHub Secrets (`ANDROID_RELEASE_KEYSTORE_BASE64`) and uses it to sign all release APKs with the same key. This ensures stable update continuity for sideloaded installs. ### What to do instead @@ -28,28 +28,50 @@ Recommended setup: 3. Store the keystore password, key password, and alias in GitHub Secrets. 4. Make the Android build workflow load those secrets and sign release APKs with that same keystore every time. -### Suggested secret names +### Generate the keystore -- `ANDROID_RELEASE_KEYSTORE_BASE64` -- `ANDROID_RELEASE_KEYSTORE_PASSWORD` -- `ANDROID_RELEASE_KEY_PASSWORD` -- `ANDROID_RELEASE_KEY_ALIAS` +Run these commands on a secure machine (not in CI). The keystore file is the only thing you keep locally. -Base64-encoding the keystore is often the easiest way to store it in GitHub Secrets. +```bash +keytool -genkey -v \ + -keystore release.keystore \ + -alias electerm \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -storepass YOUR_KEYSTORE_PASSWORD \ + -keypass YOUR_KEY_PASSWORD \ + -dname "CN=electerm, OU=electerm, O=electerm, L=Unknown, ST=Unknown, C=US" +``` -### Example signing flow +Replace `YOUR_KEYSTORE_PASSWORD` and `YOUR_KEY_PASSWORD` with strong passwords. -At release time: +### Encode and store in GitHub Secrets -1. Decode the keystore secret into a temporary file in the workflow runner. -2. Pass the keystore path and passwords to Gradle. -3. Build the release APK. -4. Publish the resulting APK to GitHub Releases. +Base64-encode the keystore file and store the result along with the other values as GitHub Actions secrets. + +```bash +# macOS / Linux +base64 -i release.keystore | pbcopy # macOS +base64 -w 0 release.keystore # Linux (print to stdout) + +# Windows (PowerShell) +[Convert]::ToBase64String([IO.File]::ReadAllBytes("release.keystore")) | Set-Clipboard +``` + +Set these four secrets in your GitHub repository (Settings → Secrets and variables → Actions): + +| Secret name | Value | +|---|---| +| `ANDROID_RELEASE_KEYSTORE_BASE64` | The base64-encoded keystore content | +| `ANDROID_RELEASE_KEYSTORE_PASSWORD` | The `-storepass` value you chose | +| `ANDROID_RELEASE_KEY_PASSWORD` | The `-keypass` value you chose | +| `ANDROID_RELEASE_KEY_ALIAS` | The `-alias` value you chose (e.g. `electerm`) | ### Operational rules - Never commit the keystore into git. -- Back up the keystore offline. +- Back up the keystore file offline (e.g. encrypted USB or password manager). - Keep the alias and passwords documented somewhere safe. - If the keystore is lost, app update continuity is lost for sideloaded releases. From 62886be6ed1774158d35cdc5eb06fbec0b9acf4e Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 17:42:31 +0800 Subject: [PATCH 14/21] fix: use printf for base64 decode and add keystore verification step --- .github/workflows/build-android.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 0df316a..f99566e 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -122,7 +122,8 @@ jobs: run: | cd build/android/android KEYSTORE="$PWD/app/release.keystore" - echo "$ANDROID_RELEASE_KEYSTORE_BASE64" | base64 -d > "$KEYSTORE" + printf '%s' "$ANDROID_RELEASE_KEYSTORE_BASE64" | base64 --decode > "$KEYSTORE" + keytool -list -keystore "$KEYSTORE" -storepass "$ANDROID_RELEASE_KEYSTORE_PASSWORD" ./gradlew assembleRelease \ -Pandroid.injected.signing.store.file="$KEYSTORE" \ -Pandroid.injected.signing.store.password="$ANDROID_RELEASE_KEYSTORE_PASSWORD" \ From 579eb436393c9d39a62958f50247b202b3b02087 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 18:16:22 +0800 Subject: [PATCH 15/21] fix5 --- build/android/build.mjs | 24 ++++++++++++++++++-- src/client/web-components/style-overide.styl | 18 +++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/build/android/build.mjs b/build/android/build.mjs index 3197fc2..86ca041 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -422,13 +422,33 @@ await import('./app.bundle.mjs') // not been created yet (e.g. during a pure `npm run build` before `cap add`). function applyResOverlay () { const overlayDir = path.resolve(__dirname, 'res-overlay') - const resDir = path.resolve(__dirname, 'android', 'app', 'src', 'main', 'res') + const mainDir = path.resolve(__dirname, 'android', 'app', 'src', 'main') + const resDir = path.resolve(mainDir, 'res') if (!fs.existsSync(resDir)) { console.log('[android] native project not found, skipping res-overlay (run cap add android + cap sync first)') return } console.log('[android] applying res-overlay →', resDir) - copyDir(overlayDir, resDir) + + // AndroidManifest.xml must go at app/src/main/, NOT inside res/. + // cap sync regenerates the default Capacitor manifest; overwrite it with + // the electerm version that sets the custom icon, splash theme, and + // cleartext-traffic / network-security-config. + const manifestSrc = path.resolve(overlayDir, 'AndroidManifest.xml') + if (fs.existsSync(manifestSrc)) { + const manifestDest = path.resolve(mainDir, 'AndroidManifest.xml') + fs.copyFileSync(manifestSrc, manifestDest) + console.log('[android] wrote', manifestDest) + } + + // Copy every *other* entry (drawable, mipmap-*, values, xml, …) into res/. + for (const entry of fs.readdirSync(overlayDir, { withFileTypes: true })) { + if (entry.name === 'AndroidManifest.xml') continue + const s = path.join(overlayDir, entry.name) + const d = path.join(resDir, entry.name) + if (entry.isDirectory()) copyDir(s, d) + else fs.copyFileSync(s, d) + } } // -------------------------------------------------------------------------- diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl index f796619..346fa32 100644 --- a/src/client/web-components/style-overide.styl +++ b/src/client/web-components/style-overide.styl @@ -1,3 +1,21 @@ @-moz-document url-prefix() .tabs-inner overflow-x hidden !important + +// Fix custom modal not rendering content on Android WebView. +// The original .custom-modal-wrap has overflow:auto on a position:fixed +// element; the inner .custom-modal-container is position:relative which +// causes the content card to not render inside the scrollable fixed +// container on Android WebView (mask shows, content doesn't). +// Override: move overflow:auto to the container and make it position:fixed. +#container .custom-modal-wrap + overflow visible + +#container .custom-modal-container + position fixed + top 0 + left 0 + right 0 + bottom 0 + overflow auto + min-height auto From 64fdedcec84a5ead8b0614b240ede0b5f6f53d70 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 18:40:00 +0800 Subject: [PATCH 16/21] fix5 --- .github/workflows/build-android.yml | 42 +++++++++++++++++------------ 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index f99566e..f2c7d2d 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -63,7 +63,31 @@ jobs: - name: Apply icon / splash overlay run: | - cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ + MANIFEST_SRC=build/android/res-overlay/AndroidManifest.xml + MANIFEST_DST=build/android/android/app/src/main/AndroidManifest.xml + + # AndroidManifest.xml must go at app/src/main/, NOT inside res/. + # Copy the electerm manifest (with icon, theme, network config) to + # the correct location, overwriting Capacitor's default. + if [ -f "$MANIFEST_SRC" ]; then + cp "$MANIFEST_SRC" "$MANIFEST_DST" + echo "Overlayed AndroidManifest.xml -> $MANIFEST_DST" + fi + + # Copy every other entry (drawable, mipmap-*, values, xml) into res/. + for entry in build/android/res-overlay/*/; do + [ -d "$entry" ] || continue + name=$(basename "$entry") + cp -r "$entry" "build/android/android/app/src/main/res/$name" + done + # Also copy any non-directory files in res-overlay (except the manifest) + for f in build/android/res-overlay/*; do + [ -f "$f" ] || continue + name=$(basename "$f") + [ "$name" = "AndroidManifest.xml" ] && continue + cp "$f" "build/android/android/app/src/main/res/$name" + done + # Capacitor's default template ships its own drawable/splash.png and # values/ic_launcher_background.xml. These collide with our overlay's # splash.xml / colors-electerm.xml (same resource name, different file), @@ -71,22 +95,6 @@ jobs: rm -f build/android/android/app/src/main/res/drawable/splash.png rm -f build/android/android/app/src/main/res/values/ic_launcher_background.xml - - name: Permit cleartext to the local Node.js backend in the manifest - # The on-device Node.js backend serves the UI + API over plain http on - # 127.0.0.1:5577. The app targets SDK 36, which blocks cleartext by - # default. res-overlay/AndroidManifest.xml sets usesCleartextTraffic + - # networkSecurityConfig, but the overlay step above copies it into res/ - # (where AndroidManifest.xml is ignored) instead of merging it, so the - # attributes never reach the compiled manifest. Inject them directly - # into the merged main manifest so http://127.0.0.1:5577 is reachable. - run: | - MF=build/android/android/app/src/main/AndroidManifest.xml - grep -q 'usesCleartextTraffic' "$MF" || \ - sed -i 's|# \n#' "$MF" - grep -n 'usesCleartextTraffic\|networkSecurityConfig' "$MF" - - name: Configure per-ABI APK splits # Produce one APK per architecture (arm64-v8a, armeabi-v7a, x86_64) # plus a universal build, instead of a single ~160MB universal blob. From 8b753bf53c17040a7551a89f875ba3dd1eec8c0a Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 18:46:47 +0800 Subject: [PATCH 17/21] fix6 --- .github/workflows/build-android.yml | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index f2c7d2d..f6b1615 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -63,30 +63,20 @@ jobs: - name: Apply icon / splash overlay run: | - MANIFEST_SRC=build/android/res-overlay/AndroidManifest.xml - MANIFEST_DST=build/android/android/app/src/main/AndroidManifest.xml - # AndroidManifest.xml must go at app/src/main/, NOT inside res/. # Copy the electerm manifest (with icon, theme, network config) to # the correct location, overwriting Capacitor's default. + MANIFEST_SRC=build/android/res-overlay/AndroidManifest.xml + MANIFEST_DST=build/android/android/app/src/main/AndroidManifest.xml if [ -f "$MANIFEST_SRC" ]; then cp "$MANIFEST_SRC" "$MANIFEST_DST" echo "Overlayed AndroidManifest.xml -> $MANIFEST_DST" fi - # Copy every other entry (drawable, mipmap-*, values, xml) into res/. - for entry in build/android/res-overlay/*/; do - [ -d "$entry" ] || continue - name=$(basename "$entry") - cp -r "$entry" "build/android/android/app/src/main/res/$name" - done - # Also copy any non-directory files in res-overlay (except the manifest) - for f in build/android/res-overlay/*; do - [ -f "$f" ] || continue - name=$(basename "$f") - [ "$name" = "AndroidManifest.xml" ] && continue - cp "$f" "build/android/android/app/src/main/res/$name" - done + # Copy all resource dirs (drawable, mipmap-*, values, xml) into res/. + # Using overlay/. merges directory contents into the destination + # without creating extra nesting levels. + cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ # Capacitor's default template ships its own drawable/splash.png and # values/ic_launcher_background.xml. These collide with our overlay's From 8a214f784053bd4667f28172323d71a4d99e5e62 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 19:27:00 +0800 Subject: [PATCH 18/21] fix 7 --- .github/workflows/build-android.yml | 23 +++++++--- build/android/build.mjs | 47 ++++++++++++++++++++ src/client/web-components/style-overide.styl | 41 +++++++++++------ 3 files changed, 91 insertions(+), 20 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index f6b1615..c149b5c 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -78,12 +78,23 @@ jobs: # without creating extra nesting levels. cp -r build/android/res-overlay/. build/android/android/app/src/main/res/ - # Capacitor's default template ships its own drawable/splash.png and - # values/ic_launcher_background.xml. These collide with our overlay's - # splash.xml / colors-electerm.xml (same resource name, different file), - # so drop the template's copies after overlaying ours. - rm -f build/android/android/app/src/main/res/drawable/splash.png - rm -f build/android/android/app/src/main/res/values/ic_launcher_background.xml + # Remove Capacitor's default resources that conflict with / shadow + # the electerm overlay. These are generated by `cap add android` and + # must be deleted AFTER the overlay copy: + # drawable-v24/ic_launcher_foreground.xml — vector foreground that + # takes precedence over our drawable/ic_launcher_foreground.png + # on API 24+, showing the generic Capacitor logo instead of electerm. + # values/ic_launcher_background.xml — same color name as our + # colors-electerm.xml → duplicate-resource build error. + # drawable/splash.png — same resource name as our splash.xml. + # mipmap-*/ic_launcher_round.png + mipmap-anydpi-v26/ic_launcher_round.xml + # — Capacitor's default round icons; remove so only electerm icons remain. + RES=build/android/android/app/src/main/res + rm -f "$RES/drawable-v24/ic_launcher_foreground.xml" + rm -f "$RES/values/ic_launcher_background.xml" + rm -f "$RES/drawable/splash.png" + rm -f "$RES"/mipmap-*/ic_launcher_round.png + rm -f "$RES/mipmap-anydpi-v26/ic_launcher_round.xml" - name: Configure per-ABI APK splits # Produce one APK per architecture (arm64-v8a, armeabi-v7a, x86_64) diff --git a/build/android/build.mjs b/build/android/build.mjs index 86ca041..f35fd5d 100644 --- a/build/android/build.mjs +++ b/build/android/build.mjs @@ -449,6 +449,53 @@ function applyResOverlay () { if (entry.isDirectory()) copyDir(s, d) else fs.copyFileSync(s, d) } + + // Remove Capacitor's default resources that conflict with the overlay. + // These are generated by `cap add android` / `cap sync` and must be + // deleted AFTER the overlay copy, otherwise they shadow or conflict with + // the electerm resources: + // + // drawable-v24/ic_launcher_foreground.xml + // Capacitor's vector foreground. On API 24+ (Android 7.0+) it takes + // precedence over our drawable/ic_launcher_foreground.png, so the + // adaptive icon shows the generic Capacitor logo instead of electerm. + // + // values/ic_launcher_background.xml + // Defines the same `ic_launcher_background` color as our + // colors-electerm.xml → duplicate-resource build error. + // + // drawable/splash.png + // Same resource name as our drawable/splash.xml → conflict. + // + // mipmap-*/ic_launcher_round.png + mipmap-anydpi-v26/ic_launcher_round.xml + // Capacitor's default round icons. Not referenced by our manifest, but + // some launchers auto-discover them; remove so only electerm icons remain. + const conflicts = [ + 'drawable-v24/ic_launcher_foreground.xml', + 'values/ic_launcher_background.xml', + 'drawable/splash.png' + ] + for (const rel of conflicts) { + const f = path.join(resDir, rel) + if (fs.existsSync(f)) { + fs.rmSync(f, { force: true }) + console.log('[android] removed conflicting resource:', rel) + } + } + // Remove round icon resources (per-density PNGs + adaptive XML). + for (const entry of fs.readdirSync(resDir, { withFileTypes: true })) { + if (!entry.isDirectory() || !entry.name.startsWith('mipmap-')) continue + const roundPng = path.join(resDir, entry.name, 'ic_launcher_round.png') + if (fs.existsSync(roundPng)) { + fs.rmSync(roundPng, { force: true }) + console.log('[android] removed conflicting resource:', entry.name + '/ic_launcher_round.png') + } + } + const roundXml = path.join(resDir, 'mipmap-anydpi-v26', 'ic_launcher_round.xml') + if (fs.existsSync(roundXml)) { + fs.rmSync(roundXml, { force: true }) + console.log('[android] removed conflicting resource: mipmap-anydpi-v26/ic_launcher_round.xml') + } } // -------------------------------------------------------------------------- diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl index 346fa32..c8754c5 100644 --- a/src/client/web-components/style-overide.styl +++ b/src/client/web-components/style-overide.styl @@ -3,19 +3,32 @@ overflow-x hidden !important // Fix custom modal not rendering content on Android WebView. -// The original .custom-modal-wrap has overflow:auto on a position:fixed -// element; the inner .custom-modal-container is position:relative which -// causes the content card to not render inside the scrollable fixed -// container on Android WebView (mask shows, content doesn't). -// Override: move overflow:auto to the container and make it position:fixed. -#container .custom-modal-wrap - overflow visible +// +// Root cause: the original .custom-modal-wrap is position:fixed WITH +// overflow:auto. On Android WebView, overflow:auto (or scroll) on a +// position:fixed element does not render its children — the mask shows +// but the content card is invisible. +// +// The previous fix moved overflow:auto to .custom-modal-container and made +// that position:fixed — but that is the SAME bug (overflow:auto on +// position:fixed). It only moved the problem one level down. +// +// Correct fix: +// 1. .custom-modal-wrap → overflow:hidden (no scroll on the fixed layer) +// 2. .custom-modal-container → height:100% + overflow:auto +// (position stays relative from modal.styl; overflow:auto works fine +// on position:relative elements in Android WebView) +// +// Use `body` prefix for specificity: this override is imported early (in +// basic.js, before the app bundle), while modal.styl is imported by the Modal +// component (loaded later). With equal specificity the later rule wins, so we +// need the extra `body` ancestor to raise specificity above modal.styl. +// `body` covers both modals inside #container AND Modal.info()/Modal.confirm() +// instances that are appended directly to document.body. +body .custom-modal-wrap + overflow hidden -#container .custom-modal-container - position fixed - top 0 - left 0 - right 0 - bottom 0 - overflow auto +body .custom-modal-container + height 100% min-height auto + overflow auto From ef4040c42b653d72e3724273d6994cc97d3700d7 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 19:48:52 +0800 Subject: [PATCH 19/21] fix again 66 --- package-lock.json | 4 +-- package.json | 2 +- src/client/web-components/style-overide.styl | 30 +++++++++----------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index ee9c373..1abdc69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electerm-android", - "version": "4.15.138", + "version": "4.15.139", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electerm-android", - "version": "4.15.138", + "version": "4.15.139", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1070773..1edc2eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electerm-android", - "version": "4.15.138", + "version": "4.15.139", "description": "electerm for Android — a free and open-source terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client, built on the electerm-web codebase with Capacitor.", "main": "src/app/app.js", "type": "module", diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl index c8754c5..0b59ff7 100644 --- a/src/client/web-components/style-overide.styl +++ b/src/client/web-components/style-overide.styl @@ -5,19 +5,17 @@ // Fix custom modal not rendering content on Android WebView. // // Root cause: the original .custom-modal-wrap is position:fixed WITH -// overflow:auto. On Android WebView, overflow:auto (or scroll) on a -// position:fixed element does not render its children — the mask shows -// but the content card is invisible. +// overflow:auto. On Android WebView, overflow on a position:fixed element +// does not render its children — the mask shows but the content card is +// invisible. Changing overflow to hidden (previous attempt) did not help +// because even overflow:hidden on position:fixed can trigger the same bug. // -// The previous fix moved overflow:auto to .custom-modal-container and made -// that position:fixed — but that is the SAME bug (overflow:auto on -// position:fixed). It only moved the problem one level down. -// -// Correct fix: -// 1. .custom-modal-wrap → overflow:hidden (no scroll on the fixed layer) -// 2. .custom-modal-container → height:100% + overflow:auto -// (position stays relative from modal.styl; overflow:auto works fine -// on position:relative elements in Android WebView) +// Fix: switch .custom-modal-wrap and .custom-modal-mask from position:fixed +// to position:absolute. The original overflow:auto / position:relative / +// min-height:100% from modal.styl all work correctly on position:absolute +// elements in Android WebView. Since the modal covers the full viewport +// (top/left/right/bottom:0) and the app body does not scroll, absolute +// behaves identically to fixed here. // // Use `body` prefix for specificity: this override is imported early (in // basic.js, before the app bundle), while modal.styl is imported by the Modal @@ -26,9 +24,7 @@ // `body` covers both modals inside #container AND Modal.info()/Modal.confirm() // instances that are appended directly to document.body. body .custom-modal-wrap - overflow hidden + position absolute -body .custom-modal-container - height 100% - min-height auto - overflow auto +body .custom-modal-mask + position absolute From 559a566582db2ac515bdc9c452474d89bc7e0471 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Fri, 17 Jul 2026 23:31:35 +0800 Subject: [PATCH 20/21] fix67 --- package-lock.json | 4 ++-- package.json | 2 +- src/client/web-components/web-pre.js | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1abdc69..f611f56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "electerm-android", - "version": "4.15.139", + "version": "4.15.140", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "electerm-android", - "version": "4.15.139", + "version": "4.15.140", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1edc2eb..37145ed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "electerm-android", - "version": "4.15.139", + "version": "4.15.140", "description": "electerm for Android — a free and open-source terminal/ssh/sftp/telnet/serialport/RDP/VNC/Spice/ftp client, built on the electerm-web codebase with Capacitor.", "main": "src/app/app.js", "type": "module", diff --git a/src/client/web-components/web-pre.js b/src/client/web-components/web-pre.js index 34f98f3..e54a989 100644 --- a/src/client/web-components/web-pre.js +++ b/src/client/web-components/web-pre.js @@ -47,7 +47,11 @@ window.pre = { 'resume', 'destroy' ], - osInfo: () => { return window.pre.osInfoData }, + // Safe defaults for API-dependent data to prevent render crashes + // before /api/get-constants response arrives (fixes Android info-modal + // showing only background with no content) + osInfoData: [], + osInfo: () => { return window.pre.osInfoData || [] }, extIconPath: window.et.extIconPath, readClipboard: () => { return window.et.clipboard || '' @@ -97,7 +101,8 @@ window.pre = { window.open(url, '_blank') }, runSync, - runGlobalAsync + runGlobalAsync, + versions: {} } const fs = { From 2e24196d274acba4cf37756cf5858dd4e30171d1 Mon Sep 17 00:00:00 2001 From: ZHAO Xudong Date: Sat, 18 Jul 2026 04:42:51 +0800 Subject: [PATCH 21/21] fix68 --- src/client/web-components/style-overide.styl | 9 ++++++++ src/client/web-components/web-pre.js | 24 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/client/web-components/style-overide.styl b/src/client/web-components/style-overide.styl index 0b59ff7..8e223f2 100644 --- a/src/client/web-components/style-overide.styl +++ b/src/client/web-components/style-overide.styl @@ -25,6 +25,15 @@ // instances that are appended directly to document.body. body .custom-modal-wrap position absolute + overflow visible body .custom-modal-mask position absolute + +body .custom-modal-container + position relative + overflow visible + +body .custom-modal-content + position relative + overflow visible diff --git a/src/client/web-components/web-pre.js b/src/client/web-components/web-pre.js index e54a989..77895d5 100644 --- a/src/client/web-components/web-pre.js +++ b/src/client/web-components/web-pre.js @@ -105,6 +105,30 @@ window.pre = { versions: {} } +// Ensure window.et.packInfo has all fields required by info-modal.jsx +// On Android/Capacitor the packInfo is minimal and missing author/bugs/releases/etc. +const _packInfoDefaults = { + author: { + name: 'ZHAO Xudong', + email: 'zxdong@gmail.com', + url: 'https://github.com/zxdong262' + }, + homepage: 'https://electerm.org', + bugs: { + url: 'https://github.com/electerm/electerm/issues' + }, + releases: 'https://github.com/electerm/electerm/releases', + sponsorLink: 'https://electerm.org/sponsor-electerm/', + knownIssuesLink: 'https://github.com/electerm/electerm/wiki/Known-issues', + langugeRepo: 'https://github.com/electerm/electerm-languages' +} +if (window.et.packInfo) { + window.et.packInfo = { + ...window.et.packInfo, + ..._packInfoDefaults + } +} + const fs = { stat: (path, cb) => { window.fs.statCustom(path)