diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml
index 0fa99a7dc..f5397f308 100644
--- a/.github/workflows/beta-release.yml
+++ b/.github/workflows/beta-release.yml
@@ -15,7 +15,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, macos-13, windows-2025, windows-2022]
+ # windows-11-arm is not working (yet), see:
+ # https://github.com/homebridge/homebridge-config-ui-x/actions/runs/16482469131/job/46600012357
+ os: [ubuntu-24.04, ubuntu-24.04-arm, ubuntu-22.04, ubuntu-22.04-arm, macos-15, macos-14, macos-13, windows-2025, windows-2022]
uses: homebridge/.github/.github/workflows/nodejs-build-and-test.yml@latest
with:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index d0cd28ca6..14d32808b 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -18,7 +18,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, macos-13, windows-2025, windows-2022]
+ # windows-11-arm is not working (yet), see:
+ # https://github.com/homebridge/homebridge-config-ui-x/actions/runs/16482469131/job/46600012357
+ os: [ubuntu-24.04, ubuntu-24.04-arm, ubuntu-22.04, ubuntu-22.04-arm, macos-15, macos-14, macos-13, windows-2025, windows-2022]
uses: homebridge/.github/.github/workflows/nodejs-build-and-test.yml@latest
with:
@@ -37,7 +39,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [ubuntu-24.04, ubuntu-22.04, macos-15, macos-14, macos-13, windows-2025, windows-2022]
+ # windows-11-arm is not working (yet), see:
+ # https://github.com/homebridge/homebridge-config-ui-x/actions/runs/16482469131/job/46600012357
+ os: [ubuntu-24.04, ubuntu-24.04-arm, ubuntu-22.04, ubuntu-22.04-arm, macos-15, macos-14, macos-13, windows-2025, windows-2022]
node-version: [20.x, 22.x, 24.x]
runs-on: ${{ matrix.os }}
diff --git a/.npmignore b/.npmignore
index 16b7a5e73..260ed8500 100644
--- a/.npmignore
+++ b/.npmignore
@@ -77,3 +77,6 @@ yarn.lock
.env
.DS_Store
.idea
+
+public/assets/monaco-0.21.3/monaco.d.ts
+!public/assets/monaco-0.21.3/min/vs/base/browser/ui/codicons/codicon/codicon.ttf
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3caca35b1..a9c320697 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,74 @@
All notable changes to `homebridge-config-ui-x` will be documented in this file. This project tries to adhere to [Semantic Versioning](http://semver.org/).
+## v5.4.1 (2025-08-05)
+
+### UI Changes
+
+- updates to the `pl.json` language file (#2522, #2523) (@mkz212)
+- reduce font size in accessory label by 1px
+- updates to the credits modal
+- make red restart child bridge button on plugin log modal
+- refreshed icons: stateless programmable switch
+
+### Other Changes
+
+- fix custom uis which use ui translation strings
+
+### Homebridge Dependencies
+
+- `@homebridge/hap-client` @ `v3.1.1`
+- `@homebridge/node-pty-prebuilt-multiarch` @ `v0.13.1`
+- `@homebridge/plugin-ui-utils` @ `v2.1.0`
+
+## v5.4.0 (2025-08-02)
+
+### UI Changes
+
+- updates to the `th.json` language file (#2520) (@tomzt)
+- json config editor to offer child bridge restarts where possible
+- improvements to the restore config file modal
+- terminal session persistence and macos shell optimization (#2493) (@seidnerj)
+ - big thank you to @seidnerj for this contribution!
+ - to read more info about these changes, see the [pull request](https://github.com/homebridge/homebridge-config-ui-x/pull/2493#issue-3226899065)
+- small tweaks to persistent terminal integration
+- show/copy user qr code secrets when setting up 2fa
+- fix json schema icons from font awesome update
+- disable plugin notes now based on keep orphans setting
+- text clarification in users support modal
+- remove glibc checks as node 18 is now unsupported
+- add restart child bridges option to plugin log modal
+- node version modal: show hb/ui/plugin compatibility
+- air purifier tile: update icon to match new style
+- robot vacuum tile: use css to use a single svg file
+- custom types: expose a switch/outlet as a garage door and vice versa
+- garage door tile: show when obstruction detected
+- air quality sensor tile: update icon to match new style
+- show in sys info widget if arch is 32-bit or 64-bit
+- fix for custom ui colouring in dark mode
+- updated icons: co, co2, smoke, contact, garage
+- thermostat accessory: show target slider when available
+- updated icon for door, window and window covering services
+- accessory info: show props on characteristic click
+- credits modal: added new section for translations
+- custom types: expose a switch/outlet as a washing machine
+- refreshed icons for fan, television, speaker + filter
+- added confirmation modal for shutting down
+
+### Other Changes
+
+- added `arm` runners to the ui workflows for testing
+- make 'keep orphans' setting available in ui settings service
+- improve types for widgets
+- upgrade `@ngx-translate/core` from `v16` to `v17`
+- fix get pairings, ignore `.json.bak` files
+
+### Homebridge Dependencies
+
+- `@homebridge/hap-client` @ `v3.1.1`
+- `@homebridge/node-pty-prebuilt-multiarch` @ `v0.13.1`
+- `@homebridge/plugin-ui-utils` @ `v2.1.0`
+
## v5.3.0 (2025-07-23)
### UI Changes
diff --git a/config.schema.json b/config.schema.json
index a529052d0..09af205f7 100644
--- a/config.schema.json
+++ b/config.schema.json
@@ -551,6 +551,38 @@
}
}
}
+ },
+ "terminal": {
+ "type": "object",
+ "title": "Terminal Settings",
+ "description": "The terminal settings for the Homebridge UI.",
+ "properties": {
+ "persistence": {
+ "title": "Terminal Session Persistence",
+ "type": "boolean",
+ "description": "When enabled, terminal sessions will persist when navigating away and can be resumed when returning to the terminal page (or status page widget). Terminal sessions are shared across Homebridge UI admin users.",
+ "default": false
+ },
+ "hideWarning": {
+ "title": "Hide Terminal Termination Warning",
+ "type": "boolean",
+ "description": "When enabled, the warning dialog will not be shown before terminating non-persistent terminal sessions.",
+ "default": false,
+ "condition": {
+ "functionBody": "return !model.terminal?.persistence"
+ }
+ },
+ "bufferSize": {
+ "title": "Terminal Buffer Character Size",
+ "type": "integer",
+ "description": "Amount of terminal output to preserve for persistent sessions. Larger values use more memory.",
+ "default": 50000,
+ "minimum": 0,
+ "condition": {
+ "functionBody": "return model.terminal?.persistence"
+ }
+ }
+ }
}
}
},
diff --git a/eslint.config.js b/eslint.config.js
index 5d965c670..7b283b0c7 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,7 +1,7 @@
const { antfu } = require('@antfu/eslint-config')
module.exports = antfu({
- ignores: ['dist', 'ui/.angular', 'ui/src/assets/monaco'],
+ ignores: ['dist', 'ui/.angular', 'ui/src/assets/monaco-0.21.3'],
rules: {
'jsdoc/check-alignment': 'error',
'jsdoc/check-line-alignment': 'error',
diff --git a/package-lock.json b/package-lock.json
index 5bf9fe0b9..cbcc92ea3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "homebridge-config-ui-x",
- "version": "5.3.0",
+ "version": "5.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "homebridge-config-ui-x",
- "version": "5.3.0",
+ "version": "5.4.1",
"funding": [
{
"type": "github",
@@ -40,7 +40,7 @@
"commander": "14.0.0",
"dayjs": "1.11.13",
"fastify": "5.4.0",
- "fs-extra": "11.3.0",
+ "fs-extra": "11.3.1",
"jsonwebtoken": "9.0.2",
"lodash": "4.17.21",
"node-cache": "5.1.2",
@@ -63,12 +63,12 @@
"hb-service": "dist/bin/hb-service.js"
},
"devDependencies": {
- "@antfu/eslint-config": "^4.18.0",
+ "@antfu/eslint-config": "^5.1.0",
"@nestjs/testing": "^11.1.5",
"@prettier/plugin-xml": "^3.4.2",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
- "@types/node": "^24.1.0",
+ "@types/node": "^24.2.0",
"@types/node-schedule": "^2.1.8",
"@types/passport-jwt": "^4.0.1",
"@types/semver": "^7.7.0",
@@ -84,7 +84,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
- "typescript": "^5.8.3",
+ "typescript": "^5.9.2",
"unplugin-swc": "^1.5.5",
"vitest": "^3.2.4"
},
@@ -108,9 +108,9 @@
}
},
"node_modules/@antfu/eslint-config": {
- "version": "4.18.0",
- "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-4.18.0.tgz",
- "integrity": "sha512-NjzC2VS0UU45xMPN7FJcIF/hhfYHb/ILVp8T6JdfPKel5QToC4bjC8P0v1tp+cy0/F+5jRJdaGrnH31s7Ku4jw==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-5.1.0.tgz",
+ "integrity": "sha512-JirdCHnt2frnUf7kmXBxvFfdca1UnC19AP89/nKgZIV71PXxhH6pX/jqF13OKpbOo4hxJQfs6yuS1Kl5LoW4Yw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -125,14 +125,14 @@
"ansis": "^4.1.0",
"cac": "^6.7.14",
"eslint-config-flat-gitignore": "^2.1.0",
- "eslint-flat-config-utils": "^2.1.0",
+ "eslint-flat-config-utils": "^2.1.1",
"eslint-merge-processors": "^2.0.0",
"eslint-plugin-antfu": "^3.1.1",
"eslint-plugin-command": "^3.3.1",
"eslint-plugin-import-lite": "^0.3.0",
- "eslint-plugin-jsdoc": "^51.4.1",
+ "eslint-plugin-jsdoc": "^52.0.0",
"eslint-plugin-jsonc": "^2.20.1",
- "eslint-plugin-n": "^17.21.0",
+ "eslint-plugin-n": "^17.21.3",
"eslint-plugin-no-only-tests": "^3.3.0",
"eslint-plugin-perfectionist": "^4.15.0",
"eslint-plugin-pnpm": "^1.1.0",
@@ -159,12 +159,14 @@
},
"peerDependencies": {
"@eslint-react/eslint-plugin": "^1.38.4",
+ "@next/eslint-plugin-next": "^15.4.0-canary.115",
"@prettier/plugin-xml": "^3.4.1",
"@unocss/eslint-plugin": ">=0.50.0",
"astro-eslint-parser": "^1.0.2",
"eslint": "^9.10.0",
"eslint-plugin-astro": "^1.2.0",
"eslint-plugin-format": ">=0.1.0",
+ "eslint-plugin-jsx-a11y": ">=6.10.2",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-solid": "^0.14.3",
@@ -178,6 +180,9 @@
"@eslint-react/eslint-plugin": {
"optional": true
},
+ "@next/eslint-plugin-next": {
+ "optional": true
+ },
"@prettier/plugin-xml": {
"optional": true
},
@@ -193,6 +198,9 @@
"eslint-plugin-format": {
"optional": true
},
+ "eslint-plugin-jsx-a11y": {
+ "optional": true
+ },
"eslint-plugin-react-hooks": {
"optional": true
},
@@ -270,9 +278,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.1",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
- "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1032,9 +1040,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.31.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz",
- "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==",
+ "version": "9.32.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz",
+ "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -2060,9 +2068,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.45.1.tgz",
- "integrity": "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.46.2.tgz",
+ "integrity": "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA==",
"cpu": [
"arm"
],
@@ -2074,9 +2082,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.45.1.tgz",
- "integrity": "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.46.2.tgz",
+ "integrity": "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ==",
"cpu": [
"arm64"
],
@@ -2088,9 +2096,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.45.1.tgz",
- "integrity": "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.46.2.tgz",
+ "integrity": "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ==",
"cpu": [
"arm64"
],
@@ -2102,9 +2110,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.45.1.tgz",
- "integrity": "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.46.2.tgz",
+ "integrity": "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA==",
"cpu": [
"x64"
],
@@ -2116,9 +2124,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.45.1.tgz",
- "integrity": "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.46.2.tgz",
+ "integrity": "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg==",
"cpu": [
"arm64"
],
@@ -2130,9 +2138,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.45.1.tgz",
- "integrity": "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.46.2.tgz",
+ "integrity": "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw==",
"cpu": [
"x64"
],
@@ -2144,9 +2152,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.45.1.tgz",
- "integrity": "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.46.2.tgz",
+ "integrity": "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA==",
"cpu": [
"arm"
],
@@ -2158,9 +2166,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.45.1.tgz",
- "integrity": "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.46.2.tgz",
+ "integrity": "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ==",
"cpu": [
"arm"
],
@@ -2172,9 +2180,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.45.1.tgz",
- "integrity": "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.46.2.tgz",
+ "integrity": "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng==",
"cpu": [
"arm64"
],
@@ -2186,9 +2194,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.45.1.tgz",
- "integrity": "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.46.2.tgz",
+ "integrity": "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg==",
"cpu": [
"arm64"
],
@@ -2200,9 +2208,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.45.1.tgz",
- "integrity": "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.46.2.tgz",
+ "integrity": "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA==",
"cpu": [
"loong64"
],
@@ -2213,10 +2221,10 @@
"linux"
]
},
- "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.45.1.tgz",
- "integrity": "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg==",
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.46.2.tgz",
+ "integrity": "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw==",
"cpu": [
"ppc64"
],
@@ -2228,9 +2236,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.45.1.tgz",
- "integrity": "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.46.2.tgz",
+ "integrity": "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ==",
"cpu": [
"riscv64"
],
@@ -2242,9 +2250,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.45.1.tgz",
- "integrity": "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.46.2.tgz",
+ "integrity": "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw==",
"cpu": [
"riscv64"
],
@@ -2256,9 +2264,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.45.1.tgz",
- "integrity": "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.46.2.tgz",
+ "integrity": "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA==",
"cpu": [
"s390x"
],
@@ -2270,9 +2278,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.45.1.tgz",
- "integrity": "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.46.2.tgz",
+ "integrity": "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA==",
"cpu": [
"x64"
],
@@ -2284,9 +2292,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.45.1.tgz",
- "integrity": "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.46.2.tgz",
+ "integrity": "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA==",
"cpu": [
"x64"
],
@@ -2298,9 +2306,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.45.1.tgz",
- "integrity": "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.46.2.tgz",
+ "integrity": "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g==",
"cpu": [
"arm64"
],
@@ -2312,9 +2320,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.45.1.tgz",
- "integrity": "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.46.2.tgz",
+ "integrity": "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ==",
"cpu": [
"ia32"
],
@@ -2326,9 +2334,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.45.1.tgz",
- "integrity": "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.46.2.tgz",
+ "integrity": "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg==",
"cpu": [
"x64"
],
@@ -2374,9 +2382,9 @@
}
},
"node_modules/@swc/core": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.2.tgz",
- "integrity": "sha512-YWqn+0IKXDhqVLKoac4v2tV6hJqB/wOh8/Br8zjqeqBkKa77Qb0Kw2i7LOFzjFNZbZaPH6AlMGlBwNrxaauaAg==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.13.3.tgz",
+ "integrity": "sha512-ZaDETVWnm6FE0fc+c2UE8MHYVS3Fe91o5vkmGfgwGXFbxYvAjKSqxM/j4cRc9T7VZNSJjriXq58XkfCp3Y6f+w==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ -2393,16 +2401,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
- "@swc/core-darwin-arm64": "1.13.2",
- "@swc/core-darwin-x64": "1.13.2",
- "@swc/core-linux-arm-gnueabihf": "1.13.2",
- "@swc/core-linux-arm64-gnu": "1.13.2",
- "@swc/core-linux-arm64-musl": "1.13.2",
- "@swc/core-linux-x64-gnu": "1.13.2",
- "@swc/core-linux-x64-musl": "1.13.2",
- "@swc/core-win32-arm64-msvc": "1.13.2",
- "@swc/core-win32-ia32-msvc": "1.13.2",
- "@swc/core-win32-x64-msvc": "1.13.2"
+ "@swc/core-darwin-arm64": "1.13.3",
+ "@swc/core-darwin-x64": "1.13.3",
+ "@swc/core-linux-arm-gnueabihf": "1.13.3",
+ "@swc/core-linux-arm64-gnu": "1.13.3",
+ "@swc/core-linux-arm64-musl": "1.13.3",
+ "@swc/core-linux-x64-gnu": "1.13.3",
+ "@swc/core-linux-x64-musl": "1.13.3",
+ "@swc/core-win32-arm64-msvc": "1.13.3",
+ "@swc/core-win32-ia32-msvc": "1.13.3",
+ "@swc/core-win32-x64-msvc": "1.13.3"
},
"peerDependencies": {
"@swc/helpers": ">=0.5.17"
@@ -2414,9 +2422,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.2.tgz",
- "integrity": "sha512-44p7ivuLSGFJ15Vly4ivLJjg3ARo4879LtEBAabcHhSZygpmkP8eyjyWxrH3OxkY1eRZSIJe8yRZPFw4kPXFPw==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.3.tgz",
+ "integrity": "sha512-ux0Ws4pSpBTqbDS9GlVP354MekB1DwYlbxXU3VhnDr4GBcCOimpocx62x7cFJkSpEBF8bmX8+/TTCGKh4PbyXw==",
"cpu": [
"arm64"
],
@@ -2432,9 +2440,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.2.tgz",
- "integrity": "sha512-Lb9EZi7X2XDAVmuUlBm2UvVAgSCbD3qKqDCxSI4jEOddzVOpNCnyZ/xEampdngUIyDDhhJLYU9duC+Mcsv5Y+A==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.13.3.tgz",
+ "integrity": "sha512-p0X6yhxmNUOMZrbeZ3ZNsPige8lSlSe1llllXvpCLkKKxN/k5vZt1sULoq6Nj4eQ7KeHQVm81/+AwKZyf/e0TA==",
"cpu": [
"x64"
],
@@ -2450,9 +2458,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.2.tgz",
- "integrity": "sha512-9TDe/92ee1x57x+0OqL1huG4BeljVx0nWW4QOOxp8CCK67Rpc/HHl2wciJ0Kl9Dxf2NvpNtkPvqj9+BUmM9WVA==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.3.tgz",
+ "integrity": "sha512-OmDoiexL2fVWvQTCtoh0xHMyEkZweQAlh4dRyvl8ugqIPEVARSYtaj55TBMUJIP44mSUOJ5tytjzhn2KFxFcBA==",
"cpu": [
"arm"
],
@@ -2468,9 +2476,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.2.tgz",
- "integrity": "sha512-KJUSl56DBk7AWMAIEcU83zl5mg3vlQYhLELhjwRFkGFMvghQvdqQ3zFOYa4TexKA7noBZa3C8fb24rI5sw9Exg==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.3.tgz",
+ "integrity": "sha512-STfKku3QfnuUj6k3g9ld4vwhtgCGYIFQmsGPPgT9MK/dI3Lwnpe5Gs5t1inoUIoGNP8sIOLlBB4HV4MmBjQuhw==",
"cpu": [
"arm64"
],
@@ -2486,9 +2494,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.2.tgz",
- "integrity": "sha512-teU27iG1oyWpNh9CzcGQ48ClDRt/RCem7mYO7ehd2FY102UeTws2+OzLESS1TS1tEZipq/5xwx3FzbVgiolCiQ==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.3.tgz",
+ "integrity": "sha512-bc+CXYlFc1t8pv9yZJGus372ldzOVscBl7encUBlU1m/Sig0+NDJLz6cXXRcFyl6ABNOApWeR4Yl7iUWx6C8og==",
"cpu": [
"arm64"
],
@@ -2504,9 +2512,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.2.tgz",
- "integrity": "sha512-dRPsyPyqpLD0HMRCRpYALIh4kdOir8pPg4AhNQZLehKowigRd30RcLXGNVZcc31Ua8CiPI4QSgjOIxK+EQe4LQ==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.3.tgz",
+ "integrity": "sha512-dFXoa0TEhohrKcxn/54YKs1iwNeW6tUkHJgXW33H381SvjKFUV53WR231jh1sWVJETjA3vsAwxKwR23s7UCmUA==",
"cpu": [
"x64"
],
@@ -2522,9 +2530,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.2.tgz",
- "integrity": "sha512-CCxETW+KkYEQDqz1SYC15YIWYheqFC+PJVOW76Maa/8yu8Biw+HTAcblKf2isrlUtK8RvrQN94v3UXkC2NzCEw==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.3.tgz",
+ "integrity": "sha512-ieyjisLB+ldexiE/yD8uomaZuZIbTc8tjquYln9Quh5ykOBY7LpJJYBWvWtm1g3pHv6AXlBI8Jay7Fffb6aLfA==",
"cpu": [
"x64"
],
@@ -2540,9 +2548,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.2.tgz",
- "integrity": "sha512-Wv/QTA6PjyRLlmKcN6AmSI4jwSMRl0VTLGs57PHTqYRwwfwd7y4s2fIPJVBNbAlXd795dOEP6d/bGSQSyhOX3A==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.3.tgz",
+ "integrity": "sha512-elTQpnaX5vESSbhCEgcwXjpMsnUbqqHfEpB7ewpkAsLzKEXZaK67ihSRYAuAx6ewRQTo7DS5iTT6X5aQD3MzMw==",
"cpu": [
"arm64"
],
@@ -2558,9 +2566,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.2.tgz",
- "integrity": "sha512-PuCdtNynEkUNbUXX/wsyUC+t4mamIU5y00lT5vJcAvco3/r16Iaxl5UCzhXYaWZSNVZMzPp9qN8NlSL8M5pPxw==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.3.tgz",
+ "integrity": "sha512-nvehQVEOdI1BleJpuUgPLrclJ0TzbEMc+MarXDmmiRFwEUGqj+pnfkTSb7RZyS1puU74IXdK/YhTirHurtbI9w==",
"cpu": [
"ia32"
],
@@ -2576,9 +2584,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.2.tgz",
- "integrity": "sha512-qlmMkFZJus8cYuBURx1a3YAG2G7IW44i+FEYV5/32ylKkzGNAr9tDJSA53XNnNXkAB5EXSPsOz7bn5C3JlEtdQ==",
+ "version": "1.13.3",
+ "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.3.tgz",
+ "integrity": "sha512-A+JSKGkRbPLVV2Kwx8TaDAV0yXIXm/gc8m98hSkVDGlPBBmydgzNdWy3X7HTUBM7IDk7YlWE7w2+RUGjdgpTmg==",
"cpu": [
"x64"
],
@@ -2853,12 +2861,12 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
- "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "version": "24.2.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
+ "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"license": "MIT",
"dependencies": {
- "undici-types": "~7.8.0"
+ "undici-types": "~7.10.0"
}
},
"node_modules/@types/node-schedule": {
@@ -2985,17 +2993,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.38.0.tgz",
- "integrity": "sha512-CPoznzpuAnIOl4nhj4tRr4gIPj5AfKgkiJmGQDaq+fQnRJTYlcBjbX3wbciGmpoPf8DREufuPRe1tNMZnGdanA==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz",
+ "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.38.0",
- "@typescript-eslint/type-utils": "8.38.0",
- "@typescript-eslint/utils": "8.38.0",
- "@typescript-eslint/visitor-keys": "8.38.0",
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/type-utils": "8.39.0",
+ "@typescript-eslint/utils": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@@ -3009,9 +3017,9 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.38.0",
+ "@typescript-eslint/parser": "^8.39.0",
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
@@ -3025,16 +3033,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.38.0.tgz",
- "integrity": "sha512-Zhy8HCvBUEfBECzIl1PKqF4p11+d0aUJS1GeUiuqK9WmOug8YCmC4h4bjyBvMyAMI9sbRczmrYL5lKg/YMbrcQ==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz",
+ "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.38.0",
- "@typescript-eslint/types": "8.38.0",
- "@typescript-eslint/typescript-estree": "8.38.0",
- "@typescript-eslint/visitor-keys": "8.38.0",
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4"
},
"engines": {
@@ -3046,18 +3054,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.38.0.tgz",
- "integrity": "sha512-dbK7Jvqcb8c9QfH01YB6pORpqX1mn5gDZc9n63Ak/+jD67oWXn3Gs0M6vddAN+eDXBCS5EmNWzbSxsn9SzFWWg==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz",
+ "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.38.0",
- "@typescript-eslint/types": "^8.38.0",
+ "@typescript-eslint/tsconfig-utils": "^8.39.0",
+ "@typescript-eslint/types": "^8.39.0",
"debug": "^4.3.4"
},
"engines": {
@@ -3068,18 +3076,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.38.0.tgz",
- "integrity": "sha512-WJw3AVlFFcdT9Ri1xs/lg8LwDqgekWXWhH3iAF+1ZM+QPd7oxQ6jvtW/JPwzAScxitILUIFs0/AnQ/UWHzbATQ==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz",
+ "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.38.0",
- "@typescript-eslint/visitor-keys": "8.38.0"
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3090,9 +3098,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.38.0.tgz",
- "integrity": "sha512-Lum9RtSE3EroKk/bYns+sPOodqb2Fv50XOl/gMviMKNvanETUuUcC9ObRbzrJ4VSd2JalPqgSAavwrPiPvnAiQ==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz",
+ "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3103,19 +3111,19 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.38.0.tgz",
- "integrity": "sha512-c7jAvGEZVf0ao2z+nnz8BUaHZD09Agbh+DY7qvBQqLiz8uJzRgVPj5YvOh8I8uEiH8oIUGIfHzMwUcGVco/SJg==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz",
+ "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.38.0",
- "@typescript-eslint/typescript-estree": "8.38.0",
- "@typescript-eslint/utils": "8.38.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0",
+ "@typescript-eslint/utils": "8.39.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@@ -3128,13 +3136,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.38.0.tgz",
- "integrity": "sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz",
+ "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3146,16 +3154,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.38.0.tgz",
- "integrity": "sha512-fooELKcAKzxux6fA6pxOflpNS0jc+nOQEEOipXFNjSlBS6fqrJOVY/whSn70SScHrcJ2LDsxWrneFoWYSVfqhQ==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz",
+ "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.38.0",
- "@typescript-eslint/tsconfig-utils": "8.38.0",
- "@typescript-eslint/types": "8.38.0",
- "@typescript-eslint/visitor-keys": "8.38.0",
+ "@typescript-eslint/project-service": "8.39.0",
+ "@typescript-eslint/tsconfig-utils": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/visitor-keys": "8.39.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -3171,20 +3179,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.38.0.tgz",
- "integrity": "sha512-hHcMA86Hgt+ijJlrD8fX0j1j8w4C92zue/8LOPAFioIno+W0+L7KqE8QZKCcPGc/92Vs9x36w/4MPTJhqXdyvg==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz",
+ "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
- "@typescript-eslint/scope-manager": "8.38.0",
- "@typescript-eslint/types": "8.38.0",
- "@typescript-eslint/typescript-estree": "8.38.0"
+ "@typescript-eslint/scope-manager": "8.39.0",
+ "@typescript-eslint/types": "8.39.0",
+ "@typescript-eslint/typescript-estree": "8.39.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3195,17 +3203,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
- "typescript": ">=4.8.4 <5.9.0"
+ "typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.38.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.38.0.tgz",
- "integrity": "sha512-pWrTcoFNWuwHlA9CvlfSsGWs14JxfN1TH25zM5L7o0pRLhsoZkDnTsXfQRJBEWJoV5DL0jf+Z+sxiud+K0mq1g==",
+ "version": "8.39.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz",
+ "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.38.0",
+ "@typescript-eslint/types": "8.39.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@@ -3890,13 +3898,13 @@
}
},
"node_modules/ast-v8-to-istanbul": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz",
- "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz",
+ "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/trace-mapping": "^0.3.25",
+ "@jridgewell/trace-mapping": "^0.3.29",
"estree-walker": "^3.0.3",
"js-tokens": "^9.0.1"
}
@@ -4176,9 +4184,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001727",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
- "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
+ "version": "1.0.30001731",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
+ "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"dev": true,
"funding": [
{
@@ -4640,9 +4648,9 @@
}
},
"node_modules/core-js-compat": {
- "version": "3.44.0",
- "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz",
- "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==",
+ "version": "3.45.0",
+ "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.0.tgz",
+ "integrity": "sha512-gRoVMBawZg0OnxaVv3zpqLLxaHmsubEGyTnqdpI/CEBvX4JadI1dMSHxagThprYRtSVbuQxvi6iUatdPxohHpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4948,9 +4956,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.190",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz",
- "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==",
+ "version": "1.5.195",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
+ "integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
"dev": true,
"license": "ISC"
},
@@ -5176,9 +5184,9 @@
}
},
"node_modules/eslint": {
- "version": "9.31.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz",
- "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==",
+ "version": "9.32.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz",
+ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -5189,8 +5197,8 @@
"@eslint/config-helpers": "^0.3.0",
"@eslint/core": "^0.15.0",
"@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.31.0",
- "@eslint/plugin-kit": "^0.3.1",
+ "@eslint/js": "9.32.0",
+ "@eslint/plugin-kit": "^0.3.4",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -5270,9 +5278,9 @@
}
},
"node_modules/eslint-flat-config-utils": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/eslint-flat-config-utils/-/eslint-flat-config-utils-2.1.0.tgz",
- "integrity": "sha512-6fjOJ9tS0k28ketkUcQ+kKptB4dBZY2VijMZ9rGn8Cwnn1SH0cZBoPXT8AHBFHxmHcLFQK9zbELDinZ2Mr1rng==",
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-flat-config-utils/-/eslint-flat-config-utils-2.1.1.tgz",
+ "integrity": "sha512-K8eaPkBemHkfbYsZH7z4lZ/tt6gNSsVh535Wh9W9gQBS2WjvfUbbVr2NZR3L1yiRCLuOEimYfPxCxODczD4Opg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5453,9 +5461,9 @@
}
},
"node_modules/eslint-plugin-jsdoc": {
- "version": "51.4.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-51.4.1.tgz",
- "integrity": "sha512-y4CA9OkachG8v5nAtrwvcvjIbdcKgSyS6U//IfQr4FZFFyeBFwZFf/tfSsMr46mWDJgidZjBTqoCRlXywfFBMg==",
+ "version": "52.0.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.2.tgz",
+ "integrity": "sha512-fYrnc7OpRifxxKjH78Y9/D/EouQDYD3G++bpR1Y+A+fy+CMzKZAdGIiHTIxCd2U10hb2y1NxN5TJt9aupq1vmw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5550,9 +5558,9 @@
}
},
"node_modules/eslint-plugin-n": {
- "version": "17.21.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.0.tgz",
- "integrity": "sha512-1+iZ8We4ZlwVMtb/DcHG3y5/bZOdazIpa/4TySo22MLKdwrLcfrX0hbadnCvykSQCCmkAnWmIP8jZVb2AAq29A==",
+ "version": "17.21.3",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.21.3.tgz",
+ "integrity": "sha512-MtxYjDZhMQgsWRm/4xYLL0i2EhusWT7itDxlJ80l1NND2AL2Vi5Mvneqv/ikG9+zpran0VsVRXTEHrpLmUZRNw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5561,8 +5569,8 @@
"eslint-plugin-es-x": "^7.8.0",
"get-tsconfig": "^4.8.1",
"globals": "^15.11.0",
+ "globrex": "^0.1.2",
"ignore": "^5.3.2",
- "minimatch": "^9.0.5",
"semver": "^7.6.3",
"ts-declaration-location": "^1.0.6"
},
@@ -5646,9 +5654,9 @@
}
},
"node_modules/eslint-plugin-regexp": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.9.0.tgz",
- "integrity": "sha512-9WqJMnOq8VlE/cK+YAo9C9YHhkOtcEtEk9d12a+H7OSZFwlpI6stiHmYPGa2VE0QhTzodJyhlyprUaXDZLgHBw==",
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.9.1.tgz",
+ "integrity": "sha512-JwK6glV/aoYDxvXcrvMQbw/pByBewZwqXVSBzzjot3GxSbmjDYuWU4LWiLdBO8JKi4o8A1+rygO6JWRBg4qAQQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5742,9 +5750,9 @@
}
},
"node_modules/eslint-plugin-vue": {
- "version": "10.3.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.3.0.tgz",
- "integrity": "sha512-A0u9snqjCfYaPnqqOaH6MBLVWDUIN4trXn8J3x67uDcXvR7X6Ut8p16N+nYhMCQ9Y7edg2BIRGzfyZsY0IdqoQ==",
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-10.4.0.tgz",
+ "integrity": "sha512-K6tP0dW8FJVZLQxa2S7LcE1lLw3X8VvB3t887Q6CLrFVxHYBXGANbXvwNzYIu6Ughx1bSJ5BDT0YB3ybPT39lw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6335,9 +6343,9 @@
"peer": true
},
"node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -6402,9 +6410,9 @@
"license": "MIT"
},
"node_modules/fs-extra": {
- "version": "11.3.0",
- "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz",
- "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==",
+ "version": "11.3.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz",
+ "integrity": "sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==",
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.0",
@@ -6597,6 +6605,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/globrex": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
+ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -7482,9 +7497,9 @@
}
},
"node_modules/log-symbols/node_modules/chalk": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
+ "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -7523,9 +7538,9 @@
}
},
"node_modules/loupe": {
- "version": "3.1.4",
- "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz",
- "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==",
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.0.tgz",
+ "integrity": "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw==",
"dev": true,
"license": "MIT"
},
@@ -9004,9 +9019,9 @@
}
},
"node_modules/ora/node_modules/chalk": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
+ "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -9838,9 +9853,9 @@
}
},
"node_modules/rollup": {
- "version": "4.45.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.45.1.tgz",
- "integrity": "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw==",
+ "version": "4.46.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.46.2.tgz",
+ "integrity": "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9854,26 +9869,26 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.45.1",
- "@rollup/rollup-android-arm64": "4.45.1",
- "@rollup/rollup-darwin-arm64": "4.45.1",
- "@rollup/rollup-darwin-x64": "4.45.1",
- "@rollup/rollup-freebsd-arm64": "4.45.1",
- "@rollup/rollup-freebsd-x64": "4.45.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.45.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.45.1",
- "@rollup/rollup-linux-arm64-gnu": "4.45.1",
- "@rollup/rollup-linux-arm64-musl": "4.45.1",
- "@rollup/rollup-linux-loongarch64-gnu": "4.45.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.45.1",
- "@rollup/rollup-linux-riscv64-musl": "4.45.1",
- "@rollup/rollup-linux-s390x-gnu": "4.45.1",
- "@rollup/rollup-linux-x64-gnu": "4.45.1",
- "@rollup/rollup-linux-x64-musl": "4.45.1",
- "@rollup/rollup-win32-arm64-msvc": "4.45.1",
- "@rollup/rollup-win32-ia32-msvc": "4.45.1",
- "@rollup/rollup-win32-x64-msvc": "4.45.1",
+ "@rollup/rollup-android-arm-eabi": "4.46.2",
+ "@rollup/rollup-android-arm64": "4.46.2",
+ "@rollup/rollup-darwin-arm64": "4.46.2",
+ "@rollup/rollup-darwin-x64": "4.46.2",
+ "@rollup/rollup-freebsd-arm64": "4.46.2",
+ "@rollup/rollup-freebsd-x64": "4.46.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.46.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.46.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.46.2",
+ "@rollup/rollup-linux-arm64-musl": "4.46.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.46.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.46.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.46.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-gnu": "4.46.2",
+ "@rollup/rollup-linux-x64-musl": "4.46.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.46.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.46.2",
+ "@rollup/rollup-win32-x64-msvc": "4.46.2",
"fsevents": "~2.3.2"
}
},
@@ -10586,9 +10601,9 @@
}
},
"node_modules/strtok3": {
- "version": "10.3.2",
- "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.2.tgz",
- "integrity": "sha512-or9w505RhhY66+uoe5YOC5QO/bRuATaoim3XTh+pGKx5VMWi/HDhMKuCjDLsLJouU2zg9Hf1nLPcNW7IHv80kQ==",
+ "version": "10.3.4",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
+ "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0"
@@ -11028,9 +11043,9 @@
}
},
"node_modules/token-types": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.3.tgz",
- "integrity": "sha512-IKJ6EzuPPWtKtEIEPpIdXv9j5j2LGJEYk0CKY2efgKoYKLBiZdh6iQkLVBow/CB3phyWAWCyk+bZeaimJn6uRQ==",
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.0.4.tgz",
+ "integrity": "sha512-MD9MjpVNhVyH4fyd5rKphjvt/1qj+PtQUz65aFqAZA6XniWAuSFRjLk3e2VALEFlh9OwBpXUN7rfeqSnT/Fmkw==",
"license": "MIT",
"dependencies": {
"@tokenizer/token": "^0.3.0",
@@ -11151,13 +11166,13 @@
}
},
"node_modules/ts-loader/node_modules/source-map": {
- "version": "0.7.4",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
- "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
- "node": ">= 8"
+ "node": ">= 12"
}
},
"node_modules/ts-node": {
@@ -11251,9 +11266,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -11303,9 +11318,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
- "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"license": "MIT"
},
"node_modules/unist-util-is": {
@@ -11502,15 +11517,15 @@
}
},
"node_modules/vite": {
- "version": "7.0.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.5.tgz",
- "integrity": "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
+ "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
- "picomatch": "^4.0.2",
+ "picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.40.0",
"tinyglobby": "^0.2.14"
@@ -11719,9 +11734,9 @@
}
},
"node_modules/webpack": {
- "version": "5.100.2",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.100.2.tgz",
- "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==",
+ "version": "5.101.0",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.0.tgz",
+ "integrity": "sha512-B4t+nJqytPeuZlHuIKTbalhljIFXeNRqrUGAQgTGlfOl2lXXKXw+yZu6bicycP+PUlM44CxBjCFD6aciKFT3LQ==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -12000,9 +12015,9 @@
}
},
"node_modules/yaml": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
- "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"bin": {
diff --git a/package.json b/package.json
index e59a9c2ae..0da72311f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "homebridge-config-ui-x",
"displayName": "Homebridge UI",
- "version": "5.3.0",
+ "version": "5.4.1",
"description": "A web based management, configuration and control platform for Homebridge.",
"author": "oznu ",
"license": "MIT",
@@ -99,7 +99,7 @@
"commander": "14.0.0",
"dayjs": "1.11.13",
"fastify": "5.4.0",
- "fs-extra": "11.3.0",
+ "fs-extra": "11.3.1",
"jsonwebtoken": "9.0.2",
"lodash": "4.17.21",
"node-cache": "5.1.2",
@@ -119,12 +119,12 @@
"unzipper": "0.12.3"
},
"devDependencies": {
- "@antfu/eslint-config": "^4.18.0",
+ "@antfu/eslint-config": "^5.1.0",
"@nestjs/testing": "^11.1.5",
"@prettier/plugin-xml": "^3.4.2",
"@types/fs-extra": "^11.0.4",
"@types/lodash": "^4.17.20",
- "@types/node": "^24.1.0",
+ "@types/node": "^24.2.0",
"@types/node-schedule": "^2.1.8",
"@types/passport-jwt": "^4.0.1",
"@types/semver": "^7.7.0",
@@ -140,7 +140,7 @@
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
- "typescript": "^5.8.3",
+ "typescript": "^5.9.2",
"unplugin-swc": "^1.5.5",
"vitest": "^3.2.4"
},
diff --git a/scripts/lang-sync.ts b/scripts/lang-sync.ts
index 3861df0d5..aa528cd75 100644
--- a/scripts/lang-sync.ts
+++ b/scripts/lang-sync.ts
@@ -13,11 +13,14 @@ import { readdir, readFile, readJson, stat, writeJson } from 'fs-extra'
const projectDir = resolve(dirname(__dirname), 'ui/src')
const ignoreKeys = [
- 'plugins.settings.custom.homebridge-gsh.label_account_linked',
- 'plugins.settings.custom.homebridge-gsh.label_link_account',
- 'plugins.settings.custom.homebridge-gsh.message_about',
- 'plugins.settings.custom.homebridge-gsh.message_account_link_required',
- 'plugins.settings.custom.homebridge-gsh.message_homebridge_restart_required',
+ 'plugins.settings.custom.homebridge-gsh.label_account_linked', // used in google-gsh plugin config
+ 'plugins.settings.custom.homebridge-gsh.label_link_account', // used in google-gsh plugin config
+ 'plugins.settings.custom.homebridge-gsh.message_about', // used in google-gsh plugin config
+ 'plugins.settings.custom.homebridge-gsh.message_account_link_required', // used in google-gsh plugin config
+ 'plugins.settings.custom.homebridge-gsh.message_homebridge_restart_required', // used in google-gsh plugin config
+ 'status.widget.update_node_yes', // used in ui/src/app/modules/status/widgets/update-info-widget/node-version-modal
+ 'status.widget.update_node_no', // used in ui/src/app/modules/status/widgets/update-info-widget/node-version-modal
+ 'status.widget.update_node_unknown', // used in ui/src/app/modules/status/widgets/update-info-widget/node-version-modal
]
async function getAllFiles(dirPath: string, arrayOfFiles: string[] = []): Promise {
diff --git a/src/core/config/config.service.ts b/src/core/config/config.service.ts
index 4680b1ca9..4da8e6851 100644
--- a/src/core/config/config.service.ts
+++ b/src/core/config/config.service.ts
@@ -65,6 +65,11 @@ export class ConfigService {
// package.json
public package = readJsonSync(resolve(process.env.UIX_BASE_PATH, 'package.json'))
+ // Startup settings
+ public hbStartupSettings = pathExistsSync(resolve(this.storagePath, '.uix-hb-service-homebridge-startup.json'))
+ ? readJsonSync(resolve(this.storagePath, '.uix-hb-service-homebridge-startup.json'))
+ : {}
+
// First user setup wizard
public setupWizardComplete = true
@@ -122,6 +127,11 @@ export class ConfigService {
scheduledBackupPath?: string
scheduledBackupDisable?: boolean
disableServerMetricsMonitoring?: boolean
+ terminal?: {
+ persistence?: boolean
+ hideWarning?: boolean
+ bufferSize?: number
+ }
}
private bridgeFreeze: this['homebridgeConfig']['bridge']
@@ -257,6 +267,11 @@ export class ConfigService {
shutdown: this.ui.linux?.shutdown,
restart: this.ui.linux?.restart,
},
+ terminal: {
+ persistence: this.ui.terminal?.persistence,
+ hideWarning: this.ui.terminal?.hideWarning,
+ bufferSize: this.ui.terminal?.bufferSize || globalThis.terminal.bufferSize,
+ },
},
menuMode: this.ui.menuMode || 'default',
wallpaper: this.ui.wallpaper,
@@ -264,6 +279,7 @@ export class ConfigService {
proxyHost: this.ui.proxyHost,
homebridgePackagePath: this.ui.homebridgePackagePath,
disableServerMetricsMonitoring: this.ui.disableServerMetricsMonitoring,
+ keepOrphans: this.hbStartupSettings?.keepOrphans || false,
}
}
diff --git a/src/globalDefaults.ts b/src/globalDefaults.ts
index 88da1a5f5..97c0dad26 100644
--- a/src/globalDefaults.ts
+++ b/src/globalDefaults.ts
@@ -7,3 +7,8 @@ globalThis.backup = {
maxBackupFileSize: 10 * 1024 * 1024,
maxBackupFileSizeText: '10MB',
}
+
+globalThis.terminal = {
+ // Default buffer size for terminal output in bytes
+ bufferSize: 50000,
+}
diff --git a/src/modules/config-editor/config-editor.controller.ts b/src/modules/config-editor/config-editor.controller.ts
index 79a1750de..700722ba7 100644
--- a/src/modules/config-editor/config-editor.controller.ts
+++ b/src/modules/config-editor/config-editor.controller.ts
@@ -126,6 +126,14 @@ export class ConfigEditorController {
return this.configEditorService.getConfigBackup(backupId)
}
+ @UseGuards(AdminGuard)
+ @ApiOperation({ summary: 'Delete the backup file for the given backup ID.' })
+ @ApiParam({ name: 'backupId', type: 'number' })
+ @Delete('/backups/:backupId')
+ deleteBackup(@Param('backupId', ParseIntPipe) backupId) {
+ return this.configEditorService.deleteConfigBackup(backupId)
+ }
+
@UseGuards(AdminGuard)
@ApiOperation({ summary: 'Delete all the Homebridge `config.json` backups.' })
@Delete('/backups')
diff --git a/src/modules/config-editor/config-editor.service.ts b/src/modules/config-editor/config-editor.service.ts
index 0b136770c..2bb4a7b05 100644
--- a/src/modules/config-editor/config-editor.service.ts
+++ b/src/modules/config-editor/config-editor.service.ts
@@ -462,6 +462,22 @@ export class ConfigEditorService {
return await readFile(requestedBackupPath)
}
+ /**
+ * Delete a config backup
+ * @param backupId
+ */
+ public async deleteConfigBackup(backupId: number) {
+ const requestedBackupPath = resolve(this.configService.configBackupPath, `config.json.${backupId}`)
+
+ // Check backup file exists
+ if (!await pathExists(requestedBackupPath)) {
+ throw new NotFoundException(`Backup ${backupId} Not Found`)
+ }
+
+ // Delete the backup file
+ await unlink(resolve(this.configService.configBackupPath, `config.json.${backupId}`))
+ }
+
/**
* Delete all config backups
*/
diff --git a/src/modules/platform-tools/terminal/terminal.controller.ts b/src/modules/platform-tools/terminal/terminal.controller.ts
new file mode 100644
index 000000000..dfc8fdc66
--- /dev/null
+++ b/src/modules/platform-tools/terminal/terminal.controller.ts
@@ -0,0 +1,23 @@
+import { Controller, Get, Post, UseGuards } from '@nestjs/common'
+import { AuthGuard } from '@nestjs/passport'
+
+import { TerminalService } from './terminal.service'
+
+@UseGuards(AuthGuard())
+@Controller('platform-tools/terminal')
+export class TerminalController {
+ constructor(
+ private readonly terminalService: TerminalService,
+ ) {}
+
+ @Get('has-persistent-session')
+ hasPersistentSession() {
+ return { hasPersistentSession: this.terminalService.hasPersistentSession() }
+ }
+
+ @Post('destroy-persistent-session')
+ destroyPersistentSession() {
+ this.terminalService.destroyPersistentSession()
+ return { success: true }
+ }
+}
diff --git a/src/modules/platform-tools/terminal/terminal.gateway.ts b/src/modules/platform-tools/terminal/terminal.gateway.ts
index aafa4631a..a1fed25e5 100644
--- a/src/modules/platform-tools/terminal/terminal.gateway.ts
+++ b/src/modules/platform-tools/terminal/terminal.gateway.ts
@@ -24,4 +24,14 @@ export class TerminalGateway {
startTerminalSession(client: WsEventEmitter, payload: TermSize) {
return this.terminalService.startSession(client, payload)
}
+
+ @SubscribeMessage('destroy-persistent-session')
+ destroyPersistentSession() {
+ return this.terminalService.destroyPersistentSession()
+ }
+
+ @SubscribeMessage('check-persistent-session')
+ checkPersistentSession() {
+ return this.terminalService.hasPersistentSession()
+ }
}
diff --git a/src/modules/platform-tools/terminal/terminal.module.ts b/src/modules/platform-tools/terminal/terminal.module.ts
index 0d1937fc0..5fbec6402 100644
--- a/src/modules/platform-tools/terminal/terminal.module.ts
+++ b/src/modules/platform-tools/terminal/terminal.module.ts
@@ -4,6 +4,7 @@ import { PassportModule } from '@nestjs/passport'
import { ConfigModule } from '../../../core/config/config.module'
import { LoggerModule } from '../../../core/logger/logger.module'
import { NodePtyModule } from '../../../core/node-pty/node-pty.module'
+import { TerminalController } from './terminal.controller'
import { TerminalGateway } from './terminal.gateway'
import { TerminalService } from './terminal.service'
@@ -14,6 +15,9 @@ import { TerminalService } from './terminal.service'
LoggerModule,
NodePtyModule,
],
+ controllers: [
+ TerminalController,
+ ],
providers: [
TerminalService,
TerminalGateway,
diff --git a/src/modules/platform-tools/terminal/terminal.service.ts b/src/modules/platform-tools/terminal/terminal.service.ts
index 75297ed60..3b1aff560 100644
--- a/src/modules/platform-tools/terminal/terminal.service.ts
+++ b/src/modules/platform-tools/terminal/terminal.service.ts
@@ -1,5 +1,7 @@
+import type { IPty } from '@homebridge/node-pty-prebuilt-multiarch'
import type { EventEmitter } from 'node:events'
+import os from 'node:os'
import process from 'node:process'
import { Injectable } from '@nestjs/common'
@@ -17,12 +19,33 @@ export interface TermSize {
@Injectable()
export class TerminalService {
private ending = false
+ private static persistentTerminal: IPty | null = null
+ private static connectedClients: Set = new Set()
+ private static dataListenerAttached = false
+ private static terminalBuffer: string = ''
+ private instanceId: string
constructor(
private configService: ConfigService,
private logger: Logger,
private nodePtyService: NodePtyService,
- ) {}
+ ) {
+ this.instanceId = Math.random().toString(36).substring(2, 11)
+ this.logger.debug(`TerminalService instance created: ${this.instanceId}`)
+ }
+
+ /**
+ * Get the preferred shell for the current platform
+ */
+ private async getPreferredShell(): Promise<'/bin/zsh' | '/bin/bash' | '/bin/sh'> {
+ // On macOS, prefer zsh if available
+ if (os.platform() === 'darwin' && await pathExists('/bin/zsh')) {
+ return '/bin/zsh'
+ }
+
+ // Fallback to bash if available, otherwise sh
+ return await pathExists('/bin/bash') ? '/bin/bash' : '/bin/sh'
+ }
/**
* Create a new terminal session
@@ -34,15 +57,26 @@ export class TerminalService {
// If terminal is not enabled, disconnect the client
if (!this.configService.enableTerminalAccess) {
- this.logger.error('Terminal is not enabled, disconnecting client...')
+ this.logger.warn('Terminal is not enabled, disconnecting client...')
client.disconnect()
return
}
- this.logger.log('Starting terminal session.')
+ // Check if terminal persistence is enabled
+ const terminalPersistence = Boolean(this.configService.ui.terminal?.persistence)
+
+ if (terminalPersistence) {
+ return this.attachToPersistentTerminal(client, size)
+ } else {
+ return this.createNewTerminal(client, size)
+ }
+ }
- // check if we should use bash or sh
- const shell = await pathExists('/bin/bash') ? '/bin/bash' : '/bin/sh'
+ private async createNewTerminal(client: WsEventEmitter, size: TermSize) {
+ this.logger.log('Starting new terminal session.')
+
+ // Get the preferred shell for the current platform
+ const shell = await this.getPreferredShell()
// Spawn a new shell
const term = this.nodePtyService.spawn(shell, [], {
@@ -59,10 +93,10 @@ export class TerminalService {
})
// Let the client know when the session ends
- term.onExit((code) => {
+ term.onExit((exitInfo: { exitCode: number, signal?: number }) => {
try {
if (!this.ending) {
- client.emit('process-exit', code)
+ client.emit('process-exit', exitInfo.exitCode)
}
} catch (e) {
// The client socket probably closed
@@ -99,6 +133,190 @@ export class TerminalService {
client.on('end', onEnd.bind(this))
client.on('disconnect', onEnd.bind(this))
}
+
+ private async attachToPersistentTerminal(client: WsEventEmitter, size: TermSize) {
+ this.logger.debug(`[${this.instanceId}] attachToPersistentTerminal called`)
+
+ // If we don't have a persistent terminal, create one
+ if (!TerminalService.persistentTerminal) {
+ this.logger.debug(`[${this.instanceId}] Creating new persistent terminal session.`)
+
+ const shell = await this.getPreferredShell()
+
+ TerminalService.persistentTerminal = this.nodePtyService.spawn(shell, [], {
+ name: 'xterm-color',
+ cols: size.cols,
+ rows: size.rows,
+ cwd: this.configService.storagePath,
+ env: process.env,
+ })
+
+ // Set up the SINGLE data listener that routes to current client
+ if (!TerminalService.dataListenerAttached) {
+ this.logger.debug(`[${this.instanceId}] Attaching data listener`)
+ TerminalService.persistentTerminal.onData((data) => {
+ try {
+ this.logger.debug(`[${this.instanceId}] Terminal output: ${data.length} characters`)
+
+ // Add to buffer for future clients
+ TerminalService.terminalBuffer += data
+
+ // Keep buffer size reasonable (configurable)
+ const maxBufferSize = this.configService.ui.terminal?.bufferSize
+ if (TerminalService.terminalBuffer.length > maxBufferSize) {
+ TerminalService.terminalBuffer = TerminalService.terminalBuffer.slice(-maxBufferSize)
+ }
+
+ if (TerminalService.connectedClients.size > 0) {
+ this.logger.debug(`[${this.instanceId}] Sending output to ${TerminalService.connectedClients.size} connected clients`)
+ TerminalService.connectedClients.forEach((client) => {
+ try {
+ client.emit('stdout', data)
+ } catch (e) {
+ this.logger.error(`[${this.instanceId}] Error sending output to a client: ${e}`)
+ // Remove client if it's no longer valid
+ TerminalService.connectedClients.delete(client)
+ }
+ })
+ }
+ } catch (e) {
+ this.logger.error(`[${this.instanceId}] Error sending output to client: ${e}`)
+ }
+ })
+ TerminalService.dataListenerAttached = true
+ }
+
+ // Handle terminal exit
+ TerminalService.persistentTerminal.onExit((exitInfo: { exitCode: number, signal?: number }) => {
+ this.logger.debug(`[${this.instanceId}] Persistent terminal exited.`)
+
+ // Notify all connected clients that the process has exited
+ TerminalService.connectedClients.forEach((client) => {
+ try {
+ client.emit('process-exit', exitInfo.exitCode)
+ } catch (e) {
+ // Client socket probably closed, remove it
+ TerminalService.connectedClients.delete(client)
+ }
+ })
+
+ TerminalService.persistentTerminal = null
+ TerminalService.connectedClients.clear()
+ TerminalService.dataListenerAttached = false
+ TerminalService.terminalBuffer = ''
+ })
+ } else {
+ this.logger.debug(`[${this.instanceId}] Attaching to existing persistent terminal.`)
+ // Resize to match current client
+ try {
+ TerminalService.persistentTerminal.resize(size.cols, size.rows)
+ } catch (e) {}
+ }
+
+ // Clean up any existing listeners on this client before adding new ones
+ this.logger.debug(`[${this.instanceId}] Cleaning up existing client listeners`)
+ client.removeAllListeners('stdin')
+ client.removeAllListeners('resize')
+
+ // Add client to connected clients set
+ this.logger.debug(`[${this.instanceId}] Adding client to connected clients`)
+ TerminalService.connectedClients.add(client)
+
+ // Send buffer to new client if this is an existing persistent terminal
+ if (TerminalService.terminalBuffer && TerminalService.terminalBuffer.length > 0) {
+ this.logger.debug(`[${this.instanceId}] Sending ${TerminalService.terminalBuffer.length} chars of buffer to new client`)
+ try {
+ client.emit('stdout', TerminalService.terminalBuffer)
+ } catch (e) {
+ this.logger.error(`[${this.instanceId}] Error sending buffer to client: ${e}`)
+ }
+ } else {
+ this.logger.debug(`[${this.instanceId}] No buffer to send to new client`)
+ }
+
+ // Always add listeners for the new client (each client needs its own listeners)
+ this.logger.debug(`[${this.instanceId}] Adding stdin and resize listeners`)
+
+ client.on('stdin', (data) => {
+ this.logger.debug(`[${this.instanceId}] Received stdin from client: ${data.length} characters`)
+ if (TerminalService.persistentTerminal) {
+ this.logger.debug(`[${this.instanceId}] Writing to persistent terminal: ${data.length} characters`)
+ TerminalService.persistentTerminal.write(data)
+ } else {
+ this.logger.warn(`[${this.instanceId}] No persistent terminal to write to!`)
+ }
+ })
+
+ client.on('resize', (resize: TermSize) => {
+ this.logger.debug(`[${this.instanceId}] Received resize from client`)
+ try {
+ if (TerminalService.persistentTerminal) {
+ TerminalService.persistentTerminal.resize(resize.cols, resize.rows)
+ }
+ } catch (e) {}
+ })
+
+ // Clean up client listeners on disconnect (but keep terminal alive)
+ const onEnd = () => {
+ this.logger.debug(`[${this.instanceId}] Client disconnecting`)
+
+ // Remove all listeners from this specific client
+ client.removeAllListeners('stdin')
+ client.removeAllListeners('resize')
+ client.removeAllListeners('end')
+ client.removeAllListeners('disconnect')
+
+ // Remove client from connected clients set
+ if (TerminalService.connectedClients.has(client)) {
+ TerminalService.connectedClients.delete(client)
+ this.logger.debug(`[${this.instanceId}] Removed client from connected clients`)
+ }
+
+ this.logger.debug(`[${this.instanceId}] Client cleanup complete`)
+ }
+
+ client.on('end', onEnd)
+ client.on('disconnect', onEnd)
+ }
+
+ /**
+ * Check if there's an active persistent terminal session
+ * This is the authoritative source of truth for backend state
+ */
+ hasPersistentSession(): boolean {
+ const hasPersistent = TerminalService.persistentTerminal !== null
+ this.logger.debug(`[${this.instanceId}] hasPersistentSession: ${hasPersistent}`)
+ return hasPersistent
+ }
+
+ /**
+ * Destroy the persistent terminal session completely
+ * This is called when terminal persistence is disabled
+ */
+ destroyPersistentSession() {
+ this.logger.debug(`[${this.instanceId}] Destroying persistent terminal session`)
+
+ if (TerminalService.persistentTerminal) {
+ try {
+ this.logger.debug(`[${this.instanceId}] Killing persistent terminal process`)
+ TerminalService.persistentTerminal.kill()
+ } catch (e) {
+ this.logger.error(`[${this.instanceId}] Error killing persistent terminal: ${e}`)
+ }
+ TerminalService.persistentTerminal = null
+ }
+
+ // Clear the terminal buffer
+ TerminalService.terminalBuffer = ''
+
+ // Clear data listener flag
+ TerminalService.dataListenerAttached = false
+
+ // Clear all connected clients
+ TerminalService.connectedClients.clear()
+
+ this.logger.debug(`[${this.instanceId}] Persistent terminal session destroyed`)
+ }
}
export interface WsEventEmitter extends EventEmitter {
diff --git a/src/modules/server/server.service.ts b/src/modules/server/server.service.ts
index d5d39058a..3c38a1a45 100644
--- a/src/modules/server/server.service.ts
+++ b/src/modules/server/server.service.ts
@@ -238,7 +238,7 @@ export class ServerService {
const persistPath = join(this.configService.storagePath, 'persist')
const devices = (await readdir(persistPath))
- .filter(x => x.match(/AccessoryInfo\.([A-F,a-f0-9]+)\.json/))
+ .filter(x => x.match(/AccessoryInfo\.([A-Fa-f0-9]+)\.json$/))
const configFile = await this.configEditorService.getConfigFile()
diff --git a/src/modules/status/status.service.ts b/src/modules/status/status.service.ts
index 6d1815572..5eb0af4f4 100644
--- a/src/modules/status/status.service.ts
+++ b/src/modules/status/status.service.ts
@@ -433,7 +433,6 @@ export class StatusService {
homebridgeRunningInPackageMode: this.configService.runningInPackageMode,
nodeVersion: process.version,
os: await this.getOsInfo(),
- glibcVersion: this.getGlibcVersion(),
time: time(),
network: await this.getDefaultInterface() || {},
}
@@ -459,15 +458,13 @@ export class StatusService {
try {
const versionList = (await firstValueFrom(this.httpService.get('https://nodejs.org/dist/index.json'))).data
- // Get the newest v18 and v20 in the list
- const latest18 = versionList.filter((x: { version: string }) => x.version.startsWith('v18'))[0]
+ // Get the newest node v22 and v24
const latest22 = versionList.filter((x: { version: string }) => x.version.startsWith('v22'))[0]
const latest24 = versionList.filter((x: { version: string }) => x.version.startsWith('v24'))[0]
let updateAvailable = false
let latestVersion = process.version
let showNodeUnsupportedWarning = false
- let showGlibcUnsupportedWarning = false
/**
* NodeJS Version - Minimum GLIBC Version
@@ -480,40 +477,6 @@ export class StatusService {
// Behaviour depends on the installed version of node
switch (process.version.split('.')[0]) {
- case 'v18': {
- // Currently using v18, but v22 is available
- // If the user is running linux, then check their glibc version
- // If they are running glibc 2.31 or higher, then show the option to update to v22
- // Otherwise we would still want to see if there is a minor/patch update available for v18
- // Otherwise, already show the option for updating to node 22
- if (platform() === 'linux') {
- const glibcVersion = this.getGlibcVersion()
- if (glibcVersion) {
- if (Number.parseFloat(glibcVersion) >= 2.31) {
- // Glibc version is high enough to support v22
- updateAvailable = true
- latestVersion = latest22.version
- } else {
- // Glibc version is too low to support v22
- // Check if there is a new minor/patch version available
- if (gt(latest18.version, process.version)) {
- updateAvailable = true
- latestVersion = latest18.version
- }
-
- // Show the user a warning about the glibc version for upcoming end-of-life Node 18
- if (Number.parseFloat(glibcVersion) < 2.31) {
- showGlibcUnsupportedWarning = true
- }
- }
- }
- } else {
- // Not running linux, so show the option for updating to node 22
- updateAvailable = true
- latestVersion = latest22.version
- }
- break
- }
case 'v20': {
// Currently using v20
// Show the option for updating to node 22
@@ -559,7 +522,6 @@ export class StatusService {
latestVersion,
updateAvailable,
showNodeUnsupportedWarning,
- showGlibcUnsupportedWarning,
installPath: dirname(process.execPath),
npmVersion,
}
@@ -572,7 +534,6 @@ export class StatusService {
latestVersion: process.version,
updateAvailable: false,
showNodeUnsupportedWarning: false,
- showGlibcUnsupportedWarning: false,
}
this.statusCache.set('nodeJsVersion', versionInformation, 3600)
return versionInformation
diff --git a/test/e2e/auth.e2e-spec.ts b/test/e2e/auth.e2e-spec.ts
index 960d88c88..326a85cbd 100644
--- a/test/e2e/auth.e2e-spec.ts
+++ b/test/e2e/auth.e2e-spec.ts
@@ -14,6 +14,8 @@ import { AuthModule } from '../../src/core/auth/auth.module'
import { AuthService } from '../../src/core/auth/auth.service'
import { ConfigService } from '../../src/core/config/config.service'
+import '../../src/globalDefaults'
+
describe('AuthController (e2e)', () => {
let app: NestFastifyApplication
diff --git a/test/e2e/plugin-settings-ui.e2e-spec.ts b/test/e2e/plugin-settings-ui.e2e-spec.ts
index bd658168b..83a9a3844 100644
--- a/test/e2e/plugin-settings-ui.e2e-spec.ts
+++ b/test/e2e/plugin-settings-ui.e2e-spec.ts
@@ -14,6 +14,8 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vites
import { AuthModule } from '../../src/core/auth/auth.module'
import { PluginsSettingsUiModule } from '../../src/modules/custom-plugins/plugins-settings-ui/plugins-settings-ui.module'
+import '../../src/globalDefaults'
+
describe('PluginsSettingsUiController (e2e)', () => {
let app: NestFastifyApplication
let httpService: HttpService
diff --git a/test/e2e/plugins.gateway.e2e-spec.ts b/test/e2e/plugins.gateway.e2e-spec.ts
index bd6914157..86e5da97f 100644
--- a/test/e2e/plugins.gateway.e2e-spec.ts
+++ b/test/e2e/plugins.gateway.e2e-spec.ts
@@ -139,7 +139,7 @@ describe('PluginsGateway (e2e)', () => {
expect(client.emit).toHaveBeenCalledWith('stdout', expect.stringContaining('Operation succeeded!'))
})
- it('ON /plugins/install (sudo)', async () => {
+ it('ON /plugins/install (sudo)', { timeout: 20_000 }, async () => {
// Sudo does not work on windows
if (platform() === 'win32') {
return
diff --git a/ui/angular.json b/ui/angular.json
index 79bf1abe6..a77c7f8d1 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -39,6 +39,7 @@
"@xterm/addon-web-links",
"@xterm/xterm",
"ajv",
+ "ajv-formats",
"ajv-keywords",
"dayjs",
"dragula",
diff --git a/ui/package-lock.json b/ui/package-lock.json
index ab9693fb1..8dead5225 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -10,21 +10,21 @@
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "@angular/animations": "20.1.2",
- "@angular/common": "20.1.2",
- "@angular/compiler": "20.1.2",
- "@angular/core": "20.1.2",
- "@angular/forms": "20.1.2",
- "@angular/localize": "20.1.2",
- "@angular/platform-browser": "20.1.2",
- "@angular/platform-browser-dynamic": "20.1.2",
- "@angular/router": "20.1.2",
+ "@angular/animations": "20.1.4",
+ "@angular/common": "20.1.4",
+ "@angular/compiler": "20.1.4",
+ "@angular/core": "20.1.4",
+ "@angular/forms": "20.1.4",
+ "@angular/localize": "20.1.4",
+ "@angular/platform-browser": "20.1.4",
+ "@angular/platform-browser-dynamic": "20.1.4",
+ "@angular/router": "20.1.4",
"@auth0/angular-jwt": "5.2.0",
"@homebridge/hap-client": "3.1.1",
"@ng-bootstrap/ng-bootstrap": "19.0.1",
"@ng-formworks/bootstrap5": "19.5.8",
"@ng-formworks/core": "19.5.8",
- "@ngx-translate/core": "16.0.4",
+ "@ngx-translate/core": "17.0.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/addon-web-links": "0.11.0",
"@xterm/xterm": "5.5.0",
@@ -55,22 +55,22 @@
"zone.js": "0.15.1"
},
"devDependencies": {
- "@angular/build": "^20.1.2",
- "@angular/cli": "^20.1.2",
- "@angular/compiler-cli": "^20.1.2",
- "@angular/language-service": "^20.1.2",
- "@fortawesome/fontawesome-free": "^7.0.0",
+ "@angular/build": "^20.1.4",
+ "@angular/cli": "^20.1.4",
+ "@angular/compiler-cli": "^20.1.4",
+ "@angular/language-service": "^20.1.4",
+ "@fortawesome/fontawesome-free": "^6.7.2",
"@homebridge/plugin-ui-utils": "^2.1.0",
"@types/emoji-js": "^3.5.2",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
- "@types/node": "^24.1.0",
+ "@types/node": "^24.2.0",
"@types/qrcode": "^1.5.5",
"@types/semver": "^7.7.0",
"he": "^1.2.0",
"patch-package": "^8.0.0",
"ts-node": "^10.9.2",
- "typescript": "^5.8.3"
+ "typescript": "^5.9.2"
}
},
"node_modules/@algolia/client-abtesting": {
@@ -280,13 +280,13 @@
}
},
"node_modules/@angular-devkit/architect": {
- "version": "0.2001.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2001.2.tgz",
- "integrity": "sha512-n6F9VMJXbesgzV4aQEhqoT83irJw+RBbo/V6F8uHilDF3bC4jHBgFhcLkajNAg6i3gLcQb6BpResO7vqQ5MsaQ==",
+ "version": "0.2001.4",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2001.4.tgz",
+ "integrity": "sha512-lZ9wYv1YDcw2Ggi2/TXXhYs7JAukAJHdZGZn6Co5s1QE774bVled1qK8pf46rSsG1BGn1a9VFsRFOlB/sx6WjA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@angular-devkit/core": "20.1.2",
+ "@angular-devkit/core": "20.1.4",
"rxjs": "7.8.2"
},
"engines": {
@@ -296,9 +296,9 @@
}
},
"node_modules/@angular-devkit/core": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.1.2.tgz",
- "integrity": "sha512-GBZoc5VxgY0xnXVwC715ubcWpVKc2m1H63Nv/msw5mmnfkjgOyG2lo4vA5VzLYVvptc8hwUhX9rsLN/C340rDg==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.1.4.tgz",
+ "integrity": "sha512-I5CllQoDrVL20/+0JZk/gmR14n/+mwYIoD1RfBDwnaiHlO9o2whRsJj+LeUd9IA5Hf9MPPx+EkOVQt3vsYU0sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -324,13 +324,13 @@
}
},
"node_modules/@angular-devkit/schematics": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.1.2.tgz",
- "integrity": "sha512-5iKTHUCMatg3G67ylLWwL4wJgZHqDuTdhYYvQMdzOACJvbMBPBpDpYhugCBZlvrkBDcT22orytry8m0oxQpAVA==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.1.4.tgz",
+ "integrity": "sha512-dyvlQcXf5XKPRC1qTqzIGkltFHh8mYujPk6qt6Ah2nKp7UeA80ZSAocwOmlBg8t7GjN8ICe4Kese5scT1ByFXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@angular-devkit/core": "20.1.2",
+ "@angular-devkit/core": "20.1.4",
"jsonc-parser": "3.3.1",
"magic-string": "0.30.17",
"ora": "8.2.0",
@@ -343,9 +343,9 @@
}
},
"node_modules/@angular/animations": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.1.2.tgz",
- "integrity": "sha512-r1JnNXZEg2Rrz53Mr4D4/S7v6ozZ3FPzJJo38lDq2WJKSkKc09R9fjFWIB/rXwEXUuiWEfNfxx+O4g6rrbXWWA==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.1.4.tgz",
+ "integrity": "sha512-y4mq2r6jhAj5QuA3UnWkVfok0EcA22uH+XVb4HBKY7q23/xaQYu2CGdVOVpdUsaPTf3zRD1DkAnTkV3J3ZHIiA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -354,19 +354,19 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/common": "20.1.2",
- "@angular/core": "20.1.2"
+ "@angular/common": "20.1.4",
+ "@angular/core": "20.1.4"
}
},
"node_modules/@angular/build": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.1.2.tgz",
- "integrity": "sha512-QCzXl/+nnlU7e6hTqWK5dkeUbZWAy/n5trbkIzBLiVQj6j1iTDoF3ABkS76jn5LUKB0Fx1AJVCSAqdxHqMHjDQ==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.1.4.tgz",
+ "integrity": "sha512-DClI15kl0t1YijptthQfw0cRSj8Opf8ACsZa1xT3o77BALpeusxS2QzSy6xGH+QnwesTyJFux1oRYjtAKmE2YA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "2.3.0",
- "@angular-devkit/architect": "0.2001.2",
+ "@angular-devkit/architect": "0.2001.4",
"@babel/core": "7.27.7",
"@babel/helper-annotate-as-pure": "7.27.3",
"@babel/helper-split-export-declaration": "7.24.7",
@@ -389,7 +389,7 @@
"semver": "7.7.2",
"source-map-support": "0.5.21",
"tinyglobby": "0.2.14",
- "vite": "7.0.0",
+ "vite": "7.0.6",
"watchpack": "2.4.4"
},
"engines": {
@@ -408,7 +408,7 @@
"@angular/platform-browser": "^20.0.0",
"@angular/platform-server": "^20.0.0",
"@angular/service-worker": "^20.0.0",
- "@angular/ssr": "^20.1.2",
+ "@angular/ssr": "^20.1.4",
"karma": "^6.4.0",
"less": "^4.2.0",
"ng-packagr": "^20.0.0",
@@ -458,9 +458,9 @@
}
},
"node_modules/@angular/cdk": {
- "version": "20.1.3",
- "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.1.3.tgz",
- "integrity": "sha512-TO/OBOPWIDJe+0g4S+ye6hewnWOhgWGa4iygvAlmQ77nyqhioHT60puyaDZRATxKh9k6KVmg9cPAk1lYbOFvaA==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.1.4.tgz",
+ "integrity": "sha512-Uz0fLZRWpKG7xniXSw3Hr4QEvTlVurov07BBz6nRWseGxeHCDkFqKc3UEriovCQ7ylJdR6miIu7j+h4PWLH48g==",
"license": "MIT",
"peer": true,
"dependencies": {
@@ -474,19 +474,19 @@
}
},
"node_modules/@angular/cli": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.1.2.tgz",
- "integrity": "sha512-DQQvL/hxVsYPGfiV8AQjqLwQ26F0X16efQZNtxdkSHoiL/EhljXoLK7CMZALg3cfks+kcuzR/cptpiby0Q3j/g==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.1.4.tgz",
+ "integrity": "sha512-VAQ/EBelBPiX1vV57TZJRPcao/e+Ee9IeLK43fsE2xL+GuEjrJ/fQXqt7OesrgIJHJBwUiX+j8pMMT6VfT1xSA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@angular-devkit/architect": "0.2001.2",
- "@angular-devkit/core": "20.1.2",
- "@angular-devkit/schematics": "20.1.2",
+ "@angular-devkit/architect": "0.2001.4",
+ "@angular-devkit/core": "20.1.4",
+ "@angular-devkit/schematics": "20.1.4",
"@inquirer/prompts": "7.6.0",
"@listr2/prompt-adapter-inquirer": "2.0.22",
"@modelcontextprotocol/sdk": "1.13.3",
- "@schematics/angular": "20.1.2",
+ "@schematics/angular": "20.1.4",
"@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.32.0",
"ini": "5.0.0",
@@ -510,9 +510,9 @@
}
},
"node_modules/@angular/common": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.1.2.tgz",
- "integrity": "sha512-MQYP+4lvw81jBRknNYgIye7N36SD68SADUB7xO+7pF5+KbGundfmZkO29uWCnTBU86C4xU4DshlFVhzFK1lreQ==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.1.4.tgz",
+ "integrity": "sha512-AL+HdsY5xL2iM1zZ55ce33U+w2LgPJZQwKvHXJJ/Hpk3rpFNamWtRPmJBeq8Z0dQV1lLTMM+2pUatH6p+5pvEg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -521,14 +521,14 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/core": "20.1.2",
+ "@angular/core": "20.1.4",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/compiler": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.1.2.tgz",
- "integrity": "sha512-BCYQArXAknOyMB5rgx9yK3p5uYFhgN91Jxo5Fbuso6M+7p1PoxOE4E9XrqQfhpVJOl9hcz7vNFnQ4Oer0R83UQ==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.1.4.tgz",
+ "integrity": "sha512-gQbchh2ziK9QxZuHgEf7BUMCm/ayu6Zr9hst6itSecinUJgUeeSp3Z4vXjIBNBUKMPB135tWw9RGiVbW8saBmg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -538,9 +538,9 @@
}
},
"node_modules/@angular/compiler-cli": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.1.2.tgz",
- "integrity": "sha512-NMSDavN+CJYvSze6wq7DpbrUA/EqiAD7GQoeJtuOknzUpPlWQmFOoHzTMKW+S34XlNEw+YQT0trv3DKcrE+T/w==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.1.4.tgz",
+ "integrity": "sha512-I603/3EmclgX4VUryBo3bxlF+8+fVucrW/V0leqNlt72ppFTphDiKiopogoJFWJxuULTo2V+7Koq8Em7kUO67Q==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.0",
@@ -560,7 +560,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/compiler": "20.1.2",
+ "@angular/compiler": "20.1.4",
"typescript": ">=5.8 <5.9"
},
"peerDependenciesMeta": {
@@ -615,9 +615,9 @@
}
},
"node_modules/@angular/core": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.1.2.tgz",
- "integrity": "sha512-8jAvpkHoXHSH0HoqNVgPstSMGmC0oaYN93HW7K2rMRxj1Uhtahkeb/7/kfnj7yLi5FDfm98ofOFT4Lxzf2eZXQ==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.1.4.tgz",
+ "integrity": "sha512-aWDux64a9usuVU2SnF0epqjXAj8JO8jViUzZAJAuFKSCtkeNzqP+Z6DjkqsCKrNvGP7xkX1XhhepUygxgh7/6A==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -626,7 +626,7 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/compiler": "20.1.2",
+ "@angular/compiler": "20.1.4",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0"
},
@@ -640,9 +640,9 @@
}
},
"node_modules/@angular/forms": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.1.2.tgz",
- "integrity": "sha512-ziOaeN0by1cTCNzwCo/IC2ekFzrM7ehc8uQHMQ6dYprSX45lJmdCsNnn+R0lx68VugvbMhHHO5ieOORf5sEmew==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.1.4.tgz",
+ "integrity": "sha512-5gUwcV+JpzJ2rSPo1nR6iNz2Dm3iRcVCvRTsVnKhFbZCIbGLihLpoCuittsgUY/C9wh/rnmXlatmLJ7giSuUZA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -651,16 +651,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/common": "20.1.2",
- "@angular/core": "20.1.2",
- "@angular/platform-browser": "20.1.2",
+ "@angular/common": "20.1.4",
+ "@angular/core": "20.1.4",
+ "@angular/platform-browser": "20.1.4",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/language-service": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.1.2.tgz",
- "integrity": "sha512-qeRrSJCfSZ5K01x+5bQntHPZOrXJOy17bLTtJdQ7iu5PBYh2hFwMipWOGp6SPeYgZ8yl88vmZI+YkLOEsNNniw==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-20.1.4.tgz",
+ "integrity": "sha512-uesg1dNjHkORfYWEXJwfPUyYVEUf5Bb8taxt1AwgYx+NxKKWaNdJQlJu6sAwmPSFlWYMX44Dzk/geLHAq++Nhg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -668,9 +668,9 @@
}
},
"node_modules/@angular/localize": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.1.2.tgz",
- "integrity": "sha512-stlG9ZmB71nBCDqu+9R3syNf/+Hny6/WlNL6whBB3hPN+LOXwb1LDHnu+YaIOoAoQ19ufNofYvGEsWWq6j4vEw==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.1.4.tgz",
+ "integrity": "sha512-yDkQef11JBkVIRiaDA2Iq/GYcu0OK4NMun2r56jTW/Kq+LnKn5q/6usWcN5rbvg7kQpc1ZOxwDGMACiyIYWHmQ==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.0",
@@ -687,8 +687,8 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/compiler": "20.1.2",
- "@angular/compiler-cli": "20.1.2"
+ "@angular/compiler": "20.1.4",
+ "@angular/compiler-cli": "20.1.4"
}
},
"node_modules/@angular/localize/node_modules/@babel/core": {
@@ -737,9 +737,9 @@
}
},
"node_modules/@angular/platform-browser": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.1.2.tgz",
- "integrity": "sha512-jsgO4atyh6T3Rt+idHI29ENaq1a4VKfvtTgWf1S0qSCsfMt2kv5AAO+LkL6lYx8TtJu5zjAETiUwSiWUqY1jOg==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.1.4.tgz",
+ "integrity": "sha512-z86NsGSwm5pXCACdWBbp7SC1Xn+UGvuoRqTsi0dNUXT/3WrP6MvZT3TfNKwM63GLUqFAICSt7uFXS84D72ukvA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -748,9 +748,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/animations": "20.1.2",
- "@angular/common": "20.1.2",
- "@angular/core": "20.1.2"
+ "@angular/animations": "20.1.4",
+ "@angular/common": "20.1.4",
+ "@angular/core": "20.1.4"
},
"peerDependenciesMeta": {
"@angular/animations": {
@@ -759,9 +759,9 @@
}
},
"node_modules/@angular/platform-browser-dynamic": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.1.2.tgz",
- "integrity": "sha512-KssXr0nDZxNjJChdyNFE1wFGaR374qEKBU6mburr2dTauV+jfaL7NrBRzQuTh7GfOOwHnW0uJ4b2dGK6m1tkNw==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.1.4.tgz",
+ "integrity": "sha512-bH4CjZ2O2oqRaKd36Xe/EhZDHx769pPf9oR4oITsZJ10bIhkWcaG9pgaW+W1PGc+nMevVpJ7XfG9m9n6+3bEfw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -770,16 +770,16 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/common": "20.1.2",
- "@angular/compiler": "20.1.2",
- "@angular/core": "20.1.2",
- "@angular/platform-browser": "20.1.2"
+ "@angular/common": "20.1.4",
+ "@angular/compiler": "20.1.4",
+ "@angular/core": "20.1.4",
+ "@angular/platform-browser": "20.1.4"
}
},
"node_modules/@angular/router": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.1.2.tgz",
- "integrity": "sha512-xMRDARfSgwDZSorrTMtv9Gdb9UtWflwn8LOgmPbj3waXyuGWUbgpoJCD0Mh6necc9fhQ60GbBRG5K2EVVr3ATQ==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.1.4.tgz",
+ "integrity": "sha512-Etd2V2Qw+clQhJORBm7tMphCCweLNKbZvUc+lh1r7yrbBPnZvK3yd69W9ZQoRzrSSI25VGQDyzQXgpLUlHoE+w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -788,9 +788,9 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
},
"peerDependencies": {
- "@angular/common": "20.1.2",
- "@angular/core": "20.1.2",
- "@angular/platform-browser": "20.1.2",
+ "@angular/common": "20.1.4",
+ "@angular/core": "20.1.4",
+ "@angular/platform-browser": "20.1.4",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@@ -1008,13 +1008,13 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.27.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz",
- "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==",
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz",
+ "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==",
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
- "@babel/types": "^7.27.6"
+ "@babel/types": "^7.28.2"
},
"engines": {
"node": ">=6.9.0"
@@ -1068,9 +1068,9 @@
}
},
"node_modules/@babel/types": {
- "version": "7.28.1",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz",
- "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==",
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1530,9 +1530,9 @@
}
},
"node_modules/@fortawesome/fontawesome-free": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.0.0.tgz",
- "integrity": "sha512-X48nISrSOa89zu2VMljC4XaRf8NmgTwQBVHfS2Nu5G00ZwM31oOVrAtGxZF3b6wDYf9lJsf/Eq4cCSFKIkOWPQ==",
+ "version": "6.7.2",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz",
+ "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==",
"dev": true,
"license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)",
"engines": {
@@ -1811,9 +1811,9 @@
}
},
"node_modules/@inquirer/search": {
- "version": "3.0.17",
- "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.17.tgz",
- "integrity": "sha512-CuBU4BAGFqRYors4TNCYzy9X3DpKtgIW4Boi0WNkm4Ei1hvY9acxKdBdyqzqBCEe4YxSdaQQsasJlFlUJNgojw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.1.0.tgz",
+ "integrity": "sha512-PMk1+O/WBcYJDq2H7foV0aAZSmDdkzZB9Mw2v/DmONRJopwA/128cS9M/TXWLKKdEQKZnKwBzqu2G4x/2Nqx8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1877,6 +1877,29 @@
}
}
},
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@isaacs/balanced-match": "^4.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -2650,9 +2673,9 @@
}
},
"node_modules/@ngx-translate/core": {
- "version": "16.0.4",
- "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-16.0.4.tgz",
- "integrity": "sha512-s8llTL2SJvROhqttxvEs7Cg+6qSf4kvZPFYO+cTOY1d8DWTjlutRkWAleZcPPoeX927Dm7ALfL07G7oYDJ7z6w==",
+ "version": "17.0.0",
+ "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-17.0.0.tgz",
+ "integrity": "sha512-Rft2D5ns2pq4orLZjEtx1uhNuEBerUdpFUG1IcqtGuipj6SavgB8SkxtNQALNDA+EVlvsNCCjC2ewZVtUeN6rg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@@ -3526,14 +3549,14 @@
]
},
"node_modules/@schematics/angular": {
- "version": "20.1.2",
- "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.1.2.tgz",
- "integrity": "sha512-8Ea+82NK6iylxwC0KDMaAQGHNWGIOnmG7s3JzCqf9m05HWcga6K1jy98kYN/WHBOuoUwzHVpLno/OLM+bbODSw==",
+ "version": "20.1.4",
+ "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.1.4.tgz",
+ "integrity": "sha512-TNpm15NKf4buxPYnGaB3JY2B/3sbL19SdlpPDxkgyVY8WDDeZX95m3Tz2qlKpsYxy2XCGUj4Sxh7zJNGC9e/4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@angular-devkit/core": "20.1.2",
- "@angular-devkit/schematics": "20.1.2",
+ "@angular-devkit/core": "20.1.4",
+ "@angular-devkit/schematics": "20.1.4",
"jsonc-parser": "3.3.1"
},
"engines": {
@@ -3713,12 +3736,12 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.20.7",
- "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz",
- "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.20.7"
+ "@babel/types": "^7.28.2"
}
},
"node_modules/@types/dragula": {
@@ -3767,13 +3790,13 @@
}
},
"node_modules/@types/node": {
- "version": "24.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz",
- "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==",
+ "version": "24.2.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz",
+ "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.8.0"
+ "undici-types": "~7.10.0"
}
},
"node_modules/@types/qrcode": {
@@ -4362,9 +4385,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001727",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
- "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
+ "version": "1.0.30001731",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz",
+ "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==",
"funding": [
{
"type": "opencollective",
@@ -4382,9 +4405,9 @@
"license": "CC-BY-4.0"
},
"node_modules/chalk": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz",
+ "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4938,9 +4961,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.190",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.190.tgz",
- "integrity": "sha512-k4McmnB2091YIsdCgkS0fMVMPOJgxl93ltFzaryXqwip1AaxeDqKCGLxkXODDA5Ab/D+tV5EL5+aTx76RvLRxw==",
+ "version": "1.5.195",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.195.tgz",
+ "integrity": "sha512-URclP0iIaDUzqcAyV1v2PgduJ9N0IdXmWsnPzPfelvBmjmZzEy6xJcjb1cXj+TbYqXgtLrjHEoaSIdTYhw4ezg==",
"license": "ISC"
},
"node_modules/emoji-datasource": {
@@ -5407,9 +5430,9 @@
}
},
"node_modules/follow-redirects": {
- "version": "1.15.9",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
- "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
@@ -5875,16 +5898,32 @@
}
},
"node_modules/ignore-walk": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz",
- "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==",
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-8.0.0.tgz",
+ "integrity": "sha512-FCeMZT4NiRQGh+YkeKMtWrOmBgWjHjMJ26WQWrRQyoyzqevdaGSakUaJW5xQYmjLlUVk2qUnCjYVBax9EKKg8A==",
"dev": true,
"license": "ISC",
"dependencies": {
- "minimatch": "^9.0.0"
+ "minimatch": "^10.0.3"
},
"engines": {
- "node": "^18.17.0 || >=20.5.0"
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/ignore-walk/node_modules/minimatch": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/immutable": {
@@ -7066,9 +7105,9 @@
"optional": true
},
"node_modules/node-gyp": {
- "version": "11.2.0",
- "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz",
- "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==",
+ "version": "11.3.0",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.3.0.tgz",
+ "integrity": "sha512-9J0+C+2nt3WFuui/mC46z2XCZ21/cKlFDuywULmseD/LlmnOrSeEAE4c/1jw6aybXLmpZnQY3/LmOJfgyHIcng==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7268,13 +7307,13 @@
}
},
"node_modules/npm-packlist": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.0.tgz",
- "integrity": "sha512-rht9U6nS8WOBDc53eipZNPo5qkAV4X2rhKE2Oj1DYUQ3DieXfj0mKkVmjnf3iuNdtMd8WfLdi2L6ASkD/8a+Kg==",
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.1.tgz",
+ "integrity": "sha512-vaC03b2PqJA6QqmwHi1jNU8fAPXEnnyv4j/W4PVfgm24C4/zZGSVut3z0YUeN0WIFCo1oGOL02+6LbvFK7JL4Q==",
"dev": true,
"license": "ISC",
"dependencies": {
- "ignore-walk": "^7.0.0"
+ "ignore-walk": "^8.0.0"
},
"engines": {
"node": "^20.17.0 || >=22.9.0"
@@ -9255,9 +9294,9 @@
}
},
"node_modules/typescript": {
- "version": "5.8.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
- "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
+ "version": "5.9.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
@@ -9269,9 +9308,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
- "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true,
"license": "MIT"
},
@@ -9413,15 +9452,15 @@
}
},
"node_modules/vite": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.0.tgz",
- "integrity": "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g==",
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.6.tgz",
+ "integrity": "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.6",
- "picomatch": "^4.0.2",
+ "picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.40.0",
"tinyglobby": "^0.2.14"
@@ -9487,6 +9526,19 @@
}
}
},
+ "node_modules/vite/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/watchpack": {
"version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
@@ -9752,9 +9804,9 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
- "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"bin": {
diff --git a/ui/package.json b/ui/package.json
index df75b8867..008964581 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -12,21 +12,21 @@
"postinstall": "patch-package"
},
"dependencies": {
- "@angular/animations": "20.1.2",
- "@angular/common": "20.1.2",
- "@angular/compiler": "20.1.2",
- "@angular/core": "20.1.2",
- "@angular/forms": "20.1.2",
- "@angular/localize": "20.1.2",
- "@angular/platform-browser": "20.1.2",
- "@angular/platform-browser-dynamic": "20.1.2",
- "@angular/router": "20.1.2",
+ "@angular/animations": "20.1.4",
+ "@angular/common": "20.1.4",
+ "@angular/compiler": "20.1.4",
+ "@angular/core": "20.1.4",
+ "@angular/forms": "20.1.4",
+ "@angular/localize": "20.1.4",
+ "@angular/platform-browser": "20.1.4",
+ "@angular/platform-browser-dynamic": "20.1.4",
+ "@angular/router": "20.1.4",
"@auth0/angular-jwt": "5.2.0",
"@homebridge/hap-client": "3.1.1",
"@ng-bootstrap/ng-bootstrap": "19.0.1",
"@ng-formworks/bootstrap5": "19.5.8",
"@ng-formworks/core": "19.5.8",
- "@ngx-translate/core": "16.0.4",
+ "@ngx-translate/core": "17.0.0",
"@xterm/addon-fit": "0.10.0",
"@xterm/addon-web-links": "0.11.0",
"@xterm/xterm": "5.5.0",
@@ -57,26 +57,29 @@
"zone.js": "0.15.1"
},
"devDependencies": {
- "@angular/build": "^20.1.2",
- "@angular/cli": "^20.1.2",
- "@angular/compiler-cli": "^20.1.2",
- "@angular/language-service": "^20.1.2",
- "@fortawesome/fontawesome-free": "^7.0.0",
+ "@angular/build": "^20.1.4",
+ "@angular/cli": "^20.1.4",
+ "@angular/compiler-cli": "^20.1.4",
+ "@angular/language-service": "^20.1.4",
+ "@fortawesome/fontawesome-free": "^6.7.2",
"@homebridge/plugin-ui-utils": "^2.1.0",
"@types/emoji-js": "^3.5.2",
"@types/file-saver": "^2.0.7",
"@types/lodash-es": "^4.17.12",
- "@types/node": "^24.1.0",
+ "@types/node": "^24.2.0",
"@types/qrcode": "^1.5.5",
"@types/semver": "^7.7.0",
"he": "^1.2.0",
"patch-package": "^8.0.0",
"ts-node": "^10.9.2",
- "typescript": "^5.8.3"
+ "typescript": "^5.9.2"
},
"overrides": {
- "@angular/animations": "20.1.2",
- "@angular/common": "20.1.2",
- "@angular/core": "20.1.2"
+ "@angular/animations": "20.1.4",
+ "@angular/build": {
+ "typescript": "^5.9.2"
+ },
+ "@angular/common": "20.1.4",
+ "@angular/core": "20.1.4"
}
}
diff --git a/ui/patches/@ng-formworks+core+19.5.8.patch b/ui/patches/@ng-formworks+core+19.5.8.patch
index dd4929335..a568c9500 100644
--- a/ui/patches/@ng-formworks+core+19.5.8.patch
+++ b/ui/patches/@ng-formworks+core+19.5.8.patch
@@ -7,7 +7,7 @@ index 752fe83..af0512a 100644
import { SortablejsModule } from 'nxt-sortablejs';
import { HttpClient } from '@angular/common/http';
+import ajvKeywords from 'ajv-keywords'
-
+
class Framework {
constructor() {
@@ -6392,7 +6393,7 @@ class JsonSchemaFormService {
@@ -33,7 +33,7 @@ index 752fe83..af0512a 100644
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "19.2.6", type: CheckboxComponent, isStandalone: false, selector: "checkbox-widget", inputs: { layoutNode: { classPropertyName: "layoutNode", publicName: "layoutNode", isSignal: true, isRequired: false, transformFunction: null }, layoutIndex: { classPropertyName: "layoutIndex", publicName: "layoutIndex", isSignal: true, isRequired: false, transformFunction: null }, dataIndex: { classPropertyName: "dataIndex", publicName: "dataIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
- `, standalone: false, styles: [".legend{font-weight:700}.expandable>legend:before,.expandable>label:before{content:\"\\25b6\";padding-right:.3em;font-family:auto}.expanded>legend:before,.expanded>label:before{content:\"\\25bc\";padding-right:.2em}\n"] }]
-+ `, standalone: false, styles: [".legend{font-weight:700}.expandable>legend,.expandable>label,.expanded>legend,.expanded>label{cursor:pointer;}.expandable>legend:before,.expandable>label:before{font-weight:900;font-family:'Font Awesome 5 Free';content:'\\f054';padding-right:.3em;}.expanded>legend:before,.expanded>label:before{font-weight:900;font-family:'Font Awesome 5 Free';content:'\\f078';padding-right:.2em;}\n"] }]
++ `, standalone: false, styles: [".legend{font-weight:700}.expandable>legend,.expandable>label,.expanded>legend,.expanded>label{cursor:pointer;}.expandable>legend:before,.expandable>label:before{font-weight:900;font-family:'Font Awesome 6 Free';content:'\\f054';padding-right:.3em;}.expanded>legend:before,.expanded>label:before{font-weight:900;font-family:'Font Awesome 6 Free';content:'\\f078';padding-right:.2em;}\n"] }]
}] });
-
+
class SelectComponent {
@@ -8615,6 +8625,16 @@ class SelectComponent {
this.options = this.layoutNode().options || {};
diff --git a/ui/patches/ngx-monaco-editor-v2+20.0.0.patch b/ui/patches/ngx-monaco-editor-v2+20.0.0.patch
new file mode 100644
index 000000000..260a42bbb
--- /dev/null
+++ b/ui/patches/ngx-monaco-editor-v2+20.0.0.patch
@@ -0,0 +1,13 @@
+diff --git a/node_modules/ngx-monaco-editor-v2/fesm2022/ngx-monaco-editor-v2.mjs b/node_modules/ngx-monaco-editor-v2/fesm2022/ngx-monaco-editor-v2.mjs
+index c1c185b..7705573 100644
+--- a/node_modules/ngx-monaco-editor-v2/fesm2022/ngx-monaco-editor-v2.mjs
++++ b/node_modules/ngx-monaco-editor-v2/fesm2022/ngx-monaco-editor-v2.mjs
+@@ -37,7 +37,7 @@ class BaseEditor {
+ let baseUrl = this.config.baseUrl;
+ // ensure backward compatibility
+ if (baseUrl === "assets" || !baseUrl) {
+- baseUrl = "./assets/monaco/min/vs";
++ baseUrl = "./assets/monaco-0.21.3/min/vs";
+ }
+ if (typeof (window.monaco) === 'object') {
+ this.initMonaco(this._options, this.insideNg);
diff --git a/ui/src/app/app-routing.module.ts b/ui/src/app/app-routing.module.ts
index f81a445d1..1abd2d084 100644
--- a/ui/src/app/app-routing.module.ts
+++ b/ui/src/app/app-routing.module.ts
@@ -30,6 +30,7 @@ const routes: Routes = [
{
path: '',
loadComponent: () => import('@/app/modules/status/status.component').then(m => m.StatusComponent),
+ canDeactivate: [(component: any) => component.canDeactivate ? component.canDeactivate() : true],
},
{
path: 'restart',
diff --git a/ui/src/app/app.component.ts b/ui/src/app/app.component.ts
index 676155525..a50b25360 100644
--- a/ui/src/app/app.component.ts
+++ b/ui/src/app/app.component.ts
@@ -62,7 +62,7 @@ export class AppComponent {
// Watch for lang changes
this.$translate.onLangChange.subscribe(() => {
- this.$settings.rtl = rtlLanguages.includes(this.$translate.currentLang)
+ this.$settings.rtl = rtlLanguages.includes(this.$translate.getCurrentLang())
})
const browserLang = languages.find(x => x === this.$translate.getBrowserLang() || x === this.$translate.getBrowserCultureLang())
@@ -75,7 +75,7 @@ export class AppComponent {
if (browserLang) {
this.$translate.use(browserLang)
} else {
- this.$translate.setDefaultLang('en')
+ this.$translate.setFallbackLang('en')
}
}
}
diff --git a/ui/src/app/core/accessories/accessory-info/accessory-info.component.html b/ui/src/app/core/accessories/accessory-info/accessory-info.component.html
index e8eb95e4e..893f09958 100644
--- a/ui/src/app/core/accessories/accessory-info/accessory-info.component.html
+++ b/ui/src/app/core/accessories/accessory-info/accessory-info.component.html
@@ -86,8 +86,41 @@ {{ 'accessories.service_info' | translate }}
@for (characteristic of service.serviceCharacteristics; track characteristic) {
- {{ characteristic.description }}
-
+
+ @if ('minStep' in characteristic || 'minValue' in characteristic || 'maxValue' in characteristic ||
+ 'validValues' in characteristic) {
+ {{ characteristic.description }}
+ } @else { {{ characteristic.description }} } @if (isDetailsVisible[characteristic.uuid]) {
+
+ @if ('minStep' in characteristic) {
+
Step: {{ characteristic.minStep }}
+ } @if ('minValue' in characteristic) {
+
Min: {{ characteristic.minValue }}
+ } @if ('maxValue' in characteristic) {
+
Max: {{ characteristic.maxValue }}
+ } @if ('validValues' in characteristic) {
+
+ Valid: @for (value of characteristic.validValues; track value; let last = $last) { {{ value }} @if (!last)
+ { · } }
+
+ }
+
+ }
+
+ @if (isDetailsVisible[characteristic.uuid]) { Value: } @switch (characteristic.unit) { @case ('percentage')
+ { {{ characteristic.value }}% } @case ('celsius') { {{ characteristic.value | convertTemp }}° } @default
+ { @switch (characteristic.type) { @case ('ColorTemperature') { {{ characteristic.value | convertMired }} }
+ @default { {{ characteristic.value }} } } } } @if (enums[characteristic.type]?.[characteristic.value]) { ({{
+ enums[characteristic.type][characteristic.value] | prettify }}) } @else if (characteristic.format ===
+ 'bool') { ({{ (characteristic.value ? 'status.widget.info.yes' : 'status.widget.info.no') | translate }}) }
+
+
+
@switch (characteristic.unit) { @case ('percentage') { {{ characteristic.value }}% } @case ('celsius') { {{
characteristic.value | convertTemp }}° } @default { @switch (characteristic.type) { @case
('ColorTemperature') { {{ characteristic.value | convertMired }} } @default { {{ characteristic.value }} } } }
@@ -113,8 +146,41 @@ {{ 'accessories.service_info' | translate }}
@for (characteristic of extraService.serviceCharacteristics; track characteristic) {
- {{ characteristic.description }}
-
+
+ @if ('minStep' in characteristic || 'minValue' in characteristic || 'maxValue' in characteristic ||
+ 'validValues' in characteristic) {
+ {{ characteristic.description }}
+ } @else { {{ characteristic.description }} } @if (isDetailsVisible[characteristic.uuid]) {
+
+ @if ('minStep' in characteristic) {
+
Step: {{ characteristic.minStep }}
+ } @if ('minValue' in characteristic) {
+
Min: {{ characteristic.minValue }}
+ } @if ('maxValue' in characteristic) {
+
Max: {{ characteristic.maxValue }}
+ } @if ('validValues' in characteristic) {
+
+ Valid: @for (value of characteristic.validValues; track value; let last = $last) { {{ value }} @if (!last)
+ { · } }
+
+ }
+
+ }
+
+ @if (isDetailsVisible[characteristic.uuid]) { Value: } @switch (characteristic.unit) { @case ('percentage')
+ { {{ characteristic.value }}% } @case ('celsius') { {{ characteristic.value | convertTemp }}° } @default
+ { @switch (characteristic.type) { @case ('ColorTemperature') { {{ characteristic.value | convertMired }} }
+ @default { {{ characteristic.value }} } } } } @if (enums[characteristic.type]?.[characteristic.value]) { ({{
+ enums[characteristic.type][characteristic.value] | prettify }}) } @else if (characteristic.format ===
+ 'bool') { ({{ (characteristic.value ? 'status.widget.info.yes' : 'status.widget.info.no') | translate }}) }
+
+
+
@switch (characteristic.unit) { @case ('percentage') { {{ characteristic.value }}% } @case ('celsius') { {{
characteristic.value | convertTemp }}° } @default { @switch (characteristic.type) { @case
('ColorTemperature') { {{ characteristic.value | convertMired }} } @default { {{ characteristic.value }} } } }
diff --git a/ui/src/app/core/accessories/accessory-info/accessory-info.component.ts b/ui/src/app/core/accessories/accessory-info/accessory-info.component.ts
index 2c57238a3..5b6113988 100644
--- a/ui/src/app/core/accessories/accessory-info/accessory-info.component.ts
+++ b/ui/src/app/core/accessories/accessory-info/accessory-info.component.ts
@@ -1,5 +1,6 @@
import { Component, inject, Input, OnInit } from '@angular/core'
import { FormsModule } from '@angular/forms'
+import { CharacteristicType } from '@homebridge/hap-client'
import { Enums } from '@homebridge/hap-client/dist/hap-types'
import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe } from '@ngx-translate/core'
@@ -41,12 +42,18 @@ export class AccessoryInfoComponent implements OnInit {
'Television',
'Valve',
'RobotVacuum',
+ 'WashingMachine',
],
[
'Switch',
'Outlet',
'LockMechanism',
],
+ [
+ 'Switch',
+ 'Outlet',
+ 'GarageDoorOpener',
+ ],
[
'Door',
'Window',
@@ -64,6 +71,7 @@ export class AccessoryInfoComponent implements OnInit {
@Input() private pairingCache: any[]
@Input() public service: ServiceTypeX
+ public isDetailsVisible: { [key: string]: boolean } = {}
public accessoryInformation: Array
public extraServices: ServiceTypeX[] = []
public matchedCachedAccessory: any = null
@@ -97,6 +105,12 @@ export class AccessoryInfoComponent implements OnInit {
ref.componentInstance.selectedBridge = this.service.instance.username.replaceAll(':', '')
}
+ public toggleDetailsVisibility(char: CharacteristicType): void {
+ if ('minStep' in char || 'minValue' in char || 'maxValue' in char || 'validValues' in char) {
+ this.isDetailsVisible[char.uuid] = !this.isDetailsVisible[char.uuid]
+ }
+ }
+
public dismissModal() {
this.$activeModal.dismiss('Dismiss')
}
diff --git a/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.html b/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.html
index bcb72c5ff..a6d8ec412 100644
--- a/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.html
+++ b/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.html
@@ -121,6 +121,10 @@
>
} @case ('RobotVacuum') {
Robot Vacuum
+ } @case ('WashingMachine') {
+ Washing Machine
} @default {
{{ service.humanType }}
} }
diff --git a/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.ts b/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.ts
index f742f8330..86ce4f5c2 100644
--- a/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.ts
+++ b/ui/src/app/core/accessories/accessory-tile/accessory-tile.component.ts
@@ -36,6 +36,7 @@ import { TemperatureSensorComponent } from '@/app/core/accessories/types/tempera
import { ThermostatComponent } from '@/app/core/accessories/types/thermostat/thermostat.component'
import { UnknownComponent } from '@/app/core/accessories/types/unknown/unknown.component'
import { ValveComponent } from '@/app/core/accessories/types/valve/valve.component'
+import { WashingMachineComponent } from '@/app/core/accessories/types/washing-machine/washing-machine.component'
import { WindowCoveringComponent } from '@/app/core/accessories/types/window-covering/window-covering.component'
import { WindowComponent } from '@/app/core/accessories/types/window/window.component'
@@ -80,6 +81,7 @@ import { WindowComponent } from '@/app/core/accessories/types/window/window.comp
UnknownComponent,
MicrophoneComponent,
RobotVacuumComponent,
+ WashingMachineComponent,
],
})
export class AccessoryTileComponent {
diff --git a/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.html b/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.html
index 5dd8d41c3..acae149a3 100644
--- a/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.html
+++ b/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.html
@@ -1,24 +1,36 @@
{{ service.customName || service.serviceName }}
- @if ((service.values.Active || service.values.On) && service.values.RotationSpeed) {
+
+ @if ((service.values.Active && !('CurrentAirPurifierState' in service.values)) || (service.values.Active &&
+ 'CurrentAirPurifierState' in service.values && service.values.CurrentAirPurifierState === 2) || service.values.On) {
+ @if ('RotationSpeed' in service.values) {
{{ service.values.RotationSpeed }}%
} @else {
-
+
{{ 'accessories.control.on' | translate }}
+ } } @else if ((service.values.Active && !('CurrentAirPurifierState' in service.values)) || (service.values.Active &&
+ 'CurrentAirPurifierState' in service.values && service.values.CurrentAirPurifierState === 1) || service.values.On) {
+
{{ 'accessories.control.idle' | translate }}
+ } @else {
+
{{ 'accessories.control.off' | translate }}
}
diff --git a/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.scss b/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.scss
index 2c91107da..b8db4e124 100644
--- a/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.scss
+++ b/ui/src/app/core/accessories/types/air-purifier/air-purifier.component.scss
@@ -1,19 +1,26 @@
::ng-deep {
- .switch-off {
+ .purifying {
svg {
- .air-purifier_off_grey {
- stroke: #808080;
- fill: #808080;
+ .air-line {
+ animation: wave-motion 5s ease-in-out infinite;
+ stroke: #1976d2;
+ stroke-opacity: 0.5;
}
- .air-purifier_off_lgrey {
- fill: #d9d9d9;
+
+ .bottom-line {
+ animation-delay: 0.4s;
}
- }
- }
- .switch-on {
- svg {
- .rectangle {
- fill: #ffffff !important;
+
+ @keyframes wave-motion {
+ 0%,
+ 100% {
+ transform: translateY(0px);
+ stroke-opacity: 0.5;
+ }
+ 50% {
+ transform: translateY(2px);
+ stroke-opacity: 1;
+ }
}
}
}
diff --git a/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.html b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.html
index 2f6a8b89c..5053b4c24 100644
--- a/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.html
+++ b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.html
@@ -1,14 +1,25 @@
- 3 }">
+
{{ service.customName || service.serviceName }}
3 }"
+ [ngClass]="{
+ 'grey-text': service.values.AirQuality < 5,
+ 'red-text': service.values.AirQuality === 5,
+ }"
>
{{ labels[service.values.AirQuality || 0] }}
diff --git a/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.scss b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.scss
new file mode 100644
index 000000000..5758e3ead
--- /dev/null
+++ b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.scss
@@ -0,0 +1,28 @@
+::ng-deep {
+ .fill-red {
+ svg {
+ .leaves {
+ fill: #d32f2f !important;
+ fill-opacity: 0.5 !important;
+ }
+ }
+ }
+
+ .fill-orange {
+ svg {
+ .leaves {
+ fill: #ff9800 !important;
+ fill-opacity: 0.5 !important;
+ }
+ }
+ }
+
+ .fill-green {
+ svg {
+ .leaves {
+ fill: #4caf50 !important;
+ fill-opacity: 0.5 !important;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.ts b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.ts
index 3325cfe28..f52bc15dd 100644
--- a/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.ts
+++ b/ui/src/app/core/accessories/types/air-quality-sensor/air-quality-sensor.component.ts
@@ -7,6 +7,7 @@ import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces'
@Component({
selector: 'app-air-quality-sensor',
templateUrl: './air-quality-sensor.component.html',
+ styleUrls: ['./air-quality-sensor.component.scss'],
standalone: true,
imports: [NgClass, InlineSVGDirective],
})
diff --git a/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.html b/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.html
index 05b01e519..730130e84 100644
--- a/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.html
+++ b/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.html
@@ -1,13 +1,7 @@
-
+
diff --git a/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.scss b/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.scss
index 7ffca92a8..e17354e84 100644
--- a/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.scss
+++ b/ui/src/app/core/accessories/types/carbon-dioxide-sensor/carbon-dioxide-sensor.component.scss
@@ -1,14 +1,24 @@
::ng-deep {
- .switch-off {
+ .switch-on {
svg {
- .smoke_sensor_trigger_lines {
- display: none;
+ .air-line {
+ stroke: #d32f2f;
+ animation: flash 1s infinite;
}
- .smoke_sensor_box {
- fill: #808080;
+
+ .type {
+ fill: #d32f2f;
+ animation: flash 1s infinite;
}
- .label-text {
- fill: #2b2b2b;
+
+ @keyframes flash {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.2;
+ }
}
}
}
diff --git a/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.html b/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.html
index 4818654e5..ea130192d 100644
--- a/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.html
+++ b/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.html
@@ -1,13 +1,7 @@
-
+
diff --git a/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.scss b/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.scss
index 7ffca92a8..e17354e84 100644
--- a/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.scss
+++ b/ui/src/app/core/accessories/types/carbon-monoxide-sensor/carbon-monoxide-sensor.component.scss
@@ -1,14 +1,24 @@
::ng-deep {
- .switch-off {
+ .switch-on {
svg {
- .smoke_sensor_trigger_lines {
- display: none;
+ .air-line {
+ stroke: #d32f2f;
+ animation: flash 1s infinite;
}
- .smoke_sensor_box {
- fill: #808080;
+
+ .type {
+ fill: #d32f2f;
+ animation: flash 1s infinite;
}
- .label-text {
- fill: #2b2b2b;
+
+ @keyframes flash {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.2;
+ }
}
}
}
diff --git a/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.html b/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.html
index df6c9f9f3..b906ce75c 100644
--- a/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.html
+++ b/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.html
@@ -1,24 +1,10 @@
-
+
- @if (service.values.ContactSensorState) {
- } @if (!service.values.ContactSensorState) {
-
- }
{{ service.customName || service.serviceName }}
@if (service.values.ContactSensorState) {
{{ 'accessories.control.open' | translate }}
diff --git a/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.scss b/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.scss
index f3990a5f4..d9d92cafe 100644
--- a/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.scss
+++ b/ui/src/app/core/accessories/types/contact-sensor/contact-sensor.component.scss
@@ -1,8 +1,19 @@
-::ng-deep body.dark-mode {
- .switch-off {
+::ng-deep {
+ .switch-on {
svg {
- .contact_sensor_split_line {
- fill: #2b2b2b;
+ .left-sensor {
+ fill: #ff9800;
+ fill-opacity: 0.5;
+ }
+
+ .right-object {
+ x: 24px;
+ }
+
+ .right-sensor {
+ x: 24px;
+ fill: #ff9800;
+ fill-opacity: 0.5;
}
}
}
diff --git a/ui/src/app/core/accessories/types/door/door.component.html b/ui/src/app/core/accessories/types/door/door.component.html
index d2b6c8ca8..6fa8dfe28 100644
--- a/ui/src/app/core/accessories/types/door/door.component.html
+++ b/ui/src/app/core/accessories/types/door/door.component.html
@@ -1,20 +1,12 @@
- @if (!service.values.TargetPosition) {
-
- } @if (service.values.TargetPosition) {
-
- }
+
{{ service.customName || service.serviceName }}
@if (service.values.PositionState === 2) {
diff --git a/ui/src/app/core/accessories/types/door/door.component.scss b/ui/src/app/core/accessories/types/door/door.component.scss
new file mode 100644
index 000000000..8c0d9bd61
--- /dev/null
+++ b/ui/src/app/core/accessories/types/door/door.component.scss
@@ -0,0 +1,43 @@
+::ng-deep {
+ svg {
+ .outline {
+ transition: width 3s ease;
+ width: 12.8px;
+ }
+
+ .panel {
+ transition: width 3s ease;
+ width: 9.6px;
+ }
+
+ .handle {
+ visibility: visible;
+ opacity: 1;
+ transition:
+ opacity 3s ease,
+ visibility 0s linear 3s;
+ }
+ }
+
+ .accessory-on {
+ svg {
+ .outline {
+ transition: width 3s ease;
+ width: 3.8px;
+ }
+
+ .panel {
+ transition: width 3s ease;
+ width: 0.6px;
+ }
+
+ .handle {
+ visibility: hidden;
+ opacity: 0;
+ transition:
+ opacity 3s ease,
+ visibility 0s linear 0s;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/door/door.component.ts b/ui/src/app/core/accessories/types/door/door.component.ts
index 8c39611b4..af3241ad3 100644
--- a/ui/src/app/core/accessories/types/door/door.component.ts
+++ b/ui/src/app/core/accessories/types/door/door.component.ts
@@ -11,6 +11,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-door',
templateUrl: './door.component.html',
+ styleUrls: ['./door.component.scss'],
standalone: true,
imports: [
LongClickDirective,
diff --git a/ui/src/app/core/accessories/types/doorbell/doorbell.component.html b/ui/src/app/core/accessories/types/doorbell/doorbell.component.html
index 65b5d19ff..d8372ae95 100644
--- a/ui/src/app/core/accessories/types/doorbell/doorbell.component.html
+++ b/ui/src/app/core/accessories/types/doorbell/doorbell.component.html
@@ -11,7 +11,7 @@
tabindex="0"
>
-
+
{{ service.customName || service.serviceName }}
@if ('CurrentMediaState' in service.values) {
diff --git a/ui/src/app/core/accessories/types/fan/fan.component.html b/ui/src/app/core/accessories/types/fan/fan.component.html
index 5f60bfbd1..f8162e550 100644
--- a/ui/src/app/core/accessories/types/fan/fan.component.html
+++ b/ui/src/app/core/accessories/types/fan/fan.component.html
@@ -6,19 +6,15 @@
tabindex="0"
>
- @if (service.values.On || service.values.Active) {
-

- } @else {
-

- }
+ 'spin': (service.values.On || service.values.Active) && (!hasRotationDirection || (hasRotationDirection && service.values.RotationDirection === 0)),
+ 'spin-counter': (service.values.On || service.values.Active) && hasRotationDirection && service.values.RotationDirection === 1,
+ }"
+ >
{{ service.customName || service.serviceName }}
@if (service.values.On && service.values.RotationSpeed) {
{{ service.values.RotationSpeed }}{{ rotationSpeedUnit }}
diff --git a/ui/src/app/core/accessories/types/fan/fan.component.scss b/ui/src/app/core/accessories/types/fan/fan.component.scss
new file mode 100644
index 000000000..bccd073d8
--- /dev/null
+++ b/ui/src/app/core/accessories/types/fan/fan.component.scss
@@ -0,0 +1,60 @@
+::ng-deep {
+ .spin {
+ svg {
+ g {
+ .fan-blades {
+ -webkit-animation: fa-spin 2s infinite linear;
+ animation: fa-spin 2s infinite linear;
+ transform-origin: 53.5% 50%;
+ }
+ }
+ }
+ }
+
+ .spin-counter {
+ svg {
+ g {
+ .fan-blades {
+ -webkit-animation: fa-spin-counter 2s infinite linear;
+ animation: fa-spin-counter 2s infinite linear;
+ transform-origin: 53.5% 50%;
+ }
+ }
+ }
+ }
+
+ @-webkit-keyframes fa-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(359deg);
+ }
+ }
+ @keyframes fa-spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(359deg);
+ }
+ }
+
+ @-webkit-keyframes fa-spin-counter {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(-359deg);
+ }
+ }
+
+ @keyframes fa-spin-counter {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(-359deg);
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/fan/fan.component.ts b/ui/src/app/core/accessories/types/fan/fan.component.ts
index 41e7ce850..d5c6c00a1 100644
--- a/ui/src/app/core/accessories/types/fan/fan.component.ts
+++ b/ui/src/app/core/accessories/types/fan/fan.component.ts
@@ -2,6 +2,7 @@ import { NgClass } from '@angular/common'
import { Component, inject, Input, OnInit } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe } from '@ngx-translate/core'
+import { InlineSVGDirective } from 'ng-inline-svg-2'
import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces'
import { FanManageComponent } from '@/app/core/accessories/types/fan/fan.manage.component'
@@ -10,11 +11,13 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-fan',
templateUrl: './fan.component.html',
+ styleUrls: ['./fan.component.scss'],
standalone: true,
imports: [
LongClickDirective,
NgClass,
TranslatePipe,
+ InlineSVGDirective,
],
})
export class FanComponent implements OnInit {
diff --git a/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.html b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.html
index 58820d2c3..da84eb8d5 100644
--- a/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.html
+++ b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.html
@@ -1,7 +1,16 @@
-
+
diff --git a/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.scss b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.scss
new file mode 100644
index 000000000..54083440f
--- /dev/null
+++ b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.scss
@@ -0,0 +1,25 @@
+::ng-deep {
+ .dirty {
+ svg {
+ .status {
+ fill: #ff9800 !important;
+ }
+ }
+ }
+
+ .replace {
+ svg {
+ .status {
+ fill: #d32f2f !important;
+ }
+ }
+ }
+
+ body.dark-mode {
+ svg {
+ .back {
+ fill: #2b2b2b !important;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.ts b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.ts
index b886dac8e..dfe604df7 100644
--- a/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.ts
+++ b/ui/src/app/core/accessories/types/filter-maintenance/filter-maintenance.component.ts
@@ -11,6 +11,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-filter-maintenance',
templateUrl: './filter-maintenance.component.html',
+ styleUrls: ['./filter-maintenance.component.scss'],
standalone: true,
imports: [
InlineSVGDirective,
diff --git a/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.html b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.html
index da52e3ad9..b81db9bbe 100644
--- a/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.html
+++ b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.html
@@ -1,29 +1,44 @@
{{ service.customName || service.serviceName }}
- @if (service.values.CurrentDoorState < 2 ) {
-
- } @else if (service.values.CurrentDoorState === 2) {
+ @if (service.values.ObstructionDetected) {
+
{{ 'accessories.control.obstructed' | translate }}
+ } @else if ('CurrentDoorState' in service.values) { @switch (service.values.CurrentDoorState) {@case (0) {
+
{{ 'accessories.control.open' | translate }}
+ } @case (1) {
+
{{ 'accessories.control.closed' | translate }}
+ } @case (2) {
{{ 'accessories.control.opening' | translate }}...
- } @else if (service.values.CurrentDoorState === 3) {
+ } @case (3) {
{{ 'accessories.control.closing' | translate }}...
- } @else if (service.values.CurrentDoorState === 4) {
+ } @case (4) {
{{ 'accessories.control.stopped' | translate }}
- }
+ } } } @else { @if (service.values.On || service.values.Active) {
+
{{ 'accessories.control.open' | translate }}
+ } @else {
+
{{ 'accessories.control.closed' | translate }}
+ } }
diff --git a/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.scss b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.scss
new file mode 100644
index 000000000..9dfedbf14
--- /dev/null
+++ b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.scss
@@ -0,0 +1,219 @@
+::ng-deep {
+ .opening {
+ svg {
+ .window {
+ fill: #ff9800 !important;
+ fill-opacity: 0.5 !important;
+ }
+
+ .line {
+ animation: fadeOut 0.5s ease-in-out forwards;
+ }
+
+ .line1 {
+ animation-delay: 9.282s;
+ }
+
+ .line2 {
+ animation-delay: 8.568s;
+ }
+
+ .line3 {
+ animation-delay: 7.854s;
+ }
+
+ .line4 {
+ animation-delay: 7.14s;
+ }
+
+ .line5 {
+ animation-delay: 6.426s;
+ }
+
+ .line6 {
+ animation-delay: 5.712s;
+ }
+
+ .line7 {
+ animation-delay: 4.998s;
+ }
+
+ .line8 {
+ animation-delay: 4.284s;
+ }
+
+ .line9 {
+ animation-delay: 3.57s;
+ }
+
+ .line10 {
+ animation-delay: 2.856s;
+ }
+
+ .line11 {
+ animation-delay: 2.142s;
+ }
+
+ .line12 {
+ animation-delay: 1.428s;
+ }
+
+ .line13 {
+ animation-delay: 0.714s;
+ }
+
+ .line14 {
+ animation-delay: 0s;
+ }
+
+ @keyframes fadeOut {
+ to {
+ opacity: 0;
+ }
+ }
+ }
+ }
+
+ .open {
+ .window {
+ fill: #d32f2f !important;
+ fill-opacity: 0.5 !important;
+ }
+
+ .line {
+ opacity: 0;
+ }
+ }
+
+ .closing {
+ svg {
+ .window {
+ fill: #ff9800 !important;
+ fill-opacity: 0.5 !important;
+ }
+
+ .line {
+ opacity: 0;
+ animation: fadeIn 0.5s ease-in-out forwards;
+ }
+
+ .line1 {
+ animation-delay: 0s;
+ }
+
+ .line2 {
+ animation-delay: 0.714s;
+ }
+
+ .line3 {
+ animation-delay: 1.428s;
+ }
+
+ .line4 {
+ animation-delay: 2.142s;
+ }
+
+ .line5 {
+ animation-delay: 2.856s;
+ }
+
+ .line6 {
+ animation-delay: 3.57s;
+ }
+
+ .line7 {
+ animation-delay: 4.284s;
+ }
+
+ .line8 {
+ animation-delay: 4.998s;
+ }
+
+ .line9 {
+ animation-delay: 5.712s;
+ }
+
+ .line10 {
+ animation-delay: 6.426s;
+ }
+
+ .line11 {
+ animation-delay: 7.14s;
+ }
+
+ .line12 {
+ animation-delay: 7.854s;
+ }
+
+ .line13 {
+ animation-delay: 8.568s;
+ }
+
+ .line14 {
+ animation-delay: 9.282s;
+ }
+
+ @keyframes fadeIn {
+ to {
+ opacity: 1;
+ }
+ }
+ }
+ }
+
+ .closed {
+ .window {
+ fill: #4caf50 !important;
+ fill-opacity: 0.5 !important;
+ }
+
+ .line {
+ opacity: 1;
+ }
+ }
+
+ .stopped {
+ .window {
+ fill: #d32f2f !important;
+ }
+
+ .line7,
+ .line8,
+ .line9,
+ .line10,
+ .line11,
+ .line12,
+ .line13,
+ .line14 {
+ opacity: 0;
+ }
+ }
+
+ .obstructed {
+ .window {
+ animation: flash 2s infinite;
+ fill: #d32f2f !important;
+ }
+
+ @keyframes flash {
+ 0%,
+ 100% {
+ fill-opacity: 0.5;
+ }
+ 50% {
+ fill-opacity: 0.1;
+ }
+ }
+
+ .line7,
+ .line8,
+ .line9,
+ .line10,
+ .line11,
+ .line12,
+ .line13,
+ .line14 {
+ opacity: 0;
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.ts b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.ts
index 1e3f9841e..ca70001c4 100644
--- a/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.ts
+++ b/ui/src/app/core/accessories/types/garage-door-opener/garage-door-opener.component.ts
@@ -9,6 +9,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-garage-door-opener',
templateUrl: './garage-door-opener.component.html',
+ styleUrls: ['./garage-door-opener.component.scss'],
standalone: true,
imports: [
LongClickDirective,
@@ -26,6 +27,12 @@ export class GarageDoorOpenerComponent {
return
}
- this.service.getCharacteristic('TargetDoorState').setValue(this.service.values.TargetDoorState ? 0 : 1)
+ if ('TargetDoorState' in this.service.values) {
+ this.service.getCharacteristic('TargetDoorState').setValue(this.service.values.TargetDoorState ? 0 : 1)
+ } else if ('On' in this.service.values) {
+ this.service.getCharacteristic('On').setValue(!this.service.values.On)
+ } else if ('Active' in this.service.values) {
+ this.service.getCharacteristic('Active').setValue(!this.service.values.Active)
+ }
}
}
diff --git a/ui/src/app/core/accessories/types/microphone/microphone.component.html b/ui/src/app/core/accessories/types/microphone/microphone.component.html
index 379c6bb21..1dcc78c76 100644
--- a/ui/src/app/core/accessories/types/microphone/microphone.component.html
+++ b/ui/src/app/core/accessories/types/microphone/microphone.component.html
@@ -11,7 +11,11 @@
tabindex="0"
>
-
+
{{ service.customName || service.serviceName }}
@if ('CurrentMediaState' in service.values) {
diff --git a/ui/src/app/core/accessories/types/outlet/outlet.component.html b/ui/src/app/core/accessories/types/outlet/outlet.component.html
index 371db18ea..acd4a9ed2 100644
--- a/ui/src/app/core/accessories/types/outlet/outlet.component.html
+++ b/ui/src/app/core/accessories/types/outlet/outlet.component.html
@@ -1,8 +1,11 @@
@@ -11,9 +14,13 @@
{{ service.customName || service.serviceName }}
diff --git a/ui/src/app/core/accessories/types/outlet/outlet.component.ts b/ui/src/app/core/accessories/types/outlet/outlet.component.ts
index 2b6e44ab7..ddd6fcd5d 100644
--- a/ui/src/app/core/accessories/types/outlet/outlet.component.ts
+++ b/ui/src/app/core/accessories/types/outlet/outlet.component.ts
@@ -32,6 +32,8 @@ export class OutletComponent {
this.service.getCharacteristic('Active').setValue(this.service.values.Active ? 0 : 1)
} else if ('LockTargetState' in this.service.values) {
this.service.getCharacteristic('LockTargetState').setValue(this.service.values.LockTargetState ? 0 : 1)
+ } else if ('TargetDoorState' in this.service.values) {
+ this.service.getCharacteristic('TargetDoorState').setValue(this.service.values.TargetDoorState ? 0 : 1)
}
}
}
diff --git a/ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.html b/ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.html
index 5244b0473..f70d14399 100644
--- a/ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.html
+++ b/ui/src/app/core/accessories/types/robot-vacuum/robot-vacuum.component.html
@@ -5,11 +5,11 @@
tabindex="0"
>
- @if (service.values.On || service.values.Active) {
-

- } @else {
-

- }
+
{{ service.customName || service.serviceName }}
+
diff --git a/ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.scss b/ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.scss
index 7ffca92a8..e17354e84 100644
--- a/ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.scss
+++ b/ui/src/app/core/accessories/types/smoke-sensor/smoke-sensor.component.scss
@@ -1,14 +1,24 @@
::ng-deep {
- .switch-off {
+ .switch-on {
svg {
- .smoke_sensor_trigger_lines {
- display: none;
+ .air-line {
+ stroke: #d32f2f;
+ animation: flash 1s infinite;
}
- .smoke_sensor_box {
- fill: #808080;
+
+ .type {
+ fill: #d32f2f;
+ animation: flash 1s infinite;
}
- .label-text {
- fill: #2b2b2b;
+
+ @keyframes flash {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.2;
+ }
}
}
}
diff --git a/ui/src/app/core/accessories/types/speaker/speaker.component.html b/ui/src/app/core/accessories/types/speaker/speaker.component.html
index 3a366d05b..14251dd42 100644
--- a/ui/src/app/core/accessories/types/speaker/speaker.component.html
+++ b/ui/src/app/core/accessories/types/speaker/speaker.component.html
@@ -5,13 +5,14 @@
? service.values.Active
: ('CurrentMediaState' in service.values ? [0, 1].includes(service.values.CurrentMediaState) : ['Speaker', 'SmartSpeaker'].includes(service.customType || service.type) ? ('Mute' in service.values && !service.values.Mute) : false),
'paused': 'CurrentMediaState' in service.values && service.values.CurrentMediaState === 1,
+ 'muted': 'Mute' in service.values && service.values.Mute,
}"
(longClick)="onLongClick()"
(shortClick)="onClick()"
tabindex="0"
>
-
+
{{ service.customName || service.serviceName }}
@if ('CurrentMediaState' in service.values) {
diff --git a/ui/src/app/core/accessories/types/speaker/speaker.component.scss b/ui/src/app/core/accessories/types/speaker/speaker.component.scss
index 6b667b5ee..13e783971 100644
--- a/ui/src/app/core/accessories/types/speaker/speaker.component.scss
+++ b/ui/src/app/core/accessories/types/speaker/speaker.component.scss
@@ -1,15 +1,46 @@
::ng-deep {
- .switch-on {
+ .switch-on:not(.paused):not(.muted) {
svg {
- .wave {
- stroke: #1976d2 !important;
+ .wave-inner {
+ animation: pulse-inner 2s infinite; // Inner waves animation
+ }
+
+ .wave-outer {
+ animation: pulse-outer 2s infinite; // Outer waves animation
+ }
+
+ @keyframes pulse-inner {
+ 0% {
+ stroke-opacity: 0.5;
+ }
+ 50% {
+ stroke-opacity: 1;
+ }
+ 100% {
+ stroke-opacity: 0.5;
+ }
+ }
+
+ @keyframes pulse-outer {
+ 0% {
+ stroke-opacity: 0.3;
+ }
+ 50% {
+ stroke-opacity: 0.8;
+ }
+ 100% {
+ stroke-opacity: 0.3;
+ }
}
}
}
- .paused {
- svg {
- .wave {
- stroke: none !important;
+
+ body.dark-mode {
+ div.accessory-box:not(.switch-on) {
+ svg {
+ .inner {
+ fill: #2b2b2b !important; // Dark mode inner circle color
+ }
}
}
}
diff --git a/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.html b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.html
index 1ef9e4b86..e57ca6354 100644
--- a/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.html
+++ b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.html
@@ -1,10 +1,18 @@
-
+
{{ service.customName || service.serviceName }}
+
{{ 'accessories.control.stateless' | translate }}
diff --git a/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.scss b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.scss
new file mode 100644
index 000000000..378963229
--- /dev/null
+++ b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.scss
@@ -0,0 +1,25 @@
+::ng-deep {
+ .press-single {
+ svg {
+ .single {
+ fill: #1976d2 !important;
+ }
+ }
+ }
+
+ .press-double {
+ svg {
+ .double {
+ fill: #1976d2 !important;
+ }
+ }
+ }
+
+ .press-long {
+ svg {
+ .long {
+ fill: #1976d2 !important;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.ts b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.ts
index 3f2bb9acf..be7f47b03 100644
--- a/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.ts
+++ b/ui/src/app/core/accessories/types/stateless-programmable-switch/stateless-programmable-switch.component.ts
@@ -1,4 +1,6 @@
+import { NgClass } from '@angular/common'
import { Component, Input } from '@angular/core'
+import { TranslatePipe } from '@ngx-translate/core'
import { InlineSVGDirective } from 'ng-inline-svg-2'
import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces'
@@ -6,8 +8,9 @@ import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces'
@Component({
selector: 'app-stateless-programmable-switch',
templateUrl: './stateless-programmable-switch.component.html',
+ styleUrls: ['./stateless-programmable-switch.component.scss'],
standalone: true,
- imports: [InlineSVGDirective],
+ imports: [InlineSVGDirective, NgClass, TranslatePipe],
})
export class StatelessProgrammableSwitchComponent {
@Input() public service: ServiceTypeX
diff --git a/ui/src/app/core/accessories/types/switch/switch.component.html b/ui/src/app/core/accessories/types/switch/switch.component.html
index 91ecbf81d..59cbca7ab 100644
--- a/ui/src/app/core/accessories/types/switch/switch.component.html
+++ b/ui/src/app/core/accessories/types/switch/switch.component.html
@@ -1,9 +1,10 @@
{{ service.customName || service.serviceName }}
diff --git a/ui/src/app/core/accessories/types/switch/switch.component.ts b/ui/src/app/core/accessories/types/switch/switch.component.ts
index 239585682..e57bb10ce 100644
--- a/ui/src/app/core/accessories/types/switch/switch.component.ts
+++ b/ui/src/app/core/accessories/types/switch/switch.component.ts
@@ -32,6 +32,8 @@ export class SwitchComponent {
this.service.getCharacteristic('Active').setValue(this.service.values.Active ? 0 : 1)
} else if ('LockTargetState' in this.service.values) {
this.service.getCharacteristic('LockTargetState').setValue(this.service.values.LockTargetState ? 0 : 1)
+ } else if ('TargetDoorState' in this.service.values) {
+ this.service.getCharacteristic('TargetDoorState').setValue(this.service.values.TargetDoorState ? 0 : 1)
}
}
}
diff --git a/ui/src/app/core/accessories/types/television/television.component.html b/ui/src/app/core/accessories/types/television/television.component.html
index e48b7ffeb..8b386a559 100644
--- a/ui/src/app/core/accessories/types/television/television.component.html
+++ b/ui/src/app/core/accessories/types/television/television.component.html
@@ -6,7 +6,11 @@
tabindex="0"
>
-
+
{{ service.customName || service.values.ConfiguredName || service.serviceName }}
diff --git a/ui/src/app/core/accessories/types/television/television.component.scss b/ui/src/app/core/accessories/types/television/television.component.scss
new file mode 100644
index 000000000..e26b04bca
--- /dev/null
+++ b/ui/src/app/core/accessories/types/television/television.component.scss
@@ -0,0 +1,31 @@
+::ng-deep {
+ body.dark-mode {
+ .screen {
+ fill: #2b2b2b;
+ stroke: #7f7f7f;
+ }
+ }
+ .switch-on {
+ svg {
+ .screen {
+ animation: color-change 3s infinite alternate; // Apply color animation
+ }
+
+ .stand {
+ stroke: #7f7f7f;
+ stroke-opacity: 1;
+ }
+ }
+ }
+}
+
+@keyframes color-change {
+ 0% {
+ fill: #4fc3f7; // Light blue
+ }
+ 100% {
+ fill: #0288d1; // Dark blue
+ }
+}
+
+// 7f7f7f
diff --git a/ui/src/app/core/accessories/types/television/television.component.ts b/ui/src/app/core/accessories/types/television/television.component.ts
index 121f4dc26..8c34186f1 100644
--- a/ui/src/app/core/accessories/types/television/television.component.ts
+++ b/ui/src/app/core/accessories/types/television/television.component.ts
@@ -11,6 +11,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-television',
templateUrl: './television.component.html',
+ styleUrls: ['./television.component.scss'],
standalone: true,
imports: [
LongClickDirective,
diff --git a/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.html b/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.html
index 41561885f..66a70bb28 100644
--- a/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.html
+++ b/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.html
@@ -75,9 +75,9 @@
}
- @if (targetStateValidValues.includes(3)) {
+ @if ('TargetTemperature' in service.values) {
- {{ 'accessories.control.target_auto' | translate }}: {{ targetTemperature.value | convertTemp | number: '1.0-1'
+ {{ 'accessories.control.target' | translate }}: {{ targetTemperature.value | convertTemp | number: '1.0-1'
}}°{{ temperatureUnits | uppercase }}
(ngModelChange)="onTemperatureStateChange()"
>
- @if (HeatingThresholdTemperature && CoolingThresholdTemperature) {
+ } @if (targetStateValidValues.includes(3)) { @if (HeatingThresholdTemperature && CoolingThresholdTemperature) {
{{ 'accessories.control.threshold_auto' | translate }}: {{ autoTemp[0] | convertTemp | number: '1.0-1' }}°{{
temperatureUnits | uppercase }} - {{ autoTemp[1] | convertTemp | number: '1.0-1' }}°{{ temperatureUnits |
diff --git a/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.ts b/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.ts
index 298b98c80..a5e4372c4 100644
--- a/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.ts
+++ b/ui/src/app/core/accessories/types/thermostat/thermostat.manage.component.ts
@@ -115,7 +115,7 @@ export class ThermostatManageComponent implements OnInit {
value: TargetTemperature.value,
min: TargetTemperature.minValue,
max: TargetTemperature.maxValue,
- step: TargetTemperature.minStep,
+ step: TargetTemperature.minStep || 0.5,
}
this.targetCoolingTemp = this.service.getCharacteristic('CoolingThresholdTemperature')?.value as number
this.targetHeatingTemp = this.service.getCharacteristic('HeatingThresholdTemperature')?.value as number
diff --git a/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.html b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.html
new file mode 100644
index 000000000..822689986
--- /dev/null
+++ b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.html
@@ -0,0 +1,19 @@
+
+
+
+
{{ service.customName || service.serviceName }}
+
+
+
diff --git a/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.scss b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.scss
new file mode 100644
index 000000000..d2f3748e5
--- /dev/null
+++ b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.scss
@@ -0,0 +1,21 @@
+::ng-deep {
+ .switch-on {
+ svg {
+ .handle {
+ fill: #1976d2;
+ fill-opacity: 0.5;
+ transform-origin: 48% 53%;
+ animation: rotate-handle 2s linear infinite;
+
+ @keyframes rotate-handle {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.ts b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.ts
new file mode 100644
index 000000000..af523d04f
--- /dev/null
+++ b/ui/src/app/core/accessories/types/washing-machine/washing-machine.component.ts
@@ -0,0 +1,36 @@
+import { NgClass } from '@angular/common'
+import { Component, Input } from '@angular/core'
+import { TranslatePipe } from '@ngx-translate/core'
+import { InlineSVGDirective } from 'ng-inline-svg-2'
+
+import { ServiceTypeX } from '@/app/core/accessories/accessories.interfaces'
+import { LongClickDirective } from '@/app/core/directives/long-click.directive'
+
+@Component({
+ selector: 'app-washing-machine',
+ templateUrl: './washing-machine.component.html',
+ styleUrls: ['./washing-machine.component.scss'],
+ standalone: true,
+ imports: [
+ LongClickDirective,
+ NgClass,
+ TranslatePipe,
+ InlineSVGDirective,
+ ],
+})
+export class WashingMachineComponent {
+ @Input() public service: ServiceTypeX
+ @Input() public readyForControl = false
+
+ public onClick() {
+ if (!this.readyForControl) {
+ return
+ }
+
+ if ('On' in this.service.values) {
+ this.service.getCharacteristic('On').setValue(!this.service.values.On)
+ } else if ('Active' in this.service.values) {
+ this.service.getCharacteristic('Active').setValue(this.service.values.Active ? 0 : 1)
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/window-covering/window-covering.component.html b/ui/src/app/core/accessories/types/window-covering/window-covering.component.html
index 1800d9628..c81c739ac 100644
--- a/ui/src/app/core/accessories/types/window-covering/window-covering.component.html
+++ b/ui/src/app/core/accessories/types/window-covering/window-covering.component.html
@@ -1,24 +1,16 @@
- @if (!service.values.TargetPosition) {
- } @if (service.values.TargetPosition) {
-
- }
{{ service.customName || service.serviceName }}
@if (service.values.PositionState === 2) {
diff --git a/ui/src/app/core/accessories/types/window-covering/window-covering.component.scss b/ui/src/app/core/accessories/types/window-covering/window-covering.component.scss
new file mode 100644
index 000000000..e57fc3ac6
--- /dev/null
+++ b/ui/src/app/core/accessories/types/window-covering/window-covering.component.scss
@@ -0,0 +1,41 @@
+::ng-deep {
+ body.dark-mode {
+ .c-blinds {
+ fill: #2b2b2b;
+ stroke: #2b2b2b;
+ stroke-opacity: 0.8;
+ fill-opacity: 0.8;
+ }
+ }
+ svg {
+ .c-blinds {
+ transition: height 3s ease;
+ height: 27.6px;
+ }
+
+ .c-light {
+ transition:
+ y 3s ease,
+ height 3s ease;
+ y: 29.9px;
+ height: 0;
+ }
+ }
+
+ .accessory-on {
+ svg {
+ .c-blinds {
+ transition: height 3s ease;
+ height: 0.3px;
+ }
+
+ .c-light {
+ transition:
+ y 3s ease,
+ height 3s ease;
+ y: 2.65px;
+ height: 27.3px;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/window-covering/window-covering.component.ts b/ui/src/app/core/accessories/types/window-covering/window-covering.component.ts
index c5b9b3c77..0d2540cef 100644
--- a/ui/src/app/core/accessories/types/window-covering/window-covering.component.ts
+++ b/ui/src/app/core/accessories/types/window-covering/window-covering.component.ts
@@ -11,6 +11,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-window-covering',
templateUrl: './window-covering.component.html',
+ styleUrls: ['./window-covering.component.scss'],
standalone: true,
imports: [
LongClickDirective,
diff --git a/ui/src/app/core/accessories/types/window/window.component.html b/ui/src/app/core/accessories/types/window/window.component.html
index 1cebbcfc9..43c4062e2 100644
--- a/ui/src/app/core/accessories/types/window/window.component.html
+++ b/ui/src/app/core/accessories/types/window/window.component.html
@@ -1,24 +1,12 @@
- @if (!service.values.TargetPosition) {
-
- } @if (service.values.TargetPosition) {
-
- }
+
{{ service.customName || service.serviceName }}
@if (service.values.PositionState === 2) {
diff --git a/ui/src/app/core/accessories/types/window/window.component.scss b/ui/src/app/core/accessories/types/window/window.component.scss
new file mode 100644
index 000000000..f65a7d45c
--- /dev/null
+++ b/ui/src/app/core/accessories/types/window/window.component.scss
@@ -0,0 +1,67 @@
+::ng-deep {
+ svg {
+ .outline-left {
+ x: 2.8px;
+ width: 12.4px;
+ }
+
+ .divider-left {
+ x: 3.66px;
+ width: 10.68px;
+ }
+
+ .handle-left {
+ x: 15.16px;
+ width: 0.1px;
+ }
+
+ .outline-right {
+ x: 16.8px;
+ width: 12.4px;
+ }
+
+ .divider-right {
+ x: 17.66px;
+ width: 10.68px;
+ }
+
+ .handle-right {
+ x: 16.76px;
+ width: 0.1px;
+ }
+ }
+
+ .accessory-on {
+ svg {
+ .outline-left {
+ x: 2.8px;
+ width: 2.4px;
+ }
+
+ .divider-left {
+ x: 3.66px;
+ width: 0.68px;
+ }
+
+ .handle-left {
+ x: 5.16px;
+ width: 0.1px;
+ }
+
+ .outline-right {
+ x: 26.8px;
+ width: 2.4px;
+ }
+
+ .divider-right {
+ x: 27.66px;
+ width: 0.68px;
+ }
+
+ .handle-right {
+ x: 26.76px;
+ width: 0.1px;
+ }
+ }
+ }
+}
diff --git a/ui/src/app/core/accessories/types/window/window.component.ts b/ui/src/app/core/accessories/types/window/window.component.ts
index e1294b5a2..4234519b5 100644
--- a/ui/src/app/core/accessories/types/window/window.component.ts
+++ b/ui/src/app/core/accessories/types/window/window.component.ts
@@ -11,6 +11,7 @@ import { LongClickDirective } from '@/app/core/directives/long-click.directive'
@Component({
selector: 'app-window',
templateUrl: './window.component.html',
+ styleUrls: ['./window.component.scss'],
standalone: true,
imports: [
LongClickDirective,
diff --git a/ui/src/app/core/components/confirm/confirm.component.html b/ui/src/app/core/components/confirm/confirm.component.html
index caa6eb572..2076d4694 100644
--- a/ui/src/app/core/components/confirm/confirm.component.html
+++ b/ui/src/app/core/components/confirm/confirm.component.html
@@ -14,6 +14,11 @@
{{ title }}
}
+ @if (message2) {
+
+ } @if (message3) {
+
+ }
diff --git a/ui/src/app/core/components/confirm/confirm.component.ts b/ui/src/app/core/components/confirm/confirm.component.ts
index 259879b7e..eb38c303b 100644
--- a/ui/src/app/core/components/confirm/confirm.component.ts
+++ b/ui/src/app/core/components/confirm/confirm.component.ts
@@ -12,9 +12,11 @@ export class ConfirmComponent {
@Input() title: string
@Input() message: string
- @Input() confirmButtonLabel: string
- @Input() confirmButtonClass: string
- @Input() faIconClass: string
+ @Input() message2?: string
+ @Input() message3?: string
+ @Input() confirmButtonLabel?: string
+ @Input() confirmButtonClass?: string
+ @Input() faIconClass?: string
public dismissModal() {
this.$activeModal.dismiss('Dismiss')
diff --git a/ui/src/app/core/components/restart-child-bridges/restart-child-bridges.component.html b/ui/src/app/core/components/restart-child-bridges/restart-child-bridges.component.html
index 6e6bd5718..d5029a6df 100644
--- a/ui/src/app/core/components/restart-child-bridges/restart-child-bridges.component.html
+++ b/ui/src/app/core/components/restart-child-bridges/restart-child-bridges.component.html
@@ -13,7 +13,7 @@
{{ 'platform.version.service_restart_required' | transla
{{ 'restart.child_bridge_list' | translate }}
-
+
@for (bridge of bridges; track bridge) {
- {{ bridge.name }} ({{ bridge.username }})
}
diff --git a/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.component.ts b/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.component.ts
index b284bf4e3..c77e79d9e 100644
--- a/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.component.ts
+++ b/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.component.ts
@@ -1,4 +1,4 @@
-import type { PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
+import type { ChildBridge, PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { NgClass } from '@angular/common'
import { Component, ElementRef, inject, Input, OnDestroy, OnInit, viewChild } from '@angular/core'
@@ -12,6 +12,7 @@ import { ApiService } from '@/app/core/api.service'
import { RestartChildBridgesComponent } from '@/app/core/components/restart-child-bridges/restart-child-bridges.component'
import { RestartHomebridgeComponent } from '@/app/core/components/restart-homebridge/restart-homebridge.component'
import { SchemaFormComponent } from '@/app/core/components/schema-form/schema-form.component'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { ManagePluginsService } from '@/app/core/manage-plugins/manage-plugins.service'
import { SettingsService } from '@/app/core/settings.service'
import { IoNamespace, WsService } from '@/app/core/ws.service'
@@ -40,7 +41,7 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
readonly customPluginUiElementTarget = viewChild('custompluginui')
- @Input() plugin: any
+ @Input() plugin: Plugin
@Input() schema: PluginSchema
@Input() pluginConfig: Record[]
@@ -61,7 +62,7 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
public formValid = true
public formUpdatedSubject = new Subject()
public formActionSubject = new Subject()
- public childBridges: any[] = []
+ public childBridges: ChildBridge[] = []
public isFirstSave = false
public formIsValid = true
public strictValidation = false
@@ -263,11 +264,12 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
break
}
case 'i18n.lang': {
- this.requestResponse(e, this.$translate.currentLang)
+ this.requestResponse(e, this.$translate.getCurrentLang())
break
}
case 'i18n.translations': {
- this.requestResponse(e, this.$translate.store.translations[this.$translate.currentLang])
+ // eslint-disable-next-line ts/no-require-imports
+ this.requestResponse(e, require(`../../../../i18n/${this.$translate.getCurrentLang()}.json`))
break
}
case 'close': {
@@ -379,6 +381,8 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
const customStyles = `
body {
height: unset !important;
+ background-color: ${darkMode ? '#242424' : '#FFFFFF'} !important;
+ color: ${darkMode ? '#FFFFFF' : '#000000'} !important;
}
`
event.source.postMessage({ action: 'inline-style', style: customStyles }, event.origin)
@@ -506,7 +510,7 @@ export class CustomPluginsComponent implements OnInit, OnDestroy {
private async getChildBridges(): Promise {
try {
- const data: any[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
+ const data: ChildBridge[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
data.forEach((bridge) => {
if (this.plugin.name === bridge.plugin) {
this.childBridges.push(bridge)
diff --git a/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.service.ts b/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.service.ts
index 8254c880a..fdc3e0cd2 100644
--- a/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.service.ts
+++ b/ui/src/app/core/manage-plugins/custom-plugins/custom-plugins.service.ts
@@ -4,6 +4,7 @@ import { firstValueFrom } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { CustomPluginsComponent } from '@/app/core/manage-plugins/custom-plugins/custom-plugins.component'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
@Injectable({
providedIn: 'root',
@@ -14,7 +15,7 @@ export class CustomPluginsService {
public plugins = {}
- async openSettings(plugin: any, schema: any) {
+ async openSettings(plugin: Plugin, schema: any) {
const pluginConfig = await this.loadPluginConfig(plugin.name)
const ref = this.$modal.open(this.plugins[plugin.name], {
size: 'lg',
@@ -27,7 +28,7 @@ export class CustomPluginsService {
return ref.result.catch(() => { /* do nothing */ })
}
- async openCustomSettingsUi(plugin: any, schema: any) {
+ async openCustomSettingsUi(plugin: Plugin, schema: any) {
const pluginConfig = await this.loadPluginConfig(plugin.name)
const ref = this.$modal.open(CustomPluginsComponent, {
size: 'lg',
diff --git a/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.html b/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.html
index b22a2a18e..b232abc14 100644
--- a/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.html
+++ b/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.html
@@ -13,17 +13,30 @@ {{ pluginName }}
- @if (isConfigured && !isConfiguredDynamicPlatform) {
+ @if (isConfigured) { @if (isConfiguredDynamicPlatform) {
- - {{ 'plugins.manage.confirm_disable_accessory_1' | translate }}
- - {{ 'plugins.manage.confirm_disable_accessory_2' | translate }}
+
+
+ @if (keepOrphans) {
+ - {{ 'plugins.manage.confirm_disable_platform_1' | translate }}
+ - {{ 'plugins.manage.confirm_disable_platform_2' | translate }}
+ } @else {
+ - {{ 'plugins.manage.confirm_disable_accessory_1' | translate }}
+ - {{ 'plugins.manage.confirm_disable_accessory_2' | translate }}
+ }
+
- } @if (isConfigured && isConfiguredDynamicPlatform) {
+ } @else {
- - {{ 'plugins.manage.confirm_disable_platform_1' | translate }}
- - {{ 'plugins.manage.confirm_disable_platform_2' | translate }}
+ - {{ 'plugins.manage.confirm_disable_accessory_1' | translate }}
+ - {{ 'plugins.manage.confirm_disable_accessory_2' | translate }}
- }
+ } }
{{ 'plugins.manage.confirm_disable' | translate: { pluginName } }}
diff --git a/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.ts b/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.ts
index f129486dc..f5c8e7da1 100644
--- a/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.ts
+++ b/ui/src/app/core/manage-plugins/disable-plugin/disable-plugin.component.ts
@@ -1,6 +1,6 @@
import { Component, inject, Input } from '@angular/core'
import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
-import { TranslatePipe } from '@ngx-translate/core'
+import { TranslatePipe, TranslateService } from '@ngx-translate/core'
@Component({
templateUrl: './disable-plugin.component.html',
@@ -12,10 +12,15 @@ import { TranslatePipe } from '@ngx-translate/core'
})
export class DisablePluginComponent {
private $activeModal = inject(NgbActiveModal)
+ private $translate = inject(TranslateService)
@Input() pluginName: string
@Input() isConfigured = false
@Input() isConfiguredDynamicPlatform = false
+ @Input() keepOrphans = false
+
+ public readonly keepOrphansName = `${this.$translate.instant('settings.startup.keep_accessories')}
`
+ public readonly keepOrphansValue = `${this.keepOrphans}
`
public dismissModal() {
this.$activeModal.dismiss('Dismiss')
diff --git a/ui/src/app/core/manage-plugins/donate/donate.component.ts b/ui/src/app/core/manage-plugins/donate/donate.component.ts
index a84ae9d92..f33071de1 100644
--- a/ui/src/app/core/manage-plugins/donate/donate.component.ts
+++ b/ui/src/app/core/manage-plugins/donate/donate.component.ts
@@ -3,6 +3,8 @@ import { Component, inject, Input, OnInit } from '@angular/core'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe } from '@ngx-translate/core'
+import { Plugin, PluginFundingOption } from '@/app/core/manage-plugins/manage-plugins.interfaces'
+
@Component({
templateUrl: './donate.component.html',
styleUrls: ['./donate.component.scss'],
@@ -15,9 +17,9 @@ import { TranslatePipe } from '@ngx-translate/core'
export class DonateComponent implements OnInit {
private $activeModal = inject(NgbActiveModal)
- @Input() plugin: any
+ @Input() plugin: Plugin
- public fundingOptions: { type: string, url: string }[]
+ public fundingOptions: PluginFundingOption[]
public ngOnInit(): void {
if (!this.plugin.funding) {
@@ -32,7 +34,7 @@ export class DonateComponent implements OnInit {
// Normalise the different funding attribute formats
if (Array.isArray(this.plugin.funding)) {
// eslint-disable-next-line array-callback-return
- this.fundingOptions = this.plugin.funding.map((option: any) => {
+ this.fundingOptions = this.plugin.funding.map((option: PluginFundingOption | string) => {
if (typeof option === 'string') {
return {
type: 'other',
diff --git a/ui/src/app/core/manage-plugins/manage-plugin/manage-plugin.component.ts b/ui/src/app/core/manage-plugins/manage-plugin/manage-plugin.component.ts
index eab47a709..7c3f2473b 100644
--- a/ui/src/app/core/manage-plugins/manage-plugin/manage-plugin.component.ts
+++ b/ui/src/app/core/manage-plugins/manage-plugin/manage-plugin.component.ts
@@ -22,6 +22,7 @@ import { firstValueFrom } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { RestartHomebridgeComponent } from '@/app/core/components/restart-homebridge/restart-homebridge.component'
import { PluginsMarkdownDirective } from '@/app/core/directives/plugins.markdown.directive'
+import { ChildBridge } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { PluginLogsComponent } from '@/app/core/manage-plugins/plugin-logs/plugin-logs.component'
import { SettingsService } from '@/app/core/settings.service'
import { IoNamespace, WsService } from '@/app/core/ws.service'
@@ -76,7 +77,7 @@ export class ManagePluginComponent implements OnInit, OnDestroy {
public actionFailed = false
public justUpdatedPlugin = false
public updateToBeta = false
- public childBridges: any[] = []
+ public childBridges: ChildBridge[] = []
public presentTenseVerb: string
public pastTenseVerb: string
public onlineUpdateOk: boolean
@@ -221,6 +222,7 @@ export class ManagePluginComponent implements OnInit, OnDestroy {
name: this.pluginName,
displayName: this.pluginDisplayName,
}
+ ref.componentInstance.childBridges = this.childBridges
} catch (error) {
console.error(error)
this.$toastr.error(this.$translate.instant('plugins.manage.child_bridge_restart_failed'), this.$translate.instant('toast.title_error'))
@@ -379,7 +381,7 @@ export class ManagePluginComponent implements OnInit, OnDestroy {
}
private async getChildBridges(): Promise {
- const data: any[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
+ const data: ChildBridge[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
data.forEach((bridge) => {
if (this.pluginName === bridge.plugin) {
this.childBridges.push(bridge)
diff --git a/ui/src/app/core/manage-plugins/manage-plugins.interfaces.ts b/ui/src/app/core/manage-plugins/manage-plugins.interfaces.ts
index f88b27f3b..5095d4332 100644
--- a/ui/src/app/core/manage-plugins/manage-plugins.interfaces.ts
+++ b/ui/src/app/core/manage-plugins/manage-plugins.interfaces.ts
@@ -1,3 +1,55 @@
+export interface PluginFundingOption {
+ type: string
+ url: string
+}
+
+export interface Plugin {
+ author: string
+ description: string
+ disabled: boolean
+ displayName: string
+ engines?: {
+ node?: string
+ homebridge?: string
+ }
+ funding?: PluginFundingOption[] | PluginFundingOption
+ globalInstall: boolean
+ hasChildBridges: boolean
+ hasChildBridgesUnpaired: boolean
+ icon?: string
+ installPath: string
+ installedVersion: string
+ isConfigured: boolean
+ isConfiguredDynamicPlatform: boolean
+ isHbMaintained: boolean
+ isHbScoped: boolean
+ lastUpdated?: string
+ latestVersion: string
+ links: {
+ npm?: string
+ homepage?: string
+ bugs?: string
+ }
+ name: string
+ newHbScope?: {
+ from: string
+ switch: string
+ to: string
+ }
+ private: boolean
+ publicPackage: boolean
+ recommendChildBridge: boolean
+ settingsSchema: boolean
+ updateAvailable: boolean
+ updateEngines?: null | {
+ homebridge?: string
+ node?: string
+ }
+ updateTag: null | string
+ verifiedPlugin: boolean
+ verifiedPlusPlugin: boolean
+}
+
export interface ChildBridge {
identifier: string
manuallyStopped: boolean
@@ -6,6 +58,7 @@ export interface ChildBridge {
pid: number
pin: string
plugin: string
+ port?: number
setupUri: string
status: string
username: string
@@ -47,7 +100,7 @@ export interface PluginSchema {
export interface VersionData {
version: string
engines?: {
- homebridge: string
- node: string
+ homebridge?: string
+ node?: string
}
}
diff --git a/ui/src/app/core/manage-plugins/manage-plugins.service.ts b/ui/src/app/core/manage-plugins/manage-plugins.service.ts
index eaf73c0de..d2d23755d 100644
--- a/ui/src/app/core/manage-plugins/manage-plugins.service.ts
+++ b/ui/src/app/core/manage-plugins/manage-plugins.service.ts
@@ -8,6 +8,7 @@ import { lt, minVersion } from 'semver'
import { ApiService } from '@/app/core/api.service'
import { CustomPluginsService } from '@/app/core/manage-plugins/custom-plugins/custom-plugins.service'
import { ManagePluginComponent } from '@/app/core/manage-plugins/manage-plugin/manage-plugin.component'
+import { ChildBridge, Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { ManageVersionComponent } from '@/app/core/manage-plugins/manage-version/manage-version.component'
import { ManualConfigComponent } from '@/app/core/manage-plugins/manual-config/manual-config.component'
import { PluginBridgeComponent } from '@/app/core/manage-plugins/plugin-bridge/plugin-bridge.component'
@@ -29,7 +30,7 @@ export class ManagePluginsService {
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- installPlugin(plugin: any, targetVersion: string) {
+ installPlugin(plugin: Plugin, targetVersion: string) {
const ref = this.$modal.open(ManagePluginComponent, {
size: 'lg',
backdrop: 'static',
@@ -40,7 +41,7 @@ export class ManagePluginsService {
ref.componentInstance.targetVersion = targetVersion
}
- uninstallPlugin(plugin: any, childBridges: any[]) {
+ uninstallPlugin(plugin: Plugin, childBridges: ChildBridge[]) {
const ref = this.$modal.open(UninstallPluginComponent, {
size: 'lg',
backdrop: 'static',
@@ -50,7 +51,7 @@ export class ManagePluginsService {
ref.componentInstance.childBridges = childBridges
}
- async checkAndUpdatePlugin(plugin: any, targetVersion: string) {
+ async checkAndUpdatePlugin(plugin: Plugin, targetVersion: string) {
if (!await this.checkHbAndNodeVersion(plugin, 'update')) {
return
}
@@ -58,7 +59,7 @@ export class ManagePluginsService {
await this.updatePlugin(plugin, targetVersion)
}
- async updatePlugin(plugin: any, targetVersion: string) {
+ async updatePlugin(plugin: Plugin, targetVersion: string) {
const ref = this.$modal.open(ManagePluginComponent, {
size: 'lg',
backdrop: 'static',
@@ -72,7 +73,7 @@ export class ManagePluginsService {
ref.componentInstance.isDisabled = plugin.disabled
}
- async upgradeHomebridge(homebridgePkg: any, targetVersion: string) {
+ async upgradeHomebridge(homebridgePkg: Plugin, targetVersion: string) {
if (!await this.checkHbAndNodeVersion(homebridgePkg, 'update')) {
return
}
@@ -94,7 +95,7 @@ export class ManagePluginsService {
*
* @param plugin
*/
- async installAlternateVersion(plugin: any) {
+ async installAlternateVersion(plugin: Plugin) {
const ref = this.$modal.open(ManageVersionComponent, {
size: 'lg',
backdrop: 'static',
@@ -127,7 +128,7 @@ export class ManagePluginsService {
* @param plugin
* @param justInstalled
*/
- async bridgeSettings(plugin: any, justInstalled = false) {
+ async bridgeSettings(plugin: Plugin, justInstalled = false) {
// Load the plugins schema
let schema: any
if (plugin.settingsSchema) {
@@ -155,7 +156,7 @@ export class ManagePluginsService {
*
* @param plugin
*/
- async settings(plugin: any) {
+ async settings(plugin: Plugin) {
// Load the plugins schema
let schema: any
if (plugin.settingsSchema) {
@@ -195,7 +196,7 @@ export class ManagePluginsService {
/**
* Open the json config modal
*/
- async jsonEditor(plugin: any) {
+ async jsonEditor(plugin: Plugin) {
// Load the plugins schema
let schema: any
if (plugin.settingsSchema) {
@@ -217,7 +218,7 @@ export class ManagePluginsService {
return ref.result.catch(error => console.error(error))
}
- async checkHbAndNodeVersion(plugin: any, action: string): Promise {
+ async checkHbAndNodeVersion(plugin: Plugin, action: string): Promise {
let isValidNode = true
let isValidHb = true
@@ -265,7 +266,7 @@ export class ManagePluginsService {
/**
* Open the reset child bridges modal
*/
- async resetChildBridges(childBridges: any[]) {
+ async resetChildBridges(childBridges: ChildBridge[]) {
const ref = this.$modal.open(ResetAccessoriesComponent, {
size: 'lg',
backdrop: 'static',
@@ -274,7 +275,7 @@ export class ManagePluginsService {
ref.componentInstance.childBridges = childBridges
}
- async switchToScoped(plugin: any) {
+ async switchToScoped(plugin: Plugin) {
const ref = this.$modal.open(SwitchToScopedComponent, {
size: 'lg',
backdrop: 'static',
diff --git a/ui/src/app/core/manage-plugins/manage-version/manage-version.component.ts b/ui/src/app/core/manage-plugins/manage-version/manage-version.component.ts
index d47e0be71..918566fd1 100644
--- a/ui/src/app/core/manage-plugins/manage-version/manage-version.component.ts
+++ b/ui/src/app/core/manage-plugins/manage-version/manage-version.component.ts
@@ -10,6 +10,7 @@ import { debounceTime } from 'rxjs/operators'
import { rcompare } from 'semver'
import { ApiService } from '@/app/core/api.service'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { SettingsService } from '@/app/core/settings.service'
@Component({
@@ -28,7 +29,7 @@ export class ManageVersionComponent implements OnInit {
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- @Input() plugin: any
+ @Input() plugin: Plugin
public isUpdateHidden: boolean = false
public hideUpdatesFormControl = new FormControl(false)
diff --git a/ui/src/app/core/manage-plugins/manual-config/manual-config.component.ts b/ui/src/app/core/manage-plugins/manual-config/manual-config.component.ts
index ece9381da..e0ee96ee2 100644
--- a/ui/src/app/core/manage-plugins/manual-config/manual-config.component.ts
+++ b/ui/src/app/core/manage-plugins/manual-config/manual-config.component.ts
@@ -1,4 +1,4 @@
-import type { PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
+import type { ChildBridge, PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { Component, inject, Input, OnInit } from '@angular/core'
import { FormsModule } from '@angular/forms'
@@ -23,6 +23,7 @@ import { firstValueFrom } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { RestartChildBridgesComponent } from '@/app/core/components/restart-child-bridges/restart-child-bridges.component'
import { RestartHomebridgeComponent } from '@/app/core/components/restart-homebridge/restart-homebridge.component'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { ManagePluginsService } from '@/app/core/manage-plugins/manage-plugins.service'
import { MobileDetectService } from '@/app/core/mobile-detect.service'
import { SettingsService } from '@/app/core/settings.service'
@@ -55,7 +56,7 @@ export class ManualConfigComponent implements OnInit {
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- @Input() plugin: any
+ @Input() plugin: Plugin
@Input() schema: PluginSchema
public pluginAlias: string
@@ -67,7 +68,7 @@ export class ManualConfigComponent implements OnInit {
public currentBlock: string
public currentBlockIndex: number | null = null
public saveInProgress = false
- public childBridges: any[] = []
+ public childBridges: ChildBridge[] = []
public isFirstSave = false
public monacoEditor: any
public editorOptions: any
@@ -282,7 +283,7 @@ export class ManualConfigComponent implements OnInit {
private async getChildBridges(): Promise {
try {
- const data: any[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
+ const data: ChildBridge[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
data.forEach((bridge) => {
if (this.plugin.name === bridge.plugin) {
this.childBridges.push(bridge)
diff --git a/ui/src/app/core/manage-plugins/plugin-bridge/plugin-bridge.component.ts b/ui/src/app/core/manage-plugins/plugin-bridge/plugin-bridge.component.ts
index 55893f66f..d7a53160f 100644
--- a/ui/src/app/core/manage-plugins/plugin-bridge/plugin-bridge.component.ts
+++ b/ui/src/app/core/manage-plugins/plugin-bridge/plugin-bridge.component.ts
@@ -12,6 +12,7 @@ import { firstValueFrom } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { QrcodeComponent } from '@/app/core/components/qrcode/qrcode.component'
import { RestartHomebridgeComponent } from '@/app/core/components/restart-homebridge/restart-homebridge.component'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { ManagePluginsService } from '@/app/core/manage-plugins/manage-plugins.service'
import { SettingsService } from '@/app/core/settings.service'
@@ -31,13 +32,13 @@ export class PluginBridgeComponent implements OnInit {
private $activeModal = inject(NgbActiveModal)
private $api = inject(ApiService)
private $modal = inject(NgbModal)
- private $plugins = inject(ManagePluginsService)
+ private $plugin = inject(ManagePluginsService)
private $router = inject(Router)
private $settings = inject(SettingsService)
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- @Input() plugin: any
+ @Input() plugin: Plugin
@Input() schema: PluginSchema
@Input() justInstalled = false
@@ -280,11 +281,11 @@ export class PluginBridgeComponent implements OnInit {
this.$activeModal.close()
// Open the plugin config modal
- this.$plugins.settings({
+ this.$plugin.settings({
name: this.plugin.name,
settingsSchema: true,
links: {},
- })
+ } as Plugin)
}
private generateUsername() {
diff --git a/ui/src/app/core/manage-plugins/plugin-compatibility/plugin-compatibility.component.ts b/ui/src/app/core/manage-plugins/plugin-compatibility/plugin-compatibility.component.ts
index 3d6b4915e..5b4312cb4 100644
--- a/ui/src/app/core/manage-plugins/plugin-compatibility/plugin-compatibility.component.ts
+++ b/ui/src/app/core/manage-plugins/plugin-compatibility/plugin-compatibility.component.ts
@@ -3,6 +3,7 @@ import { NgbActiveModal, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe } from '@ngx-translate/core'
import { minVersion, SemVer } from 'semver'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { SettingsService } from '@/app/core/settings.service'
@Component({
@@ -14,7 +15,7 @@ export class PluginCompatibilityComponent implements OnInit {
private $activeModal = inject(NgbActiveModal)
private $settings = inject(SettingsService)
- @Input() plugin: any
+ @Input() plugin: Plugin
@Input() isValidNode: boolean
@Input() isValidHb: boolean
@Input() action: 'install' | 'update' | 'alternate'
diff --git a/ui/src/app/core/manage-plugins/plugin-config/plugin-config.component.ts b/ui/src/app/core/manage-plugins/plugin-config/plugin-config.component.ts
index 810d0a5f5..0e52435a4 100644
--- a/ui/src/app/core/manage-plugins/plugin-config/plugin-config.component.ts
+++ b/ui/src/app/core/manage-plugins/plugin-config/plugin-config.component.ts
@@ -1,4 +1,4 @@
-import type { PluginConfigBlock, PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
+import type { ChildBridge, PluginConfigBlock, PluginSchema } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { NgClass } from '@angular/common'
import { Component, inject, Input, OnInit } from '@angular/core'
@@ -26,6 +26,7 @@ import { SchemaFormComponent } from '@/app/core/components/schema-form/schema-fo
import { PluginsMarkdownDirective } from '@/app/core/directives/plugins.markdown.directive'
import { HomebridgeDeconzComponent } from '@/app/core/manage-plugins/custom-plugins/homebridge-deconz/homebridge-deconz.component'
import { HomebridgeHueComponent } from '@/app/core/manage-plugins/custom-plugins/homebridge-hue/homebridge-hue.component'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { ManagePluginsService } from '@/app/core/manage-plugins/manage-plugins.service'
import { InterpolateMdPipe } from '@/app/core/pipes/interpolate-md.pipe'
import { SettingsService } from '@/app/core/settings.service'
@@ -61,7 +62,7 @@ export class PluginConfigComponent implements OnInit {
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- @Input() plugin: any
+ @Input() plugin: Plugin
@Input() schema: PluginSchema
public pluginAlias: string
@@ -70,7 +71,7 @@ export class PluginConfigComponent implements OnInit {
public form: any = {}
public show = ''
public saveInProgress: boolean
- public childBridges: any[] = []
+ public childBridges: ChildBridge[] = []
public isFirstSave = false
public formBlocksValid: { [key: number]: boolean } = {}
public formIsValid = true
@@ -232,7 +233,7 @@ export class PluginConfigComponent implements OnInit {
private async getChildBridges(): Promise {
try {
- const data: any[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
+ const data: ChildBridge[] = await firstValueFrom(this.$api.get('/status/homebridge/child-bridges'))
data.forEach((bridge) => {
if (this.plugin.name === bridge.plugin) {
this.childBridges.push(bridge)
diff --git a/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.html b/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.html
index 6e84ce0e6..7edde7eae 100644
--- a/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.html
+++ b/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.html
@@ -7,6 +7,7 @@ {{ plugin.displayName || plugin.name }}
data-bs-dismiss="modal"
[attr.aria-label]="'form.button_close' | translate"
(click)="dismissModal()"
+ [disabled]="midAction"
>
@@ -20,13 +21,41 @@
{{ plugin.displayName || plugin.name }}
data-bs-dismiss="modal"
(click)="dismissModal()"
[attr.aria-label]="'form.button_close' | translate"
+ [disabled]="midAction"
>
{{ 'form.button_close' | translate }}
-
+
+ @if (childBridges.length) {
+
+ }
diff --git a/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.ts b/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.ts
index 9c03fec74..782383f05 100644
--- a/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.ts
+++ b/ui/src/app/core/manage-plugins/plugin-logs/plugin-logs.component.ts
@@ -1,19 +1,20 @@
import { HttpErrorResponse, HttpResponse } from '@angular/common/http'
import { Component, ElementRef, HostListener, inject, Input, OnDestroy, OnInit, viewChild } from '@angular/core'
-import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { NgbActiveModal, NgbModal, NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
import { TranslatePipe, TranslateService } from '@ngx-translate/core'
import { saveAs } from 'file-saver'
import { ToastrService } from 'ngx-toastr'
-import { Subject } from 'rxjs'
+import { firstValueFrom, Subject } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { ConfirmComponent } from '@/app/core/components/confirm/confirm.component'
import { LogService } from '@/app/core/log.service'
+import { ChildBridge, Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
@Component({
templateUrl: './plugin-logs.component.html',
standalone: true,
- imports: [TranslatePipe],
+ imports: [TranslatePipe, NgbTooltip],
})
export class PluginLogsComponent implements OnInit, OnDestroy {
private $activeModal = inject(NgbActiveModal)
@@ -25,10 +26,13 @@ export class PluginLogsComponent implements OnInit, OnDestroy {
private resizeEvent = new Subject()
private pluginAlias: string
- @Input() plugin: any
+ @Input() plugin: Plugin
+ @Input() childBridges: ChildBridge[] = []
readonly termTarget = viewChild
('pluginlogoutput')
+ public midAction = false
+
@HostListener('window:resize', ['$event'])
onWindowResize() {
this.resizeEvent.next(undefined)
@@ -38,7 +42,26 @@ export class PluginLogsComponent implements OnInit, OnDestroy {
this.getPluginLog()
}
+ public async restartChildBridges() {
+ this.midAction = true
+ try {
+ for (const bridge of this.childBridges) {
+ await firstValueFrom(this.$api.put(`/server/restart/${bridge.username}`, {}))
+ }
+ this.$toastr.success(
+ this.$translate.instant('plugins.manage.child_bridge_restart'),
+ this.$translate.instant('toast.title_success'),
+ )
+ this.midAction = false
+ } catch (error) {
+ console.error(error)
+ this.$toastr.error(this.$translate.instant('plugins.manage.child_bridge_restart_failed'), this.$translate.instant('toast.title_error'))
+ this.midAction = false
+ }
+ }
+
public downloadLogFile() {
+ this.midAction = true
const ref = this.$modal.open(ConfirmComponent, {
size: 'lg',
backdrop: 'static',
@@ -68,20 +91,21 @@ export class PluginLogsComponent implements OnInit, OnDestroy {
if (line.match(/36m\[.*?\]/)) {
includeNextLine = false
} else {
- // eslint-disable-next-line no-control-regex
+ // eslint-disable-next-line no-control-regex
finalOutput += `${line.replace(/\x1B\[(\d{1,3}(;\d{1,2})?)?[mGK]/g, '')}\r\n`
return
}
}
if (line.includes(`36m[${this.pluginAlias}]`)) {
- // eslint-disable-next-line no-control-regex
+ // eslint-disable-next-line no-control-regex
finalOutput += `${line.replace(/\x1B\[(\d{1,3}(;\d{1,2})?)?[mGK]/g, '')}\r\n`
includeNextLine = true
}
})
saveAs(new Blob([finalOutput], { type: 'text/plain;charset=utf-8' }), `${this.plugin.name}.log.txt`)
+ this.midAction = false
},
error: async (err: HttpErrorResponse) => {
let message: string
@@ -91,10 +115,13 @@ export class PluginLogsComponent implements OnInit, OnDestroy {
console.error(error)
}
this.$toastr.error(message || this.$translate.instant('logs.download.error'), this.$translate.instant('toast.title_error'))
+ this.midAction = false
},
})
})
- .catch(() => { /* do nothing */ })
+ .catch(() => {
+ this.midAction = false
+ })
}
public ngOnDestroy() {
diff --git a/ui/src/app/core/manage-plugins/reset-accessories/reset-accessories.component.html b/ui/src/app/core/manage-plugins/reset-accessories/reset-accessories.component.html
index f9181113c..93725ca9e 100644
--- a/ui/src/app/core/manage-plugins/reset-accessories/reset-accessories.component.html
+++ b/ui/src/app/core/manage-plugins/reset-accessories/reset-accessories.component.html
@@ -91,7 +91,7 @@ {{ 'child_bridge.reset_accessories' | translate }}
[disabled]="!toDelete.length || clicked"
(click)="cleanBridges()"
>
- @if (!clicked) { {{ 'form.button_remove' | translate }} @if (toDelete.length > 0) { ({{ toDelete.length }}) }}
+ @if (!clicked) { {{ 'form.button_reset' | translate }} @if (toDelete.length > 0) { ({{ toDelete.length }}) }}
@if (clicked) {
}
diff --git a/ui/src/app/core/manage-plugins/switch-to-scoped/switch-to-scoped.component.ts b/ui/src/app/core/manage-plugins/switch-to-scoped/switch-to-scoped.component.ts
index a3b0ac7bd..d1680aecd 100644
--- a/ui/src/app/core/manage-plugins/switch-to-scoped/switch-to-scoped.component.ts
+++ b/ui/src/app/core/manage-plugins/switch-to-scoped/switch-to-scoped.component.ts
@@ -10,6 +10,7 @@ import { saveAs } from 'file-saver'
import { ToastrService } from 'ngx-toastr'
import { ApiService } from '@/app/core/api.service'
+import { Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { SettingsService } from '@/app/core/settings.service'
import { IoNamespace, WsService } from '@/app/core/ws.service'
@@ -37,7 +38,7 @@ export class SwitchToScopedComponent implements OnInit, OnDestroy {
private webLinksAddon = new WebLinksAddon()
private errorLog = ''
- @Input() plugin: any
+ @Input() plugin: Plugin
public installing = false
public installed = false
diff --git a/ui/src/app/core/manage-plugins/uninstall-plugin/uninstall-plugin.component.ts b/ui/src/app/core/manage-plugins/uninstall-plugin/uninstall-plugin.component.ts
index de8099be2..ce1e11c4f 100644
--- a/ui/src/app/core/manage-plugins/uninstall-plugin/uninstall-plugin.component.ts
+++ b/ui/src/app/core/manage-plugins/uninstall-plugin/uninstall-plugin.component.ts
@@ -7,6 +7,7 @@ import { firstValueFrom } from 'rxjs'
import { ApiService } from '@/app/core/api.service'
import { ManagePluginComponent } from '@/app/core/manage-plugins/manage-plugin/manage-plugin.component'
+import { ChildBridge, Plugin } from '@/app/core/manage-plugins/manage-plugins.interfaces'
import { SettingsService } from '@/app/core/settings.service'
@Component({
@@ -26,8 +27,8 @@ export class UninstallPluginComponent implements OnInit {
private $toastr = inject(ToastrService)
private $translate = inject(TranslateService)
- @Input() plugin: any
- @Input() childBridges: any[]
+ @Input() plugin: Plugin
+ @Input() childBridges: ChildBridge[]
@Input() action: string
public loading = true
diff --git a/ui/src/app/core/settings.interfaces.ts b/ui/src/app/core/settings.interfaces.ts
index 6e360bcdc..593485f01 100644
--- a/ui/src/app/core/settings.interfaces.ts
+++ b/ui/src/app/core/settings.interfaces.ts
@@ -47,6 +47,11 @@ export interface EnvInterface {
shutdown?: string
restart?: string
}
+ terminal?: {
+ persistence?: boolean
+ hideWarning?: boolean
+ bufferSize?: number
+ }
host?: string
proxyHost?: string
homebridgePackagePath?: string
@@ -62,4 +67,5 @@ export interface AppSettingsInterface {
menuMode: 'default' | 'freeze'
wallpaper: string
serverTimestamp: string
+ keepOrphans: boolean
}
diff --git a/ui/src/app/core/settings.service.ts b/ui/src/app/core/settings.service.ts
index 93ff3ab66..f2040a71a 100644
--- a/ui/src/app/core/settings.service.ts
+++ b/ui/src/app/core/settings.service.ts
@@ -31,6 +31,7 @@ export class SettingsService {
public actualLightingMode: 'light' | 'dark'
public browserLightingMode: 'light' | 'dark'
public menuMode: 'default' | 'freeze'
+ public keepOrphans: boolean
public wallpaper: string
public serverTimeOffset = 0
public rtl = false // set true if current translation is RLT
@@ -66,6 +67,7 @@ export class SettingsService {
this.setLightingMode(this.lightingMode, 'user')
this.setTheme(data.theme)
this.setMenuMode(data.menuMode)
+ this.setKeepOrphans(data.keepOrphans)
this.setTitle(this.env.homebridgeInstanceName)
this.checkServerTime(data.serverTimestamp)
this.setUiVersion(data.env.packageVersion)
@@ -131,20 +133,32 @@ export class SettingsService {
if (iframeDoc) {
const iframeBody = iframeDoc.body
- iframeBody.classList.remove(`config-ui-x-${this.theme}`)
- iframeBody.classList.remove(`config-ui-x-dark-mode-${this.theme}`)
if (this.actualLightingMode === 'dark') {
- iframeBody.classList.add(`config-ui-x-dark-mode-${this.theme}`)
-
+ if (iframeBody.classList.contains(`config-ui-x-${this.theme}`)) {
+ iframeBody.classList.remove(`config-ui-x-${this.theme}`)
+ }
+ if (!iframeBody.classList.contains(`config-ui-x-dark-mode-${this.theme}`)) {
+ iframeBody.classList.add(`config-ui-x-dark-mode-${this.theme}`)
+ }
if (!iframeBody.classList.contains('dark-mode')) {
iframeBody.classList.add('dark-mode')
}
- } else {
- iframeBody.classList.add(`config-ui-x-${this.theme}`)
+ iframeBody.style.backgroundColor = '#242424 !important'
+ iframeBody.style.color = '#ffffff !important'
+ } else {
+ if (!iframeBody.classList.contains(`config-ui-x-${this.theme}`)) {
+ iframeBody.classList.add(`config-ui-x-${this.theme}`)
+ }
+ if (iframeBody.classList.contains(`config-ui-x-dark-mode-${this.theme}`)) {
+ iframeBody.classList.remove(`config-ui-x-dark-mode-${this.theme}`)
+ }
if (iframeBody.classList.contains('dark-mode')) {
iframeBody.classList.remove('dark-mode')
}
+
+ iframeBody.style.backgroundColor = '#ffffff !important'
+ iframeBody.style.color = '#000000 !important'
}
// Notify iframe Angular app
@@ -163,6 +177,10 @@ export class SettingsService {
this.menuMode = value
}
+ public setKeepOrphans(value: boolean) {
+ this.keepOrphans = value
+ }
+
public setLang(lang: string) {
if (lang) {
this.$translate.use(lang)
diff --git a/ui/src/app/core/terminal-navigation-guard.service.ts b/ui/src/app/core/terminal-navigation-guard.service.ts
new file mode 100644
index 000000000..4c6fcbe07
--- /dev/null
+++ b/ui/src/app/core/terminal-navigation-guard.service.ts
@@ -0,0 +1,69 @@
+import { inject, Injectable } from '@angular/core'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { TranslateService } from '@ngx-translate/core'
+
+import { ConfirmComponent } from '@/app/core/components/confirm/confirm.component'
+import { SettingsService } from '@/app/core/settings.service'
+import { TerminalService } from '@/app/core/terminal.service'
+
+@Injectable({
+ providedIn: 'root',
+})
+export class TerminalNavigationGuardService {
+ private $terminal = inject(TerminalService)
+ private $settings = inject(SettingsService)
+ private $modal = inject(NgbModal)
+ private $translate = inject(TranslateService)
+
+ public handleBeforeUnload(event: BeforeUnloadEvent): string | undefined {
+ // Only show warning if persistence is disabled, warning is enabled, there's an active session, and user has typed
+ if (!this.$settings.env.terminal?.persistence
+ && !this.$settings.env.terminal?.hideWarning
+ && this.$terminal.hasActiveSession()
+ && this.$terminal.hasUserTypedInSession()) {
+ const message = this.$translate.instant('platform.terminal.terminate_unload')
+ event.preventDefault()
+ event.returnValue = message
+ return message // For other browsers
+ }
+ return undefined
+ }
+
+ public canDeactivate(): Promise | boolean {
+ // If persistence is enabled, allow navigation without prompt
+ if (this.$settings.env.terminal?.persistence) {
+ return true
+ }
+
+ // If warning is disabled, allow navigation without prompt (preserve current behavior)
+ if (this.$settings.env.terminal?.hideWarning) {
+ return true
+ }
+
+ // If there's no active session, allow navigation without prompt
+ if (!this.$terminal.hasActiveSession()) {
+ return true
+ }
+
+ // If user hasn't typed anything, allow navigation without prompt
+ if (!this.$terminal.hasUserTypedInSession()) {
+ return true
+ }
+
+ // Show confirmation dialog when persistence is disabled, warning is enabled, there's an active session, and user has typed
+ const ref = this.$modal.open(ConfirmComponent, {
+ size: 'lg',
+ backdrop: 'static',
+ })
+
+ ref.componentInstance.title = this.$translate.instant('platform.terminal.terminate_title')
+ ref.componentInstance.message = this.$translate.instant('platform.terminal.terminate_message_1')
+ ref.componentInstance.message2 = this.$translate.instant('platform.terminal.terminate_message_2')
+ ref.componentInstance.message3 = this.$translate.instant('common.phrases.are_you_sure')
+ ref.componentInstance.confirmButtonLabel = this.$translate.instant('form.button_continue')
+ ref.componentInstance.confirmButtonClass = 'btn-primary'
+ ref.componentInstance.faIconClass = 'fas fa-exclamation-triangle text-warning'
+
+ return ref.result.then(() => true).catch(() => false)
+ }
+}
diff --git a/ui/src/app/core/terminal.service.ts b/ui/src/app/core/terminal.service.ts
index 957217a32..b927d1577 100644
--- a/ui/src/app/core/terminal.service.ts
+++ b/ui/src/app/core/terminal.service.ts
@@ -1,10 +1,11 @@
import { ElementRef, inject, Injectable } from '@angular/core'
import { FitAddon } from '@xterm/addon-fit'
import { WebLinksAddon } from '@xterm/addon-web-links'
-import { ITerminalOptions, Terminal } from '@xterm/xterm'
+import { IDisposable, ITerminalOptions, Terminal } from '@xterm/xterm'
import { Subject } from 'rxjs'
import { debounceTime } from 'rxjs/operators'
+import { ApiService } from '@/app/core/api.service'
import { IoNamespace, WsService } from '@/app/core/ws.service'
@Injectable({
@@ -12,28 +13,199 @@ import { IoNamespace, WsService } from '@/app/core/ws.service'
})
export class TerminalService {
private $ws = inject(WsService)
+ private $api = inject(ApiService)
private io: IoNamespace
private fitAddon: FitAddon
private webLinksAddon: WebLinksAddon
private resize: Subject
private elementResize: Subject | undefined
-
+ private dataDisposable: IDisposable | null = null
+ private isInitializing = false
+ private hasUserTyped = false
public term: Terminal
public destroyTerminal() {
- this.io.end()
- this.term.dispose()
- this.resize.complete()
+ if (this.dataDisposable) {
+ this.dataDisposable.dispose()
+ this.dataDisposable = null
+ }
+ if (this.io) {
+ this.io.end()
+ }
+ if (this.term) {
+ this.term.dispose()
+ this.term = null
+ }
+ if (this.resize) {
+ this.resize.complete()
+ }
if (this.elementResize) {
this.elementResize.complete()
}
+ this.isInitializing = false
+ this.hasUserTyped = false
+ }
+
+ public destroyPersistentSession() {
+ // First destroy the frontend terminal
+ this.destroyTerminal()
+
+ // Then tell the backend to destroy the persistent session via HTTP API
+ this.$api.post('/platform-tools/terminal/destroy-persistent-session', {}).subscribe({
+ error: error => console.error('Failed to destroy persistent session:', error),
+ })
+ }
+
+ public detachTerminal() {
+ // Clean up UI components but keep socket connection alive for persistence
+ if (this.dataDisposable) {
+ this.dataDisposable.dispose()
+ this.dataDisposable = null
+ }
+ if (this.term) {
+ this.term.dispose()
+ }
+ if (this.resize) {
+ this.resize.complete()
+ }
+ if (this.elementResize) {
+ this.elementResize.complete()
+ }
+ // Note: We intentionally do NOT call this.io.end() here to keep the connection alive
+ // Keep hasUserTyped state for persistence mode
+
+ this.isInitializing = false
+ }
+
+ public hasActiveSession(): boolean {
+ return this.io && this.io.socket && this.io.socket.connected
+ }
+
+ public async checkBackendPersistentSession(): Promise {
+ try {
+ const response = await this.$api.get('/platform-tools/terminal/has-persistent-session').toPromise() as { hasPersistentSession: boolean }
+ return response.hasPersistentSession
+ } catch (error) {
+ console.error('Failed to check backend persistent session:', error)
+ return false
+ }
+ }
+
+ public hasUserTypedInSession(): boolean {
+ return this.hasUserTyped
+ }
+
+ public isTerminalReady(): boolean {
+ return this.term && !this.isInitializing
+ }
+
+ public reconnectTerminal(
+ targetElement: ElementRef,
+ termOpts: ITerminalOptions = {},
+ elementResize?: Subject,
+ ): boolean {
+ if (this.isInitializing) {
+ return false
+ }
+
+ this.isInitializing = true
+
+ // Handle element resize events
+ this.elementResize = elementResize
+
+ // Reuse existing connection if still active
+ if (this.io && this.io.socket && this.io.socket.connected) {
+ // Create a new terminal instance for the UI
+ this.term = new Terminal(termOpts)
+
+ // Load addons
+ this.fitAddon = new FitAddon()
+ this.webLinksAddon = new WebLinksAddon()
+
+ setTimeout(() => {
+ this.term.loadAddon(this.fitAddon)
+ this.term.loadAddon(this.webLinksAddon)
+ })
+
+ // Create a subject to listen for resize events
+ this.resize = new Subject()
+
+ // Open the terminal in the target element
+ this.term.open(targetElement.nativeElement)
+
+ // Fit to the element
+ setTimeout(() => {
+ this.fitAddon.activate(this.term)
+ this.fitAddon.fit()
+ })
+
+ // Remove existing listeners to avoid duplicates
+ this.io.socket.removeAllListeners('stdout')
+ this.io.socket.removeAllListeners('process-exit')
+
+ // Subscribe to incoming data events from server to client
+ this.io.socket.on('stdout', (data: string) => {
+ this.term.write(data)
+ })
+
+ // Handle terminal process exit - immediately start new session
+ this.io.socket.on('process-exit', () => {
+ this.startSession()
+ })
+
+ // Handle outgoing data events from client to server
+ // Dispose any existing data listener first
+ if (this.dataDisposable) {
+ this.dataDisposable.dispose()
+ }
+ this.dataDisposable = this.term.onData((data) => {
+ this.hasUserTyped = true
+ this.io.socket.emit('stdin', data)
+ })
+
+ // Handle resize events from the client
+ this.term.onResize((size) => {
+ this.resize.next(size)
+ })
+
+ // Send resize events to server
+ this.resize.pipe(debounceTime(500)).subscribe((size) => {
+ this.io.socket.emit('resize', size)
+ })
+
+ if (this.elementResize) {
+ // Subscribe to grid resize event
+ this.elementResize.pipe(debounceTime(100)).subscribe({
+ next: () => {
+ this.fitAddon.fit()
+ },
+ })
+ }
+
+ // Rejoin the existing session
+ this.io.socket.emit('start-session', {
+ cols: this.term.cols,
+ rows: this.term.rows,
+ })
+
+ this.isInitializing = false
+ } else {
+ // No active connection, start fresh
+ this.startTerminal(targetElement, termOpts, elementResize)
+ }
}
public startTerminal(
targetElement: ElementRef,
termOpts: ITerminalOptions = {},
elementResize?: Subject,
- ) {
+ ): boolean {
+ if (this.isInitializing) {
+ return false
+ }
+
+ this.isInitializing = true
+
// Handle element resize events
this.elementResize = elementResize
@@ -71,12 +243,13 @@ export class TerminalService {
// Handle disconnect events
this.io.socket.on('disconnect', () => {
- this.term.write('\n\r\n\rTerminal disconnected. Is the server running?\n\r\n\r')
+ this.term.write(
+ '\n\r\n\rTerminal disconnected. Is the server running?\n\r\n\r',
+ )
})
- // Handle the events
+ // Handle terminal process exit - immediately start new session
this.io.socket.on('process-exit', () => {
- this.io.socket.emit('end')
this.startSession()
})
@@ -91,7 +264,8 @@ export class TerminalService {
})
// Handle outgoing data events from client to server
- this.term.onData((data) => {
+ this.dataDisposable = this.term.onData((data) => {
+ this.hasUserTyped = true
this.io.socket.emit('stdin', data)
})
@@ -108,11 +282,17 @@ export class TerminalService {
},
})
}
+ return true
}
private startSession() {
this.term.reset()
- this.io.socket.emit('start-session', { cols: this.term.cols, rows: this.term.rows })
+ this.hasUserTyped = false
+ this.io.socket.emit('start-session', {
+ cols: this.term.cols,
+ rows: this.term.rows,
+ })
this.resize.next({ cols: this.term.cols, rows: this.term.rows })
+ this.isInitializing = false
}
}
diff --git a/ui/src/app/modules/config-editor/config-editor.component.html b/ui/src/app/modules/config-editor/config-editor.component.html
index 2f6378dde..931ff95e5 100644
--- a/ui/src/app/modules/config-editor/config-editor.component.html
+++ b/ui/src/app/modules/config-editor/config-editor.component.html
@@ -4,20 +4,7 @@
{{ 'menu.config_json_editor' | translate }}
- @if (!originalConfig) {
-
- } @if (originalConfig) {
+ @if (originalConfig) {