Compare commits

...

15 Commits

Author SHA1 Message Date
Eli Bosley
9fede1b3f5 feat: initial CPU stats monitor 2025-09-12 16:45:17 -04:00
Eli Bosley
01d353fa08 fix: set input color in SSO field rather than inside of the main.css 2025-09-12 15:36:10 -04:00
Eli Bosley
4a07953457 refactor: move global input text color for SSO button to SsoButton component 2025-09-12 15:33:28 -04:00
github-actions[bot]
0b20e3ea9f chore(main): release 4.22.0 (#1692)
🤖 I have created a release *beep* *boop*
---


## [4.22.0](https://github.com/unraid/api/compare/v4.21.0...v4.22.0)
(2025-09-12)


### Features

* improved update ui
([#1691](https://github.com/unraid/api/issues/1691))
([a59b363](a59b363ebc))


### Bug Fixes

* **deps:** update dependency camelcase-keys to v10
([#1687](https://github.com/unraid/api/issues/1687))
([95faeaa](95faeaa2f3))
* **deps:** update dependency p-retry to v7
([#1608](https://github.com/unraid/api/issues/1608))
([c782cf0](c782cf0e87))
* **deps:** update dependency uuid to v13
([#1688](https://github.com/unraid/api/issues/1688))
([2fef10c](2fef10c94a))
* **deps:** update dependency vue-sonner to v2
([#1475](https://github.com/unraid/api/issues/1475))
([f95ca9c](f95ca9c9cb))
* display settings fix for languages on less than 7.2-beta.2.3
([#1696](https://github.com/unraid/api/issues/1696))
([03dae7c](03dae7ce66))
* hide reset help option when sso is being checked
([#1695](https://github.com/unraid/api/issues/1695))
([222ced7](222ced7518))
* progressFrame white on black
([0990b89](0990b898bd))

---
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-09-12 14:59:21 -04:00
Eli Bosley
3f4af09db5 chore(deps): update conventional commit (#1693)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated changelog tooling dependencies and CI to fetch full Git
history; added conventional-changelog-conventionalcommits.
* **Tests**
* Added comprehensive tests for changelog output, header/tag handling,
fallback behavior, and compatibility with the updated changelog API.
* **Refactor**
* Reworked changelog generation to use the newer changelog API, improve
tag-aware headers, and support deriving PR-style changelogs with
graceful fallbacks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-12 14:24:49 -04:00
Eli Bosley
222ced7518 fix: hide reset help option when sso is being checked (#1695)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Enhancements
- Improved SSO sign-in experience on the login screen. The “Forgot your
password?” link is now automatically hidden while SSO status is being
checked or a provider is loading, and restored once ready. This reduces
confusion and visual flicker during authentication, ensuring a cleaner,
more stable layout throughout the process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-12 13:32:37 -04:00
Eli Bosley
03dae7ce66 fix: display settings fix for languages on less than 7.2-beta.2.3 (#1696)
…less
2025-09-12 13:32:26 -04:00
Eli Bosley
0990b898bd fix: progressFrame white on black 2025-09-12 08:50:47 -04:00
renovate[bot]
95faeaa2f3 fix(deps): update dependency camelcase-keys to v10 (#1687)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[camelcase-keys](https://redirect.github.com/sindresorhus/camelcase-keys)
| [`9.1.3` ->
`10.0.0`](https://renovatebot.com/diffs/npm/camelcase-keys/9.1.3/10.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/camelcase-keys/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/camelcase-keys/9.1.3/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>sindresorhus/camelcase-keys (camelcase-keys)</summary>

###
[`v10.0.0`](https://redirect.github.com/sindresorhus/camelcase-keys/releases/tag/v10.0.0)

[Compare
Source](https://redirect.github.com/sindresorhus/camelcase-keys/compare/v9.1.3...v10.0.0)

##### Breaking

- Require Node.js 20
[`2cc9388`](https://redirect.github.com/sindresorhus/camelcase-keys/commit/2cc9388)

##### Fixes

- Fix handling of circular references
[`3936f15`](https://redirect.github.com/sindresorhus/camelcase-keys/commit/3936f15)
- Fix TypeScript interface compatibility with stricter constraints
[`c89299a`](https://redirect.github.com/sindresorhus/camelcase-keys/commit/c89299a)
- Fix TypeScript types for union types in arrays
[`26e186e`](https://redirect.github.com/sindresorhus/camelcase-keys/commit/26e186e)

***

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these
updates again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 22:25:23 -04:00
renovate[bot]
b49ef5a762 chore(deps): update dependency @faker-js/faker to v10 (#1619)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [@faker-js/faker](https://fakerjs.dev)
([source](https://redirect.github.com/faker-js/faker)) | [`9.9.0` ->
`10.0.0`](https://renovatebot.com/diffs/npm/@faker-js%2ffaker/9.9.0/10.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@faker-js%2ffaker/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@faker-js%2ffaker/9.9.0/10.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>faker-js/faker (@&#8203;faker-js/faker)</summary>

###
[`v10.0.0`](https://redirect.github.com/faker-js/faker/blob/HEAD/CHANGELOG.md#1000-2025-08-21)

[Compare
Source](https://redirect.github.com/faker-js/faker/compare/v9.9.0...v10.0.0)

##### New Locales

- **locale:** extended list of colors in Polish
([#&#8203;3586](https://redirect.github.com/faker-js/faker/issues/3586))
([9940d54](9940d54f75))

##### Features

- **locales:** add animal vocabulary(bear, bird, cat, rabbit, pet\_name)
in Korean
([#&#8203;3535](https://redirect.github.com/faker-js/faker/issues/3535))
([0d2143c](0d2143c75d))

##### Changed Locales

- **locale:** remove invalid credit card issuer patterns
([#&#8203;3568](https://redirect.github.com/faker-js/faker/issues/3568))
([9783d95](9783d95a8e))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuODIuNyIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 22:21:51 -04:00
renovate[bot]
c782cf0e87 fix(deps): update dependency p-retry to v7 (#1608)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [p-retry](https://redirect.github.com/sindresorhus/p-retry) | [`6.2.1`
-> `7.0.0`](https://renovatebot.com/diffs/npm/p-retry/6.2.1/7.0.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/p-retry/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/p-retry/6.2.1/7.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>sindresorhus/p-retry (p-retry)</summary>

###
[`v7.0.0`](https://redirect.github.com/sindresorhus/p-retry/releases/tag/v7.0.0)

[Compare
Source](https://redirect.github.com/sindresorhus/p-retry/compare/v6.2.1...v7.0.0)

##### Breaking

- Require Node.js 20
[`3bdb53a`](https://redirect.github.com/sindresorhus/p-retry/commit/3bdb53a)
- `onFailedAttempt` and `shouldRetry` now receive a `context` object
instead of a decorated `error`
[`bff36bb`](https://redirect.github.com/sindresorhus/p-retry/commit/bff36bb)
- You must now must access the error as `object.error` instead of
`object`.
  - The use of `.attemptNumber` and `.retriesLeft` did not change.
- Remove the `forever` option
([#&#8203;79](https://redirect.github.com/sindresorhus/p-retry/issues/79))
[`6a89827`](https://redirect.github.com/sindresorhus/p-retry/commit/6a89827)
- Many use-cases can use `{retries: Infinity}` instead for infinite
retries.

##### Improvements

- Rewrite the package to not depend on the `retry` package
([#&#8203;79](https://redirect.github.com/sindresorhus/p-retry/issues/79))
[`6a89827`](https://redirect.github.com/sindresorhus/p-retry/commit/6a89827)
  - This is a full rewrite, so test carefully.
- Add
[`makeRetriable`](https://redirect.github.com/sindresorhus/p-retry#makeretriablefunction-options)
method
[`1a81c1e`](https://redirect.github.com/sindresorhus/p-retry/commit/1a81c1e)

***

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS44MS4yIiwidXBkYXRlZEluVmVyIjoiNDEuODEuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 22:21:23 -04:00
renovate[bot]
f95ca9c9cb fix(deps): update dependency vue-sonner to v2 (#1475)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [vue-sonner](https://redirect.github.com/xiaoluoboding/vue-sonner) |
[`1.3.2` ->
`2.0.8`](https://renovatebot.com/diffs/npm/vue-sonner/1.3.2/2.0.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-sonner/2.0.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-sonner/1.3.2/2.0.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>xiaoluoboding/vue-sonner (vue-sonner)</summary>

###
[`v2.0.8`](https://redirect.github.com/xiaoluoboding/vue-sonner/blob/HEAD/CHANGELOG.md#208-2025-08-18)

[Compare
Source](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v2.0.7...v2.0.8)

##### Bug Fixes

- fixed the type for nuxt 4
([e60b0bd](e60b0bd56f))

###
[`v2.0.7`](https://redirect.github.com/xiaoluoboding/vue-sonner/blob/HEAD/CHANGELOG.md#207-2025-08-17)

[Compare
Source](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v2.0.2...v2.0.7)

##### Bug Fixes

- custom component not trigger on dismiss function
([d5a69c6](d5a69c6ae1))
- fixed the build error
([5ec4bca](5ec4bca24f))
- improve expanded state management in Toaster component
([5acca24](5acca24250))
- postcss-calc will change the style
([f0d6add](f0d6add116))

##### Features

- add a new example
([cb08aef](cb08aef616))
- add example for close all
([70ef6e2](70ef6e2b7f))

###
[`v2.0.2`](https://redirect.github.com/xiaoluoboding/vue-sonner/blob/HEAD/CHANGELOG.md#202-2025-07-17)

[Compare
Source](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v2.0.1...v2.0.2)

###
[`v2.0.1`](https://redirect.github.com/xiaoluoboding/vue-sonner/blob/HEAD/CHANGELOG.md#201-2025-06-23)

[Compare
Source](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v2.0.0...v2.0.1)

##### Bug Fixes

- fixed multiple position bug
([9b07801](9b07801f5f))
- **theme:** make theme='system' reactive with watchEffect
([3b57e90](3b57e90498))
- **tsconfig:** add tsconfig.includes files
([e0d469b](e0d469b84e))

##### Features

- add do not generate css logic
([61458fb](61458fb7aa))

###
[`v2.0.0`](https://redirect.github.com/xiaoluoboding/vue-sonner/blob/HEAD/CHANGELOG.md#200-2025-05-21)

[Compare
Source](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v1.3.2...v2.0.0)

##### Bug Fixes

- add packages path
([35490b3](35490b3fb5))
- add packages path
([c7424e9](c7424e9070))
- fixed for nuxt module
([261eaf0](261eaf0be2))
- fixed for nuxt module
([29751cf](29751cfd5b))
- format
([9033f2b](9033f2b935))

####
[1.3.2](https://redirect.github.com/xiaoluoboding/vue-sonner/compare/v1.3.0...v1.3.2)
(2025-04-12)

##### Bug Fixes

- improve CSS insertion logic to handle document loading state
([6b22d24](6b22d2458b))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS4xNy4yIiwidXBkYXRlZEluVmVyIjoiNDEuNzEuMSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-09-11 22:14:46 -04:00
Eli Bosley
a59b363ebc feat: improved update ui (#1691)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
  * Global awaitable confirmation modal for notification actions.
  * “Ignore this release” toggle that persists to the server when used.
* New test pages and standalone test controls for the update modal and
theme switching.

* **Refactor**
* Update modal rebuilt with a responsive layout, unified “Update
Available” title, revised action logic, and centralized modal plumbing.

* **Style**
* OS Update highlight block, improved spacing, refreshed iconography,
and tooltips clarifying actions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-11 21:35:13 -04:00
renovate[bot]
2fef10c94a fix(deps): update dependency uuid to v13 (#1688)
This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
| [uuid](https://redirect.github.com/uuidjs/uuid) | [`11.1.0` ->
`13.0.0`](https://renovatebot.com/diffs/npm/uuid/11.1.0/13.0.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/uuid/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/uuid/11.1.0/13.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>uuidjs/uuid (uuid)</summary>

###
[`v13.0.0`](https://redirect.github.com/uuidjs/uuid/blob/HEAD/CHANGELOG.md#1300-2025-09-08)

[Compare
Source](https://redirect.github.com/uuidjs/uuid/compare/v12.0.0...v13.0.0)

##### ⚠ BREAKING CHANGES

- make browser exports the default
([#&#8203;901](https://redirect.github.com/uuidjs/uuid/issues/901))

##### Bug Fixes

- make browser exports the default
([#&#8203;901](https://redirect.github.com/uuidjs/uuid/issues/901))
([bce9d72](bce9d72a3a))

###
[`v12.0.0`](https://redirect.github.com/uuidjs/uuid/blob/HEAD/CHANGELOG.md#1200-2025-09-05)

[Compare
Source](https://redirect.github.com/uuidjs/uuid/compare/v11.1.0...v12.0.0)

##### ⚠ BREAKING CHANGES

- update to typescript\@&#8203;5.2
([#&#8203;887](https://redirect.github.com/uuidjs/uuid/issues/887))
- remove CommonJS support
([#&#8203;886](https://redirect.github.com/uuidjs/uuid/issues/886))
- drop node\@&#8203;16 support
([#&#8203;883](https://redirect.github.com/uuidjs/uuid/issues/883))

##### Features

- add node\@&#8203;24 to ci matrix
([#&#8203;879](https://redirect.github.com/uuidjs/uuid/issues/879))
([42b6178](42b6178aa2))
- drop node\@&#8203;16 support
([#&#8203;883](https://redirect.github.com/uuidjs/uuid/issues/883))
([0f38cf1](0f38cf1036))
- remove CommonJS support
([#&#8203;886](https://redirect.github.com/uuidjs/uuid/issues/886))
([ae786e2](ae786e2726))
- update to typescript\@&#8203;5.2
([#&#8203;887](https://redirect.github.com/uuidjs/uuid/issues/887))
([c7ee405](c7ee40598e))

##### Bug Fixes

- improve v4() performance
([#&#8203;894](https://redirect.github.com/uuidjs/uuid/issues/894))
([5fd974c](5fd974c127))
- restore node: prefix
([#&#8203;889](https://redirect.github.com/uuidjs/uuid/issues/889))
([e1f42a3](e1f42a3545))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/unraid/api).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0MS45Ny4xMCIsInVwZGF0ZWRJblZlciI6IjQxLjk3LjEwIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-11 21:20:48 -04:00
Eli Bosley
1c73a4af42 chore: rename .ce.vue files to .standalone.vue (#1690)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Documentation
- Updated contributor guide to use “standalone” naming for web
components.
- Refactor
- Migrated app and component references from legacy variants to
standalone components.
- Unified component registry and updated global component typings to
standalone names.
- Tests
- Updated test suites to target standalone components; no behavior
changes.

No user-facing changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-10 16:36:11 -04:00
85 changed files with 5706 additions and 1716 deletions

View File

@@ -20,6 +20,8 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v4

View File

@@ -1,71 +0,0 @@
name: Test Libvirt
on:
push:
branches:
- main
paths:
- "libvirt/**"
pull_request:
paths:
- "libvirt/**"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./libvirt
steps:
- uses: actions/checkout@v5
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: "3.13.7"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: libvirt-dev
version: 1.0
- name: Set Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10.15.0
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('libvirt/package.json') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: pnpm install
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: test
run: pnpm run test

View File

@@ -1 +1 @@
{".":"4.21.0"}
{".":"4.22.0"}

View File

@@ -3,4 +3,3 @@
@import './unraid-theme.css';
@import './theme-variants.css';
@import './base-utilities.css';
@import './sonner.css';

View File

@@ -1,708 +0,0 @@
/**------------------------------------------------------------------------------------------------
* SONNER.CSS
* This is a copy of Sonner's `style.css` as of commit a5b77c2df08d5c05aa923170176168102855533d
*
* This was necessary because I couldn't find a simple way to include Sonner's styles in vite's
* css build output. They wouldn't show up even though the toaster was included, and vue-sonner
* currently doesn't export its stylesheet (it appears to be inlined, but styles weren't applied
* to the unraid-toaster component for some reason).
*------------------------------------------------------------------------------------------------**/
:where(html[dir='ltr']),
:where([data-sonner-toaster][dir='ltr']) {
--toast-icon-margin-start: -3px;
--toast-icon-margin-end: 4px;
--toast-svg-margin-start: -1px;
--toast-svg-margin-end: 0px;
--toast-button-margin-start: auto;
--toast-button-margin-end: 0;
--toast-close-button-start: 0;
--toast-close-button-end: unset;
--toast-close-button-transform: translate(-35%, -35%);
}
:where(html[dir='rtl']),
:where([data-sonner-toaster][dir='rtl']) {
--toast-icon-margin-start: 4px;
--toast-icon-margin-end: -3px;
--toast-svg-margin-start: 0px;
--toast-svg-margin-end: -1px;
--toast-button-margin-start: 0;
--toast-button-margin-end: auto;
--toast-close-button-start: unset;
--toast-close-button-end: 0;
--toast-close-button-transform: translate(35%, -35%);
}
:where([data-sonner-toaster]) {
position: fixed;
width: var(--width);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial,
Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
--gray1: hsl(0, 0%, 99%);
--gray2: hsl(0, 0%, 97.3%);
--gray3: hsl(0, 0%, 95.1%);
--gray4: hsl(0, 0%, 93%);
--gray5: hsl(0, 0%, 90.9%);
--gray6: hsl(0, 0%, 88.7%);
--gray7: hsl(0, 0%, 85.8%);
--gray8: hsl(0, 0%, 78%);
--gray9: hsl(0, 0%, 56.1%);
--gray10: hsl(0, 0%, 52.3%);
--gray11: hsl(0, 0%, 43.5%);
--gray12: hsl(0, 0%, 9%);
--border-radius: 8px;
box-sizing: border-box;
padding: 0;
margin: 0;
list-style: none;
outline: none;
z-index: 999999999;
transition: transform 400ms ease;
}
:where([data-sonner-toaster][data-lifted='true']) {
transform: translateY(-10px);
}
@media (hover: none) and (pointer: coarse) {
:where([data-sonner-toaster][data-lifted='true']) {
transform: none;
}
}
:where([data-sonner-toaster][data-x-position='right']) {
right: max(var(--offset), env(safe-area-inset-right));
}
:where([data-sonner-toaster][data-x-position='left']) {
left: max(var(--offset), env(safe-area-inset-left));
}
:where([data-sonner-toaster][data-x-position='center']) {
left: 50%;
transform: translateX(-50%);
}
:where([data-sonner-toaster][data-y-position='top']) {
top: max(var(--offset), env(safe-area-inset-top));
}
:where([data-sonner-toaster][data-y-position='bottom']) {
bottom: max(var(--offset), env(safe-area-inset-bottom));
}
:where([data-sonner-toast]) {
--y: translateY(100%);
--lift-amount: calc(var(--lift) * var(--gap));
z-index: var(--z-index);
position: absolute;
opacity: 0;
transform: var(--y);
filter: blur(0);
/* https://stackoverflow.com/questions/48124372/pointermove-event-not-working-with-touch-why-not */
touch-action: none;
transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;
box-sizing: border-box;
outline: none;
overflow-wrap: anywhere;
}
:where([data-sonner-toast][data-styled='true']) {
padding: 16px;
background: var(--normal-bg);
border: 1px solid var(--normal-border);
color: var(--normal-text);
border-radius: var(--border-radius);
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
width: var(--width);
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
:where([data-sonner-toast]:focus-visible) {
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
}
:where([data-sonner-toast][data-y-position='top']) {
top: 0;
--y: translateY(-100%);
--lift: 1;
--lift-amount: calc(1 * var(--gap));
}
:where([data-sonner-toast][data-y-position='bottom']) {
bottom: 0;
--y: translateY(100%);
--lift: -1;
--lift-amount: calc(var(--lift) * var(--gap));
}
:where([data-sonner-toast]) :where([data-description]) {
font-weight: 400;
line-height: 1.4;
color: inherit;
}
:where([data-sonner-toast]) :where([data-title]) {
font-weight: 500;
line-height: 1.5;
color: inherit;
}
:where([data-sonner-toast]) :where([data-icon]) {
display: flex;
height: 16px;
width: 16px;
position: relative;
justify-content: flex-start;
align-items: center;
flex-shrink: 0;
margin-left: var(--toast-icon-margin-start);
margin-right: var(--toast-icon-margin-end);
}
:where([data-sonner-toast][data-promise='true']) :where([data-icon]) > svg {
opacity: 0;
transform: scale(0.8);
transform-origin: center;
animation: sonner-fade-in 300ms ease forwards;
}
:where([data-sonner-toast]) :where([data-icon]) > * {
flex-shrink: 0;
}
:where([data-sonner-toast]) :where([data-icon]) svg {
margin-left: var(--toast-svg-margin-start);
margin-right: var(--toast-svg-margin-end);
}
:where([data-sonner-toast]) :where([data-content]) {
display: flex;
flex-direction: column;
gap: 2px;
}
[data-sonner-toast][data-styled='true'] [data-button] {
border-radius: 4px;
padding-left: 8px;
padding-right: 8px;
height: 24px;
font-size: 12px;
color: var(--normal-bg);
background: var(--normal-text);
margin-left: var(--toast-button-margin-start);
margin-right: var(--toast-button-margin-end);
border: none;
cursor: pointer;
outline: none;
display: flex;
align-items: center;
flex-shrink: 0;
transition: opacity 400ms, box-shadow 200ms;
}
:where([data-sonner-toast]) :where([data-button]):focus-visible {
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.4);
}
:where([data-sonner-toast]) :where([data-button]):first-of-type {
margin-left: var(--toast-button-margin-start);
margin-right: var(--toast-button-margin-end);
}
:where([data-sonner-toast]) :where([data-cancel]) {
color: var(--normal-text);
background: rgba(0, 0, 0, 0.08);
}
:where([data-sonner-toast][data-theme='dark']) :where([data-cancel]) {
background: rgba(255, 255, 255, 0.3);
}
[data-sonner-toast] [data-close-button] {
position: absolute;
left: var(--toast-close-button-start);
right: var(--toast-close-button-end);
top: 0;
height: 20px;
width: 20px;
min-width: inherit !important;
margin: 0 !important;
display: flex;
justify-content: center;
align-items: center;
padding: 0;
color: hsl(var(--foreground));
border: 1px solid hsl(var(--border));
transform: var(--toast-close-button-transform);
border-radius: 50%;
cursor: pointer;
z-index: 1;
transition: opacity 100ms, background 200ms, border-color 200ms;
}
[data-sonner-toast] [data-close-button] {
background: hsl(var(--background));
}
:where([data-sonner-toast]) :where([data-close-button]):focus-visible {
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2);
}
:where([data-sonner-toast]) :where([data-disabled='true']) {
cursor: not-allowed;
}
[data-sonner-toast]:hover [data-close-button]:hover {
background: hsl(var(--muted));
border-color: hsl(var(--border));
}
/* Leave a ghost div to avoid setting hover to false when swiping out */
:where([data-sonner-toast][data-swiping='true'])::before {
content: '';
position: absolute;
left: 0;
right: 0;
height: 100%;
z-index: -1;
}
:where([data-sonner-toast][data-y-position='top'][data-swiping='true'])::before {
/* y 50% needed to distribute height additional height evenly */
bottom: 50%;
transform: scaleY(3) translateY(50%);
}
:where([data-sonner-toast][data-y-position='bottom'][data-swiping='true'])::before {
/* y -50% needed to distribute height additional height evenly */
top: 50%;
transform: scaleY(3) translateY(-50%);
}
/* Leave a ghost div to avoid setting hover to false when transitioning out */
:where([data-sonner-toast][data-swiping='false'][data-removed='true'])::before {
content: '';
position: absolute;
inset: 0;
transform: scaleY(2);
}
/* Needed to avoid setting hover to false when inbetween toasts */
:where([data-sonner-toast])::after {
content: '';
position: absolute;
left: 0;
height: calc(var(--gap) + 1px);
bottom: 100%;
width: 100%;
}
:where([data-sonner-toast][data-mounted='true']) {
--y: translateY(0);
opacity: 1;
}
:where([data-sonner-toast][data-expanded='false'][data-front='false']) {
--scale: var(--toasts-before) * 0.05 + 1;
--y: translateY(calc(var(--lift-amount) * var(--toasts-before))) scale(calc(-1 * var(--scale)));
height: var(--front-toast-height);
}
:where([data-sonner-toast]) > * {
transition: opacity 400ms;
}
:where([data-sonner-toast][data-expanded='false'][data-front='false'][data-styled='true']) > * {
opacity: 0;
}
:where([data-sonner-toast][data-visible='false']) {
opacity: 0;
pointer-events: none;
}
:where([data-sonner-toast][data-mounted='true'][data-expanded='true']) {
--y: translateY(calc(var(--lift) * var(--offset)));
height: var(--initial-height);
}
:where([data-sonner-toast][data-removed='true'][data-front='true'][data-swipe-out='false']) {
--y: translateY(calc(var(--lift) * -100%));
opacity: 0;
}
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='true']) {
--y: translateY(calc(var(--lift) * var(--offset) + var(--lift) * -100%));
opacity: 0;
}
:where([data-sonner-toast][data-removed='true'][data-front='false'][data-swipe-out='false'][data-expanded='false']) {
--y: translateY(40%);
opacity: 0;
transition: transform 500ms, opacity 200ms;
}
/* Bump up the height to make sure hover state doesn't get set to false */
:where([data-sonner-toast][data-removed='true'][data-front='false'])::before {
height: calc(var(--initial-height) + 20%);
}
[data-sonner-toast][data-swiping='true'] {
transform: var(--y) translateY(var(--swipe-amount, 0px));
transition: none;
}
[data-sonner-toast][data-swiped='true'] {
user-select: none;
}
[data-sonner-toast][data-swipe-out='true'][data-y-position='bottom'],
[data-sonner-toast][data-swipe-out='true'][data-y-position='top'] {
animation: swipe-out 200ms ease-out forwards;
}
@keyframes swipe-out {
from {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount)));
opacity: 1;
}
to {
transform: translateY(calc(var(--lift) * var(--offset) + var(--swipe-amount) + var(--lift) * -100%));
opacity: 0;
}
}
@media (max-width: 600px) {
[data-sonner-toaster] {
position: fixed;
--mobile-offset: 16px;
right: var(--mobile-offset);
left: var(--mobile-offset);
width: 100%;
}
[data-sonner-toaster][dir='rtl'] {
left: calc(var(--mobile-offset) * -1);
}
[data-sonner-toaster] [data-sonner-toast] {
left: 0;
right: 0;
width: calc(100% - var(--mobile-offset) * 2);
}
[data-sonner-toaster][data-x-position='left'] {
left: var(--mobile-offset);
}
[data-sonner-toaster][data-y-position='bottom'] {
bottom: 20px;
}
[data-sonner-toaster][data-y-position='top'] {
top: 20px;
}
[data-sonner-toaster][data-x-position='center'] {
left: var(--mobile-offset);
right: var(--mobile-offset);
transform: none;
}
}
[data-sonner-toaster][data-theme='light'] {
--normal-bg: hsl(var(--background));
--normal-border: hsl(var(--border));
--normal-text: hsl(var(--foreground));
--success-bg: hsl(var(--background));
--success-border: hsl(var(--border));
--success-text: hsl(140, 100%, 27%);
--info-bg: hsl(var(--background));
--info-border: hsl(var(--border));
--info-text: hsl(210, 92%, 45%);
--warning-bg: hsl(var(--background));
--warning-border: hsl(var(--border));
--warning-text: hsl(31, 92%, 45%);
--error-bg: hsl(var(--background));
--error-border: hsl(var(--border));
--error-text: hsl(360, 100%, 45%);
/* Old colors, preserved for reference
--success-bg: hsl(143, 85%, 96%);
--success-border: hsl(145, 92%, 91%);
--success-text: hsl(140, 100%, 27%);
--info-bg: hsl(208, 100%, 97%);
--info-border: hsl(221, 91%, 91%);
--info-text: hsl(210, 92%, 45%);
--warning-bg: hsl(49, 100%, 97%);
--warning-border: hsl(49, 91%, 91%);
--warning-text: hsl(31, 92%, 45%);
--error-bg: hsl(359, 100%, 97%);
--error-border: hsl(359, 100%, 94%);
--error-text: hsl(360, 100%, 45%); */
}
[data-sonner-toaster][data-theme='light'] [data-sonner-toast][data-invert='true'] {
--normal-bg: hsl(0 0% 3.9%);
--normal-border: hsl(0 0% 14.9%);
--normal-text: hsl(0 0% 98%);
}
[data-sonner-toaster][data-theme='dark'] [data-sonner-toast][data-invert='true'] {
--normal-bg: hsl(0 0% 100%);
--normal-border: hsl(0 0% 89.8%);
--normal-text: hsl(0 0% 3.9%);
}
[data-sonner-toaster][data-theme='dark'] {
--normal-bg: hsl(var(--background));
--normal-border: hsl(var(--border));
--normal-text: hsl(var(--foreground));
--success-bg: hsl(var(--background));
--success-border: hsl(var(--border));
--success-text: hsl(150, 86%, 65%);
--info-bg: hsl(var(--background));
--info-border: hsl(var(--border));
--info-text: hsl(216, 87%, 65%);
--warning-bg: hsl(var(--background));
--warning-border: hsl(var(--border));
--warning-text: hsl(46, 87%, 65%);
--error-bg: hsl(var(--background));
--error-border: hsl(var(--border));
--error-text: hsl(358, 100%, 81%);
/* Old colors, preserved for reference
--success-bg: hsl(150, 100%, 6%);
--success-border: hsl(147, 100%, 12%);
--success-text: hsl(150, 86%, 65%);
--info-bg: hsl(215, 100%, 6%);
--info-border: hsl(223, 100%, 12%);
--info-text: hsl(216, 87%, 65%);
--warning-bg: hsl(64, 100%, 6%);
--warning-border: hsl(60, 100%, 12%);
--warning-text: hsl(46, 87%, 65%);
--error-bg: hsl(358, 76%, 10%);
--error-border: hsl(357, 89%, 16%);
--error-text: hsl(358, 100%, 81%); */
}
[data-rich-colors='true'][data-sonner-toast][data-type='success'] {
background: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='success'] [data-close-button] {
background: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='info'] {
background: var(--info-bg);
border-color: var(--info-border);
color: var(--info-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='info'] [data-close-button] {
background: var(--info-bg);
border-color: var(--info-border);
color: var(--info-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] {
background: var(--warning-bg);
border-color: var(--warning-border);
color: var(--warning-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='warning'] [data-close-button] {
background: var(--warning-bg);
border-color: var(--warning-border);
color: var(--warning-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='error'] {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
[data-rich-colors='true'][data-sonner-toast][data-type='error'] [data-close-button] {
background: var(--error-bg);
border-color: var(--error-border);
color: var(--error-text);
}
.sonner-loading-wrapper {
--size: 16px;
height: var(--size);
width: var(--size);
position: absolute;
inset: 0;
z-index: 10;
}
.sonner-loading-wrapper[data-visible='false'] {
transform-origin: center;
animation: sonner-fade-out 0.2s ease forwards;
}
.sonner-spinner {
position: relative;
top: 50%;
left: 50%;
height: var(--size);
width: var(--size);
}
.sonner-loading-bar {
animation: sonner-spin 1.2s linear infinite;
background: hsl(var(--muted-foreground));
border-radius: 6px;
height: 8%;
left: -10%;
position: absolute;
top: -3.9%;
width: 24%;
}
.sonner-loading-bar:nth-child(1) {
animation-delay: -1.2s;
transform: rotate(0.0001deg) translate(146%);
}
.sonner-loading-bar:nth-child(2) {
animation-delay: -1.1s;
transform: rotate(30deg) translate(146%);
}
.sonner-loading-bar:nth-child(3) {
animation-delay: -1s;
transform: rotate(60deg) translate(146%);
}
.sonner-loading-bar:nth-child(4) {
animation-delay: -0.9s;
transform: rotate(90deg) translate(146%);
}
.sonner-loading-bar:nth-child(5) {
animation-delay: -0.8s;
transform: rotate(120deg) translate(146%);
}
.sonner-loading-bar:nth-child(6) {
animation-delay: -0.7s;
transform: rotate(150deg) translate(146%);
}
.sonner-loading-bar:nth-child(7) {
animation-delay: -0.6s;
transform: rotate(180deg) translate(146%);
}
.sonner-loading-bar:nth-child(8) {
animation-delay: -0.5s;
transform: rotate(210deg) translate(146%);
}
.sonner-loading-bar:nth-child(9) {
animation-delay: -0.4s;
transform: rotate(240deg) translate(146%);
}
.sonner-loading-bar:nth-child(10) {
animation-delay: -0.3s;
transform: rotate(270deg) translate(146%);
}
.sonner-loading-bar:nth-child(11) {
animation-delay: -0.2s;
transform: rotate(300deg) translate(146%);
}
.sonner-loading-bar:nth-child(12) {
animation-delay: -0.1s;
transform: rotate(330deg) translate(146%);
}
@keyframes sonner-fade-in {
0% {
opacity: 0;
transform: scale(0.8);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes sonner-fade-out {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.8);
}
}
@keyframes sonner-spin {
0% {
opacity: 1;
}
100% {
opacity: 0.15;
}
}
@media (prefers-reduced-motion) {
[data-sonner-toast],
[data-sonner-toast] > *,
.sonner-loading-bar {
transition: none !important;
animation: none !important;
}
}
.sonner-loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transform-origin: center;
transition: opacity 200ms, transform 200ms;
}
.sonner-loader[data-visible='false'] {
opacity: 0;
transform: scale(0.8) translate(-50%, -50%);
}
/* Override Unraid webgui docker icon styles on sonner containers */
[data-sonner-toast] [data-icon]:before,
[data-sonner-toast] .fa-docker:before {
font-family: inherit !important;
content: '' !important;
}

View File

@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
This is the Unraid API monorepo containing multiple packages that provide API functionality for Unraid servers. It uses pnpm workspaces with the following structure:
- `/api` - Core NestJS API server with GraphQL
- `/web` - Nuxt.js frontend application
- `/web` - Vue 3 frontend application
- `/unraid-ui` - Vue 3 component library
- `/plugin` - Unraid plugin package (.plg)
- `/packages` - Shared packages and API plugins
@@ -128,9 +128,6 @@ Enables GraphQL playground at `http://tower.local/graphql`
- **Use Mocks Correctly**: Mocks should be used as nouns, not verbs.
#### Vue Component Testing
- This is a Nuxt.js app but we are testing with vitest outside of the Nuxt environment
- Nuxt is currently set to auto import so some vue files may need compute or ref imported
- Use pnpm when running terminal commands and stay within the web directory
- Tests are located under `web/__test__`, run with `pnpm test`
- Use `mount` from Vue Test Utils for component testing

View File

@@ -1,5 +1,23 @@
# Changelog
## [4.22.0](https://github.com/unraid/api/compare/v4.21.0...v4.22.0) (2025-09-12)
### Features
* improved update ui ([#1691](https://github.com/unraid/api/issues/1691)) ([a59b363](https://github.com/unraid/api/commit/a59b363ebc1e660f854c55d50fc02c823c2fd0cc))
### Bug Fixes
* **deps:** update dependency camelcase-keys to v10 ([#1687](https://github.com/unraid/api/issues/1687)) ([95faeaa](https://github.com/unraid/api/commit/95faeaa2f39bf7bd16502698d7530aaa590b286d))
* **deps:** update dependency p-retry to v7 ([#1608](https://github.com/unraid/api/issues/1608)) ([c782cf0](https://github.com/unraid/api/commit/c782cf0e8710c6690050376feefda3edb30dd549))
* **deps:** update dependency uuid to v13 ([#1688](https://github.com/unraid/api/issues/1688)) ([2fef10c](https://github.com/unraid/api/commit/2fef10c94aae910e95d9f5bcacf7289e2cca6ed9))
* **deps:** update dependency vue-sonner to v2 ([#1475](https://github.com/unraid/api/issues/1475)) ([f95ca9c](https://github.com/unraid/api/commit/f95ca9c9cb69725dcf3bb4bcbd0b558a2074e311))
* display settings fix for languages on less than 7.2-beta.2.3 ([#1696](https://github.com/unraid/api/issues/1696)) ([03dae7c](https://github.com/unraid/api/commit/03dae7ce66b3409593eeee90cd5b56e2a920ca44))
* hide reset help option when sso is being checked ([#1695](https://github.com/unraid/api/issues/1695)) ([222ced7](https://github.com/unraid/api/commit/222ced7518d40c207198a3b8548f0e024bc865b0))
* progressFrame white on black ([0990b89](https://github.com/unraid/api/commit/0990b898bd02c231153157c20d5142e5fd4513cd))
## [4.21.0](https://github.com/unraid/api/compare/v4.20.4...v4.21.0) (2025-09-10)

View File

@@ -1,5 +1,5 @@
{
"version": "4.19.1",
"version": "4.22.0",
"extraOrigins": [],
"sandbox": true,
"ssoSubIds": [],

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.21.0",
"version": "4.22.0",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -84,7 +84,7 @@
"bytes": "3.1.2",
"cache-manager": "7.2.0",
"cacheable-lookup": "7.0.0",
"camelcase-keys": "9.1.3",
"camelcase-keys": "10.0.0",
"casbin": "5.38.0",
"change-case": "5.4.4",
"chokidar": "4.0.3",
@@ -127,7 +127,7 @@
"node-cache": "5.1.2",
"node-window-polyfill": "1.0.4",
"openid-client": "6.6.4",
"p-retry": "6.2.1",
"p-retry": "7.0.0",
"passport-custom": "1.1.1",
"passport-http-header-strategy": "1.1.0",
"path-type": "6.0.0",
@@ -141,7 +141,7 @@
"strftime": "0.10.3",
"systeminformation": "5.27.8",
"undici": "7.15.0",
"uuid": "11.1.0",
"uuid": "13.0.0",
"ws": "8.18.3",
"zen-observable-ts": "1.1.0",
"zod": "3.25.76"

View File

@@ -29,16 +29,16 @@ export class CpuService {
return {
id: 'info/cpu-load',
percentTotal: loadData.currentLoad,
percentTotal: Math.floor(loadData.currentLoad),
cpus: loadData.cpus.map((cpu) => ({
percentTotal: cpu.load,
percentUser: cpu.loadUser,
percentSystem: cpu.loadSystem,
percentNice: cpu.loadNice,
percentIdle: cpu.loadIdle,
percentIrq: cpu.loadIrq,
percentGuest: cpu.loadGuest || 0,
percentSteal: cpu.loadSteal || 0,
percentTotal: Math.floor(cpu.load),
percentUser: Math.floor(cpu.loadUser),
percentSystem: Math.floor(cpu.loadSystem),
percentNice: Math.floor(cpu.loadNice),
percentIdle: Math.floor(cpu.loadIdle),
percentIrq: Math.floor(cpu.loadIrq),
percentGuest: Math.floor(cpu.loadGuest || 0),
percentSteal: Math.floor(cpu.loadSteal || 0),
})),
};
}

View File

@@ -1,6 +1,9 @@
import { readFile } from 'node:fs/promises';
import { FileModification } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
import {
FileModification,
ShouldApplyWithReason,
} from '@app/unraid-api/unraid-file-modifier/file-modification.js';
export default class DisplaySettingsModification extends FileModification {
id: string = 'display-settings';
@@ -34,4 +37,15 @@ export default class DisplaySettingsModification extends FileModification {
return this.createPatchWithDiff(overridePath ?? this.filePath, fileContent, newContent);
}
async shouldApply(): Promise<ShouldApplyWithReason> {
const superShouldApply = await super.shouldApply();
if (!superShouldApply.shouldApply) {
return superShouldApply;
}
return {
shouldApply: true,
reason: 'Display settings modification needed for Unraid version <= 7.2.0-beta.2.3',
};
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.21.0",
"version": "4.22.0",
"scripts": {
"build": "pnpm -r build",
"build:watch": "pnpm -r --parallel --filter '!@unraid/ui' build:watch",

View File

@@ -25,7 +25,7 @@
"description": "Unraid Connect plugin for Unraid API",
"devDependencies": {
"@apollo/client": "3.14.0",
"@faker-js/faker": "9.9.0",
"@faker-js/faker": "10.0.0",
"@graphql-codegen/cli": "5.0.7",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.6.3",
@@ -43,7 +43,7 @@
"@types/lodash-es": "4.17.12",
"@types/node": "22.18.0",
"@types/ws": "8.18.1",
"camelcase-keys": "9.1.3",
"camelcase-keys": "10.0.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"execa": "9.6.0",
@@ -84,7 +84,7 @@
"@nestjs/graphql": "13.1.0",
"@nestjs/schedule": "6.0.0",
"@runonflux/nat-upnp": "1.0.2",
"camelcase-keys": "9.1.3",
"camelcase-keys": "10.0.0",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"execa": "9.6.0",

View File

@@ -0,0 +1,181 @@
import { describe, it, expect, beforeAll } from "vitest";
import { execSync } from "child_process";
import { getStagingChangelogFromGit } from "./changelog.js";
describe.sequential("getStagingChangelogFromGit", () => {
let currentCommitMessage: string | null = null;
beforeAll(() => {
// Get the current commit message to validate it appears in changelog
try {
currentCommitMessage = execSync('git log -1 --pretty=%s', { encoding: 'utf8' }).trim();
} catch (e) {
// Ignore if we can't get commit
}
});
it("should generate changelog header with version", { timeout: 20000 }, async () => {
const result = await getStagingChangelogFromGit({
pluginVersion: "99.99.99",
tag: undefined as any,
});
expect(result).toBeDefined();
expect(typeof result).toBe("string");
// Should contain version header
expect(result).toContain("99.99.99");
// Should have markdown header formatting
expect(result).toMatch(/##\s+/);
});
it("should generate changelog with tag parameter", { timeout: 20000 }, async () => {
// When tag is provided, it should generate changelog with tag in header
const result = await getStagingChangelogFromGit({
pluginVersion: "99.99.99",
tag: "test-tag-99",
});
expect(result).toBeDefined();
expect(typeof result).toBe("string");
expect(result).toContain("test-tag-99");
// Should have a version header
expect(result).toMatch(/##\s+/);
// IMPORTANT: Verify that actual commits are included in the changelog
// This ensures the gitRawCommitsOpts is working correctly
// The changelog should include commits if there are any between origin/main and HEAD
// We check for common changelog patterns that indicate actual content
if (result.length > 100) {
// If we have a substantial changelog, it should contain commit information
expect(
result.includes("### Features") ||
result.includes("### Bug Fixes") ||
result.includes("### ") ||
result.includes("* ") // Commit entries typically start with asterisk
).toBe(true);
}
});
it("should handle error gracefully and return tag", { timeout: 20000 }, async () => {
// The function catches errors and returns the tag
// An empty version might not cause an error, so let's just verify
// the function completes without throwing
const result = await getStagingChangelogFromGit({
pluginVersion: "test-version",
tag: "fallback-tag",
});
expect(result).toBeDefined();
expect(typeof result).toBe("string");
// Should either return a changelog or the fallback tag
expect(result.length).toBeGreaterThan(0);
});
it("should use conventional-changelog v7 API correctly", { timeout: 20000 }, async () => {
// This test validates that the v7 API is being called correctly
// by checking that the function executes without throwing
let error: any = null;
try {
await getStagingChangelogFromGit({
pluginVersion: "99.99.99",
tag: undefined as any,
});
} catch (e) {
error = e;
}
// The v7 API should work without errors
expect(error).toBeNull();
});
it("should validate changelog structure", { timeout: 20000 }, async () => {
// Create a changelog with high version number to avoid conflicts
const result = await getStagingChangelogFromGit({
pluginVersion: "999.0.0",
tag: "v999-test",
});
expect(result).toBeDefined();
expect(typeof result).toBe("string");
// Verify basic markdown structure
if (result.length > 50) {
// Should have tag in header when tag is provided
expect(result).toMatch(/##\s+\[?v999-test/);
// Should be valid markdown with proper line breaks
expect(result).toMatch(/\n/);
}
});
it("should include actual commits when using gitRawCommitsOpts with tag", { timeout: 20000 }, async () => {
// This test ensures that gitRawCommitsOpts is working correctly
// and actually fetching commits between origin/main and HEAD
const result = await getStagingChangelogFromGit({
pluginVersion: "99.99.99",
tag: "CI-TEST",
});
expect(result).toBeDefined();
expect(typeof result).toBe("string");
// The header should contain the tag
expect(result).toContain("CI-TEST");
// Critical: The changelog should NOT be just the tag (error fallback)
expect(result).not.toBe("CI-TEST");
// The changelog should have a proper markdown header
expect(result).toMatch(/^##\s+/);
// Check if we're in a git repo with commits ahead of the base branch
let commitCount = 0;
try {
// Try to detect the base branch (same logic as in changelog.ts)
let baseBranch = "origin/main";
try {
const originHead = execSync("git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
}).trim();
if (originHead) {
baseBranch = originHead.replace("refs/remotes/", "");
}
} catch {
// Try common branches
const branches = ["origin/main", "origin/master", "origin/develop"];
for (const branch of branches) {
try {
execSync(`git rev-parse --verify ${branch} 2>/dev/null`, { stdio: "ignore" });
baseBranch = branch;
break;
} catch {
// Continue to next branch
}
}
}
commitCount = parseInt(execSync(`git rev-list --count ${baseBranch}..HEAD`, { encoding: "utf8" }).trim());
} catch {
// If we can't determine, we'll check for minimal content
}
// If there are commits on this branch, the changelog MUST include them
if (commitCount > 0) {
// The changelog must be more than just a header
// A minimal header is "## CI-TEST (2025-09-12)\n\n" which is ~30 chars
expect(result.length).toBeGreaterThan(50);
// Should have actual commit content
const hasCommitContent =
result.includes("### ") || // Section headers like ### Features
result.includes("* ") || // Commit bullet points
result.includes("- "); // Alternative bullet style
if (!hasCommitContent) {
throw new Error(`Expected changelog to contain commits but got only: ${result.substring(0, 100)}...`);
}
expect(hasCommitContent).toBe(true);
}
});
});

View File

@@ -1,40 +1,167 @@
import conventionalChangelog from "conventional-changelog";
import { ConventionalChangelog } from "conventional-changelog";
import { execSync } from "child_process";
import { PluginEnv } from "../cli/setup-plugin-environment";
import { PluginEnv } from "../cli/setup-plugin-environment.js";
/**
* Detects the base branch and finds the merge base for PR changelog generation
* Returns the merge-base commit to only show commits from the current PR
*/
function getMergeBase(): string | null {
try {
// First, find the base branch
let baseBranch: string | null = null;
// Try to get the default branch from origin/HEAD
try {
const originHead = execSync("git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null", {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
}).trim();
if (originHead) {
baseBranch = originHead.replace("refs/remotes/", "");
}
} catch {
// origin/HEAD not set, continue to next strategy
}
// Try common default branch names if origin/HEAD didn't work
if (!baseBranch) {
const commonBranches = ["origin/main", "origin/master", "origin/develop"];
for (const branch of commonBranches) {
try {
execSync(`git rev-parse --verify ${branch} 2>/dev/null`, { stdio: "ignore" });
baseBranch = branch;
break;
} catch {
// Branch doesn't exist, try next
}
}
}
if (!baseBranch) {
return null;
}
// Find the merge-base between the current branch and the base branch
// This gives us the commit where the PR branch diverged from main
try {
const mergeBase = execSync(`git merge-base ${baseBranch} HEAD`, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"]
}).trim();
return mergeBase;
} catch {
// If merge-base fails, fall back to the base branch itself
return baseBranch;
}
} catch {
// Git command failed entirely, return null
return null;
}
}
/**
* Generate a simple changelog for PR builds
*/
function generatePRChangelog(tag: string, mergeBase: string): string | null {
try {
// Get commits from this PR only with conventional commit parsing
const commits = execSync(
`git log ${mergeBase}..HEAD --pretty=format:"%s|%h" --reverse`,
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }
).trim();
if (!commits) {
return null;
}
const lines = commits.split('\n').filter(Boolean);
const features: string[] = [];
const fixes: string[] = [];
const other: string[] = [];
for (const line of lines) {
const [message, hash] = line.split('|');
const formatted = `* ${message} (${hash})`;
if (message.startsWith('feat')) {
features.push(formatted);
} else if (message.startsWith('fix')) {
fixes.push(formatted);
} else {
other.push(formatted);
}
}
let changelog = `## [${tag}](https://github.com/unraid/api/${tag})\n\n`;
if (features.length > 0) {
changelog += `### Features\n\n${features.join('\n')}\n\n`;
}
if (fixes.length > 0) {
changelog += `### Bug Fixes\n\n${fixes.join('\n')}\n\n`;
}
if (other.length > 0) {
changelog += `### Other Changes\n\n${other.join('\n')}\n\n`;
}
return changelog;
} catch {
return null;
}
}
export const getStagingChangelogFromGit = async ({
pluginVersion,
tag,
}: Pick<PluginEnv, "pluginVersion" | "tag">): Promise<string> => {
try {
const changelogStream = conventionalChangelog(
{
preset: "conventionalcommits",
},
{
version: pluginVersion,
},
tag
? {
from: "origin/main",
to: "HEAD",
}
: {},
undefined,
tag
? {
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
}
: undefined
);
// For PR builds with a tag, try to generate a simple PR-specific changelog
if (tag) {
const mergeBase = getMergeBase();
if (mergeBase) {
const prChangelog = generatePRChangelog(tag, mergeBase);
if (prChangelog) {
return prChangelog;
}
}
}
// Fall back to conventional-changelog for non-PR builds or if PR detection fails
const options: any = {
releaseCount: 1,
};
if (tag) {
options.writerOpts = {
headerPartial: `## [${tag}](https://github.com/unraid/api/${tag})\n\n`,
};
}
const generator = new ConventionalChangelog()
.loadPreset("conventionalcommits")
.context({
version: tag || pluginVersion,
...(tag && {
linkCompare: false,
}),
})
.options(options);
let changelog = "";
for await (const chunk of changelogStream) {
for await (const chunk of generator.write()) {
changelog += chunk;
}
// Encode HTML entities using the 'he' library
return changelog ?? "";
return changelog || "";
} catch (err) {
console.log('Non-fatal error: Failed to get changelog from git:', err);
return tag;
// Return a properly formatted fallback with markdown header
if (tag) {
return `## [${tag}](https://github.com/unraid/api/${tag})\n\n`;
}
return `## ${pluginVersion}\n\n`;
}
};
};

View File

@@ -1,10 +1,11 @@
{
"name": "@unraid/connect-plugin",
"version": "4.21.0",
"version": "4.22.0",
"private": true,
"dependencies": {
"commander": "14.0.0",
"conventional-changelog": "6.0.0",
"conventional-changelog": "7.1.1",
"conventional-changelog-conventionalcommits": "^9.1.0",
"date-fns": "4.1.0",
"glob": "11.0.3",
"html-sloppy-escaper": "0.1.0",

4034
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/ui",
"version": "4.21.0",
"version": "4.22.0",
"private": true,
"license": "GPL-2.0-or-later",
"type": "module",
@@ -66,7 +66,7 @@
"shadcn-vue": "2.2.0",
"tailwind-merge": "2.6.0",
"tw-animate-css": "1.3.7",
"vue-sonner": "1.3.2"
"vue-sonner": "2.0.8"
},
"devDependencies": {
"@eslint/js": "9.34.0",

View File

@@ -51,7 +51,7 @@ const classes = computed(() => {
});
const needsBrandGradientBackground = computed(() => {
return ['outline-solid', 'outline-primary'].includes(props.variant ?? '');
return ['outline', 'outline-solid', 'outline-primary'].includes(props.variant ?? '');
});
const isLink = computed(() => Boolean(props.href));

View File

@@ -1,8 +1,16 @@
<script lang="ts" setup>
import { onMounted } from 'vue';
import { Toaster as Sonner, toast, type ToasterProps } from 'vue-sonner';
import 'vue-sonner/style.css';
const props = defineProps<ToasterProps>();
// Accept theme as a prop, default to 'light' if not provided
interface Props extends ToasterProps {
theme?: 'light' | 'dark' | 'system';
}
const props = withDefaults(defineProps<Props>(), {
theme: 'light',
});
onMounted(() => {
globalThis.toast = toast;
@@ -27,3 +35,17 @@ onMounted(() => {
}"
/>
</template>
<style>
/* Override styles for Unraid environment */
[data-sonner-toast] [data-close-button] {
min-width: inherit !important;
}
/* Override Unraid webgui docker icon styles on sonner containers */
[data-sonner-toast] [data-icon]:before,
[data-sonner-toast] .fa-docker:before {
font-family: inherit !important;
content: '' !important;
}
</style>

View File

@@ -1,5 +1,5 @@
import { createAjv } from '@jsonforms/core';
import type Ajv from 'ajv';
import type { Ajv } from 'ajv';
import addErrors from 'ajv-errors';
export interface JsonFormsConfig {

View File

@@ -31,6 +31,8 @@ export default function createConfig() {
external: [
'vue',
'tailwindcss',
'ajv',
'ajv-errors',
...(process.env.npm_lifecycle_script?.includes('storybook') ? [/^storybook\//] : []),
],
input: {

View File

@@ -4,7 +4,7 @@
For legacy compatibility, Unraid ships web components to the webgui. These components
are written as Vue and turned into web components as a build step. By convention,
Vue components that are built as top-level web components are suffixed with `*.ce.vue`
Vue components that are built as top-level web components are suffixed with `*.standalone.vue`
for "**c**ustom **e**lement", which comes from the tool used for compilation: `nuxt-custom-elements`.
Note: `nuxt-custom-elements` is currently pinned to a specific version because

View File

@@ -9,7 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ComposerTranslation } from 'vue-i18n';
import WelcomeModal from '~/components/Activation/WelcomeModal.ce.vue';
import WelcomeModal from '~/components/Activation/WelcomeModal.standalone.vue';
vi.mock('@unraid/ui', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>;
@@ -76,7 +76,7 @@ vi.mock('~/store/theme', () => ({
useThemeStore: () => mockThemeStore,
}));
describe('Activation/WelcomeModal.ce.vue', () => {
describe('Activation/WelcomeModal.standalone.vue', () => {
let mockSetProperty: ReturnType<typeof vi.fn>;
let mockQuerySelector: ReturnType<typeof vi.fn>;

View File

@@ -11,7 +11,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { ServerconnectPluginInstalled } from '~/types/server';
import Auth from '~/components/Auth.ce.vue';
import Auth from '~/components/Auth.standalone.vue';
import { useServerStore } from '~/store/server';
vi.mock('vue-i18n', () => ({

View File

@@ -12,7 +12,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import type { MockInstance } from 'vitest';
import ColorSwitcher from '~/components/ColorSwitcher.ce.vue';
import ColorSwitcher from '~/components/ColorSwitcher.standalone.vue';
import { useThemeStore } from '~/store/theme';
// Explicitly mock @unraid/ui to ensure we use the actual components

View File

@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import DowngradeOs from '~/components/DowngradeOs.ce.vue';
import DowngradeOs from '~/components/DowngradeOs.standalone.vue';
import { useServerStore } from '~/store/server';
vi.mock('crypto-js/aes', () => ({

View File

@@ -8,7 +8,7 @@ import { BrandButton } from '@unraid/ui';
import { createTestingPinia } from '@pinia/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import DownloadApiLogs from '~/components/DownloadApiLogs.ce.vue';
import DownloadApiLogs from '~/components/DownloadApiLogs.standalone.vue';
vi.mock('~/helpers/urls', () => ({
CONNECT_FORUMS: new URL('http://mock-forums.local'),

View File

@@ -14,7 +14,7 @@ import type { VueWrapper } from '@vue/test-utils';
import type { Error as CustomApiError } from '~/store/errors';
import type { ServerUpdateOsResponse } from '~/types/server';
import HeaderOsVersion from '~/components/HeaderOsVersion.ce.vue';
import HeaderOsVersion from '~/components/HeaderOsVersion.standalone.vue';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';

View File

@@ -12,7 +12,7 @@ import type { VueWrapper } from '@vue/test-utils';
import type { ServerconnectPluginInstalled } from '~/types/server';
import type { Pinia } from 'pinia';
import Registration from '~/components/Registration.ce.vue';
import Registration from '~/components/Registration.standalone.vue';
import { usePurchaseStore } from '~/store/purchase';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
@@ -116,7 +116,7 @@ vi.mock('vue-i18n', () => ({
useI18n: () => ({ t }),
}));
describe('Registration.ce.vue', () => {
describe('Registration.standalone.vue', () => {
let wrapper: VueWrapper<unknown>;
let pinia: Pinia;
let serverStore: ReturnType<typeof useServerStore>;

View File

@@ -46,14 +46,14 @@ const mockLocation = {
vi.stubGlobal('location', mockLocation);
describe('ThemeSwitcher.ce.vue', () => {
describe('ThemeSwitcher.standalone.vue', () => {
let consoleDebugSpy: MockInstance;
let consoleLogSpy: MockInstance;
let consoleErrorSpy: MockInstance;
let ThemeSwitcher: unknown;
beforeEach(async () => {
ThemeSwitcher = (await import('~/components/ThemeSwitcher.ce.vue')).default;
ThemeSwitcher = (await import('~/components/ThemeSwitcher.standalone.vue')).default;
vi.useFakeTimers();
vi.clearAllMocks();

View File

@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import { createTestingPinia } from '@pinia/testing';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import UpdateOs from '~/components/UpdateOs.ce.vue';
import UpdateOs from '~/components/UpdateOs.standalone.vue';
vi.mock('@unraid/ui', () => ({
PageContainer: { template: '<div><slot /></div>' },
@@ -61,7 +61,7 @@ const UpdateOsThirdPartyDriversStub = {
props: ['t'],
};
describe('UpdateOs.ce.vue', () => {
describe('UpdateOs.standalone.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
mockRebootType.value = '';

View File

@@ -11,7 +11,7 @@ import type { VueWrapper } from '@vue/test-utils';
import type { Server, ServerconnectPluginInstalled, ServerState } from '~/types/server';
import type { Pinia } from 'pinia';
import UserProfile from '~/components/UserProfile.ce.vue';
import UserProfile from '~/components/UserProfile.standalone.vue';
import { useServerStore } from '~/store/server';
import { useThemeStore } from '~/store/theme';
@@ -101,7 +101,7 @@ const stubs = {
UpcDropdownTrigger: { template: '<button data-testid="dropdown-trigger"></button>' },
};
describe('UserProfile.ce.vue', () => {
describe('UserProfile.standalone.vue', () => {
let wrapper: VueWrapper<InstanceType<typeof UserProfile>>;
let pinia: Pinia;
let serverStore: ReturnType<typeof useServerStore>;

View File

@@ -93,7 +93,7 @@ const WanIpCheckStub = defineComponent({
},
});
describe('WanIpCheck.ce.vue', () => {
describe('WanIpCheck.standalone.vue', () => {
beforeEach(() => {
vi.clearAllMocks();

View File

@@ -63,7 +63,7 @@ function calculateTitle(partnerName: string | null) {
: 'Welcome to Unraid!';
}
describe('WelcomeModal.ce.vue', () => {
describe('WelcomeModal.standalone.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
mockPartnerName.mockReturnValue(null);

View File

@@ -1,55 +1,55 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// Mock all the component imports
vi.mock('~/components/Auth.ce.vue', () => ({
vi.mock('~/components/Auth.standalone.vue', () => ({
default: { name: 'MockAuth', template: '<div>Auth</div>' },
}));
vi.mock('~/components/ConnectSettings/ConnectSettings.ce.vue', () => ({
vi.mock('~/components/ConnectSettings/ConnectSettings.standalone.vue', () => ({
default: { name: 'MockConnectSettings', template: '<div>ConnectSettings</div>' },
}));
vi.mock('~/components/DownloadApiLogs.ce.vue', () => ({
vi.mock('~/components/DownloadApiLogs.standalone.vue', () => ({
default: { name: 'MockDownloadApiLogs', template: '<div>DownloadApiLogs</div>' },
}));
vi.mock('~/components/HeaderOsVersion.ce.vue', () => ({
vi.mock('~/components/HeaderOsVersion.standalone.vue', () => ({
default: { name: 'MockHeaderOsVersion', template: '<div>HeaderOsVersion</div>' },
}));
vi.mock('~/components/Modals.ce.vue', () => ({
vi.mock('~/components/Modals.standalone.vue', () => ({
default: { name: 'MockModals', template: '<div>Modals</div>' },
}));
vi.mock('~/components/UserProfile.ce.vue', () => ({
vi.mock('~/components/UserProfile.standalone.vue', () => ({
default: { name: 'MockUserProfile', template: '<div>UserProfile</div>' },
}));
vi.mock('~/components/UpdateOs.ce.vue', () => ({
vi.mock('~/components/UpdateOs.standalone.vue', () => ({
default: { name: 'MockUpdateOs', template: '<div>UpdateOs</div>' },
}));
vi.mock('~/components/DowngradeOs.ce.vue', () => ({
vi.mock('~/components/DowngradeOs.standalone.vue', () => ({
default: { name: 'MockDowngradeOs', template: '<div>DowngradeOs</div>' },
}));
vi.mock('~/components/Registration.ce.vue', () => ({
vi.mock('~/components/Registration.standalone.vue', () => ({
default: { name: 'MockRegistration', template: '<div>Registration</div>' },
}));
vi.mock('~/components/WanIpCheck.ce.vue', () => ({
vi.mock('~/components/WanIpCheck.standalone.vue', () => ({
default: { name: 'MockWanIpCheck', template: '<div>WanIpCheck</div>' },
}));
vi.mock('~/components/Activation/WelcomeModal.ce.vue', () => ({
vi.mock('~/components/Activation/WelcomeModal.standalone.vue', () => ({
default: { name: 'MockWelcomeModal', template: '<div>WelcomeModal</div>' },
}));
vi.mock('~/components/SsoButton.ce.vue', () => ({
vi.mock('~/components/SsoButton.standalone.vue', () => ({
default: { name: 'MockSsoButton', template: '<div>SsoButton</div>' },
}));
vi.mock('~/components/Logs/LogViewer.ce.vue', () => ({
vi.mock('~/components/Logs/LogViewer.standalone.vue', () => ({
default: { name: 'MockLogViewer', template: '<div>LogViewer</div>' },
}));
vi.mock('~/components/ThemeSwitcher.ce.vue', () => ({
vi.mock('~/components/ThemeSwitcher.standalone.vue', () => ({
default: { name: 'MockThemeSwitcher', template: '<div>ThemeSwitcher</div>' },
}));
vi.mock('~/components/ApiKeyPage.ce.vue', () => ({
vi.mock('~/components/ApiKeyPage.standalone.vue', () => ({
default: { name: 'MockApiKeyPage', template: '<div>ApiKeyPage</div>' },
}));
vi.mock('~/components/DevModalTest.ce.vue', () => ({
vi.mock('~/components/DevModalTest.standalone.vue', () => ({
default: { name: 'MockDevModalTest', template: '<div>DevModalTest</div>' },
}));
vi.mock('~/components/ApiKeyAuthorize.ce.vue', () => ({
vi.mock('~/components/ApiKeyAuthorize.standalone.vue', () => ({
default: { name: 'MockApiKeyAuthorize', template: '<div>ApiKeyAuthorize</div>' },
}));
vi.mock('~/components/UnraidToaster.vue', () => ({

86
web/auto-imports.d.ts vendored
View File

@@ -6,57 +6,57 @@
// biome-ignore lint: disable
export {}
declare global {
const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey']
const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale']
const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts']
const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale']
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']
const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch']
const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup']
const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload']
const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField']
const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd']
const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale']
const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay']
const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal']
const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable']
const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy']
const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast']
const avatarGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['avatarGroupInjectionKey']
const defineLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['defineLocale']
const defineShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['defineShortcuts']
const extendLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineLocale.js')['extendLocale']
const extractShortcuts: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.js')['extractShortcuts']
const fieldGroupInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['fieldGroupInjectionKey']
const formBusInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formBusInjectionKey']
const formFieldInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formFieldInjectionKey']
const formInputsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formInputsInjectionKey']
const formLoadingInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formLoadingInjectionKey']
const formOptionsInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['formOptionsInjectionKey']
const inputIdInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['inputIdInjectionKey']
const kbdKeysMap: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['kbdKeysMap']
const localeContextInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['localeContextInjectionKey']
const portalTargetInjectionKey: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['portalTargetInjectionKey']
const useAppConfig: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/composables/useAppConfig.js')['useAppConfig']
const useAvatarGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useAvatarGroup.js')['useAvatarGroup']
const useComponentIcons: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.js')['useComponentIcons']
const useContentSearch: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useContentSearch.js')['useContentSearch']
const useFieldGroup: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFieldGroup.js')['useFieldGroup']
const useFileUpload: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.js')['useFileUpload']
const useFormField: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFormField.js')['useFormField']
const useKbd: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.js')['useKbd']
const useLocale: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useLocale.js')['useLocale']
const useOverlay: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.js')['useOverlay']
const usePortal: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/usePortal.js')['usePortal']
const useResizable: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.js')['useResizable']
const useScrollspy: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useScrollspy.js')['useScrollspy']
const useToast: typeof import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.js')['useToast']
}
// for type re-export
declare global {
// @ts-ignore
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
export type { ShortcutConfig, ShortcutsConfig, ShortcutsOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/defineShortcuts.d')
// @ts-ignore
export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
export type { UseComponentIconsProps } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useComponentIcons.d')
// @ts-ignore
export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
export type { UseFileUploadOptions } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useFileUpload.d')
// @ts-ignore
export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
export type { KbdKey, KbdKeySpecific } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useKbd.d')
// @ts-ignore
export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
export type { OverlayOptions, Overlay } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useOverlay.d')
// @ts-ignore
export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
export type { UseResizableProps, UseResizableReturn } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useResizable.d')
// @ts-ignore
export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
export type { Toast } from '../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d'
import('../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/composables/useToast.d')
}

70
web/components.d.ts vendored
View File

@@ -12,17 +12,17 @@ declare module 'vue' {
ActivationPartnerLogo: typeof import('./src/components/Activation/ActivationPartnerLogo.vue')['default']
ActivationPartnerLogoImg: typeof import('./src/components/Activation/ActivationPartnerLogoImg.vue')['default']
ActivationSteps: typeof import('./src/components/Activation/ActivationSteps.vue')['default']
'ApiKeyAuthorize.ce': typeof import('./src/components/ApiKeyAuthorize.ce.vue')['default']
'ApiKeyAuthorize.standalone': typeof import('./src/components/ApiKeyAuthorize.standalone.vue')['default']
ApiKeyCreate: typeof import('./src/components/ApiKey/ApiKeyCreate.vue')['default']
ApiKeyManager: typeof import('./src/components/ApiKey/ApiKeyManager.vue')['default']
'ApiKeyPage.ce': typeof import('./src/components/ApiKeyPage.ce.vue')['default']
'Auth.ce': typeof import('./src/components/Auth.ce.vue')['default']
'ApiKeyPage.standalone': typeof import('./src/components/ApiKeyPage.standalone.vue')['default']
'Auth.standalone': typeof import('./src/components/Auth.standalone.vue')['default']
Avatar: typeof import('./src/components/Brand/Avatar.vue')['default']
Beta: typeof import('./src/components/UserProfile/Beta.vue')['default']
CallbackButton: typeof import('./src/components/UpdateOs/CallbackButton.vue')['default']
CallbackFeedback: typeof import('./src/components/UserProfile/CallbackFeedback.vue')['default']
CallbackFeedbackStatus: typeof import('./src/components/UserProfile/CallbackFeedbackStatus.vue')['default']
'CallbackHandler.ce': typeof import('./src/components/CallbackHandler.ce.vue')['default']
'CallbackHandler.standalone': typeof import('./src/components/CallbackHandler.standalone.vue')['default']
Card: typeof import('./src/components/LayoutViews/Card/Card.vue')['default']
CardGrid: typeof import('./src/components/LayoutViews/Card/CardGrid.vue')['default']
CardGroupHeader: typeof import('./src/components/LayoutViews/Card/CardGroupHeader.vue')['default']
@@ -30,20 +30,22 @@ declare module 'vue' {
CardItem: typeof import('./src/components/LayoutViews/Card/CardItem.vue')['default']
ChangelogModal: typeof import('./src/components/UpdateOs/ChangelogModal.vue')['default']
CheckUpdateResponseModal: typeof import('./src/components/UpdateOs/CheckUpdateResponseModal.vue')['default']
'ColorSwitcher.ce': typeof import('./src/components/ColorSwitcher.ce.vue')['default']
'ConnectSettings.ce': typeof import('./src/components/ConnectSettings/ConnectSettings.ce.vue')['default']
'ColorSwitcher.standalone': typeof import('./src/components/ColorSwitcher.standalone.vue')['default']
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
'ConnectSettings.standalone': typeof import('./src/components/ConnectSettings/ConnectSettings.standalone.vue')['default']
Console: typeof import('./src/components/Docker/Console.vue')['default']
'CpuStats.standalone': typeof import('./src/components/CpuStats/CpuStats.standalone.vue')['default']
Detail: typeof import('./src/components/LayoutViews/Detail/Detail.vue')['default']
DetailContentHeader: typeof import('./src/components/LayoutViews/Detail/DetailContentHeader.vue')['default']
DetailLeftNavigation: typeof import('./src/components/LayoutViews/Detail/DetailLeftNavigation.vue')['default']
DetailRightContent: typeof import('./src/components/LayoutViews/Detail/DetailRightContent.vue')['default']
'DetailTest.ce': typeof import('./src/components/LayoutViews/Detail/DetailTest.ce.vue')['default']
'DetailTest.standalone': typeof import('./src/components/LayoutViews/Detail/DetailTest.standalone.vue')['default']
DeveloperAuthorizationLink: typeof import('./src/components/ApiKey/DeveloperAuthorizationLink.vue')['default']
'DevModalTest.ce': typeof import('./src/components/DevModalTest.ce.vue')['default']
'DevModalTest.standalone': typeof import('./src/components/DevModalTest.standalone.vue')['default']
DevSettings: typeof import('./src/components/DevSettings.vue')['default']
Downgrade: typeof import('./src/components/UpdateOs/Downgrade.vue')['default']
'DowngradeOs.ce': typeof import('./src/components/DowngradeOs.ce.vue')['default']
'DownloadApiLogs.ce': typeof import('./src/components/DownloadApiLogs.ce.vue')['default']
'DowngradeOs.standalone': typeof import('./src/components/DowngradeOs.standalone.vue')['default']
'DownloadApiLogs.standalone': typeof import('./src/components/DownloadApiLogs.standalone.vue')['default']
DropdownConnectStatus: typeof import('./src/components/UserProfile/DropdownConnectStatus.vue')['default']
DropdownContent: typeof import('./src/components/UserProfile/DropdownContent.vue')['default']
DropdownError: typeof import('./src/components/UserProfile/DropdownError.vue')['default']
@@ -57,7 +59,7 @@ declare module 'vue' {
FileViewer: typeof import('./src/components/FileViewer.vue')['default']
FilteredLogModal: typeof import('./src/components/Logs/FilteredLogModal.vue')['default']
HeaderContent: typeof import('./src/components/Docker/HeaderContent.vue')['default']
'HeaderOsVersion.ce': typeof import('./src/components/HeaderOsVersion.ce.vue')['default']
'HeaderOsVersion.standalone': typeof import('./src/components/HeaderOsVersion.standalone.vue')['default']
IgnoredRelease: typeof import('./src/components/UpdateOs/IgnoredRelease.vue')['default']
Indicator: typeof import('./src/components/Notifications/Indicator.vue')['default']
Item: typeof import('./src/components/Notifications/Item.vue')['default']
@@ -68,11 +70,11 @@ declare module 'vue' {
LogFilterInput: typeof import('./src/components/Logs/LogFilterInput.vue')['default']
Logo: typeof import('./src/components/Brand/Logo.vue')['default']
Logs: typeof import('./src/components/Docker/Logs.vue')['default']
'LogViewer.ce': typeof import('./src/components/Logs/LogViewer.ce.vue')['default']
'LogViewer.standalone': typeof import('./src/components/Logs/LogViewer.standalone.vue')['default']
LogViewerToolbar: typeof import('./src/components/Logs/LogViewerToolbar.vue')['default']
Mark: typeof import('./src/components/Brand/Mark.vue')['default']
Modal: typeof import('./src/components/Modal.vue')['default']
'Modals.ce': typeof import('./src/components/Modals.ce.vue')['default']
'Modals.standalone': typeof import('./src/components/Modals.standalone.vue')['default']
OidcDebugButton: typeof import('./src/components/Logs/OidcDebugButton.vue')['default']
OidcDebugLogs: typeof import('./src/components/ConnectSettings/OidcDebugLogs.vue')['default']
Overview: typeof import('./src/components/Docker/Overview.vue')['default']
@@ -81,7 +83,7 @@ declare module 'vue' {
RawChangelogRenderer: typeof import('./src/components/UpdateOs/RawChangelogRenderer.vue')['default']
RCloneConfig: typeof import('./src/components/RClone/RCloneConfig.vue')['default']
RCloneOverview: typeof import('./src/components/RClone/RCloneOverview.vue')['default']
'Registration.ce': typeof import('./src/components/Registration.ce.vue')['default']
'Registration.standalone': typeof import('./src/components/Registration.standalone.vue')['default']
ReleaseNotesModal: typeof import('./src/components/ReleaseNotesModal.vue')['default']
RemoteItem: typeof import('./src/components/RClone/RemoteItem.vue')['default']
ReplaceCheck: typeof import('./src/components/Registration/ReplaceCheck.vue')['default']
@@ -93,35 +95,37 @@ declare module 'vue' {
ServerStatus: typeof import('./src/components/UserProfile/ServerStatus.vue')['default']
Sidebar: typeof import('./src/components/Notifications/Sidebar.vue')['default']
SingleLogViewer: typeof import('./src/components/Logs/SingleLogViewer.vue')['default']
'SsoButton.ce': typeof import('./src/components/SsoButton.ce.vue')['default']
'SsoButton.standalone': typeof import('./src/components/SsoButton.standalone.vue')['default']
SsoButtons: typeof import('./src/components/sso/SsoButtons.vue')['default']
SsoProviderButton: typeof import('./src/components/sso/SsoProviderButton.vue')['default']
Status: typeof import('./src/components/UpdateOs/Status.vue')['default']
'ThemeSwitcher.ce': typeof import('./src/components/ThemeSwitcher.ce.vue')['default']
'TestThemeSwitcher.standalone': typeof import('./src/components/TestThemeSwitcher.standalone.vue')['default']
'TestUpdateModal.standalone': typeof import('./src/components/UpdateOs/TestUpdateModal.standalone.vue')['default']
'ThemeSwitcher.standalone': typeof import('./src/components/ThemeSwitcher.standalone.vue')['default']
ThirdPartyDrivers: typeof import('./src/components/UpdateOs/ThirdPartyDrivers.vue')['default']
Trial: typeof import('./src/components/UserProfile/Trial.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UBadge: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Badge.vue')['default']
UButton: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Button.vue')['default']
UCard: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Card.vue')['default']
UCheckbox: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Checkbox.vue')['default']
UDrawer: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Drawer.vue')['default']
UDropdownMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/DropdownMenu.vue')['default']
UFormField: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/FormField.vue')['default']
UIcon: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/vue/components/Icon.vue')['default']
UInput: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Input.vue')['default']
UNavigationMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/NavigationMenu.vue')['default']
UnraidToaster: typeof import('./src/components/UnraidToaster.vue')['default']
Update: typeof import('./src/components/UpdateOs/Update.vue')['default']
UpdateExpiration: typeof import('./src/components/Registration/UpdateExpiration.vue')['default']
UpdateExpirationAction: typeof import('./src/components/Registration/UpdateExpirationAction.vue')['default']
UpdateIneligible: typeof import('./src/components/UpdateOs/UpdateIneligible.vue')['default']
'UpdateOs.ce': typeof import('./src/components/UpdateOs.ce.vue')['default']
'UpdateOs.standalone': typeof import('./src/components/UpdateOs.standalone.vue')['default']
UptimeExpire: typeof import('./src/components/UserProfile/UptimeExpire.vue')['default']
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
'UserProfile.ce': typeof import('./src/components/UserProfile.ce.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.3_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_717626353d7e2aa0e50ed397345224b8/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
'WanIpCheck.ce': typeof import('./src/components/WanIpCheck.ce.vue')['default']
'WelcomeModal.ce': typeof import('./src/components/Activation/WelcomeModal.ce.vue')['default']
USelectMenu: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/SelectMenu.vue')['default']
'UserProfile.standalone': typeof import('./src/components/UserProfile.standalone.vue')['default']
USwitch: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Switch.vue')['default']
UTabs: typeof import('./../node_modules/.pnpm/@nuxt+ui@4.0.0-alpha.0_@babel+parser@7.28.4_@netlify+blobs@9.1.2_change-case@5.4.4_db0@_655bac6707ae017754653173419b3890/node_modules/@nuxt/ui/dist/runtime/components/Tabs.vue')['default']
'WanIpCheck.standalone': typeof import('./src/components/WanIpCheck.standalone.vue')['default']
'WelcomeModal.standalone': typeof import('./src/components/Activation/WelcomeModal.standalone.vue')['default']
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.21.0",
"version": "4.22.0",
"private": true,
"type": "module",
"license": "GPL-2.0-or-later",
@@ -105,6 +105,7 @@
"@vueuse/integrations": "13.8.0",
"ajv": "8.17.1",
"ansi_up": "6.0.6",
"chart.js": "^4.5.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"crypto-js": "4.2.0",
@@ -123,6 +124,7 @@
"postcss-import": "16.1.1",
"semver": "7.7.2",
"tailwind-merge": "2.6.0",
"vue-chartjs": "^5.3.2",
"vue-i18n": "11.1.11",
"vue-router": "4.5.1",
"vue-web-component-wrapper": "1.7.7",

View File

@@ -103,6 +103,13 @@
</div>
</div>
<div class="card">
<h2>CPU Statistics</h2>
<div class="component-mount" data-component="unraid-cpu-stats">
<unraid-cpu-stats></unraid-cpu-stats>
</div>
</div>
<div class="card">
<h2>Theme Settings</h2>
<div class="component-mount" data-component="unraid-theme-switcher">

View File

@@ -179,6 +179,14 @@
</div>
<a href="/test-pages/os-management.html">Open →</a>
</div>
<div class="page-item">
<div>
<h3>Update Modal Testing <span class="badge new">NEW</span></h3>
<p>Test various update scenarios including expired licenses, renewals, and auth requirements</p>
</div>
<a href="/test-update-modal.html">Open →</a>
</div>
</div>
</div>

View File

@@ -146,9 +146,5 @@
/* Style for Unraid progress frame */
iframe#progressFrame {
background-color: var(--background-color);
color-scheme: light;
}
/* Global input text color when SSO button is present (for login page) */
body:has(unraid-sso-button) input {
color: #1b1b1b !important;
}

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
import {
Button,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
} from '@unraid/ui';
import { useConfirm } from '~/composables/useConfirm';
const { isOpen, state, handleConfirm, handleCancel } = useConfirm();
</script>
<template>
<DialogRoot :open="isOpen" @update:open="!$event && handleCancel()">
<DialogContent>
<DialogHeader v-if="state">
<DialogTitle>{{ state.title }}</DialogTitle>
<DialogDescription v-if="state.description">
{{ state.description }}
</DialogDescription>
</DialogHeader>
<DialogFooter v-if="state">
<div class="flex w-full justify-between gap-3">
<Button variant="outline" @click="handleCancel">
{{ state.cancelText }}
</Button>
<Button :variant="state.confirmVariant" @click="handleConfirm">
{{ state.confirmText }}
</Button>
</div>
</DialogFooter>
</DialogContent>
</DialogRoot>
</template>

View File

@@ -11,7 +11,7 @@ import { watchDebounced } from '@vueuse/core';
import { BrandButton, jsonFormsAjv, jsonFormsRenderers, Label, SettingsGrid } from '@unraid/ui';
import { JsonForms } from '@jsonforms/vue';
import Auth from '~/components/Auth.ce.vue';
import Auth from '~/components/Auth.standalone.vue';
// unified settings values are returned as JSON, so use a generic record type
// import type { ConnectSettingsValues } from '~/composables/gql/graphql';
@@ -20,7 +20,7 @@ import {
updateConnectSettings,
} from '~/components/ConnectSettings/graphql/settings.query';
import OidcDebugLogs from '~/components/ConnectSettings/OidcDebugLogs.vue';
import DownloadApiLogs from '~/components/DownloadApiLogs.ce.vue';
import DownloadApiLogs from '~/components/DownloadApiLogs.standalone.vue';
import { useServerStore } from '~/store/server';
// Disable automatic attribute inheritance

View File

@@ -0,0 +1,308 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, shallowRef, watch } from 'vue';
import { useQuery, useSubscription } from '@vue/apollo-composable';
import { GET_CPU_INFO, CPU_METRICS_SUBSCRIPTION } from './cpu-stats.query';
import { Line } from 'vue-chartjs';
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler,
type ChartOptions,
type ChartData
} from 'chart.js';
import { Button, Select } from '@unraid/ui';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
Filler
);
const showDetails = ref(true);
const cpuHistory = ref<number[]>([]);
// History duration options
type HistoryDuration = '10s' | '30s' | '1m' | '2m' | '5m';
const historyDuration = ref<HistoryDuration>('30s');
const historyConfigs: Record<HistoryDuration, { points: number; interval: number }> = {
'10s': { points: 60, interval: 167 }, // ~6 fps
'30s': { points: 60, interval: 500 }, // 2 fps
'1m': { points: 60, interval: 1000 }, // 1 fps
'2m': { points: 60, interval: 2000 }, // 0.5 fps
'5m': { points: 60, interval: 5000 }, // 0.2 fps
};
const historyOptions = [
{ value: '10s', label: '10 seconds' },
{ value: '30s', label: '30 seconds' },
{ value: '1m', label: '1 minute' },
{ value: '2m', label: '2 minutes' },
{ value: '5m', label: '5 minutes' },
];
const currentHistoryConfig = computed(() =>
historyConfigs[historyDuration.value] || historyConfigs['30s']
);
const { result: cpuInfoResult } = useQuery(GET_CPU_INFO);
const { result: cpuMetricsResult } = useSubscription(CPU_METRICS_SUBSCRIPTION);
const cpuInfo = computed(() => cpuInfoResult.value?.info?.cpu);
const cpuMetrics = computed(() => cpuMetricsResult.value?.systemMetricsCpu);
const cpuBrand = computed(() => {
if (!cpuInfo.value) return 'Loading...';
const brand = cpuInfo.value.brand || cpuInfo.value.model || 'Unknown CPU';
return brand;
});
const overallLoad = computed(() => {
if (!cpuMetrics.value) return 0;
return Math.floor(cpuMetrics.value.percentTotal);
});
const cpuCores = computed(() => {
if (!cpuMetrics.value?.cpus) return [];
return cpuMetrics.value.cpus.map((cpu, index) => ({
index: index * 2, // Assuming HT, so multiply by 2
htIndex: index * 2 + 1,
percent: Math.floor(cpu.percentTotal),
percentUser: Math.floor(cpu.percentUser),
percentSystem: Math.floor(cpu.percentSystem),
}));
});
// Keep chart data simple - just the last 60 data points
const chartDataRef = shallowRef<ChartData<'line'>>({
labels: [],
datasets: [{
label: 'CPU Usage %',
data: [],
borderColor: '#ff8c2e',
backgroundColor: 'rgba(255, 140, 46, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 3,
}]
});
// Update chart data without triggering computed re-evaluation
const updateChartData = () => {
// Create simple numeric labels (no timestamps, just indices)
const labels = Array.from({ length: cpuHistory.value.length }, (_, i) => '');
// Update the ref value directly
chartDataRef.value = {
labels,
datasets: [{
label: 'CPU Usage %',
data: [...cpuHistory.value], // Clone to prevent reactivity issues
borderColor: '#ff8c2e',
backgroundColor: 'rgba(255, 140, 46, 0.1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 0,
pointHoverRadius: 0, // Disable hover points for performance
}]
};
};
const chartData = computed(() => chartDataRef.value);
const chartOptions = computed<ChartOptions<'line'>>(() => ({
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 0 // Disable all animations for performance
},
plugins: {
legend: {
display: false
},
tooltip: {
enabled: false // Disable tooltips for performance
}
},
scales: {
x: {
grid: {
display: false
},
display: false // Hide x-axis completely for performance
},
y: {
min: 0,
max: 100,
grid: {
color: 'rgb(229, 231, 235)'
},
ticks: {
stepSize: 25,
color: 'rgb(107, 114, 128)',
font: {
size: 11
},
callback: (value) => `${value}%`
}
}
}
}));
let updateInterval: NodeJS.Timeout | null = null;
let tickInterval: NodeJS.Timeout | null = null;
let lastKnownValue = 0;
// Update with actual data from subscription
const updateFromMetrics = () => {
if (cpuMetrics.value) {
lastKnownValue = Math.floor(cpuMetrics.value.percentTotal);
}
};
// Tick the chart forward with the last known value
const tick = () => {
// Always push a value (either new or repeated last known)
cpuHistory.value.push(lastKnownValue);
// Keep only the configured number of data points
if (cpuHistory.value.length > currentHistoryConfig.value.points) {
cpuHistory.value.shift();
}
// Update chart data
updateChartData();
};
// Watch for actual metric changes
watch(cpuMetrics, updateFromMetrics, { immediate: true });
// Restart ticker when duration changes
const restartTicker = () => {
if (tickInterval) {
clearInterval(tickInterval);
}
// Clear history when changing duration for clean transition
cpuHistory.value = [];
// Start new ticker with appropriate interval
tickInterval = setInterval(tick, currentHistoryConfig.value.interval);
};
watch(historyDuration, restartTicker);
onMounted(() => {
// Start ticker with initial interval
tickInterval = setInterval(tick, currentHistoryConfig.value.interval);
tick(); // Initial tick
});
onUnmounted(() => {
if (updateInterval) {
clearInterval(updateInterval);
}
if (tickInterval) {
clearInterval(tickInterval);
}
});
const toggleDetails = () => {
showDetails.value = !showDetails.value;
};
</script>
<template>
<div class="bg-background rounded-md border-2 border-muted shadow-md p-4">
<div class="space-y-4">
<!-- Header Section -->
<div>
<h3 class="text-lg font-semibold text-foreground">Processor</h3>
<div class="text-sm text-muted-foreground mt-1">{{ cpuBrand }}</div>
<div class="flex items-center justify-between mt-2">
<div class="text-sm">
<span class="text-foreground">Overall Load: </span>
<span class="font-semibold" style="color: var(--color-orange, #ff8c2f)">{{ overallLoad }}%</span>
</div>
<Button
@click="toggleDetails"
variant="outline"
size="sm"
>
{{ showDetails ? 'Hide details' : 'Show details' }}
</Button>
</div>
</div>
<!-- CPU Cores Details -->
<Transition name="slide-fade">
<div v-if="showDetails" class="bg-muted/30 rounded-md p-4">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3">
<div
v-for="core in cpuCores"
:key="core.index"
class="bg-background rounded border border-border p-2"
>
<div class="text-xs text-muted-foreground">
CPU {{ core.index }} - HT {{ core.htIndex }}
</div>
<div class="text-sm font-semibold mt-1" style="color: var(--color-orange, #ff8c2f)">{{ core.percent }}%</div>
</div>
</div>
</div>
</Transition>
<!-- Chart Section -->
<div class="border-t border-border pt-4">
<div class="flex items-center justify-between mb-3">
<h4 class="text-sm font-semibold text-foreground">CPU Usage</h4>
<Select
v-model="historyDuration"
:items="historyOptions"
placeholder="Duration"
class="w-32"
/>
</div>
<div v-if="chartData.labels && chartData.labels.length > 0" class="h-40">
<Line :data="chartData" :options="chartOptions" />
</div>
<div v-else class="h-40 flex items-center justify-center text-muted-foreground text-sm">
Collecting data...
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* Smooth transition for details panel */
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.3s ease;
}
.slide-fade-enter-from {
transform: translateY(-10px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateY(-10px);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,30 @@
import { graphql } from '~/composables/gql/gql';
export const GET_CPU_INFO = graphql(/* GraphQL */ `
query GetCpuInfo {
info {
cpu {
id
manufacturer
brand
vendor
family
model
}
}
}
`);
export const CPU_METRICS_SUBSCRIPTION = graphql(/* GraphQL */ `
subscription CpuMetrics {
systemMetricsCpu {
id
percentTotal
cpus {
percentTotal
percentUser
percentSystem
}
}
}
`);

View File

@@ -21,6 +21,7 @@ import {
} from '@unraid/ui';
import { Settings } from 'lucide-vue-next';
import ConfirmDialog from '~/components/ConfirmDialog.vue';
import {
archiveAllNotifications,
deleteArchivedNotifications,
@@ -37,10 +38,12 @@ import NotificationsList from '~/components/Notifications/List.vue';
import { useTrackLatestSeenNotification } from '~/composables/api/use-notifications';
import { useFragment } from '~/composables/gql';
import { NotificationImportance as Importance, NotificationType } from '~/composables/gql/graphql';
import { useConfirm } from '~/composables/useConfirm';
const { mutate: archiveAll, loading: loadingArchiveAll } = useMutation(archiveAllNotifications);
const { mutate: deleteArchives, loading: loadingDeleteAll } = useMutation(deleteArchivedNotifications);
const { mutate: recalculateOverview } = useMutation(resetOverview);
const { confirm } = useConfirm();
const importance = ref<Importance | undefined>(undefined);
const filterItems = [
@@ -52,17 +55,26 @@ const filterItems = [
];
const confirmAndArchiveAll = async () => {
if (confirm('This will archive all notifications on your Unraid server. Continue?')) {
const confirmed = await confirm({
title: 'Archive All Notifications',
description: 'This will archive all notifications on your Unraid server. Continue?',
confirmText: 'Archive All',
confirmVariant: 'primary',
});
if (confirmed) {
await archiveAll();
}
};
const confirmAndDeleteArchives = async () => {
if (
confirm(
'This will permanently delete all archived notifications currently on your Unraid server. Continue?'
)
) {
const confirmed = await confirm({
title: 'Delete All Archived Notifications',
description:
'This will permanently delete all archived notifications currently on your Unraid server. This action cannot be undone.',
confirmText: 'Delete All',
confirmVariant: 'destructive',
});
if (confirmed) {
await deleteArchives();
}
};
@@ -230,4 +242,7 @@ const prepareToViewNotifications = () => {
</div>
</SheetContent>
</Sheet>
<!-- Global Confirm Dialog -->
<ConfirmDialog />
</template>

View File

@@ -1,9 +0,0 @@
<script setup lang="ts">
import SsoButtons from '~/components/sso/SsoButtons.vue';
</script>
<template>
<SsoButtons />
</template>
<!-- Font size overrides are handled in component-registry.ts for custom elements -->

View File

@@ -0,0 +1,55 @@
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import SsoButtons from '~/components/sso/SsoButtons.vue';
const handleSsoStatus = (status: { checking: boolean; loading: boolean }) => {
// Find the password recovery link on the page
const passwordRecoveryLink = document.querySelector(
'a[href*="lost-root-password"]'
) as HTMLAnchorElement;
if (passwordRecoveryLink) {
// Hide the link when checking API or loading
if (status.checking || status.loading) {
passwordRecoveryLink.style.display = 'none';
} else {
// Show it again when not checking/loading
passwordRecoveryLink.style.display = '';
}
}
};
// Also hide password recovery initially while checking
onMounted(() => {
const passwordRecoveryLink = document.querySelector(
'a[href*="lost-root-password"]'
) as HTMLAnchorElement;
if (passwordRecoveryLink) {
// Store original display value
const originalDisplay = passwordRecoveryLink.style.display || '';
passwordRecoveryLink.dataset.originalDisplay = originalDisplay;
}
});
// Restore on unmount
onUnmounted(() => {
const passwordRecoveryLink = document.querySelector(
'a[href*="lost-root-password"]'
) as HTMLAnchorElement;
if (passwordRecoveryLink && passwordRecoveryLink.dataset.originalDisplay !== undefined) {
passwordRecoveryLink.style.display = passwordRecoveryLink.dataset.originalDisplay;
}
});
</script>
<template>
<SsoButtons @sso-status="handleSsoStatus" />
</template>
<style>
/* Global input text color when SSO button is present (for login page) */
body:has(unraid-sso-button) input {
color: #1b1b1b !important;
}
</style>

View File

@@ -0,0 +1,40 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { Select } from '@unraid/ui';
import type { Theme } from '~/themes/types';
import { useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
const { theme } = storeToRefs(themeStore);
// Available theme options
const items = [
{ value: 'white', label: 'Light' },
{ value: 'black', label: 'Dark' },
{ value: 'azure', label: 'Azure' },
{ value: 'gray', label: 'Gray' },
];
// Current theme value
const currentTheme = computed({
get: () => theme.value.name,
set: (value: string) => {
const newTheme: Theme = {
...theme.value,
name: value,
};
themeStore.setTheme(newTheme);
},
});
</script>
<template>
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-white">Theme:</span>
<Select v-model="currentTheme" :items="items" placeholder="Select theme" class="w-32" />
</div>
</template>

View File

@@ -1,6 +1,15 @@
<script lang="ts" setup>
import { computed } from 'vue';
import { Toaster } from '@unraid/ui';
import { useThemeStore } from '~/store/theme';
const themeStore = useThemeStore();
// Get dark mode from theme store
const theme = computed(() => (themeStore.darkMode ? 'dark' : 'light'));
withDefaults(
defineProps<{
position?:
@@ -18,5 +27,5 @@ withDefaults(
</script>
<template>
<Toaster rich-colors close-button :position="position" />
<Toaster rich-colors close-button :position="position" :theme="theme" />
</template>

View File

@@ -2,27 +2,30 @@
import { computed, onBeforeMount, ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { ArrowDownTrayIcon } from '@heroicons/vue/24/outline';
import {
ArrowTopRightOnSquareIcon,
CogIcon,
EyeIcon,
IdentificationIcon,
KeyIcon,
XMarkIcon,
} from '@heroicons/vue/24/solid';
import {
BrandButton,
BrandLoading,
Button,
cn,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
Label,
ResponsiveModal,
ResponsiveModalFooter,
ResponsiveModalHeader,
ResponsiveModalTitle,
Switch,
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@unraid/ui';
import type { BrandButtonProps } from '@unraid/ui';
@@ -96,6 +99,9 @@ watch(updateOsIgnoredReleases, (newVal, oldVal) => {
}
});
// Get the localized 'Close' text for comparison
const localizedCloseText = computed(() => props.t('Close'));
const notificationsSettings = computed(() => {
return !updateOsNotificationsEnabled.value
? props.t(
@@ -115,30 +121,18 @@ const modalCopy = computed((): ModalCopy | null => {
};
}
// Use the release date
let formattedReleaseDate = '';
if (availableReleaseDate.value) {
// build string with prefix
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
}
if (availableWithRenewal.value) {
const description = regUpdatesExpired.value
? `${props.t('Eligible for updates released on or before {0}.', [formattedRegExp.value])} ${props.t('Extend your license to access the latest updates.')}`
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
return {
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
title: props.t('Update Available'),
description: description,
};
} else if (available.value) {
const description = availableRequiresAuth.value
? props.t('Release requires verification to update')
: undefined;
return {
title: props.t('Unraid OS {0} Update Available', [available.value]),
description: description
? `<p>${formattedReleaseDate}</p><p>${description}</p>`
: formattedReleaseDate,
title: props.t('Update Available'),
description: undefined,
};
} else if (!available.value && !availableWithRenewal.value) {
return {
@@ -169,8 +163,18 @@ const extraLinks = computed((): BrandButtonProps[] => {
});
const actionButtons = computed((): BrandButtonProps[] | null => {
// If ignoring release, show close button as primary action
if (ignoreThisRelease.value && (available.value || availableWithRenewal.value)) {
return [
{
click: () => close(),
text: props.t('Close'),
},
];
}
// update not available or no action buttons default closing
if (!available.value || ignoreThisRelease.value) {
if (!available.value && !availableWithRenewal.value) {
return null;
}
@@ -274,20 +278,58 @@ const modalWidth = computed(() => {
</script>
<template>
<DialogRoot :open="open" @update:open="(value) => !value && close()">
<DialogContent :class="modalWidth" :show-close-button="!checkForUpdatesLoading">
<DialogHeader v-if="modalCopy?.title">
<DialogTitle>
<ResponsiveModal
:open="open"
:dialog-class="modalWidth"
:sheet-class="'h-full'"
:show-close-button="!checkForUpdatesLoading"
@update:open="(value) => !value && close()"
>
<div class="flex h-full flex-col">
<ResponsiveModalHeader v-if="modalCopy?.title">
<ResponsiveModalTitle>
{{ modalCopy.title }}
</DialogTitle>
</ResponsiveModalTitle>
<DialogDescription v-if="modalCopy?.description">
<span v-html="modalCopy.description" />
</DialogDescription>
</DialogHeader>
</ResponsiveModalHeader>
<div v-if="renderMainSlot" class="flex flex-col gap-4">
<div v-if="renderMainSlot" class="flex flex-1 flex-col gap-6 overflow-y-auto px-6">
<BrandLoading v-if="checkForUpdatesLoading" class="mx-auto w-[150px]" />
<div v-else class="flex flex-col gap-y-4">
<div v-else class="flex flex-col gap-y-6">
<!-- OS Update highlight section -->
<div v-if="available || availableWithRenewal" class="flex flex-col items-center gap-4 py-4">
<div class="bg-primary/10 flex items-center justify-center rounded-full p-4">
<ArrowDownTrayIcon class="text-primary h-8 w-8" />
</div>
<div class="text-center">
<h2 class="text-foreground text-3xl font-bold">
{{ availableWithRenewal || available }}
</h2>
<p v-if="userFormattedReleaseDate" class="text-muted-foreground mt-2 text-center text-sm">
Released on {{ userFormattedReleaseDate }}
</p>
<p
v-if="availableRequiresAuth && !availableWithRenewal"
class="mt-2 text-center text-sm text-amber-500"
>
{{ t('Requires verification to update') }}
</p>
</div>
<div class="mt-4">
<div
class="hover:bg-muted/50 flex cursor-pointer items-center gap-3 rounded-lg p-2 transition-colors"
@click="ignoreThisRelease = !ignoreThisRelease"
>
<Switch v-model="ignoreThisRelease" @click.stop />
<Label class="text-muted-foreground cursor-pointer text-sm">
{{ t('Ignore this release until next reboot') }}
</Label>
</div>
</div>
</div>
<div
v-if="extraLinks.length > 0"
:class="cn('xs:!flex-row flex flex-col justify-center gap-2')"
@@ -295,7 +337,7 @@ const modalWidth = computed(() => {
<BrandButton
v-for="item in extraLinks"
:key="item.text"
:btn-style="item.variant ?? undefined"
:variant="item.variant ?? undefined"
:href="item.href ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
@@ -306,23 +348,8 @@ const modalWidth = computed(() => {
/>
</div>
<div v-if="available || availableWithRenewal" class="mx-auto">
<div class="flex items-center justify-center gap-2 rounded p-2">
<Switch
v-model="ignoreThisRelease"
:class="
ignoreThisRelease
? 'from-unraid-red to-orange bg-linear-to-r'
: 'data-[state=unchecked]:bg-opacity-10 data-[state=unchecked]:bg-foreground data-[state=unchecked]:bg-transparent'
"
/>
<Label class="text-base">
{{ t('Ignore this release until next reboot') }}
</Label>
</div>
</div>
<div
v-else-if="updateOsIgnoredReleases.length > 0"
v-if="updateOsIgnoredReleases.length > 0 && !(available || availableWithRenewal)"
class="mx-auto flex w-full max-w-[640px] flex-col gap-2"
>
<h3 class="text-left text-base font-semibold italic">
@@ -338,7 +365,7 @@ const modalWidth = computed(() => {
</div>
</div>
<DialogFooter>
<ResponsiveModalFooter>
<div
:class="
cn(
@@ -347,31 +374,75 @@ const modalWidth = computed(() => {
)
"
>
<div :class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-2')">
<Button variant="ghost" @click="close">
<XMarkIcon class="mr-2 h-4 w-4" />
{{ t('Close') }}
</Button>
<Button variant="ghost" @click="accountStore.updateOs()">
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
{{ t('More options') }}
</Button>
<div
v-if="actionButtons"
:class="cn('xs:!flex-row flex flex-col-reverse justify-start gap-3')"
>
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger as-child>
<Button variant="ghost" @click="accountStore.updateOs()">
<ArrowTopRightOnSquareIcon class="mr-2 h-4 w-4" />
{{ t('More Options') }}
</Button>
</TooltipTrigger>
<TooltipContent class="max-w-xs">
<div class="flex items-start gap-2">
<ArrowTopRightOnSquareIcon
class="text-muted-foreground mt-0.5 h-4 w-4 flex-shrink-0"
/>
<p class="text-left text-sm">
{{
t(
'Manage update preferences including beta access and version selection at account.unraid.net'
)
}}
</p>
</div>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-2')">
<BrandButton
v-for="item in actionButtons"
:key="item.text"
:btn-style="item.variant ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="t(item.text ?? '')"
:title="item.title ? t(item.title) : undefined"
@click="item.click?.()"
/>
<div v-if="actionButtons" :class="cn('xs:!flex-row flex flex-col justify-end gap-3')">
<template v-for="item in actionButtons" :key="item.text">
<TooltipProvider v-if="ignoreThisRelease && item.text === localizedCloseText">
<Tooltip :delay-duration="300">
<TooltipTrigger as-child>
<BrandButton
:variant="item.variant ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="item.text ?? ''"
:title="item.title ? item.title : undefined"
@click="item.click?.()"
/>
</TooltipTrigger>
<TooltipContent>
<p>
{{
t(
'You can opt back in to an ignored release by clicking on the Check for Updates button in the header anytime'
)
}}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<BrandButton
v-else
:variant="item.variant ?? undefined"
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="item.text ?? ''"
:title="item.title ? item.title : undefined"
@click="item.click?.()"
/>
</template>
</div>
</div>
</DialogFooter>
</DialogContent>
</DialogRoot>
</ResponsiveModalFooter>
</div>
</ResponsiveModal>
</template>

View File

@@ -0,0 +1,292 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Button, Label, Switch } from '@unraid/ui';
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
const { t } = useI18n();
const updateOsStore = useUpdateOsStore();
const serverStore = useServerStore();
// Test scenarios
const testScenarios = [
{
id: 'expired-ineligible',
name: 'Expired key with ineligible update',
description: 'License expired, update available but not eligible',
serverState: 'EEXPIRED' as ServerState,
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: false,
changelog:
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: undefined, // requires auth
},
},
{
id: 'normal-update',
name: 'Normal update available',
description: 'Active license with eligible update',
serverState: 'BASIC' as ServerState,
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog:
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: 'abc123def456789',
},
},
{
id: 'renewal-required',
name: 'Update requires renewal',
description: 'License expired > 1 year, update requires renewal',
serverState: 'STARTER' as ServerState,
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: false,
changelog:
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.1.0.md',
changelogPretty: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: undefined,
},
},
{
id: 'no-update',
name: 'No update available',
description: 'Already on latest version',
serverState: 'BASIC' as ServerState,
updateResponse: {
version: '7.0.0',
name: 'Unraid 7.0.0',
date: '2024-01-15',
isNewer: false,
isEligible: true,
changelog:
'https://raw.githubusercontent.com/unraid/docs/main/docs/unraid-os/release-notes/7.0.0.md',
sha256: 'xyz789abc123',
},
},
{
id: 'trial-update',
name: 'Trial with update',
description: 'Trial license with update available',
serverState: 'TRIAL' as ServerState,
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: 'def456ghi789',
},
},
{
id: 'pro-auth-required',
name: 'Pro license - auth required',
description: 'Pro license but authentication required for download',
serverState: 'PRO' as ServerState,
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: undefined, // requires auth
},
},
];
// Component state
const selectedScenario = ref('normal-update');
const ignoreRelease = ref(false);
const checkingForUpdates = ref(false);
const ignoredReleases = ref<string[]>([]);
// Use the store's modal state directly
const modalOpen = computed({
get: () => updateOsStore.updateOsModalVisible,
set: (value) => updateOsStore.setModalOpen(value),
});
// Apply scenario
const applyScenario = () => {
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
if (!scenario) return;
// Set server state
const currentTime = Date.now();
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
const regExp =
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
// Apply update response
if (scenario.serverState === 'EEXPIRED') {
serverStore.$patch({
expireTime: expiredTime,
state: scenario.serverState,
regExp: undefined,
});
} else if (scenario.serverState === 'STARTER') {
serverStore.$patch({
state: scenario.serverState,
regExp: regExp,
regTy: 'Starter',
});
} else {
serverStore.$patch({
state: scenario.serverState,
regExp: undefined,
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
});
}
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
// Apply ignored releases
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
ignoredReleases.value.push(scenario.updateResponse.version);
}
} else {
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
}
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
};
// Watch for scenario changes
watch([selectedScenario, ignoreRelease], () => {
applyScenario();
});
// Watch for loading state changes
watch(checkingForUpdates, (value) => {
updateOsStore.checkForUpdatesLoading = value;
});
// Open modal with scenario
const openModal = () => {
applyScenario();
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
updateOsStore.setModalOpen(true);
};
// Initialize
applyScenario();
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
</script>
<template>
<div class="container mx-auto max-w-4xl p-6">
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
<div class="mb-6">
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
<p class="text-muted-foreground">
Test various update scenarios for the CheckUpdateResponseModal component
</p>
</div>
<div class="space-y-6">
<!-- Scenario Selection -->
<div class="space-y-4">
<Label class="text-lg font-semibold">Select Test Scenario</Label>
<div class="space-y-3">
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
<input
type="radio"
:id="scenario.id"
:value="scenario.id"
v-model="selectedScenario"
class="mt-1 rounded-full"
/>
<div class="flex-1">
<Label :for="scenario.id" class="block cursor-pointer font-medium">
{{ scenario.name }}
</Label>
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
</div>
</div>
</div>
</div>
<!-- Options -->
<div class="space-y-4 border-t pt-4">
<h3 class="font-semibold">Options</h3>
<div class="flex items-center space-x-3">
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
</div>
<div class="flex items-center space-x-3">
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
<Label for="checking-updates" class="cursor-pointer"
>Show checking for updates loading state</Label
>
</div>
</div>
<!-- Current State Display -->
<div class="space-y-2 border-t pt-4">
<h3 class="font-semibold">Current Scenario Details</h3>
<div class="space-y-1 font-mono text-sm">
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
<p>
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
</p>
<p>
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
</p>
<p>
<span class="font-semibold">Is Eligible:</span>
{{ currentScenario?.updateResponse.isEligible }}
</p>
<p>
<span class="font-semibold">Has SHA256:</span>
{{ !!currentScenario?.updateResponse.sha256 }}
</p>
<p>
<span class="font-semibold">Ignored Releases:</span>
{{ ignoredReleases.join(', ') || 'None' }}
</p>
</div>
</div>
<!-- Open Modal Button -->
<div class="border-t pt-4">
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
</div>
</div>
</div>
<!-- The Modal Component -->
<CheckUpdateResponseModal
:open="modalOpen"
@update:open="
(val: boolean) => {
modalOpen = val;
}
"
:t="t"
/>
</div>
</template>

View File

@@ -3,7 +3,21 @@
import { provideApolloClient } from '@vue/apollo-composable';
import { ensureTeleportContainer } from '@unraid/ui';
// Copy the ensureTeleportContainer function to avoid importing from @unraid/ui
// which causes ESM/CommonJS issues with ajv-errors
function ensureTeleportContainer(): HTMLElement {
const containerId = 'unraid-teleport-container';
let container = document.getElementById(containerId);
if (!container) {
container = document.createElement('div');
container.id = containerId;
container.style.position = 'relative';
container.classList.add('unapi');
container.style.zIndex = '999999';
document.body.appendChild(container);
}
return container;
}
import {
autoMountAllComponents,
autoMountComponent,

View File

@@ -6,13 +6,16 @@ import type { Component } from 'vue';
// Import CSS for bundling - this ensures Tailwind styles are included
import '~/assets/main.css';
// Import @unraid/ui styles which includes vue-sonner styles
import '@unraid/ui/styles';
// Static imports for critical components that are always present
// These are included in the main bundle for faster initial render
import HeaderOsVersionCe from '@/components/HeaderOsVersion.ce.vue';
import ModalsCe from '@/components/Modals.ce.vue';
import ThemeSwitcherCe from '@/components/ThemeSwitcher.ce.vue';
import UserProfileCe from '@/components/UserProfile.ce.vue';
import HeaderOsVersionCe from '@/components/HeaderOsVersion.standalone.vue';
import ModalsCe from '@/components/Modals.standalone.vue';
import ThemeSwitcherCe from '@/components/ThemeSwitcher.standalone.vue';
import UnraidToaster from '@/components/UnraidToaster.vue';
import UserProfileCe from '@/components/UserProfile.standalone.vue';
// Type for Vue component module
type VueComponentModule = { default: object } | object;
@@ -31,17 +34,17 @@ export type ComponentMapping = {
// Page-specific components use dynamic imports (lazy loaded)
export const componentMappings: ComponentMapping[] = [
{
loader: () => import('../Auth.ce.vue'),
loader: () => import('../Auth.standalone.vue'),
selector: 'unraid-auth',
appId: 'auth',
},
{
loader: () => import('../ConnectSettings/ConnectSettings.ce.vue'),
loader: () => import('../ConnectSettings/ConnectSettings.standalone.vue'),
selector: 'unraid-connect-settings',
appId: 'connect-settings',
},
{
loader: () => import('../DownloadApiLogs.ce.vue'),
loader: () => import('../DownloadApiLogs.standalone.vue'),
selector: 'unraid-download-api-logs',
appId: 'download-api-logs',
},
@@ -61,42 +64,42 @@ export const componentMappings: ComponentMapping[] = [
appId: 'user-profile',
},
{
loader: () => import('../Registration.ce.vue'),
loader: () => import('../Registration.standalone.vue'),
selector: 'unraid-registration',
appId: 'registration',
},
{
loader: () => import('../WanIpCheck.ce.vue'),
loader: () => import('../WanIpCheck.standalone.vue'),
selector: 'unraid-wan-ip-check',
appId: 'wan-ip-check',
},
{
loader: () => import('../CallbackHandler.ce.vue'),
loader: () => import('../CallbackHandler.standalone.vue'),
selector: 'unraid-callback-handler',
appId: 'callback-handler',
},
{
loader: () => import('../Logs/LogViewer.ce.vue'),
loader: () => import('../Logs/LogViewer.standalone.vue'),
selector: 'unraid-log-viewer',
appId: 'log-viewer',
},
{
loader: () => import('../SsoButton.ce.vue'),
loader: () => import('../SsoButton.standalone.vue'),
selector: 'unraid-sso-button',
appId: 'sso-button',
},
{
loader: () => import('../Activation/WelcomeModal.ce.vue'),
loader: () => import('../Activation/WelcomeModal.standalone.vue'),
selector: 'unraid-welcome-modal',
appId: 'welcome-modal',
},
{
loader: () => import('../UpdateOs.ce.vue'),
loader: () => import('../UpdateOs.standalone.vue'),
selector: 'unraid-update-os',
appId: 'update-os',
},
{
loader: () => import('../DowngradeOs.ce.vue'),
loader: () => import('../DowngradeOs.standalone.vue'),
selector: 'unraid-downgrade-os',
appId: 'downgrade-os',
},
@@ -106,22 +109,22 @@ export const componentMappings: ComponentMapping[] = [
appId: 'dev-settings',
},
{
loader: () => import('../ApiKeyPage.ce.vue'),
loader: () => import('../ApiKeyPage.standalone.vue'),
selector: ['unraid-apikey-page', 'unraid-api-key-manager'],
appId: 'apikey-page',
},
{
loader: () => import('../ApiKeyAuthorize.ce.vue'),
loader: () => import('../ApiKeyAuthorize.standalone.vue'),
selector: 'unraid-apikey-authorize',
appId: 'apikey-authorize',
},
{
loader: () => import('../DevModalTest.ce.vue'),
loader: () => import('../DevModalTest.standalone.vue'),
selector: 'unraid-dev-modal-test',
appId: 'dev-modal-test',
},
{
loader: () => import('../LayoutViews/Detail/DetailTest.ce.vue'),
loader: () => import('../LayoutViews/Detail/DetailTest.standalone.vue'),
selector: 'unraid-detail-test',
appId: 'detail-test',
},
@@ -131,13 +134,28 @@ export const componentMappings: ComponentMapping[] = [
appId: 'theme-switcher',
},
{
loader: () => import('../ColorSwitcher.ce.vue'),
loader: () => import('../ColorSwitcher.standalone.vue'),
selector: 'unraid-color-switcher',
appId: 'color-switcher',
},
{
loader: () => import('../UnraidToaster.vue'),
component: UnraidToaster, // Static import - toaster styles need to be in main bundle
selector: ['unraid-toaster', 'uui-toaster'],
appId: 'toaster',
},
{
loader: () => import('../UpdateOs/TestUpdateModal.standalone.vue'),
selector: 'unraid-test-update-modal',
appId: 'test-update-modal',
},
{
loader: () => import('../TestThemeSwitcher.standalone.vue'),
selector: 'unraid-test-theme-switcher',
appId: 'test-theme-switcher',
},
{
loader: () => import('../CpuStats/CpuStats.standalone.vue'),
selector: 'unraid-cpu-stats',
appId: 'cpu-stats',
},
];

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import { computed } from 'vue';
import { computed, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import SsoProviderButton from '~/components/sso/SsoProviderButton.vue';
import { useSsoAuth } from '~/components/sso/useSsoAuth';
import { useSsoProviders } from '~/components/sso/useSsoProviders';
const emit = defineEmits<{
'sso-status': [status: { checking: boolean; loading: boolean }];
}>();
const { t } = useI18n();
const { oidcProviders, hasProviders, checkingApi } = useSsoProviders();
const { currentState, error, navigateToProvider } = useSsoAuth();
@@ -13,6 +17,15 @@ const { currentState, error, navigateToProvider } = useSsoAuth();
const showError = computed(() => currentState.value === 'error');
const showOr = computed(() => (currentState.value === 'idle' || showError.value) && hasProviders.value);
const isLoading = computed(() => currentState.value === 'loading');
// Emit status changes
watch(
[checkingApi, isLoading],
([checking, loading]) => {
emit('sso-status', { checking, loading });
},
{ immediate: true }
);
</script>
<template>

View File

@@ -28,6 +28,8 @@ type Documents = {
"\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n": typeof types.GetPermissionsForRolesDocument,
"\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n": typeof types.UnifiedDocument,
"\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": typeof types.UpdateConnectSettingsDocument,
"\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n": typeof types.GetCpuInfoDocument,
"\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n": typeof types.CpuMetricsDocument,
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": typeof types.LogFilesDocument,
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": typeof types.LogFileContentDocument,
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": typeof types.LogFileSubscriptionDocument,
@@ -73,6 +75,8 @@ const documents: Documents = {
"\n query GetPermissionsForRoles($roles: [Role!]!) {\n getPermissionsForRoles(roles: $roles) {\n resource\n actions\n }\n }\n": types.GetPermissionsForRolesDocument,
"\n query Unified {\n settings {\n unified {\n id\n dataSchema\n uiSchema\n values\n }\n }\n }\n": types.UnifiedDocument,
"\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n": types.UpdateConnectSettingsDocument,
"\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n": types.GetCpuInfoDocument,
"\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n": types.CpuMetricsDocument,
"\n query LogFiles {\n logFiles {\n name\n path\n size\n modifiedAt\n }\n }\n": types.LogFilesDocument,
"\n query LogFileContent($path: String!, $lines: Int, $startLine: Int) {\n logFile(path: $path, lines: $lines, startLine: $startLine) {\n path\n content\n totalLines\n startLine\n }\n }\n": types.LogFileContentDocument,
"\n subscription LogFileSubscription($path: String!) {\n logFile(path: $path) {\n path\n content\n totalLines\n }\n }\n": types.LogFileSubscriptionDocument,
@@ -174,6 +178,14 @@ export function graphql(source: "\n query Unified {\n settings {\n unif
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"): (typeof documents)["\n mutation UpdateConnectSettings($input: JSON!) {\n updateSettings(input: $input) {\n restartRequired\n values\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n"): (typeof documents)["\n query GetCpuInfo {\n info {\n cpu {\n id\n manufacturer\n brand\n vendor\n family\n model\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n"): (typeof documents)["\n subscription CpuMetrics {\n systemMetricsCpu {\n id\n percentTotal\n cpus {\n percentTotal\n percentUser\n percentSystem\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/

View File

@@ -241,6 +241,8 @@ export type ArrayDisk = Node & {
id: Scalars['PrefixedID']['output'];
/** Array slot number. Parity1 is always 0 and Parity2 is always 29. Array slots will be 1 - 28. Cache slots are 30 - 53. Flash is 54. */
idx: Scalars['Int']['output'];
/** Whether the disk is currently spinning */
isSpinning?: Maybe<Scalars['Boolean']['output']>;
name?: Maybe<Scalars['String']['output']>;
/** Number of unrecoverable errors reported by the device I/O drivers. Missing data due to unrecoverable array read errors is filled in on-the-fly using parity reconstruct (and we attempt to write this data back to the sector(s) which failed). Any unrecoverable write error results in disabling the disk. */
numErrors?: Maybe<Scalars['BigInt']['output']>;
@@ -607,6 +609,8 @@ export type Disk = Node & {
id: Scalars['PrefixedID']['output'];
/** The interface type of the disk */
interfaceType: DiskInterfaceType;
/** Whether the disk is spinning or not */
isSpinning: Scalars['Boolean']['output'];
/** The model name of the disk */
name: Scalars['String']['output'];
/** The partitions on the disk */
@@ -674,6 +678,7 @@ export enum DiskSmartStatus {
export type Docker = Node & {
__typename?: 'Docker';
containerUpdateStatuses: Array<ExplicitStatusItem>;
containers: Array<DockerContainer>;
id: Scalars['PrefixedID']['output'];
networks: Array<DockerNetwork>;
@@ -699,6 +704,8 @@ export type DockerContainer = Node & {
id: Scalars['PrefixedID']['output'];
image: Scalars['String']['output'];
imageId: Scalars['String']['output'];
isRebuildReady?: Maybe<Scalars['Boolean']['output']>;
isUpdateAvailable?: Maybe<Scalars['Boolean']['output']>;
labels?: Maybe<Scalars['JSON']['output']>;
mounts?: Maybe<Array<Scalars['JSON']['output']>>;
names: Array<Scalars['String']['output']>;
@@ -770,6 +777,12 @@ export type EnableDynamicRemoteAccessInput = {
url: AccessUrlInput;
};
export type ExplicitStatusItem = {
__typename?: 'ExplicitStatusItem';
name: Scalars['String']['output'];
updateStatus: UpdateStatus;
};
export type Flash = Node & {
__typename?: 'Flash';
guid: Scalars['String']['output'];
@@ -1225,6 +1238,7 @@ export type Mutation = {
rclone: RCloneMutations;
/** Reads each notification to recompute & update the overview. */
recalculateOverview: NotificationOverview;
refreshDockerDigests: Scalars['Boolean']['output'];
/** Remove one or more plugins from the API. Returns false if restart was triggered automatically, true if manual restart is required. */
removePlugin: Scalars['Boolean']['output'];
setDockerFolderChildren: ResolvedOrganizerV1;
@@ -2260,6 +2274,14 @@ export type UpdateSettingsResponse = {
warnings?: Maybe<Array<Scalars['String']['output']>>;
};
/** Update status of a container. */
export enum UpdateStatus {
REBUILD_READY = 'REBUILD_READY',
UNKNOWN = 'UNKNOWN',
UPDATE_AVAILABLE = 'UPDATE_AVAILABLE',
UP_TO_DATE = 'UP_TO_DATE'
}
export type Uptime = {
__typename?: 'Uptime';
timestamp?: Maybe<Scalars['String']['output']>;
@@ -2640,6 +2662,16 @@ export type UpdateConnectSettingsMutationVariables = Exact<{
export type UpdateConnectSettingsMutation = { __typename?: 'Mutation', updateSettings: { __typename?: 'UpdateSettingsResponse', restartRequired: boolean, values: any } };
export type GetCpuInfoQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCpuInfoQuery = { __typename?: 'Query', info: { __typename?: 'Info', cpu: { __typename?: 'InfoCpu', id: string, manufacturer?: string | null, brand?: string | null, vendor?: string | null, family?: string | null, model?: string | null } } };
export type CpuMetricsSubscriptionVariables = Exact<{ [key: string]: never; }>;
export type CpuMetricsSubscription = { __typename?: 'Subscription', systemMetricsCpu: { __typename?: 'CpuUtilization', id: string, percentTotal: number, cpus: Array<{ __typename?: 'CpuLoad', percentTotal: number, percentUser: number, percentSystem: number }> } };
export type LogFilesQueryVariables = Exact<{ [key: string]: never; }>;
@@ -2838,6 +2870,8 @@ export const PreviewEffectivePermissionsDocument = {"kind":"Document","definitio
export const GetPermissionsForRolesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetPermissionsForRoles"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roles"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Role"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getPermissionsForRoles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"roles"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roles"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resource"}},{"kind":"Field","name":{"kind":"Name","value":"actions"}}]}}]}}]} as unknown as DocumentNode<GetPermissionsForRolesQuery, GetPermissionsForRolesQueryVariables>;
export const UnifiedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"Unified"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"settings"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unified"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSchema"}},{"kind":"Field","name":{"kind":"Name","value":"uiSchema"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}}]}}]} as unknown as DocumentNode<UnifiedQuery, UnifiedQueryVariables>;
export const UpdateConnectSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateConnectSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSON"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"restartRequired"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}}]} as unknown as DocumentNode<UpdateConnectSettingsMutation, UpdateConnectSettingsMutationVariables>;
export const GetCpuInfoDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCpuInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cpu"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"manufacturer"}},{"kind":"Field","name":{"kind":"Name","value":"brand"}},{"kind":"Field","name":{"kind":"Name","value":"vendor"}},{"kind":"Field","name":{"kind":"Name","value":"family"}},{"kind":"Field","name":{"kind":"Name","value":"model"}}]}}]}}]}}]} as unknown as DocumentNode<GetCpuInfoQuery, GetCpuInfoQueryVariables>;
export const CpuMetricsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"CpuMetrics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"systemMetricsCpu"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"percentTotal"}},{"kind":"Field","name":{"kind":"Name","value":"cpus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"percentTotal"}},{"kind":"Field","name":{"kind":"Name","value":"percentUser"}},{"kind":"Field","name":{"kind":"Name","value":"percentSystem"}}]}}]}}]}}]} as unknown as DocumentNode<CpuMetricsSubscription, CpuMetricsSubscriptionVariables>;
export const LogFilesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFiles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"modifiedAt"}}]}}]}}]} as unknown as DocumentNode<LogFilesQuery, LogFilesQueryVariables>;
export const LogFileContentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LogFileContent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"lines"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"lines"},"value":{"kind":"Variable","name":{"kind":"Name","value":"lines"}}},{"kind":"Argument","name":{"kind":"Name","value":"startLine"},"value":{"kind":"Variable","name":{"kind":"Name","value":"startLine"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}},{"kind":"Field","name":{"kind":"Name","value":"startLine"}}]}}]}}]} as unknown as DocumentNode<LogFileContentQuery, LogFileContentQueryVariables>;
export const LogFileSubscriptionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"LogFileSubscription"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"content"}},{"kind":"Field","name":{"kind":"Name","value":"totalLines"}}]}}]}}]} as unknown as DocumentNode<LogFileSubscriptionSubscription, LogFileSubscriptionSubscriptionVariables>;

View File

@@ -1,2 +1,2 @@
export * from './fragment-masking';
export * from './gql';
export * from "./fragment-masking";
export * from "./gql";

View File

@@ -0,0 +1,58 @@
import { ref, shallowRef } from 'vue';
export interface ConfirmOptions {
title: string;
description?: string;
confirmText?: string;
cancelText?: string;
confirmVariant?: 'primary' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link';
}
interface ConfirmState extends ConfirmOptions {
resolve: (value: boolean) => void;
}
const isOpen = ref(false);
const state = shallowRef<ConfirmState | null>(null);
export function useConfirm() {
const confirm = (options: ConfirmOptions): Promise<boolean> => {
// Resolve any existing dialog promise with false before opening a new one
if (state.value?.resolve) {
const previousResolve = state.value.resolve;
previousResolve(false);
state.value = null;
}
return new Promise((resolve) => {
state.value = {
...options,
confirmText: options.confirmText ?? 'Confirm',
cancelText: options.cancelText ?? 'Cancel',
confirmVariant: options.confirmVariant ?? 'primary',
resolve,
};
isOpen.value = true;
});
};
const handleConfirm = () => {
state.value?.resolve(true);
isOpen.value = false;
state.value = null;
};
const handleCancel = () => {
state.value?.resolve(false);
isOpen.value = false;
state.value = null;
};
return {
confirm,
isOpen,
state,
handleConfirm,
handleCancel,
};
}

View File

@@ -10,16 +10,16 @@ import AES from 'crypto-js/aes';
import type { SendPayloads } from '@unraid/shared-callbacks';
import WelcomeModalCe from '~/components/Activation/WelcomeModal.ce.vue';
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.ce.vue';
import DowngradeOsCe from '~/components/DowngradeOs.ce.vue';
import HeaderOsVersionCe from '~/components/HeaderOsVersion.ce.vue';
import LogViewerCe from '~/components/Logs/LogViewer.ce.vue';
import ModalsCe from '~/components/Modals.ce.vue';
import RegistrationCe from '~/components/Registration.ce.vue';
import SsoButtonCe from '~/components/SsoButton.ce.vue';
import UpdateOsCe from '~/components/UpdateOs.ce.vue';
import UserProfileCe from '~/components/UserProfile.ce.vue';
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
import ConnectSettingsCe from '~/components/ConnectSettings/ConnectSettings.standalone.vue';
import DowngradeOsCe from '~/components/DowngradeOs.standalone.vue';
import HeaderOsVersionCe from '~/components/HeaderOsVersion.standalone.vue';
import LogViewerCe from '~/components/Logs/LogViewer.standalone.vue';
import ModalsCe from '~/components/Modals.standalone.vue';
import RegistrationCe from '~/components/Registration.standalone.vue';
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
import UpdateOsCe from '~/components/UpdateOs.standalone.vue';
import UserProfileCe from '~/components/UserProfile.standalone.vue';
import { useThemeStore } from '~/store/theme';
const serverStore = useDummyServerStore();

View File

@@ -7,7 +7,7 @@ import { useQuery } from '@vue/apollo-composable';
import { Button, Dialog, Input } from '@unraid/ui';
import { SERVER_INFO_QUERY } from '~/pages/login.query';
import SsoButtonCe from '~/components/SsoButton.ce.vue';
import SsoButtonCe from '~/components/SsoButton.standalone.vue';
const { t } = useI18n();
const { result } = useQuery(SERVER_INFO_QUERY);

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import ApiKeyAuthorize from '~/components/ApiKeyAuthorize.ce.vue';
import ApiKeyAuthorize from '~/components/ApiKeyAuthorize.standalone.vue';
</script>
<template>

View File

@@ -0,0 +1,288 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { Button, Label, Switch } from '@unraid/ui';
import { useDummyServerStore } from '~/_data/serverState';
import type { ServerState, ServerUpdateOsResponse } from '~/types/server';
import CheckUpdateResponseModal from '~/components/UpdateOs/CheckUpdateResponseModal.vue';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
const { t } = useI18n();
const updateOsStore = useUpdateOsStore();
const serverStore = useServerStore();
const dummyServerStore = useDummyServerStore();
// Test scenarios
const testScenarios = [
{
id: 'expired-ineligible',
name: 'Expired key with ineligible update',
description: 'License expired, update available but not eligible',
serverState: 'EEXPIRED',
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: false,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
changelogPretty:
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
sha256: undefined, // requires auth
},
},
{
id: 'normal-update',
name: 'Normal update available',
description: 'Active license with eligible update',
serverState: 'BASIC',
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
changelogPretty:
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
sha256: 'abc123def456789',
},
},
{
id: 'renewal-required',
name: 'Update requires renewal',
description: 'License expired > 1 year, update requires renewal',
serverState: 'STARTER',
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: false,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
changelogPretty:
'## Unraid 7.1.0\n\n### New Features\n- Feature 1\n- Feature 2\n\n### Bug Fixes\n- Fix 1\n- Fix 2',
sha256: undefined,
},
},
{
id: 'no-update',
name: 'No update available',
description: 'Already on latest version',
serverState: 'BASIC',
updateResponse: {
version: '7.0.0',
name: 'Unraid 7.0.0',
date: '2024-01-15',
isNewer: false,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.0.0/',
sha256: 'xyz789abc123',
},
},
{
id: 'trial-update',
name: 'Trial with update',
description: 'Trial license with update available',
serverState: 'TRIAL',
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: 'def456ghi789',
},
},
{
id: 'pro-auth-required',
name: 'Pro license - auth required',
description: 'Pro license but authentication required for download',
serverState: 'PRO',
updateResponse: {
version: '7.1.0',
name: 'Unraid 7.1.0',
date: '2024-12-15',
isNewer: true,
isEligible: true,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/7.1.0/',
sha256: undefined, // requires auth
},
},
];
// Component state
const selectedScenario = ref('normal-update');
const modalOpen = ref(false);
const ignoreRelease = ref(false);
const checkingForUpdates = ref(false);
const ignoredReleases = ref<string[]>([]);
// Apply scenario
const applyScenario = () => {
const scenario = testScenarios.find((s) => s.id === selectedScenario.value);
if (!scenario) return;
// Apply server state
dummyServerStore.selector =
scenario.serverState === 'EEXPIRED' || scenario.serverState === 'STARTER' ? 'default' : 'default';
// Set server state
const currentTime = Date.now();
const expiredTime = scenario.serverState === 'EEXPIRED' ? currentTime - 24 * 60 * 60 * 1000 : 0;
const regExp =
scenario.serverState === 'STARTER' ? currentTime - 400 * 24 * 60 * 60 * 1000 : undefined;
// Apply update response
if (scenario.serverState === 'EEXPIRED') {
serverStore.$patch({
expireTime: expiredTime,
state: 'EEXPIRED' as ServerState,
regExp: undefined,
});
} else if (scenario.serverState === 'STARTER') {
serverStore.$patch({
state: 'STARTER' as ServerState,
regExp: regExp,
regTy: 'Starter',
});
} else {
serverStore.$patch({
state: scenario.serverState as ServerState,
regExp: undefined,
expireTime: scenario.serverState === 'TRIAL' ? currentTime + 7 * 24 * 60 * 60 * 1000 : 0,
});
}
serverStore.setUpdateOsResponse(scenario.updateResponse as ServerUpdateOsResponse);
// Apply ignored releases
if (ignoreRelease.value && scenario.updateResponse.isNewer) {
if (!ignoredReleases.value.includes(scenario.updateResponse.version)) {
ignoredReleases.value.push(scenario.updateResponse.version);
}
} else {
ignoredReleases.value = ignoredReleases.value.filter((v) => v !== scenario.updateResponse.version);
}
serverStore.$patch({ updateOsIgnoredReleases: ignoredReleases.value });
};
// Watch for scenario changes
watch([selectedScenario, ignoreRelease], () => {
applyScenario();
});
// Open modal with scenario
const openModal = () => {
applyScenario();
updateOsStore.checkForUpdatesLoading = checkingForUpdates.value;
modalOpen.value = true;
updateOsStore.setModalOpen(true);
};
// Initialize
applyScenario();
const currentScenario = computed(() => testScenarios.find((s) => s.id === selectedScenario.value));
</script>
<template>
<div class="container mx-auto max-w-4xl p-6">
<div class="rounded-lg bg-white p-6 shadow-lg dark:bg-zinc-900">
<div class="mb-6">
<h2 class="mb-2 text-2xl font-bold">Update Modal Test Page</h2>
<p class="text-muted-foreground">
Test various update scenarios for the CheckUpdateResponseModal component
</p>
</div>
<div class="space-y-6">
<!-- Scenario Selection -->
<div class="space-y-4">
<Label class="text-lg font-semibold">Select Test Scenario</Label>
<div class="space-y-3">
<div v-for="scenario in testScenarios" :key="scenario.id" class="flex items-start space-x-3">
<input
type="radio"
:id="scenario.id"
:value="scenario.id"
v-model="selectedScenario"
class="mt-1 rounded-full"
/>
<div class="flex-1">
<Label :for="scenario.id" class="block cursor-pointer font-medium">
{{ scenario.name }}
</Label>
<p class="text-muted-foreground mt-1 text-sm">{{ scenario.description }}</p>
</div>
</div>
</div>
</div>
<!-- Options -->
<div class="space-y-4 border-t pt-4">
<h3 class="font-semibold">Options</h3>
<div class="flex items-center space-x-3">
<Switch id="ignore-release" v-model:checked="ignoreRelease" />
<Label for="ignore-release" class="cursor-pointer">Ignore this release</Label>
</div>
<div class="flex items-center space-x-3">
<Switch id="checking-updates" v-model:checked="checkingForUpdates" />
<Label for="checking-updates" class="cursor-pointer"
>Show checking for updates loading state</Label
>
</div>
</div>
<!-- Current State Display -->
<div class="space-y-2 border-t pt-4">
<h3 class="font-semibold">Current Scenario Details</h3>
<div class="space-y-1 font-mono text-sm">
<p><span class="font-semibold">Server State:</span> {{ currentScenario?.serverState }}</p>
<p>
<span class="font-semibold">Version:</span> {{ currentScenario?.updateResponse.version }}
</p>
<p>
<span class="font-semibold">Is Newer:</span> {{ currentScenario?.updateResponse.isNewer }}
</p>
<p>
<span class="font-semibold">Is Eligible:</span>
{{ currentScenario?.updateResponse.isEligible }}
</p>
<p>
<span class="font-semibold">Has SHA256:</span>
{{ !!currentScenario?.updateResponse.sha256 }}
</p>
<p>
<span class="font-semibold">Ignored Releases:</span>
{{ ignoredReleases.join(', ') || 'None' }}
</p>
</div>
</div>
<!-- Open Modal Button -->
<div class="border-t pt-4">
<Button @click="openModal" variant="primary" class="w-full"> Open Update Modal </Button>
</div>
</div>
</div>
<!-- The Modal Component -->
<CheckUpdateResponseModal
:open="modalOpen"
@update:open="
(val: boolean) => {
modalOpen = val;
updateOsStore.setModalOpen(val);
}
"
:t="t"
/>
</div>
</template>

View File

@@ -5,8 +5,8 @@ import { storeToRefs } from 'pinia';
import { useActivationCodeDataStore } from '~/components/Activation/store/activationCodeData';
import { useActivationCodeModalStore } from '~/components/Activation/store/activationCodeModal';
import { useWelcomeModalDataStore } from '~/components/Activation/store/welcomeModalData';
import WelcomeModalCe from '~/components/Activation/WelcomeModal.ce.vue';
import ModalsCe from '~/components/Modals.ce.vue';
import WelcomeModalCe from '~/components/Activation/WelcomeModal.standalone.vue';
import ModalsCe from '~/components/Modals.standalone.vue';
import { useCallbackActionsStore } from '~/store/callbackActions';
const welcomeModalRef = ref<InstanceType<typeof WelcomeModalCe>>();

View File

@@ -1,327 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standalone Vue Apps Test Page</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.test-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
h2 {
color: #666;
margin-top: 0;
margin-bottom: 15px;
font-size: 18px;
}
.status {
padding: 10px;
border-radius: 4px;
margin-bottom: 10px;
font-family: monospace;
font-size: 14px;
}
.status.loading {
background: #fff3cd;
color: #856404;
}
.status.success {
background: #d4edda;
color: #155724;
}
.status.error {
background: #f8d7da;
color: #721c24;
}
.mount-target {
padding: 20px;
background: #fafafa;
border: 2px dashed #ddd;
border-radius: 4px;
min-height: 100px;
position: relative;
}
.mount-target::before {
content: attr(data-label);
position: absolute;
top: -10px;
left: 10px;
background: white;
padding: 0 5px;
color: #999;
font-size: 12px;
}
.debug-info {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
white-space: pre-wrap;
}
.multiple-mounts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 20px;
}
.test-button {
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.test-button:hover {
background: #0056b3;
}
.test-button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
</head>
<body>
<!-- Teleport target for dropdowns and modals -->
<div id="teleports"></div>
<!-- Mount point for Modals component -->
<unraid-modals></unraid-modals>
<div class="container">
<h1>🧪 Standalone Vue Apps Test Page</h1>
<div id="status" class="status loading">Loading...</div>
<!-- Test Section 1: Single Mount -->
<div class="test-section">
<h2>Test 1: Single Component Mount</h2>
<p>Testing single instance of HeaderOsVersion component</p>
<div class="mount-target" data-label="HeaderOsVersion Mount">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
<!-- Test Section 2: Multiple Mounts -->
<div class="test-section">
<h2>Test 2: Multiple Component Mounts (Shared Pinia Store)</h2>
<p>Testing that multiple instances share the same Pinia store</p>
<div class="multiple-mounts">
<div class="mount-target" data-label="Instance 1">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 2">
<unraid-header-os-version></unraid-header-os-version>
</div>
<div class="mount-target" data-label="Instance 3">
<unraid-header-os-version></unraid-header-os-version>
</div>
</div>
</div>
<!-- Test Section 3: Dynamic Mount -->
<div class="test-section">
<h2>Test 3: Dynamic Component Creation</h2>
<p>Test dynamically adding components after page load</p>
<button class="test-button" id="addComponent">Add New Component</button>
<button class="test-button" id="removeComponent">Remove Last Component</button>
<button class="test-button" id="remountAll">Remount All</button>
<div id="dynamicContainer" style="margin-top: 20px;">
<!-- Dynamic components will be added here -->
</div>
</div>
<!-- Test Section 4: Modal Testing -->
<div class="test-section">
<h2>Test 4: Modal Components</h2>
<p>Test modal functionality</p>
<button class="test-button" onclick="testTrialModal()">Open Trial Modal</button>
<button class="test-button" onclick="testUpdateModal()">Open Update Modal</button>
<button class="test-button" onclick="testApiKeyModal()">Open API Key Modal</button>
<div style="margin-top: 10px;">
<small>Note: Modals require proper store state to display</small>
</div>
</div>
<!-- Debug Info -->
<div class="test-section">
<h2>Debug Information</h2>
<div class="debug-info" id="debugInfo">
Waiting for initialization...
</div>
</div>
</div>
<!-- Mock configurations for local testing -->
<script>
// Set GraphQL endpoint directly to API server
// Change this to match your API server port
window.GRAPHQL_ENDPOINT = 'http://localhost:3001/graphql';
// Mock webGui path for images
window.__WEBGUI_PATH__ = '';
// Add some debug logging
window.addEventListener('DOMContentLoaded', () => {
const status = document.getElementById('status');
const debugInfo = document.getElementById('debugInfo');
// Log when scripts are loaded
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeName === 'SCRIPT') {
console.log('Script loaded:', node.src || 'inline');
}
});
}
});
});
observer.observe(document.head, { childList: true });
observer.observe(document.body, { childList: true });
// Check for Vue app mounting
let checkInterval = setInterval(() => {
const mountedElements = document.querySelectorAll('unraid-header-os-version');
let mountedCount = 0;
mountedElements.forEach(el => {
if (el.innerHTML.trim() !== '') {
mountedCount++;
}
});
if (mountedCount > 0) {
status.className = 'status success';
status.textContent = `✅ Successfully mounted ${mountedCount} component(s)`;
// Update debug info
debugInfo.textContent = `
Components Found: ${mountedElements.length}
Components Mounted: ${mountedCount}
Vue Apps: ${window.mountedApps ? Object.keys(window.mountedApps).length : 0}
Pinia Store: ${window.globalPinia ? 'Initialized' : 'Not found'}
GraphQL Endpoint: ${window.GRAPHQL_ENDPOINT || 'Not configured'}
`.trim();
clearInterval(checkInterval);
}
}, 500);
// Timeout after 10 seconds
setTimeout(() => {
if (checkInterval) {
clearInterval(checkInterval);
if (status.className === 'status loading') {
status.className = 'status error';
status.textContent = '❌ Failed to mount components (timeout)';
}
}
}, 10000);
});
// Dynamic component controls
document.addEventListener('DOMContentLoaded', () => {
let dynamicCount = 0;
const dynamicContainer = document.getElementById('dynamicContainer');
document.getElementById('addComponent').addEventListener('click', () => {
dynamicCount++;
const wrapper = document.createElement('div');
wrapper.className = 'mount-target';
wrapper.setAttribute('data-label', `Dynamic Instance ${dynamicCount}`);
wrapper.style.marginBottom = '10px';
wrapper.innerHTML = '<unraid-header-os-version></unraid-header-os-version>';
dynamicContainer.appendChild(wrapper);
// Trigger mount if app is already loaded
if (window.mountVueApp) {
window.mountVueApp({
component: window.HeaderOsVersion,
selector: 'unraid-header-os-version',
appId: `dynamic-${dynamicCount}`,
});
}
});
document.getElementById('removeComponent').addEventListener('click', () => {
const lastChild = dynamicContainer.lastElementChild;
if (lastChild) {
dynamicContainer.removeChild(lastChild);
dynamicCount = Math.max(0, dynamicCount - 1);
}
});
document.getElementById('remountAll').addEventListener('click', () => {
// This would require the mount function to be exposed globally
console.log('Remounting all components...');
location.reload();
});
});
// Modal test functions
window.testTrialModal = function() {
console.log('Testing trial modal...');
if (window.globalPinia) {
const trialStore = window.globalPinia._s.get('trial');
if (trialStore) {
trialStore.trialModalVisible = true;
console.log('Trial modal triggered');
} else {
console.error('Trial store not found');
}
}
};
window.testUpdateModal = function() {
console.log('Testing update modal...');
if (window.globalPinia) {
const updateStore = window.globalPinia._s.get('updateOs');
if (updateStore) {
updateStore.updateOsModalVisible = true;
console.log('Update modal triggered');
} else {
console.error('Update store not found');
}
}
};
window.testApiKeyModal = function() {
console.log('Testing API key modal...');
if (window.globalPinia) {
const apiKeyStore = window.globalPinia._s.get('apiKey');
if (apiKeyStore) {
apiKeyStore.showCreateModal = true;
console.log('API key modal triggered');
} else {
console.error('API key store not found');
}
}
};
</script>
<!-- Load the standalone app -->
<script type="module" src=".nuxt/standalone-apps/standalone-apps.js"></script>
</body>
</html>

View File

@@ -129,6 +129,10 @@ export default defineConfig({
terserOptions: sharedTerserOptions,
},
optimizeDeps: {
include: ['ajv', 'ajv-errors', 'ajv-formats'],
},
server: {
port: 3000,
proxy: {