Compare commits

...

13 Commits

Author SHA1 Message Date
github-actions[bot]
79c57b8ed0 chore(main): release 4.9.3 (#1495)
🤖 I have created a release *beep* *boop*
---


## [4.9.3](https://github.com/unraid/api/compare/v4.9.2...v4.9.3)
(2025-07-09)


### Bug Fixes

* duplicated header logo after api stops
([#1493](https://github.com/unraid/api/issues/1493))
([4168f43](4168f43e3e))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-09 11:06:47 -04:00
Pujit Mehrotra
4168f43e3e fix: duplicated header logo after api stops (#1493)
Move legacy header logo omission from API file modifier to plg install
step to avoid breaking rollback (ie upon `unraid-api stop`) due to web
component upgrade.

Tested on 7.1.4 & 7.2.0-beta.0.16

Testing & Reproduction procedure:

1. Install connect plugin
2. Run `unraid-api stop` on server
3. Refresh page. Before Unraid 7.2, Plugin versions prior to this will
display a duplicated logo that blocks the nav menu. Now, it will not.
Plugin uninstall behavior remains unchanged.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* The plugin installation process now updates the header logo by
removing the old logo from the interface during installation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 10:59:10 -04:00
github-actions[bot]
20de3ec8d6 chore(main): release 4.9.2 (#1488)
🤖 I have created a release *beep* *boop*
---


## [4.9.2](https://github.com/unraid/api/compare/v4.9.1...v4.9.2)
(2025-07-09)


### Bug Fixes

* invalid configs no longer crash API
([#1491](https://github.com/unraid/api/issues/1491))
([6bf3f77](6bf3f77638))
* invalid state for unraid plugin
([#1492](https://github.com/unraid/api/issues/1492))
([39b8f45](39b8f453da))
* release note escaping
([5b6bcb6](5b6bcb6043))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-09 09:23:25 -04:00
Eli Bosley
39b8f453da fix: invalid state for unraid plugin (#1492)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved connection status handling by introducing a new service that
writes connection status to a JSON file for enhanced integration.
* Updated system components to read connection status and allowed
origins from the new JSON file, ensuring more reliable and up-to-date
information.

* **Chores**
* Expanded allowed Bash command permissions to include commands starting
with "mv:".
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 09:21:43 -04:00
Eli Bosley
6bf3f77638 fix: invalid configs no longer crash API (#1491)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved error handling when loading and parsing configuration files,
preventing crashes and ensuring fallback to default settings if issues
occur.
* Enhanced logging for configuration errors, including warnings for
empty files and detailed error messages for JSON parsing failures.
* Added error handling to plugin listing to avoid failures when
configuration loading encounters errors.

* **Chores**
* Updated permissions to allow linting only for the `./api` package
using a filtered command.

* **Tests**
* Added comprehensive tests for configuration loading, parsing,
persistence, and updating, covering various file states and error
scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-09 08:50:43 -04:00
Eli Bosley
a79d049865 chore: lint for renovate PR (#1481) 2025-07-08 17:01:49 -04:00
Eli Bosley
5b6bcb6043 fix: release note escaping 2025-07-08 16:41:05 -04:00
github-actions[bot]
6ee3cae962 chore(main): release 4.9.1 (#1487)
🤖 I have created a release *beep* *boop*
---


## [4.9.1](https://github.com/unraid/api/compare/v4.9.0...v4.9.1)
(2025-07-08)


### Bug Fixes

* **HeaderOsVersion:** adjust top margin for header component
([#1485](https://github.com/unraid/api/issues/1485))
([862b54d](862b54de8c))
* sign out doesn't work
([#1486](https://github.com/unraid/api/issues/1486))
([f3671c3](f3671c3e07))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 16:30:00 -04:00
Eli Bosley
f3671c3e07 fix: sign out doesn't work (#1486)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Improved handling of Connect account sign-in and sign-out with
persistent mutation instances for better status updates and error
reporting.

* **Chores**
* Expanded allowed command patterns in configuration for development,
build, and testing tasks.

* **Tests**
* Enhanced mutation mocks in component tests to increase test
reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 16:28:38 -04:00
Zack Spear
862b54de8c fix(HeaderOsVersion): adjust top margin for header component (#1485)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
  * Increased top margin in the OS version header for improved spacing.
* Updated user profile container to use padding instead of margin for
better layout consistency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 16:11:02 -04:00
github-actions[bot]
9624ca5c39 chore(main): release 4.9.0 (#1378)
🤖 I have created a release *beep* *boop*
---


## [4.9.0](https://github.com/unraid/api/compare/v4.8.0...v4.9.0)
(2025-07-08)


### Features

* add graphql resource for API plugins
([#1420](https://github.com/unraid/api/issues/1420))
([642a220](642a220c3a))
* add management page for API keys
([#1408](https://github.com/unraid/api/issues/1408))
([0788756](0788756b91))
* add rclone ([#1362](https://github.com/unraid/api/issues/1362))
([5517e75](5517e7506b))
* API key management
([#1407](https://github.com/unraid/api/issues/1407))
([d37dc3b](d37dc3bce2))
* api plugin management via CLI
([#1416](https://github.com/unraid/api/issues/1416))
([3dcbfbe](3dcbfbe489))
* build out docker components
([#1427](https://github.com/unraid/api/issues/1427))
([711cc9a](711cc9ac92))
* docker and info resolver issues
([#1423](https://github.com/unraid/api/issues/1423))
([9901039](9901039a38))
* fix shading in UPC to be less severe
([#1438](https://github.com/unraid/api/issues/1438))
([b7c2407](b7c2407840))
* info resolver cleanup
([#1425](https://github.com/unraid/api/issues/1425))
([1b279bb](1b279bbab3))
* initial codeql setup
([#1390](https://github.com/unraid/api/issues/1390))
([2ade7eb](2ade7eb527))
* initialize claude code in codebse
([#1418](https://github.com/unraid/api/issues/1418))
([b6c4ee6](b6c4ee6eb4))
* move api key fetching to use api key service
([#1439](https://github.com/unraid/api/issues/1439))
([86bea56](86bea56272))
* move to cron v4 ([#1428](https://github.com/unraid/api/issues/1428))
([b8035c2](b8035c207a))
* move to iframe for changelog
([#1388](https://github.com/unraid/api/issues/1388))
([fcd6fbc](fcd6fbcdd4))
* native slackware package
([#1381](https://github.com/unraid/api/issues/1381))
([4f63b4c](4f63b4cf3b))
* send active unraid theme to docs
([#1400](https://github.com/unraid/api/issues/1400))
([f71943b](f71943b62b))
* slightly better watch mode
([#1398](https://github.com/unraid/api/issues/1398))
([881f1e0](881f1e0960))
* upgrade nuxt-custom-elements
([#1461](https://github.com/unraid/api/issues/1461))
([345e83b](345e83bfb0))
* use bigint instead of long
([#1403](https://github.com/unraid/api/issues/1403))
([574d572](574d572d65))


### Bug Fixes

* activation indicator removed
([5edfd82](5edfd823b8))
* alignment of settings on ManagementAccess settings page
([#1421](https://github.com/unraid/api/issues/1421))
([70c790f](70c790ff89))
* allow rclone to fail to initialize
([#1453](https://github.com/unraid/api/issues/1453))
([7c6f02a](7c6f02a5cb))
* always download 7.1 versioned files for patching
([edc0d15](edc0d1578b))
* api `pnpm type-check`
([#1442](https://github.com/unraid/api/issues/1442))
([3122bdb](3122bdb953))
* **api:** connect config `email` validation
([#1454](https://github.com/unraid/api/issues/1454))
([b9a1b9b](b9a1b9b087))
* backport
unraid/webgui[#2269](https://github.com/unraid/api/issues/2269) rc.nginx
update ([#1436](https://github.com/unraid/api/issues/1436))
([a7ef06e](a7ef06ea25))
* bigint
([e54d27a](e54d27aede))
* config migration from `myservers.cfg`
([#1440](https://github.com/unraid/api/issues/1440))
([c4c9984](c4c99843c7))
* **connect:** fatal race-condition in websocket disposal
([#1462](https://github.com/unraid/api/issues/1462))
([0ec0de9](0ec0de982f))
* **connect:** mothership connection
([#1464](https://github.com/unraid/api/issues/1464))
([7be8bc8](7be8bc84d3))
* console hidden
([9b85e00](9b85e009b8))
* debounce is too long
([#1426](https://github.com/unraid/api/issues/1426))
([f12d231](f12d231e63))
* delete legacy connect keys and ensure description
([22fe91c](22fe91cd56))
* **deps:** pin dependencies
([#1465](https://github.com/unraid/api/issues/1465))
([ba75a40](ba75a409a4))
* **deps:** pin dependencies
([#1470](https://github.com/unraid/api/issues/1470))
([412b329](412b32996d))
* **deps:** storybook v9
([#1476](https://github.com/unraid/api/issues/1476))
([45bb49b](45bb49bcd6))
* **deps:** update all non-major dependencies
([#1366](https://github.com/unraid/api/issues/1366))
([291ee47](291ee475fb))
* **deps:** update all non-major dependencies
([#1379](https://github.com/unraid/api/issues/1379))
([8f70326](8f70326d0f))
* **deps:** update all non-major dependencies
([#1389](https://github.com/unraid/api/issues/1389))
([cb43f95](cb43f95233))
* **deps:** update all non-major dependencies
([#1399](https://github.com/unraid/api/issues/1399))
([68df344](68df344a4b))
* **deps:** update dependency @types/diff to v8
([#1393](https://github.com/unraid/api/issues/1393))
([00da27d](00da27d04f))
* **deps:** update dependency cache-manager to v7
([#1413](https://github.com/unraid/api/issues/1413))
([9492c2a](9492c2ae6a))
* **deps:** update dependency commander to v14
([#1394](https://github.com/unraid/api/issues/1394))
([106ea09](106ea09399))
* **deps:** update dependency diff to v8
([#1386](https://github.com/unraid/api/issues/1386))
([e580f64](e580f646a5))
* **deps:** update dependency dotenv to v17
([#1474](https://github.com/unraid/api/issues/1474))
([d613bfa](d613bfa041))
* **deps:** update dependency lucide-vue-next to ^0.509.0
([#1383](https://github.com/unraid/api/issues/1383))
([469333a](469333acd4))
* **deps:** update dependency marked to v16
([#1444](https://github.com/unraid/api/issues/1444))
([453a5b2](453a5b2c95))
* **deps:** update dependency shadcn-vue to v2
([#1302](https://github.com/unraid/api/issues/1302))
([26ecf77](26ecf779e6))
* **deps:** update dependency vue-sonner to v2
([#1401](https://github.com/unraid/api/issues/1401))
([53ca414](53ca41404f))
* disable file changes on Unraid 7.2
([#1382](https://github.com/unraid/api/issues/1382))
([02de89d](02de89d130))
* do not start API with doinst.sh
([7d88b33](7d88b3393c))
* do not uninstall fully on 7.2
([#1484](https://github.com/unraid/api/issues/1484))
([2263881](22638811a9))
* drop console with terser
([a87d455](a87d455bac))
* error logs from `cloud` query when connect is not installed
([#1450](https://github.com/unraid/api/issues/1450))
([719f460](719f460016))
* flash backup integration with Unraid Connect config
([#1448](https://github.com/unraid/api/issues/1448))
([038c582](038c582aed))
* header padding regression
([#1477](https://github.com/unraid/api/issues/1477))
([e791cc6](e791cc680d))
* incorrect state merging in redux store
([#1437](https://github.com/unraid/api/issues/1437))
([17b7428](17b7428779))
* lanip copy button not present
([#1459](https://github.com/unraid/api/issues/1459))
([a280786](a2807864ac))
* move to bigint scalar
([b625227](b625227913))
* node_modules dir removed on plugin update
([#1406](https://github.com/unraid/api/issues/1406))
([7b005cb](7b005cbbf6))
* omit Connect actions in UPC when plugin is not installed
([#1417](https://github.com/unraid/api/issues/1417))
([8c8a527](8c8a5276b4))
* parsing of `ssoEnabled` in state.php
([#1455](https://github.com/unraid/api/issues/1455))
([f542c8e](f542c8e0bd))
* pin ranges ([#1460](https://github.com/unraid/api/issues/1460))
([f88400e](f88400eea8))
* pr plugin promotion workflow
([#1456](https://github.com/unraid/api/issues/1456))
([13bd9bb](13bd9bb567))
* proper fallback if missing paths config modules
([7067e9e](7067e9e3dd))
* rc.unraid-api now cleans up older dependencies
([#1404](https://github.com/unraid/api/issues/1404))
([83076bb](83076bb940))
* remote access lifecycle during boot & shutdown
([#1422](https://github.com/unraid/api/issues/1422))
([7bc583b](7bc583b186))
* sign out correctly on error
([#1452](https://github.com/unraid/api/issues/1452))
([d08fc94](d08fc94afb))
* simplify usb listing
([#1402](https://github.com/unraid/api/issues/1402))
([5355115](5355115af2))
* theme issues when sent from graph
([#1424](https://github.com/unraid/api/issues/1424))
([75ad838](75ad8381bd))
* **ui:** notifications positioning regression
([#1445](https://github.com/unraid/api/issues/1445))
([f73e5e0](f73e5e0058))
* use some instead of every for connect detection
([9ce2fee](9ce2fee380))


### Reverts

* revert package.json dependency updates from commit 711cc9a for api and
packages/*
([94420e4](94420e4d45))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-08 15:45:49 -04:00
Eli Bosley
22638811a9 fix: do not uninstall fully on 7.2 (#1484)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Uninstallation process now adapts based on Unraid version: for version
7.2 or higher, users receive a notification and are prompted to reboot
to complete plugin removal.
* **Bug Fixes**
* Improved handling of plugin removal to ensure compatibility with
different Unraid versions and prevent unintended reinstalls.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-08 15:44:30 -04:00
Eli Bosley
5edfd823b8 fix: activation indicator removed 2025-07-08 15:42:41 -04:00
35 changed files with 1052 additions and 183 deletions

View File

@@ -2,7 +2,16 @@
"permissions": {
"allow": [
"Bash(rg:*)",
"Bash(find:*)"
"Bash(find:*)",
"Bash(pnpm codegen:*)",
"Bash(pnpm dev:*)",
"Bash(pnpm build:*)",
"Bash(pnpm test:*)",
"Bash(grep:*)",
"Bash(pnpm type-check:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm --filter ./api lint)",
"Bash(mv:*)"
]
},
"enableAllProjectMcpServers": false

View File

@@ -32,7 +32,9 @@ jobs:
with:
node-version: '22.17.0'
- run: |
echo '${{ steps.release-info.outputs.body }}' >> release-notes.txt
cat << 'EOF' > release-notes.txt
${{ steps.release-info.outputs.body }}
EOF
- run: npm install html-escaper@2 xml2js
- name: Update Plugin Changelog
uses: actions/github-script@v7

View File

@@ -1 +1 @@
{".":"4.8.0"}
{".":"4.9.3"}

View File

@@ -1,5 +1,116 @@
# Changelog
## [4.9.3](https://github.com/unraid/api/compare/v4.9.2...v4.9.3) (2025-07-09)
### Bug Fixes
* duplicated header logo after api stops ([#1493](https://github.com/unraid/api/issues/1493)) ([4168f43](https://github.com/unraid/api/commit/4168f43e3ecd51479bec3aae585abbe6dcd3e416))
## [4.9.2](https://github.com/unraid/api/compare/v4.9.1...v4.9.2) (2025-07-09)
### Bug Fixes
* invalid configs no longer crash API ([#1491](https://github.com/unraid/api/issues/1491)) ([6bf3f77](https://github.com/unraid/api/commit/6bf3f776380edeff5133517e6aca223556e30144))
* invalid state for unraid plugin ([#1492](https://github.com/unraid/api/issues/1492)) ([39b8f45](https://github.com/unraid/api/commit/39b8f453da23793ef51f8e7f7196370aada8c5aa))
* release note escaping ([5b6bcb6](https://github.com/unraid/api/commit/5b6bcb6043a5269bff4dc28714d787a5a3f07e22))
## [4.9.1](https://github.com/unraid/api/compare/v4.9.0...v4.9.1) (2025-07-08)
### Bug Fixes
* **HeaderOsVersion:** adjust top margin for header component ([#1485](https://github.com/unraid/api/issues/1485)) ([862b54d](https://github.com/unraid/api/commit/862b54de8cd793606f1d29e76c19d4a0e1ae172f))
* sign out doesn't work ([#1486](https://github.com/unraid/api/issues/1486)) ([f3671c3](https://github.com/unraid/api/commit/f3671c3e0750b79be1f19655a07a0e9932289b3f))
## [4.9.0](https://github.com/unraid/api/compare/v4.8.0...v4.9.0) (2025-07-08)
### Features
* add graphql resource for API plugins ([#1420](https://github.com/unraid/api/issues/1420)) ([642a220](https://github.com/unraid/api/commit/642a220c3a796829505d8449dc774968c9d5c222))
* add management page for API keys ([#1408](https://github.com/unraid/api/issues/1408)) ([0788756](https://github.com/unraid/api/commit/0788756b918a8e99be51f34bf6f96bbe5b67395a))
* add rclone ([#1362](https://github.com/unraid/api/issues/1362)) ([5517e75](https://github.com/unraid/api/commit/5517e7506b05c7bef5012bb9f8d2103e91061997))
* API key management ([#1407](https://github.com/unraid/api/issues/1407)) ([d37dc3b](https://github.com/unraid/api/commit/d37dc3bce28bad1c893ae7eff96ca5ffd9177648))
* api plugin management via CLI ([#1416](https://github.com/unraid/api/issues/1416)) ([3dcbfbe](https://github.com/unraid/api/commit/3dcbfbe48973b8047f0c6c560068808d86ac6970))
* build out docker components ([#1427](https://github.com/unraid/api/issues/1427)) ([711cc9a](https://github.com/unraid/api/commit/711cc9ac926958bcf2996455b023ad265b041530))
* docker and info resolver issues ([#1423](https://github.com/unraid/api/issues/1423)) ([9901039](https://github.com/unraid/api/commit/9901039a3863de06b520e23cb2573b610716c673))
* fix shading in UPC to be less severe ([#1438](https://github.com/unraid/api/issues/1438)) ([b7c2407](https://github.com/unraid/api/commit/b7c240784052276fc60e064bd7d64dd6e801ae90))
* info resolver cleanup ([#1425](https://github.com/unraid/api/issues/1425)) ([1b279bb](https://github.com/unraid/api/commit/1b279bbab3a51e7d032e7e3c9898feac8bfdbafa))
* initial codeql setup ([#1390](https://github.com/unraid/api/issues/1390)) ([2ade7eb](https://github.com/unraid/api/commit/2ade7eb52792ef481aaf711dc07029629ea107d9))
* initialize claude code in codebse ([#1418](https://github.com/unraid/api/issues/1418)) ([b6c4ee6](https://github.com/unraid/api/commit/b6c4ee6eb4b9ebb6d6e59a341e1f51b253578752))
* move api key fetching to use api key service ([#1439](https://github.com/unraid/api/issues/1439)) ([86bea56](https://github.com/unraid/api/commit/86bea5627270a2a18c5b7db36dd59061ab61e753))
* move to cron v4 ([#1428](https://github.com/unraid/api/issues/1428)) ([b8035c2](https://github.com/unraid/api/commit/b8035c207a6e387c7af3346593a872664f6c867b))
* move to iframe for changelog ([#1388](https://github.com/unraid/api/issues/1388)) ([fcd6fbc](https://github.com/unraid/api/commit/fcd6fbcdd48e7f224b3bd8799a668d9e01967f0c))
* native slackware package ([#1381](https://github.com/unraid/api/issues/1381)) ([4f63b4c](https://github.com/unraid/api/commit/4f63b4cf3bb9391785f07a38defe54ec39071caa))
* send active unraid theme to docs ([#1400](https://github.com/unraid/api/issues/1400)) ([f71943b](https://github.com/unraid/api/commit/f71943b62b30119e17766e56534962630f52a591))
* slightly better watch mode ([#1398](https://github.com/unraid/api/issues/1398)) ([881f1e0](https://github.com/unraid/api/commit/881f1e09607d1e4a8606f8d048636ba09d8fcac1))
* upgrade nuxt-custom-elements ([#1461](https://github.com/unraid/api/issues/1461)) ([345e83b](https://github.com/unraid/api/commit/345e83bfb0904381d784fc77b3dcd3ad7e53d898))
* use bigint instead of long ([#1403](https://github.com/unraid/api/issues/1403)) ([574d572](https://github.com/unraid/api/commit/574d572d6567c652057b29776694e86267316ca7))
### Bug Fixes
* activation indicator removed ([5edfd82](https://github.com/unraid/api/commit/5edfd823b862cfc1f864565021f12334fe9317c6))
* alignment of settings on ManagementAccess settings page ([#1421](https://github.com/unraid/api/issues/1421)) ([70c790f](https://github.com/unraid/api/commit/70c790ff89075a785d7f0623bbf3c34a3806bbdc))
* allow rclone to fail to initialize ([#1453](https://github.com/unraid/api/issues/1453)) ([7c6f02a](https://github.com/unraid/api/commit/7c6f02a5cb474fb285db294ec6f80d1c2c57e142))
* always download 7.1 versioned files for patching ([edc0d15](https://github.com/unraid/api/commit/edc0d1578b89c3b3e56e637de07137e069656fa8))
* api `pnpm type-check` ([#1442](https://github.com/unraid/api/issues/1442)) ([3122bdb](https://github.com/unraid/api/commit/3122bdb953eec58469fd9cf6f468e75621781040))
* **api:** connect config `email` validation ([#1454](https://github.com/unraid/api/issues/1454)) ([b9a1b9b](https://github.com/unraid/api/commit/b9a1b9b08746b6d4cb2128d029a3dab7cdd47677))
* backport unraid/webgui[#2269](https://github.com/unraid/api/issues/2269) rc.nginx update ([#1436](https://github.com/unraid/api/issues/1436)) ([a7ef06e](https://github.com/unraid/api/commit/a7ef06ea252545cef084e21cea741a8ec866e7cc))
* bigint ([e54d27a](https://github.com/unraid/api/commit/e54d27aede1b1e784971468777c5e65cde66f2ac))
* config migration from `myservers.cfg` ([#1440](https://github.com/unraid/api/issues/1440)) ([c4c9984](https://github.com/unraid/api/commit/c4c99843c7104414120bffc5dd5ed78ab6c8ba02))
* **connect:** fatal race-condition in websocket disposal ([#1462](https://github.com/unraid/api/issues/1462)) ([0ec0de9](https://github.com/unraid/api/commit/0ec0de982f017b61a145c7a4176718b484834f41))
* **connect:** mothership connection ([#1464](https://github.com/unraid/api/issues/1464)) ([7be8bc8](https://github.com/unraid/api/commit/7be8bc84d3831f9cea7ff62d0964612ad366a976))
* console hidden ([9b85e00](https://github.com/unraid/api/commit/9b85e009b833706294a841a54498e45a8e0204ed))
* debounce is too long ([#1426](https://github.com/unraid/api/issues/1426)) ([f12d231](https://github.com/unraid/api/commit/f12d231e6376d0f253cee67b7ed690c432c63ec5))
* delete legacy connect keys and ensure description ([22fe91c](https://github.com/unraid/api/commit/22fe91cd561e88aa24e8f8cfa5a6143e7644e4e0))
* **deps:** pin dependencies ([#1465](https://github.com/unraid/api/issues/1465)) ([ba75a40](https://github.com/unraid/api/commit/ba75a409a4d3e820308b78fd5a5380021d3757b0))
* **deps:** pin dependencies ([#1470](https://github.com/unraid/api/issues/1470)) ([412b329](https://github.com/unraid/api/commit/412b32996d9c8352c25309cc0d549a57468d0fb5))
* **deps:** storybook v9 ([#1476](https://github.com/unraid/api/issues/1476)) ([45bb49b](https://github.com/unraid/api/commit/45bb49bcd60a9753be492203111e489fd37c1a5f))
* **deps:** update all non-major dependencies ([#1366](https://github.com/unraid/api/issues/1366)) ([291ee47](https://github.com/unraid/api/commit/291ee475fb9ef44f6da7b76a9eb11b7dd29a5d13))
* **deps:** update all non-major dependencies ([#1379](https://github.com/unraid/api/issues/1379)) ([8f70326](https://github.com/unraid/api/commit/8f70326d0fe3e4c3bcd3e8e4e6566766f1c05eb7))
* **deps:** update all non-major dependencies ([#1389](https://github.com/unraid/api/issues/1389)) ([cb43f95](https://github.com/unraid/api/commit/cb43f95233590888a8e20a130e62cadc176c6793))
* **deps:** update all non-major dependencies ([#1399](https://github.com/unraid/api/issues/1399)) ([68df344](https://github.com/unraid/api/commit/68df344a4b412227cffa96867f086177b251f028))
* **deps:** update dependency @types/diff to v8 ([#1393](https://github.com/unraid/api/issues/1393)) ([00da27d](https://github.com/unraid/api/commit/00da27d04f2ee2ca8b8b9cdcc6ea3c490c02a3a4))
* **deps:** update dependency cache-manager to v7 ([#1413](https://github.com/unraid/api/issues/1413)) ([9492c2a](https://github.com/unraid/api/commit/9492c2ae6a0086d14e73d280c55746206b73a7b0))
* **deps:** update dependency commander to v14 ([#1394](https://github.com/unraid/api/issues/1394)) ([106ea09](https://github.com/unraid/api/commit/106ea093996f2d0c71c1511bc009ecc9a6be91ec))
* **deps:** update dependency diff to v8 ([#1386](https://github.com/unraid/api/issues/1386)) ([e580f64](https://github.com/unraid/api/commit/e580f646a52b8bda605132cf44ec58137e08dd42))
* **deps:** update dependency dotenv to v17 ([#1474](https://github.com/unraid/api/issues/1474)) ([d613bfa](https://github.com/unraid/api/commit/d613bfa0410e7ef8451fc8ea20e57a7db67f7994))
* **deps:** update dependency lucide-vue-next to ^0.509.0 ([#1383](https://github.com/unraid/api/issues/1383)) ([469333a](https://github.com/unraid/api/commit/469333acd4a0cbeecc9e9cbadb2884289d83aee3))
* **deps:** update dependency marked to v16 ([#1444](https://github.com/unraid/api/issues/1444)) ([453a5b2](https://github.com/unraid/api/commit/453a5b2c9591f755ce07548a9996d7a6cf0925c4))
* **deps:** update dependency shadcn-vue to v2 ([#1302](https://github.com/unraid/api/issues/1302)) ([26ecf77](https://github.com/unraid/api/commit/26ecf779e675d0bc533d61e045325ab062effcbf))
* **deps:** update dependency vue-sonner to v2 ([#1401](https://github.com/unraid/api/issues/1401)) ([53ca414](https://github.com/unraid/api/commit/53ca41404f13c057c340dcf9010af72c3365e499))
* disable file changes on Unraid 7.2 ([#1382](https://github.com/unraid/api/issues/1382)) ([02de89d](https://github.com/unraid/api/commit/02de89d1309f67e4b6d4f8de5f66815ee4d2464c))
* do not start API with doinst.sh ([7d88b33](https://github.com/unraid/api/commit/7d88b3393cbd8ab1e93a86dfa1b7b74cc97255cc))
* do not uninstall fully on 7.2 ([#1484](https://github.com/unraid/api/issues/1484)) ([2263881](https://github.com/unraid/api/commit/22638811a9fdb524420b1347ac49cfaa51bbecb5))
* drop console with terser ([a87d455](https://github.com/unraid/api/commit/a87d455bace04aab9d7fa0e63cb61d26ef9b3b72))
* error logs from `cloud` query when connect is not installed ([#1450](https://github.com/unraid/api/issues/1450)) ([719f460](https://github.com/unraid/api/commit/719f460016d769255582742d7d71ca97d132022b))
* flash backup integration with Unraid Connect config ([#1448](https://github.com/unraid/api/issues/1448)) ([038c582](https://github.com/unraid/api/commit/038c582aed5f5efaea3583372778b9baa318e1ea))
* header padding regression ([#1477](https://github.com/unraid/api/issues/1477)) ([e791cc6](https://github.com/unraid/api/commit/e791cc680de9c40378043348ddca70902da6d250))
* incorrect state merging in redux store ([#1437](https://github.com/unraid/api/issues/1437)) ([17b7428](https://github.com/unraid/api/commit/17b74287796e6feb75466033e279dc3bcf57f1e6))
* lanip copy button not present ([#1459](https://github.com/unraid/api/issues/1459)) ([a280786](https://github.com/unraid/api/commit/a2807864acef742e454d87bb093ee91806e527e5))
* move to bigint scalar ([b625227](https://github.com/unraid/api/commit/b625227913e80e4731a13b54b525ec7385918c51))
* node_modules dir removed on plugin update ([#1406](https://github.com/unraid/api/issues/1406)) ([7b005cb](https://github.com/unraid/api/commit/7b005cbbf682a1336641f5fc85022e9d651569d0))
* omit Connect actions in UPC when plugin is not installed ([#1417](https://github.com/unraid/api/issues/1417)) ([8c8a527](https://github.com/unraid/api/commit/8c8a5276b49833c08bca133e374e1e66273b41aa))
* parsing of `ssoEnabled` in state.php ([#1455](https://github.com/unraid/api/issues/1455)) ([f542c8e](https://github.com/unraid/api/commit/f542c8e0bd9596d9d3abf75b58b97d95fb033215))
* pin ranges ([#1460](https://github.com/unraid/api/issues/1460)) ([f88400e](https://github.com/unraid/api/commit/f88400eea820ac80c867fdb63cd503ed91493146))
* pr plugin promotion workflow ([#1456](https://github.com/unraid/api/issues/1456)) ([13bd9bb](https://github.com/unraid/api/commit/13bd9bb5670bb96b158068114d62572d88c7cae9))
* proper fallback if missing paths config modules ([7067e9e](https://github.com/unraid/api/commit/7067e9e3dd3966309013b52c90090cc82de4e4fb))
* rc.unraid-api now cleans up older dependencies ([#1404](https://github.com/unraid/api/issues/1404)) ([83076bb](https://github.com/unraid/api/commit/83076bb94088095de8b1a332a50bbef91421f0c1))
* remote access lifecycle during boot & shutdown ([#1422](https://github.com/unraid/api/issues/1422)) ([7bc583b](https://github.com/unraid/api/commit/7bc583b18621c8140232772ca36c6d9b8d8a9cd7))
* sign out correctly on error ([#1452](https://github.com/unraid/api/issues/1452)) ([d08fc94](https://github.com/unraid/api/commit/d08fc94afb94e386907da44402ee5a24cfb3d00a))
* simplify usb listing ([#1402](https://github.com/unraid/api/issues/1402)) ([5355115](https://github.com/unraid/api/commit/5355115af2f4122af9afa3f63ed8f830b33cbf5c))
* theme issues when sent from graph ([#1424](https://github.com/unraid/api/issues/1424)) ([75ad838](https://github.com/unraid/api/commit/75ad8381bd4f4045ab1d3aa84e08ecddfba27617))
* **ui:** notifications positioning regression ([#1445](https://github.com/unraid/api/issues/1445)) ([f73e5e0](https://github.com/unraid/api/commit/f73e5e0058fcc3bedebfbe7380ffcb44aea981b8))
* use some instead of every for connect detection ([9ce2fee](https://github.com/unraid/api/commit/9ce2fee380c4db1395f5d4df7f16ae6c57d1a748))
### Reverts
* revert package.json dependency updates from commit 711cc9a for api and packages/* ([94420e4](https://github.com/unraid/api/commit/94420e4d45735b8def3915b5789c15c1c3121f1e))
## [4.8.0](https://github.com/unraid/api/compare/v4.7.0...v4.8.0) (2025-05-01)

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.8.0",
"version": "4.9.3",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {

View File

@@ -124,7 +124,15 @@ export const parseConfig = <T extends Record<string, any>>(
throw new AppError('Invalid Parameters Passed to ParseConfig');
}
const data: Record<string, any> = parseIni(fileContents);
let data: Record<string, any>;
try {
data = parseIni(fileContents);
} catch (error) {
throw new AppError(
`Failed to parse config file: ${error instanceof Error ? error.message : String(error)}`
);
}
// Remove quotes around keys
const dataWithoutQuoteKeys = Object.fromEntries(
Object.entries(data).map(([key, value]) => [key.replace(/^"(.+(?="$))"$/, '$1'), value])

View File

@@ -12,6 +12,8 @@ import { ConfigPersistenceHelper } from '@app/unraid-api/config/persistence.help
export { type ApiConfig };
const logger = new Logger('ApiConfig');
const createDefaultConfig = (): ApiConfig => ({
version: API_VERSION,
extraOrigins: [],
@@ -33,21 +35,54 @@ export const persistApiConfig = async (config: ApiConfig) => {
};
export const loadApiConfig = async () => {
const defaultConfig = createDefaultConfig();
const apiConfig = new ApiStateConfig<ApiConfig>(
{
name: 'api',
defaultConfig,
parse: (data) => data as ApiConfig,
},
new ConfigPersistenceHelper()
);
const diskConfig = await apiConfig.parseConfig();
return {
...defaultConfig,
...diskConfig,
version: API_VERSION,
};
try {
const defaultConfig = createDefaultConfig();
const apiConfig = new ApiStateConfig<ApiConfig>(
{
name: 'api',
defaultConfig,
parse: (data) => data as ApiConfig,
},
new ConfigPersistenceHelper()
);
let diskConfig: ApiConfig | undefined;
try {
diskConfig = await apiConfig.parseConfig();
} catch (error) {
logger.error('Failed to load API config from disk, using defaults:', error);
diskConfig = undefined;
// Try to overwrite the invalid config with defaults to fix the issue
try {
const configToWrite = {
...defaultConfig,
version: API_VERSION,
};
const writeSuccess = await apiConfig.persist(configToWrite);
if (writeSuccess) {
logger.log('Successfully overwrote invalid config file with defaults.');
} else {
logger.error(
'Failed to overwrite invalid config file. Continuing with defaults in memory only.'
);
}
} catch (persistError) {
logger.error('Error during config file repair:', persistError);
}
}
return {
...defaultConfig,
...diskConfig,
version: API_VERSION,
};
} catch (outerError) {
// This should never happen, but ensures the config factory never throws
logger.error('Critical error in loadApiConfig, using minimal defaults:', outerError);
return createDefaultConfig();
}
};
/**
@@ -81,21 +116,29 @@ export class ApiConfigPersistence {
}
async onModuleInit() {
if (!(await fileExists(this.filePath))) {
this.migrateFromMyServersConfig();
try {
if (!(await fileExists(this.filePath))) {
this.migrateFromMyServersConfig();
}
await this.persistenceHelper.persistIfChanged(this.filePath, this.config);
this.configService.changes$.pipe(bufferTime(25)).subscribe({
next: async (changes) => {
if (changes.some((change) => change.path.startsWith('api'))) {
this.logger.verbose(`API Config changed ${JSON.stringify(changes)}`);
try {
await this.persistenceHelper.persistIfChanged(this.filePath, this.config);
} catch (persistError) {
this.logger.error('Error persisting config changes:', persistError);
}
}
},
error: (err) => {
this.logger.error('Error receiving config changes:', err);
},
});
} catch (error) {
this.logger.error('Error during API config module initialization:', error);
}
await this.persistenceHelper.persistIfChanged(this.filePath, this.config);
this.configService.changes$.pipe(bufferTime(25)).subscribe({
next: async (changes) => {
if (changes.some((change) => change.path.startsWith('api'))) {
this.logger.verbose(`API Config changed ${JSON.stringify(changes)}`);
await this.persistenceHelper.persistIfChanged(this.filePath, this.config);
}
},
error: (err) => {
this.logger.error('Error receiving config changes:', err);
},
});
}
convertLegacyConfig(

View File

@@ -2,9 +2,26 @@ import { ConfigService } from '@nestjs/config';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ApiConfigPersistence } from '@app/unraid-api/config/api-config.module.js';
import { fileExists } from '@app/core/utils/files/file-exists.js';
import { ApiConfigPersistence, loadApiConfig } from '@app/unraid-api/config/api-config.module.js';
import { ConfigPersistenceHelper } from '@app/unraid-api/config/persistence.helper.js';
// Mock the core file-exists utility used by ApiStateConfig
vi.mock('@app/core/utils/files/file-exists.js', () => ({
fileExists: vi.fn(),
}));
// Mock the shared file-exists utility used by ConfigPersistenceHelper
vi.mock('@unraid/shared/util/file.js', () => ({
fileExists: vi.fn(),
}));
// Mock fs/promises for file I/O operations
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
writeFile: vi.fn(),
}));
describe('ApiConfigPersistence', () => {
let service: ApiConfigPersistence;
let configService: ConfigService;
@@ -135,3 +152,127 @@ describe('ApiConfigPersistence', () => {
});
});
});
describe('loadApiConfig', () => {
let readFile: any;
let writeFile: any;
beforeEach(async () => {
vi.clearAllMocks();
// Reset modules to ensure fresh imports
vi.resetModules();
// Get mocked functions
const fsMocks = await import('fs/promises');
readFile = fsMocks.readFile;
writeFile = fsMocks.writeFile;
});
it('should return default config when file does not exist', async () => {
vi.mocked(fileExists).mockResolvedValue(false);
const result = await loadApiConfig();
expect(result).toEqual({
version: expect.any(String),
extraOrigins: [],
sandbox: false,
ssoSubIds: [],
plugins: [],
});
});
it('should merge disk config with defaults when file exists', async () => {
const diskConfig = {
extraOrigins: ['https://example.com'],
sandbox: true,
ssoSubIds: ['sub1', 'sub2'],
};
vi.mocked(fileExists).mockResolvedValue(true);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(diskConfig));
const result = await loadApiConfig();
expect(result).toEqual({
version: expect.any(String),
extraOrigins: ['https://example.com'],
sandbox: true,
ssoSubIds: ['sub1', 'sub2'],
plugins: [],
});
});
it('should use default config and overwrite file when JSON parsing fails', async () => {
const { fileExists: sharedFileExists } = await import('@unraid/shared/util/file.js');
vi.mocked(fileExists).mockResolvedValue(true);
vi.mocked(readFile).mockResolvedValue('{ invalid json }');
vi.mocked(sharedFileExists).mockResolvedValue(false); // For persist operation
vi.mocked(writeFile).mockResolvedValue(undefined);
const result = await loadApiConfig();
// Error logging is handled by NestJS Logger, just verify the config is returned
expect(writeFile).toHaveBeenCalled();
expect(result).toEqual({
version: expect.any(String),
extraOrigins: [],
sandbox: false,
ssoSubIds: [],
plugins: [],
});
});
it('should handle write failure gracefully when JSON parsing fails', async () => {
const { fileExists: sharedFileExists } = await import('@unraid/shared/util/file.js');
vi.mocked(fileExists).mockResolvedValue(true);
vi.mocked(readFile).mockResolvedValue('{ invalid json }');
vi.mocked(sharedFileExists).mockResolvedValue(false); // For persist operation
vi.mocked(writeFile).mockRejectedValue(new Error('Permission denied'));
const result = await loadApiConfig();
// Error logging is handled by NestJS Logger, just verify the config is returned
expect(writeFile).toHaveBeenCalled();
expect(result).toEqual({
version: expect.any(String),
extraOrigins: [],
sandbox: false,
ssoSubIds: [],
plugins: [],
});
});
it('should use default config when file is empty', async () => {
vi.mocked(fileExists).mockResolvedValue(true);
vi.mocked(readFile).mockResolvedValue('');
const result = await loadApiConfig();
// No error logging expected for empty files
expect(result).toEqual({
version: expect.any(String),
extraOrigins: [],
sandbox: false,
ssoSubIds: [],
plugins: [],
});
});
it('should always override version with current API_VERSION', async () => {
const diskConfig = {
version: 'old-version',
extraOrigins: ['https://example.com'],
};
vi.mocked(fileExists).mockResolvedValue(true);
vi.mocked(readFile).mockResolvedValue(JSON.stringify(diskConfig));
const result = await loadApiConfig();
expect(result.version).not.toBe('old-version');
expect(result.version).toBeTruthy();
});
});

View File

@@ -0,0 +1,364 @@
import { Logger } from '@nestjs/common';
import { readFile } from 'node:fs/promises';
import { join } from 'path';
import type { Mock } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fileExists } from '@app/core/utils/files/file-exists.js';
import { ApiStateConfig } from '@app/unraid-api/config/factory/api-state.model.js';
import { ConfigPersistenceHelper } from '@app/unraid-api/config/persistence.helper.js';
vi.mock('node:fs/promises');
vi.mock('@app/core/utils/files/file-exists.js');
vi.mock('@app/environment.js', () => ({
PATHS_CONFIG_MODULES: '/test/config/path',
}));
describe('ApiStateConfig', () => {
let mockPersistenceHelper: ConfigPersistenceHelper;
let mockLogger: Logger;
interface TestConfig {
name: string;
value: number;
enabled: boolean;
}
const defaultConfig: TestConfig = {
name: 'test',
value: 42,
enabled: true,
};
const parseFunction = (data: unknown): TestConfig => {
if (!data || typeof data !== 'object') {
throw new Error('Invalid config format');
}
return data as TestConfig;
};
beforeEach(() => {
vi.clearAllMocks();
mockPersistenceHelper = {
persistIfChanged: vi.fn().mockResolvedValue(true),
} as any;
mockLogger = {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
} as any;
vi.spyOn(Logger.prototype, 'log').mockImplementation(mockLogger.log);
vi.spyOn(Logger.prototype, 'warn').mockImplementation(mockLogger.warn);
vi.spyOn(Logger.prototype, 'error').mockImplementation(mockLogger.error);
vi.spyOn(Logger.prototype, 'debug').mockImplementation(mockLogger.debug);
});
describe('constructor', () => {
it('should initialize with cloned default config', () => {
const config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
expect(config.config).toEqual(defaultConfig);
expect(config.config).not.toBe(defaultConfig);
});
});
describe('token', () => {
it('should generate correct token', () => {
const config = new ApiStateConfig(
{
name: 'my-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
expect(config.token).toBe('ApiConfig.my-config');
});
});
describe('file paths', () => {
it('should generate correct file name', () => {
const config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
expect(config.fileName).toBe('test-config.json');
});
it('should generate correct file path', () => {
const config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
expect(config.filePath).toBe(join('/test/config/path', 'test-config.json'));
});
});
describe('parseConfig', () => {
let config: ApiStateConfig<TestConfig>;
beforeEach(() => {
config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
});
it('should return undefined when file does not exist', async () => {
(fileExists as Mock).mockResolvedValue(false);
const result = await config.parseConfig();
expect(result).toBeUndefined();
expect(readFile).not.toHaveBeenCalled();
});
it('should parse valid JSON config', async () => {
const validConfig = { name: 'custom', value: 100, enabled: false };
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue(JSON.stringify(validConfig));
const result = await config.parseConfig();
expect(result).toEqual(validConfig);
expect(readFile).toHaveBeenCalledWith(config.filePath, 'utf8');
});
it('should return undefined for empty file', async () => {
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue('');
const result = await config.parseConfig();
expect(result).toBeUndefined();
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('is empty'));
});
it('should return undefined for whitespace-only file', async () => {
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue(' \n\t ');
const result = await config.parseConfig();
expect(result).toBeUndefined();
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('is empty'));
});
it('should throw error for invalid JSON', async () => {
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue('{ invalid json }');
await expect(config.parseConfig()).rejects.toThrow();
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Failed to parse JSON')
);
expect(mockLogger.debug).toHaveBeenCalledWith(expect.stringContaining('{ invalid json }'));
});
it('should throw error for incomplete JSON', async () => {
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue('{ "name": "test"');
await expect(config.parseConfig()).rejects.toThrow();
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Failed to parse JSON')
);
});
it('should use custom file path when provided', async () => {
const customPath = '/custom/path/config.json';
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue(JSON.stringify(defaultConfig));
await config.parseConfig({ filePath: customPath });
expect(fileExists).toHaveBeenCalledWith(customPath);
expect(readFile).toHaveBeenCalledWith(customPath, 'utf8');
});
});
describe('persist', () => {
let config: ApiStateConfig<TestConfig>;
beforeEach(() => {
config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
});
it('should persist current config when no argument provided', async () => {
const result = await config.persist();
expect(result).toBe(true);
expect(mockPersistenceHelper.persistIfChanged).toHaveBeenCalledWith(
config.filePath,
defaultConfig
);
});
it('should persist provided config', async () => {
const customConfig = { name: 'custom', value: 999, enabled: false };
const result = await config.persist(customConfig);
expect(result).toBe(true);
expect(mockPersistenceHelper.persistIfChanged).toHaveBeenCalledWith(
config.filePath,
customConfig
);
});
it('should return false and log error on persistence failure', async () => {
(mockPersistenceHelper.persistIfChanged as Mock).mockResolvedValue(false);
const result = await config.persist();
expect(result).toBe(false);
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Could not write config')
);
});
});
describe('load', () => {
let config: ApiStateConfig<TestConfig>;
beforeEach(() => {
config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
});
it('should load config from file when it exists', async () => {
const savedConfig = { name: 'saved', value: 200, enabled: true };
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue(JSON.stringify(savedConfig));
await config.load();
expect(config.config).toEqual(savedConfig);
});
it('should create default config when file does not exist', async () => {
(fileExists as Mock).mockResolvedValue(false);
await config.load();
expect(config.config).toEqual(defaultConfig);
expect(mockLogger.log).toHaveBeenCalledWith(
expect.stringContaining('Config file does not exist')
);
expect(mockPersistenceHelper.persistIfChanged).toHaveBeenCalledWith(
config.filePath,
defaultConfig
);
});
it('should not modify config when file is invalid', async () => {
(fileExists as Mock).mockResolvedValue(true);
(readFile as Mock).mockResolvedValue('invalid json');
await config.load();
expect(config.config).toEqual(defaultConfig);
expect(mockLogger.warn).toHaveBeenCalledWith(
expect.any(Error),
expect.stringContaining('is invalid')
);
});
it('should not throw even when persist fails', async () => {
(fileExists as Mock).mockResolvedValue(false);
(mockPersistenceHelper.persistIfChanged as Mock).mockResolvedValue(false);
await expect(config.load()).resolves.not.toThrow();
expect(config.config).toEqual(defaultConfig);
});
});
describe('update', () => {
let config: ApiStateConfig<TestConfig>;
beforeEach(() => {
config = new ApiStateConfig(
{
name: 'test-config',
defaultConfig,
parse: parseFunction,
},
mockPersistenceHelper
);
});
it('should update config with partial values', () => {
config.update({ value: 123 });
expect(config.config).toEqual({
name: 'test',
value: 123,
enabled: true,
});
});
it('should return self for chaining', () => {
const result = config.update({ enabled: false });
expect(result).toBe(config);
});
it('should validate updated config through parse function', () => {
const badParseFunction = vi.fn().mockImplementation(() => {
throw new Error('Validation failed');
});
const strictConfig = new ApiStateConfig(
{
name: 'strict-config',
defaultConfig,
parse: badParseFunction,
},
mockPersistenceHelper
);
expect(() => strictConfig.update({ value: -1 })).toThrow('Validation failed');
});
});
});

View File

@@ -56,13 +56,11 @@ export class ApiStateConfig<T> {
* @returns True if the config was written successfully, false otherwise.
*/
async persist(config = this.#config) {
try {
await this.persistenceHelper.persistIfChanged(this.filePath, config);
return true;
} catch (error) {
this.logger.error(error, `Could not write config to ${this.filePath}.`);
return false;
const success = await this.persistenceHelper.persistIfChanged(this.filePath, config);
if (!success) {
this.logger.error(`Could not write config to ${this.filePath}.`);
}
return success;
}
/**
@@ -76,8 +74,23 @@ export class ApiStateConfig<T> {
const { filePath = this.filePath } = opts;
if (!(await fileExists(filePath))) return undefined;
const rawConfig = JSON.parse(await readFile(filePath, 'utf8'));
return this.options.parse(rawConfig);
const fileContent = await readFile(filePath, 'utf8');
if (!fileContent || fileContent.trim() === '') {
this.logger.warn(`Config file '${filePath}' is empty.`);
return undefined;
}
try {
const rawConfig = JSON.parse(fileContent);
return this.options.parse(rawConfig);
} catch (error) {
this.logger.error(
`Failed to parse JSON from '${filePath}': ${error instanceof Error ? error.message : String(error)}`
);
this.logger.debug(`File content: ${fileContent.substring(0, 100)}...`);
throw error;
}
}
/**

View File

@@ -12,24 +12,59 @@ export class ConfigPersistenceHelper {
*
* @param filePath - The path to the config file.
* @param data - The data to persist.
* @returns `true` if the config was persisted, `false` otherwise.
* @returns `true` if the config was persisted, `false` if no changes were needed or if persistence failed.
*
* @throws {Error} if the config file does not exist or is unreadable.
* @throws {Error} if the config file is not valid JSON.
* @throws {Error} if given data is not JSON (de)serializable.
* @throws {Error} if the config file is not writable.
* This method is designed to never throw errors. If the existing file is corrupted or unreadable,
* it will attempt to overwrite it with the new data. If write operations fail, it returns false
* but does not crash the application.
*/
async persistIfChanged(filePath: string, data: unknown): Promise<boolean> {
if (!(await fileExists(filePath))) {
await writeFile(filePath, JSON.stringify(data ?? {}, null, 2));
return true;
try {
const jsonString = JSON.stringify(data ?? {}, null, 2);
await writeFile(filePath, jsonString);
return true;
} catch (error) {
// JSON serialization or write failed, but don't crash - just return false
return false;
}
}
const currentData = JSON.parse(await readFile(filePath, 'utf8'));
const stagedData = JSON.parse(JSON.stringify(data));
let currentData: unknown;
try {
const fileContent = await readFile(filePath, 'utf8');
currentData = JSON.parse(fileContent);
} catch (error) {
// If existing file is corrupted, treat it as if it doesn't exist
// and write the new data
try {
const jsonString = JSON.stringify(data ?? {}, null, 2);
await writeFile(filePath, jsonString);
return true;
} catch (writeError) {
// JSON serialization or write failed, but don't crash - just return false
return false;
}
}
let stagedData: unknown;
try {
stagedData = JSON.parse(JSON.stringify(data));
} catch (error) {
// If data can't be serialized to JSON, we can't persist it
return false;
}
if (isEqual(currentData, stagedData)) {
return false;
}
await writeFile(filePath, JSON.stringify(stagedData, null, 2));
return true;
try {
await writeFile(filePath, JSON.stringify(stagedData, null, 2));
return true;
} catch (error) {
// Write failed, but don't crash - just return false
return false;
}
}
}

View File

@@ -65,7 +65,16 @@ export class PluginService {
* @returns A tuple of the plugin name and version.
*/
static async listPlugins(): Promise<[string, string][]> {
const { plugins = [] } = await loadApiConfig();
let plugins: string[] = [];
try {
const config = await loadApiConfig();
plugins = config.plugins || [];
} catch (error) {
PluginService.logger.error(
'Failed to load API config for plugin discovery, using empty list:',
error
);
}
const pluginNames = new Set(
plugins.map((plugin) => {
const { name } = parsePackageArg(plugin);

View File

@@ -711,7 +711,7 @@ $.ajaxPrefilter(function(s, orig, xhr){
<div class="upgrade_notice" style="display:none"></div>
<div id="header" class="<?=$display['banner']?>">
<div class="logo">
<a href="https://unraid.net" target="_blank"><?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?></a>
<unraid-i18n-host><unraid-header-os-version></unraid-header-os-version></unraid-i18n-host>
</div>
<?include "$docroot/plugins/dynamix.my.servers/include/myservers2.php"?>

View File

@@ -65,13 +65,6 @@ if (is_localhost() && !is_good_session()) {
return this.prependDoctypeWithPhp(source, newPhpCode);
}
private hideHeaderLogo(source: string): string {
return source.replace(
'<a href="https://unraid.net" target="_blank"><?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?></a>',
''
);
}
private addModalsWebComponent(source: string): string {
return source.replace('<body>', '<body>\n<unraid-modals></unraid-modals>');
}
@@ -81,7 +74,6 @@ if (is_localhost() && !is_good_session()) {
this.replaceToasts.bind(this),
this.addToaster.bind(this),
this.patchGuiBootAuth.bind(this),
this.hideHeaderLogo.bind(this),
this.addModalsWebComponent.bind(this),
];

View File

@@ -53,7 +53,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
}
function closeNotifier() {
@@ -695,15 +704,16 @@
@@ -695,10 +704,11 @@
});
</script>
<?include "$docroot/plugins/dynamix.my.servers/include/myservers1.php"?>
@@ -64,13 +64,7 @@ Index: /usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php
<div class="upgrade_notice" style="display:none"></div>
<div id="header" class="<?=$display['banner']?>">
<div class="logo">
- <a href="https://unraid.net" target="_blank"><?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?></a>
+
<unraid-i18n-host><unraid-header-os-version></unraid-header-os-version></unraid-i18n-host>
</div>
<?include "$docroot/plugins/dynamix.my.servers/include/myservers2.php"?>
</div>
<a href="#" class="move_to_end" title="<?=_('Move To End')?>"><i class="fa fa-arrow-circle-down"></i></a>
<a href="https://unraid.net" target="_blank"><?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?></a>
@@ -748,12 +758,12 @@
}
// create list of nchan scripts to be started

View File

@@ -1,7 +1,7 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.8.0",
"version": "4.9.3",
"scripts": {
"build": "pnpm -r build",
"build:watch": " pnpm -r --parallel build:watch",

View File

@@ -0,0 +1,69 @@
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { writeFile } from 'fs/promises';
import { ConnectionMetadata, ConfigType } from './connect.config.js';
@Injectable()
export class ConnectStatusWriterService implements OnModuleInit {
constructor(private readonly configService: ConfigService<ConfigType, true>) {}
private logger = new Logger(ConnectStatusWriterService.name);
get statusFilePath() {
// Write to /var/local/emhttp/connectStatus.json so PHP can read it
return '/var/local/emhttp/connectStatus.json';
}
async onModuleInit() {
this.logger.verbose(`Status file path: ${this.statusFilePath}`);
// Write initial status
await this.writeStatus();
// Listen for changes to connection status
this.configService.changes$.subscribe({
next: async (change) => {
const connectionChanged = change.path && change.path.startsWith('connect.mothership');
if (connectionChanged) {
await this.writeStatus();
}
},
error: (err) => {
this.logger.error('Error receiving config changes:', err);
},
});
}
private async writeStatus() {
try {
const connectionMetadata = this.configService.get<ConnectionMetadata>('connect.mothership');
// Try to get allowed origins from the store
let allowedOrigins = '';
try {
// We can't import from @app here, so we'll skip allowed origins for now
// This can be added later if needed
allowedOrigins = '';
} catch (error) {
this.logger.debug('Could not get allowed origins:', error);
}
const statusData = {
connectionStatus: connectionMetadata?.status || 'PRE_INIT',
error: connectionMetadata?.error || null,
lastPing: connectionMetadata?.lastPing || null,
allowedOrigins: allowedOrigins,
timestamp: Date.now()
};
const data = JSON.stringify(statusData, null, 2);
this.logger.verbose(`Writing connection status: ${data}`);
await writeFile(this.statusFilePath, data);
this.logger.verbose(`Status written to ${this.statusFilePath}`);
} catch (error) {
this.logger.error(error, `Error writing status to '${this.statusFilePath}'`);
}
}
}

View File

@@ -3,6 +3,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConnectConfigPersister } from './config/config.persistence.js';
import { configFeature } from './config/connect.config.js';
import { ConnectStatusWriterService } from './config/connect-status-writer.service.js';
import { MothershipModule } from './mothership-proxy/mothership.module.js';
import { ConnectModule } from './unraid-connect/connect.module.js';
@@ -10,7 +11,7 @@ export const adapter = 'nestjs';
@Module({
imports: [ConfigModule.forFeature(configFeature), ConnectModule, MothershipModule],
providers: [ConnectConfigPersister],
providers: [ConnectConfigPersister, ConnectStatusWriterService],
exports: [],
})
class ConnectPluginModule {

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/connect-plugin",
"version": "4.8.0",
"version": "4.9.3",
"private": true,
"dependencies": {
"commander": "14.0.0",

View File

@@ -138,21 +138,89 @@ exit 0
</INLINE>
</FILE>
<FILE Run="/bin/bash" Method="install">
<INLINE>
<![CDATA[
echo "Patching header logo if necessary..."
# We do this here instead of via API FileModification to avoid undesirable
# rollback when the API is stopped.
#
# This is necessary on < 7.2 because the unraid-header-os-version web component
# that ships with the base OS only displayes the version, not the logo as well.
#
# Rolling back in this case (i.e when stopping the API) yields a duplicate logo
# that blocks interaction with the navigation menu.
# Remove the old header logo from DefaultPageLayout.php if present
if [ -f "/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php" ]; then
sed -i 's|<a href="https://unraid.net" target="_blank"><?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?></a>||g' "/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php"
fi
]]>
</INLINE>
</FILE>
<FILE Run="/bin/bash" Method="remove">
<INLINE>
MAINNAME="&name;"
<![CDATA[
echo "Removing Plugin"
# Find any installed dynamix.unraid.net package
pkg_installed=$(ls -1 /var/log/packages/dynamix.unraid.net* 2>/dev/null | head -1)
if [ -n "$pkg_installed" ]; then
pkg_basename=$(basename "$pkg_installed")
echo "Removing package: $pkg_basename"
removepkg --terse "$pkg_basename"
# Check Unraid version
UNRAID_VERSION=""
is_7_2_or_higher=false
# Check if version file exists and is readable
if [ -f "/etc/unraid-version" ] && [ -r "/etc/unraid-version" ]; then
UNRAID_VERSION=$(cat /etc/unraid-version | grep "^version=" | cut -d'"' -f2 2>/dev/null)
if [ -z "$UNRAID_VERSION" ]; then
echo "Warning: Unable to parse version from /etc/unraid-version"
echo "Using safe removal method (plugin file removal + reboot)"
is_7_2_or_higher=true # Default to safe method
else
# Check if this is Unraid 7.2 or higher (including RCs and prereleases)
if [[ "$UNRAID_VERSION" =~ ^7\.([2-9]|[1-9][0-9]+)\. ]] || [[ "$UNRAID_VERSION" =~ ^([8-9]|[1-9][0-9]+)\. ]]; then
is_7_2_or_higher=true
fi
fi
else
echo "No dynamix.unraid.net package found. Trying with basic package name."
removepkg --terse "${MAINNAME}"
echo "Warning: /etc/unraid-version file not found or not readable"
echo "Using safe removal method (plugin file removal + reboot)"
is_7_2_or_higher=true # Default to safe method
fi
if [ "$is_7_2_or_higher" = true ]; then
echo "Unraid 7.2+ detected. Using safe removal method."
# Send notification to user
/usr/local/emhttp/webGui/scripts/notify \
-e "Unraid Connect" \
-s "Reboot Required for Unraid Connect Removal" \
-d "Unraid Connect plugin has been marked for removal. Please reboot your server to complete the uninstallation." \
-i "warning"
# Remove the plugin file so it won't be installed on reboot
PLUGIN_FILE="/boot/config/plugins/${MAINNAME}.plg"
if [ -f "$PLUGIN_FILE" ]; then
echo "Removing plugin file: $PLUGIN_FILE"
rm -f "$PLUGIN_FILE"
fi
echo "Plugin marked for removal. Reboot required to complete uninstallation."
else
# Original removal method for older versions
# Find any installed dynamix.unraid.net package
pkg_installed=$(ls -1 /var/log/packages/dynamix.unraid.net* 2>/dev/null | head -1)
if [ -n "$pkg_installed" ]; then
pkg_basename=$(basename "$pkg_installed")
echo "Removing package: $pkg_basename"
removepkg --terse "$pkg_basename"
else
echo "No dynamix.unraid.net package found. Trying with basic package name."
removepkg --terse "${MAINNAME}"
fi
fi
# File restoration function

View File

@@ -23,9 +23,16 @@ $myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
$isRegistered = !empty($myservers['remote']['username']);
$myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
$mystatus = (file_exists($myservers_memory_cfg_path)) ? @parse_ini_file($myservers_memory_cfg_path) : [];
$isConnected = (($mystatus['minigraph']??'')==='CONNECTED') ? true : false;
// Read connection status from the new API status file
$statusFilePath = '/var/local/emhttp/connectStatus.json';
$connectionStatus = '';
if (file_exists($statusFilePath)) {
$statusData = @json_decode(file_get_contents($statusFilePath), true);
$connectionStatus = $statusData['connectionStatus'] ?? '';
}
$isConnected = ($connectionStatus === 'CONNECTED') ? true : false;
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';

View File

@@ -168,9 +168,8 @@ class ServerState
private function getMyServersCfgValues()
{
/**
* @todo can we read this from somewhere other than the flash? Connect page uses this path and /boot/config/plugins/dynamix.my.servers/myservers.cfg
* - $myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
* - $mystatus = (file_exists($myservers_memory_cfg_path)) ? @parse_ini_file($myservers_memory_cfg_path) : [];
* Memory config is now written by the new API to /usr/local/emhttp/state/myservers.cfg
* This contains runtime state including connection status.
*/
$flashCfgPath = '/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$this->myServersFlashCfg = file_exists($flashCfgPath) ? @parse_ini_file($flashCfgPath, true) : [];
@@ -212,11 +211,19 @@ class ServerState
* Include localhost in the test, but only display HTTP(S) URLs that do not include localhost.
*/
$this->host = $_SERVER['HTTP_HOST'] ?? "unknown";
$memoryCfgPath = '/var/local/emhttp/myservers.cfg';
$this->myServersMemoryCfg = (file_exists($memoryCfgPath)) ? @parse_ini_file($memoryCfgPath) : [];
$this->myServersMiniGraphConnected = (($this->myServersMemoryCfg['minigraph'] ?? '') === 'CONNECTED');
// Read connection status and allowed origins from the new API status file
$statusFilePath = '/var/local/emhttp/connectStatus.json';
$connectionStatus = '';
$allowedOrigins = '';
if (file_exists($statusFilePath)) {
$statusData = @json_decode(file_get_contents($statusFilePath), true);
$connectionStatus = $statusData['connectionStatus'] ?? '';
$allowedOrigins = $statusData['allowedOrigins'] ?? '';
}
$this->myServersMiniGraphConnected = ($connectionStatus === 'CONNECTED');
$allowedOrigins = $this->myServersMemoryCfg['allowedOrigins'] ?? "";
$extraOrigins = $this->myServersFlashCfg['api']['extraOrigins'] ?? "";
$combinedOrigins = $allowedOrigins . "," . $extraOrigins; // combine the two strings for easier searching
$combinedOrigins = str_replace(" ", "", $combinedOrigins); // replace any spaces with nothing

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/ui",
"version": "4.8.0",
"version": "4.9.3",
"private": true,
"license": "GPL-2.0-or-later",
"type": "module",

View File

@@ -91,11 +91,4 @@ describe('ActivationSteps', () => {
expect(wrapper.find('[data-testid="stepper"]').attributes('default-value')).toBe('2');
});
it('renders separators between steps', () => {
const wrapper = mountComponent();
const separators = wrapper.findAll('[data-testid="stepper-separator"]');
expect(separators).toHaveLength(2);
});
});

View File

@@ -38,6 +38,11 @@ vi.mock('@vue/apollo-composable', () => ({
onResult: vi.fn(),
onError: vi.fn(),
}),
useMutation: () => ({
mutate: vi.fn(),
onDone: vi.fn(),
onError: vi.fn(),
}),
provideApolloClient: vi.fn(),
}));

View File

@@ -41,6 +41,11 @@ vi.mock('@vue/apollo-composable', () => ({
onResult: vi.fn(),
onError: vi.fn(),
}),
useMutation: () => ({
mutate: vi.fn(),
onDone: vi.fn(),
onError: vi.fn(),
}),
provideApolloClient: vi.fn(),
}));

View File

@@ -10,7 +10,6 @@ import {
Stepper,
StepperDescription,
StepperItem,
StepperSeparator,
StepperTitle,
StepperTrigger,
} from '@unraid/ui';
@@ -75,7 +74,7 @@ const steps: readonly Step[] = [
<template>
<Stepper :default-value="activeStep" class="text-foreground flex w-full items-start gap-2 text-16px">
<StepperItem
v-for="(step, index) in steps"
v-for="step in steps"
:key="step.step"
v-slot="{ state }: { state: StepState }"
class="relative flex w-full flex-col items-center justify-center data-[disabled]:opacity-100"
@@ -111,7 +110,6 @@ const steps: readonly Step[] = [
</StepperDescription>
</div>
</StepperTrigger>
<StepperSeparator v-if="index < steps.length - 1" class="w-[50px] bg-black h-[30px]" />
</StepperItem>
</Stepper>
</template>

View File

@@ -78,7 +78,7 @@ const updateOsStatus = computed(() => {
</script>
<template>
<div class="flex flex-col gap-y-2 mt-2">
<div class="flex flex-col gap-y-2 mt-6">
<a
:href="unraidLogoHeaderLink.href"
:title="unraidLogoHeaderLink.title"

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, type Component } from 'vue';
import { computed } from 'vue';
import type { Component } from 'vue';
import { BellIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { cn } from '@unraid/ui';

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed, reactive, type Component } from 'vue';
import { computed, reactive } from 'vue';
import type { Component } from 'vue';
import { computedAsync } from '@vueuse/core';
import { Markdown } from '@/helpers/markdown';
import {

View File

@@ -100,7 +100,7 @@ onMounted(() => {
<template>
<div
id="UserProfile"
class="text-foreground relative z-20 flex flex-col h-full gap-y-1 mt-2 mr-2"
class="text-foreground relative z-20 flex flex-col h-full gap-y-1 pt-2 pr-2"
>
<div
v-if="bannerGradient"

View File

@@ -1,4 +1,5 @@
import { InMemoryCache, type InMemoryCacheConfig } from '@apollo/client/core';
import { InMemoryCache } from '@apollo/client/core';
import type { InMemoryCacheConfig } from '@apollo/client/core';
import type { NotificationOverview } from '~/composables/gql/graphql';

View File

@@ -1,5 +1,6 @@
import DOMPurify from 'isomorphic-dompurify';
import { Marked, type MarkedExtension } from 'marked';
import { Marked } from 'marked';
import type { MarkedExtension } from 'marked';
const defaultMarkedExtension: MarkedExtension = {
hooks: {

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.8.0",
"version": "4.9.3",
"private": true,
"license": "GPL-2.0-or-later",
"scripts": {

View File

@@ -61,6 +61,59 @@ export const useAccountStore = defineStore('account', () => {
accountActionStatus.value = 'waiting';
}
};
// Initialize mutations during store setup to maintain Apollo context
const { mutate: signOutMutation, onDone: onSignOutDone, onError: onSignOutError } = useMutation(CONNECT_SIGN_OUT);
const { mutate: signInMutation, onDone: onSignInDone, onError: onSignInError } = useMutation(CONNECT_SIGN_IN);
// Handle sign out mutation results
onSignOutDone((res) => {
console.debug('[connectSignOutMutation]', res);
accountActionStatus.value = 'success';
setQueueConnectSignOut(false); // reset
});
onSignOutError((error) => {
logErrorMessages(error);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'Failed to update Connect account configuration',
message: error.message,
level: 'error',
ref: 'connectSignOutMutation',
type: 'account',
});
});
// Handle sign in mutation results
onSignInDone((res) => {
if (res.data?.connectSignIn) {
accountActionStatus.value = 'success';
setConnectSignInPayload(undefined); // reset
return;
}
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'unraid-api failed to update Connect account configuration',
message: 'Sign In mutation unsuccessful',
level: 'error',
ref: 'connectSignInMutation',
type: 'account',
});
});
onSignInError((error) => {
logErrorMessages(error);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'unraid-api failed to update Connect account configuration',
message: error.message,
level: 'error',
ref: 'connectSignInMutation',
type: 'account',
});
});
watchEffect(() => {
if (unraidApiClient.value && connectSignInPayload.value) {
// connectSignInMutation();
@@ -258,7 +311,7 @@ export const useAccountStore = defineStore('account', () => {
);
};
const connectSignInMutation = async () => {
const connectSignInMutation = () => {
if (
!connectSignInPayload.value ||
(connectSignInPayload.value &&
@@ -271,83 +324,21 @@ export const useAccountStore = defineStore('account', () => {
}
accountActionStatus.value = 'updating';
const {
mutate: signInMutation,
onDone,
onError,
} = await useMutation(CONNECT_SIGN_IN, {
variables: {
input: {
apiKey: connectSignInPayload.value.apiKey,
userInfo: {
email: connectSignInPayload.value.email,
preferred_username: connectSignInPayload.value.preferred_username,
},
return signInMutation({
input: {
apiKey: connectSignInPayload.value.apiKey,
userInfo: {
email: connectSignInPayload.value.email,
preferred_username: connectSignInPayload.value.preferred_username,
},
},
});
signInMutation();
onDone((res) => {
if (res.data?.connectSignIn) {
accountActionStatus.value = 'success';
setConnectSignInPayload(undefined); // reset
return;
}
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'unraid-api failed to update Connect account configuration',
message: 'Sign In mutation unsuccessful',
level: 'error',
ref: 'connectSignInMutation',
type: 'account',
});
});
onError((error) => {
logErrorMessages(error);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'unraid-api failed to update Connect account configuration',
message: error.message,
level: 'error',
ref: 'connectSignInMutation',
type: 'account',
});
});
};
const connectSignOutMutation = async () => {
const connectSignOutMutation = () => {
accountActionStatus.value = 'updating';
// @todo is this still needed here with the change to a mutation?
// if (!serverStore.registered && accountAction.value && !accountAction.value?.user) {
// accountActionHide.value = true;
// accountActionStatus.value = 'success';
// return;
// }
const { mutate: signOutMutation, onDone, onError } = await useMutation(CONNECT_SIGN_OUT);
signOutMutation();
onDone((res) => {
console.debug('[connectSignOutMutation]', res);
accountActionStatus.value = 'success';
setQueueConnectSignOut(false); // reset
});
onError((error) => {
logErrorMessages(error);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'Failed to update Connect account configuration',
message: error.message,
level: 'error',
ref: 'connectSignOutMutation',
type: 'account',
});
});
return signOutMutation();
};
const setAccountAction = (action: ExternalSignIn | ExternalSignOut) => {