Compare commits

...

158 Commits

Author SHA1 Message Date
github-actions[bot]
6b6b78fa2e chore(main): release 4.26.2 (#1794) 2025-11-19 06:38:32 -05:00
Eli Bosley
e2fdf6cadb fix(theme): Missing header background color 2025-11-19 06:25:03 -05:00
github-actions[bot]
3d4f193fa4 chore(main): release 4.26.1 (#1790)
🤖 I have created a release *beep* *boop*
---


## [4.26.1](https://github.com/unraid/api/compare/v4.26.0...v4.26.1)
(2025-11-18)


### Bug Fixes

* **theme:** update theme class naming and scoping logic
([b28ef1e](b28ef1ea33))

---
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-11-18 13:37:18 -05:00
Eli Bosley
b28ef1ea33 fix(theme): update theme class naming and scoping logic
- Changed theme class names from `.theme-*` to `.Theme--*` for consistency.
- Updated scoping logic to prevent scoping of `.Theme--` classes, ensuring they remain global.
- Enhanced theme store logic to check for existing `.Theme--` classes before applying new theme classes, preventing conflicts.
- Adjusted class cleaning logic to retain `.Theme--` classes when necessary.
2025-11-18 13:29:39 -05:00
github-actions[bot]
ee0f240233 chore(main): release 4.26.0 (#1744)
🤖 I have created a release *beep* *boop*
---


## [4.26.0](https://github.com/unraid/api/compare/v4.25.3...v4.26.0)
(2025-11-17)


### Features

* add cpu power query & subscription
([#1745](https://github.com/unraid/api/issues/1745))
([d7aca81](d7aca81c60))
* add schema publishing to apollo studio
([#1772](https://github.com/unraid/api/issues/1772))
([7e13202](7e13202aa1))
* add workflow_dispatch trigger to schema publishing workflow
([818e7ce](818e7ce997))
* apollo studio readme link
([c4cd0c6](c4cd0c6352))
* **cli:** make `unraid-api plugins remove` scriptable
([#1774](https://github.com/unraid/api/issues/1774))
([64eb9ce](64eb9ce9b5))
* use persisted theme css to fix flashes on header
([#1784](https://github.com/unraid/api/issues/1784))
([854b403](854b403fbd))


### Bug Fixes

* **api:** decode html entities before parsing notifications
([#1768](https://github.com/unraid/api/issues/1768))
([42406e7](42406e795d))
* **connect:** disable api plugin if unraid plugin is absent
([#1773](https://github.com/unraid/api/issues/1773))
([c264a18](c264a1843c))
* detection of flash backup activation state
([#1769](https://github.com/unraid/api/issues/1769))
([d18eaf2](d18eaf2364))
* re-add missing header gradient styles
([#1787](https://github.com/unraid/api/issues/1787))
([f8a6785](f8a6785e9c))
* respect OS safe mode in plugin loader
([#1775](https://github.com/unraid/api/issues/1775))
([92af3b6](92af3b6115))

---
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-11-17 13:03:50 -05:00
Eli Bosley
3aacaa1fb5 chore: trigger release please 2025-11-17 12:54:59 -05:00
Pujit Mehrotra
0cd4c0ae16 chore: remove fetch-depth limit in release-please git checkout (#1789)
Hopefully fixes release please over-scoping its changelog generation

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

## Summary by CodeRabbit

* **Chores**
* Updated CI/CD workflow configuration to enable full repository history
retrieval during build processes, improving the reliability of version
control operations in automated deployments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-17 12:33:40 -05:00
Eli Bosley
66625ded6a New Crowdin updates (#1786)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Expanded localization support with comprehensive validation error
messages across 22 languages including Arabic, Chinese, French, German,
Hindi, Japanese, Portuguese, Russian, Spanish, and others.
* Enhanced form validation messaging for API key management, OIDC
provider configuration, SSO setup, and related settings to provide
localized guidance for users fixing errors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-17 11:04:10 -05:00
Eli Bosley
f8a6785e9c fix: re-add missing header gradient styles (#1787)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Enhanced header banner styling: centered, non-repeating cover images
with layered gradient overlays and adjusted user-profile banner
positioning for improved layout.
* **Bug Fixes**
* Banner display logic updated so "image" is treated like "yes" for
showing banner images.
* **Tests**
  * Added unit tests covering banner/theme display behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-17 11:03:37 -05:00
Pujit Mehrotra
d7aca81c60 feat: add cpu power query & subscription (#1745)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Per-package CPU power and temperature displayed in hardware info
(total and per-package values).
* CPU package topology (cores/threads per package) included in CPU info.
* Real-time per-package CPU telemetry exposed via a new system metrics
subscription.

* **Chores**
* Added an automated deployment script and npm deploy script for the
shared package.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Co-authored-by: Simon Fairweather <simon.n.fairweather@gmail.com>
Co-authored-by: SimonFair <39065407+SimonFair@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-11-14 14:27:49 -05:00
Eli Bosley
854b403fbd feat: use persisted theme css to fix flashes on header (#1784)
## Summary
- install the pinia-plugin-persistedstate integration directly inside
the theme store and hydrate cached themes before applying CSS variables
- fall back to the active/global Pinia instance while ensuring persisted
state is only wired once per store instance
- update the theme store tests to reset the shared Pinia state between
runs and rely on the plugin-backed persistence

## Testing
- pnpm --filter web test __test__/store/theme.test.ts

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_69156c5e8de48323841f7dbfdadec51d)

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

* **New Features**
* Theme preferences now persist across sessions and are restored on
return.
* **Behavior Change**
* Theme switching may now update the URL/address bar to reflect the
selected theme.
* **Chores**
* Added a persistence integration to enable storing/restoring theme
data.
* **Tests**
* Updated/added tests covering hydration from storage and persistence of
server-provided themes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 16:24:30 -05:00
Pujit Mehrotra
c264a1843c fix(connect): disable api plugin if unraid plugin is absent (#1773)
Mitigates an edge case where the connect api plugin does not uninstall
itself when Unraid version < 7.2.0, resulting in retention of undesired
connect functionality on stock unraid after upgrading to 7.2.0+.

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

* **New Features**
* App now detects Connect plugin availability at startup and falls back
gracefully with a no-op mode and a logged warning if the plugin is
absent.
* Added an environment option to skip the plugin availability check when
needed.
* Export behavior adjusted so the application uses the appropriate
module based on plugin presence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 12:26:35 -05:00
Eli Bosley
45cda4af80 chore: nunjucks template engine for test pages (#1783) 2025-11-13 11:35:43 -05:00
Pujit Mehrotra
64eb9ce9b5 feat(cli): make unraid-api plugins remove scriptable (#1774)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added --bypass-npm and --npm flags, support for passing plugin names
as command args, and a restart option; CLI params now merge with
interactive prompts.

* **Bug Fixes**
* Vendor archive rebuild is performed only when actual uninstalls occur.
  * Restart behavior uses resolved options for consistent restarts.
  * Removal can run as "config-only" without running package operations.

* **Tests**
* Expanded tests for bypass scenarios, prompt flows, config-only
removals, and removal control flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 10:49:50 -05:00
Eli Bosley
d56797c59f chore: add dev mode language selection (#1782)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a language switcher widget to all test pages for convenient
locale selection.
* Displays supported language options in a dropdown menu with extensive
multi-language support.
  * Persists user's locale preference across page reloads.

* **Tests**
* Enhanced test utilities with improved multi-locale support and locale
switching capabilities.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 10:49:36 -05:00
Pujit Mehrotra
92af3b6115 fix: respect OS safe mode in plugin loader (#1775)
- also adds util for reading ini configs synchronously

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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added safe mode support to prevent plugin loading when enabled,
returning an empty plugin list in safe mode.

* **Tests**
* Added comprehensive test coverage for safe mode functionality and
state file loading mechanisms.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 10:15:12 -05:00
Eli Bosley
35f8bc2258 refactor: remove unnecessary network stack reload in ConnectSettingsService (#1751)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Chores**
* Streamlined the remote access update workflow by removing an
unnecessary step previously executed during remote access configuration
changes, resulting in a more efficient update process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-13 09:58:50 -05:00
Eli Bosley
c4cd0c6352 feat: apollo studio readme link 2025-11-10 12:18:32 -05:00
Eli Bosley
818e7ce997 feat: add workflow_dispatch trigger to schema publishing workflow 2025-11-10 12:05:46 -05:00
Eli Bosley
7e13202aa1 feat: add schema publishing to apollo studio (#1772) 2025-11-10 12:03:47 -05:00
Pujit Mehrotra
d18eaf2364 fix: detection of flash backup activation state (#1769)
Resolves #1767 


plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/UpdateFlashBackup.php:415
still reads the API version from _var($mystatus,'version'), but commit
441e1805c removed the code that populates $mystatus (the parse of
/var/local/emhttp/myservers.cfg). As a result $mystatus is undefined, so
we now send api_version= to the flash activation endpoint. The PHP
runtime also emits “Undefined variable: mystatus” / “Trying to access
array offset on value of type null” notices before headers are written.
Those notices corrupt the JSON response, the keyserver rejects the
request because the api_version is missing, and the flash backup state
file is never updated—so the web GUI stays stuck at “Loading”.

Because the status request always invokes UpdateFlashBackup.php, every
page load trips the same failure path, leaving
/var/local/emhttp/flashbackup.ini with loading=Loading. The frontend
only listens for /sub/flashbackup events, so until that INI file is
rewritten the spinner never clears and the enable button never becomes
active.

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

* **New Features**
* UI now initializes flash backup status on page load so backup controls
reflect current server state immediately.
* Backup state saves now publish remote updates, improving
synchronization of backup status.

* **Bug Fixes**
* Improved API version handling for flash backup operations: sends
version when available and falls back gracefully when unknown.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 14:52:18 -05:00
Pujit Mehrotra
42406e795d fix(api): decode html entities before parsing notifications (#1768)
so the parser does not treat them as comments.

This surfaces a new bug: `#`'s in notification subject or descriptions
are treated as comments, and content following a `#` will not be
displayed in queries from the api, unless the values are explicitly
quoted as strings:
```
subject=Warning #1 OS      #  Truncates after "Warning"
subject=\#1 OS             #  Backslash escape doesn't work
subject="Warning #1 OS"    #  Double quotes work!
subject='Warning #1 OS'    #  Single quotes work!
```

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

## Summary by CodeRabbit

## Version 4.25.3

* **Improvements**
* Enhanced notification system with improved handling of special
characters and HTML-formatted content in messages.
  * Better text rendering accuracy across all notification types.

* **Chores**
  * Updated application dependencies.
  * Version bumped to 4.25.3.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-07 14:49:22 -05:00
Pujit Mehrotra
11d2de5d08 chore(web): reduce lint duration by 90% (#1766)
Replace the eslint prettier plugin (which profiling revealed to be the
bottleneck) with separate `prettier` invocations. This yielded a 73 second reduction (89%) in the environment described below.

Before:
```
pnpm --filter web lint:fix  81.79s user 1.85s system 110% cpu 1:15.81 total
```
After: 
```
pnpm --filter web lint:fix  8.83s user 0.93s system 170% cpu 5.737 total
```
System specs (Nov 5 2025):
```
OS: macOS Sequoia 15.6.1 (24G90) arm64
Host: MacBook Air (15-inch, M2, 2023)
Kernel: Darwin 24.6.0
Uptime: 44 days, 5 hours, 22 mins
Packages: 52 (nix-default), 195 (brew), 4 (brew-cask)
Shell: zsh 5.9
CPU: Apple M2 (8) @ 3.50 GHz
GPU: Apple M2 (10) @ 1.40 GHz [Integrated]
Memory: 19.51 GiB / 24.00 GiB (81%)
Swap: 4.83 GiB / 6.00 GiB (80%)
```

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

## Summary by CodeRabbit

* **Chores**
* Separated code formatting and linting tools into independent workflows
for improved developer efficiency
* Updated development tool configuration to streamline the linting and
formatting process

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-06 09:22:45 -05:00
Eli Bosley
031c1ab5dc New Crowdin updates (#1760)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Localization**
  * Added proper Arabic translation for server action status messages
  * Added proper French translation for server action status messages
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-11-01 07:34:39 -04:00
Pujit Mehrotra
34075e44c5 fix: flaky watch on boot drive's dynamix config (#1753)
On FAT32, `fs.stat()` updates accesstime, which means file reads are
also writes, which means we can't use `usePoll` without degrading users'
flash drives.

To keep file reads lazy without a larger refactor, I override
`getters.dynamix()` as the entrypoint to re-read the boot drive's
dynamix config.

Consecutive calls to `getters.dynamix()` are a common access pattern,
which means we have to memoize to avoid many redundant file reads, so I
used a TTL cache with a 250ms lifetime, hoping to scope config files to
each request.

`getters.dynamix()` is also used synchonously, so bit the bullet and
switched away from async reads for simplicity, considering that most
reads will be occurring from memory, even during cache misses.

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

* **New Features**
  * Added a TTL memoized loader utility with exported types.
  * Added a public function to load Dynamix configuration at startup.

* **Refactor**
* Startup now uses the deterministic, cached config loader; runtime
file-watch for Dynamix config removed.
* Simplified config state handling and load-status reporting for more
predictable startup behavior.

* **Tests**
* Added tests for TTL caching, eviction, keying, and conditional
caching.

* **Chores**
  * Bumped package versions and updated changelog.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-27 11:05:13 -04:00
Pujit Mehrotra
ff2906e52a chore: fix header capture from changelog in generate-release-notes.yml (#1759)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated release notes extraction workflow to improve handling of
version headers in generated release notes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-22 16:19:38 -04:00
Pujit Mehrotra
a0d6cc92c8 chore: decouple manual-release from release-production (#1758) 2025-10-22 15:57:41 -04:00
Pujit Mehrotra
57acfaacf0 chore: avoid altering formatting via jq in manual-release workflow 2025-10-22 15:36:20 -04:00
Pujit Mehrotra
ea816c7a5c chore: use jq to update package versions in manual-release (#1757) 2025-10-22 15:19:28 -04:00
Pujit Mehrotra
cafde72d38 chore: add version check & changelog generation to manual-release (#1756)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Automated release-notes generation with layered fallbacks (use
provided notes, extract from changelog, generate from previous release,
call provider APIs, or default message).
* New version-validation step to ensure package versions are consistent
before publishing.

* **Chores**
* Moved release-notes logic into a reusable workflow and rewired the
manual release process to consume its outputs for more consistent
releases.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-22 15:13:17 -04:00
Pujit Mehrotra
2b481c397c chore: add manual-release workflow and extract build-artifacts workflow (#1755)
Adds a workflow to create or override (github) releases with a release
produced from a specific git ref. Refactors the main build process into
a workflow call for reusability.

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

* **Chores**
* Consolidated multi-target build pipeline for API, UI library, and web
app with unified artifact publishing, improved caching, and simplified
downstream wiring.
* **New Features**
* Added a manual, parameterized release workflow to create/update draft
releases with optional prerelease tagging and generated release notes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-22 13:49:15 -04:00
Eli Bosley
8c4e9dd7ae New Crowdin updates (#1750)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* New Features
* Expanded and refined translations across the UI for Arabic, Bengali,
Catalan, Czech, Danish, German, Spanish, French, Hindi, Croatian,
Hungarian, Italian, Japanese, Korean, Latvian, Dutch, Norwegian, Polish,
Portuguese, Romanian, Russian, Swedish, Ukrainian, and Chinese.
* Updated labels, titles, and descriptions for API key management,
OIDC/SSO configuration, buttons, and restrictions to native-language
equivalents.
* Improves readability and consistency in localized interfaces; no
functional changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-21 13:15:11 -04:00
Eli Bosley
f212dce88b fix: use relative URLs in the web links and fix color in PM2 startup (#1752)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Refactor**
* Consolidated URL resolution and handling logic for improved
consistency across the application
* Enhanced GraphQL endpoint configuration with better fallback
mechanisms for more reliable connections
* Optimized platform command execution through improved default
parameter handling

* **Chores**
  * Infrastructure configuration updates and maintenance

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-21 10:10:50 -04:00
Eli Bosley
8cd2a4c124 chore: add translations.php to backup and restore lists 2025-10-15 15:23:39 -04:00
Eli Bosley
10f048ee1f chore: re-add translations.php to prevent breaking uninstalls 2025-10-15 12:05:02 -04:00
Eli Bosley
e9e271ade5 fix(#1729): api key authorize component not mounted when on Unraid OS 2025-10-13 21:37:28 -04:00
Eli Bosley
31c41027fc feat: translations now use crowdin (translate.unraid.net) (#1739)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- App-wide internationalization: dynamic locale detection/loading, many
new locale bundles, and CLI helpers to extract/sort translation keys.

- **Accessibility**
  - Brand button supports keyboard activation (Enter/Space).

- **Documentation**
  - Internationalization guidance added to API and Web READMEs.

- **Refactor**
- UI updated to use centralized i18n keys and a unified locale loading
approach.

- **Tests**
  - Test utilities updated to support i18n and localized assertions.

- **Chores**
- Crowdin config and i18n scripts added; runtime locale exposed for
selection.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-13 16:56:08 -04:00
Eli Bosley
fabe6a2c4b chore: Delete .github/workflows/claude-code-review.yml 2025-10-13 09:27:40 -04:00
Pujit Mehrotra
754966d5d3 fix: api auth from web during local dev (#1743) 2025-10-07 19:45:50 -04:00
Pujit Mehrotra
ed594e9147 chore(readme): add link to Deep Wiki for automated code documentation (#1735)
Deep Wiki from Cognition Labs already has some useful documentation that
they generated for us: https://deepwiki.com/unraid/api. This PR adds
their badge to our readme so contributors can access their documentation
directly.

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

## Summary by CodeRabbit

- Documentation
- Added an “Ask DeepWiki” shield badge to the top of the README’s
project shields, giving users quick access to a dedicated Q&A/help
resource.
- Improves discoverability of support and learning materials directly
from the project homepage.
- No functional or behavioral changes to the application; this is an
informational enhancement aimed at easing onboarding and providing
faster guidance for users exploring the project.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-02 11:01:12 -04:00
github-actions[bot]
50d83313a1 chore(main): release 4.25.2 (#1734)
🤖 I have created a release *beep* *boop*
---


## [4.25.2](https://github.com/unraid/api/compare/v4.25.1...v4.25.2)
(2025-09-30)


### Bug Fixes

* enhance activation code modal visibility logic
([#1733](https://github.com/unraid/api/issues/1733))
([e57ec00](e57ec00627))

---
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-30 17:16:08 -04:00
Eli Bosley
e57ec00627 fix: enhance activation code modal visibility logic (#1733)
Updated the visibility logic in the activation code modal store to
include a check for the presence of an activation code. The modal will
now be shown if it is not explicitly hidden, the installation is fresh,
there is no callback data, and an activation code is present.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Activation modal visibility refined: it now appears only when an
activation code is available, it’s a fresh install, the modal isn’t
hidden, and there’s no callback data. Prevents unnecessary prompts.

* **Tests**
* Expanded coverage to include activation code presence/absence and
combined scenarios with fresh install and hidden states, ensuring
accurate visibility logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 17:08:16 -04:00
github-actions[bot]
84f4a7221d chore(main): release 4.25.1 (#1732)
🤖 I have created a release *beep* *boop*
---


## [4.25.1](https://github.com/unraid/api/compare/v4.25.0...v4.25.1)
(2025-09-30)


### Bug Fixes

* add cache busting to web component extractor
([#1731](https://github.com/unraid/api/issues/1731))
([0d165a6](0d165a6087))
* Connect won't appear within Apps - Previous Apps
([#1727](https://github.com/unraid/api/issues/1727))
([d73953f](d73953f8ff))

---
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-30 13:01:57 -04:00
Squidly271
d73953f8ff fix: Connect won't appear within Apps - Previous Apps (#1727)
Manual removal of the .plg is never necessary. plugin script will
automatically move the .plg to /config/plugins-removed

Manual removal results in PHP errors and possible indeterminate state

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

- Bug Fixes
- Updated plugin removal on Unraid 7.2+ to no longer delete the plugin
file during boot. You’ll now be clearly prompted to reboot to complete
uninstallation, reducing unexpected removals and improving guidance.
Behavior on earlier Unraid versions remains unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 12:32:16 -04:00
Eli Bosley
0d165a6087 fix: add cache busting to web component extractor (#1731)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- Bug Fixes
- Ensures UI assets use content-hashed filenames so browsers load the
latest scripts and styles after updates, reducing stale-cache issues.
- Keeps scripts and their related styles in sync for consistent
rendering and fewer cache-related glitches.
- Ignores non-asset manifest entries to prevent accidental inclusion of
invalid items and ensure correct asset loading.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-30 12:27:48 -04:00
github-actions[bot]
f4f3e3c44b chore(main): release 4.25.0 (#1725)
🤖 I have created a release *beep* *boop*
---


## [4.25.0](https://github.com/unraid/api/compare/v4.24.1...v4.25.0)
(2025-09-26)


### Features

* add Tailwind scoping plugin and integrate into Vite config
([#1722](https://github.com/unraid/api/issues/1722))
([b7afaf4](b7afaf4632))
* notification filter controls pill buttons
([#1718](https://github.com/unraid/api/issues/1718))
([661865f](661865f976))


### Bug Fixes

* enable auth guard for nested fields - thanks
[@ingel81](https://github.com/ingel81)
([7bdeca8](7bdeca8338))
* enhance user context validation in auth module
([#1726](https://github.com/unraid/api/issues/1726))
([cd5eff1](cd5eff11bc))

---
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-26 12:58:08 -04:00
Eli Bosley
cd5eff11bc fix: enhance user context validation in auth module (#1726)
Fixes #1723

- Improved error handling in the auth module to ensure user context is
present and valid.
- Added checks for user roles and identifiers, throwing appropriate
exceptions for missing or invalid data.
- Introduced a new integration test suite for AuthZGuard, validating
role-based access control for various actions in the application.
- Tests cover scenarios for viewer and admin roles, ensuring correct
permissions are enforced.

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

## Summary by CodeRabbit

* **Bug Fixes**
* Hardened authorization: properly rejects requests with missing users
or invalid roles and ensures a valid subject is derived for permission
checks, improving reliability and security of access control responses.

* **Tests**
* Added comprehensive integration tests for authorization, covering
admin/viewer role behaviors, API key permissions, and various resource
actions to verify expected allow/deny outcomes across scenarios.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 12:50:31 -04:00
Eli Bosley
7bdeca8338 fix: enable auth guard for nested fields - thanks @ingel81 2025-09-26 11:12:17 -04:00
Eli Bosley
661865f976 feat: notification filter controls pill buttons (#1718)
## Summary
- replace the notification type dropdown with inline pill buttons for
quick filtering
- expose accessible role and pressed state on the new filter buttons

## Testing
- pnpm --filter @unraid/web lint

------
https://chatgpt.com/codex/tasks/task_e_68d184ad60348323b60c9b8e19146025

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

* **New Features**
* Notifications sidebar now uses a pill-style button group instead of a
dropdown for filtering by importance/type.
  * One-tap switching applies filters instantly for faster navigation.
* Active filters are more visible, improving clarity and accessibility.
* No changes to existing workflows or public behavior; settings and
filtering semantics remain unchanged.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 10:15:27 -04:00
Eli Bosley
b7afaf4632 feat: add Tailwind scoping plugin and integrate into Vite config (#1722)
- Introduced a new PostCSS plugin, `scopeTailwindToUnapi`, to scope
Tailwind CSS classes to specific elements.
- Updated Vite configuration to include the new PostCSS plugin for CSS
processing.
- Enhanced theme management in the theme store to apply scoped classes
and dynamic CSS variables to multiple targets, including the document
root and elements with the `.unapi` class.

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

* **New Features**
* Scoped styling for embedded (.unapi) contexts and a PostCSS plugin to
automate it.
* Theme refresh after mount to propagate CSS variables to embedded
roots.
  * Exposed idempotent restart action for the Unraid API when offline.

* **Bug Fixes**
* Consistent dark mode and theme variable application across main and
embedded views.
  * Interactive element and SSO styles now apply in embedded contexts.
* Simplified changelog iframe with a reliable fallback renderer;
improved logs styling scope.

* **Tests**
* New unit tests for the scoping plugin, changelog iframe, and related
components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-26 09:56:27 -04:00
github-actions[bot]
b3ca40c639 chore(main): release 4.24.1 (#1721)
🤖 I have created a release *beep* *boop*
---


## [4.24.1](https://github.com/unraid/api/compare/v4.24.0...v4.24.1)
(2025-09-23)


### Bug Fixes

* cleanup leftover removed packages on upgrade
([#1719](https://github.com/unraid/api/issues/1719))
([9972a5f](9972a5f178))
* enhance version comparison logic in installation script
([d9c561b](d9c561bfeb))
* issue with incorrect permissions on viewer / other roles
([378cdb7](378cdb7f10))

---
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-23 16:06:18 -04:00
Eli Bosley
378cdb7f10 fix: issue with incorrect permissions on viewer / other roles 2025-09-23 15:46:22 -04:00
Eli Bosley
d9c561bfeb fix: enhance version comparison logic in installation script
- Added normalization for version strings to improve semantic versioning comparisons.
- Updated the version comparison function to handle leading 'v' and ignore build metadata, ensuring accurate version checks during installation.
2025-09-23 11:40:13 -04:00
Eli Bosley
9972a5f178 fix: cleanup leftover removed packages on upgrade (#1719)
---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1211428391025524

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

* **New Features**
* Adds API version awareness for Unraid Connect: detects server vs.
connector API versions, notifies users, and skips installation to avoid
downgrades.

* **Bug Fixes**
* Enhanced pre-install cleanup removing stale files and leftovers to
improve install/upgrade reliability and clearer status reporting.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 11:35:06 -04:00
Eli Bosley
a44473c1d1 chore(api): update API version and enhance installation script (#1685)
- Enhanced the installation script in `dynamix.unraid.net.plg` to
include version comparison logic, preventing downgrades if a newer API
version is already installed.
- Added functionality to notify users of version conflicts during
installation.

This update improves the robustness of the installation process and
ensures compatibility with existing API versions.

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

* **New Features**
* Version-aware installation for the Connect API to prevent downgrades
when the server API is newer.
  * Web GUI notification when a version conflict is detected.

* **Improvements**
* Clearer install messaging when API installation is skipped or
proceeds.
* Safer, guarded install flow that only performs cleanup and
installation when appropriate.
  * Preserves existing behavior for README updates when applicable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-23 11:21:25 -04:00
github-actions[bot]
ed9a5c5ff9 chore(main): release 4.24.0 (#1717)
🤖 I have created a release *beep* *boop*
---


## [4.24.0](https://github.com/unraid/api/compare/v4.23.1...v4.24.0)
(2025-09-18)


### Features

* improve dom content loading by being more efficient about component
mounting ([#1716](https://github.com/unraid/api/issues/1716))
([d8b166e](d8b166e4b6))

---
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-18 12:59:58 -04:00
Eli Bosley
d8b166e4b6 feat: improve dom content loading by being more efficient about component mounting (#1716)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
  - Faster, more scalable component auto-mounting via batch processing.
- More robust prop parsing (handles JSON vs. strings and HTML entities).
  - Improved locale data initialization during setup.

- Bug Fixes
- Prevents duplicate mounts and improves handling of empty/irrelevant
attributes.

- Refactor
  - Consolidated mounting flow and removed legacy runtime debug globals.

- Tests
  - Removed outdated tests tied to previous global exposures.

- Chores
- Updated type declarations; global client is now optional for improved
flexibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-18 12:50:24 -04:00
github-actions[bot]
8b862ecef5 chore(main): release 4.23.1 (#1715)
🤖 I have created a release *beep* *boop*
---


## [4.23.1](https://github.com/unraid/api/compare/v4.23.0...v4.23.1)
(2025-09-17)


### Bug Fixes

* cleanup ini parser logic with better fallbacks
([#1713](https://github.com/unraid/api/issues/1713))
([1691362](16913627de))

---
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-17 14:46:58 -04:00
Eli Bosley
16913627de fix: cleanup ini parser logic with better fallbacks (#1713)
Added a new parser for INI boolean values, including functions to
convert INI boolean strings to JavaScript booleans and handle malformed
inputs. Introduced unit tests to validate the functionality of both
`iniBooleanToJsBoolean` and `iniBooleanOrAutoToJsBoolean`, covering
various valid, malformed, and edge case scenarios. Updated state parsers
to utilize the new functions for improved reliability.
2025-09-17 13:59:57 -04:00
Eli Bosley
6b2f331941 chore: remove deprecated API documentation files and workflow (#1714)
This commit deletes the outdated API documentation files and the
associated GitHub Actions workflow for updating the API documentation.
All content has been permanently moved to the Unraid Docs repository.
2025-09-17 13:23:42 -04:00
renovate[bot]
8f02d96464 chore(deps): update dependency @types/uuid to v11 (#1711)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Change | Age | Confidence |
|---|---|---|---|
|
[@types/uuid](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/uuid)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid))
| [`10.0.0` ->
`11.0.0`](https://renovatebot.com/diffs/npm/@types%2fuuid/10.0.0/11.0.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fuuid/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fuuid/10.0.0/11.0.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### 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-17 09:41:08 -04:00
Eli Bosley
caff5a78ba chore: fix webhook for release 2025-09-16 13:57:44 -04:00
github-actions[bot]
810be7a679 chore(main): release 4.23.0 (#1707)
🤖 I have created a release *beep* *boop*
---


## [4.23.0](https://github.com/unraid/api/compare/v4.22.2...v4.23.0)
(2025-09-16)


### Features

* add unraid api status manager
([#1708](https://github.com/unraid/api/issues/1708))
([1d9ce0a](1d9ce0aa3d))


### Bug Fixes

* **logging:** remove colorized logs
([#1705](https://github.com/unraid/api/issues/1705))
([1d2c670](1d2c6701ce))
* no sizeRootFs unless queried
([#1710](https://github.com/unraid/api/issues/1710))
([9714b21](9714b21c5c))
* use virtual-modal-container
([#1709](https://github.com/unraid/api/issues/1709))
([44b4d77](44b4d77d80))

---
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-16 13:18:09 -04:00
Eli Bosley
1d9ce0aa3d feat: add unraid api status manager (#1708)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Added “Unraid API Status” page under Management Access to view current
API status, refresh it, and restart the API with confirmation.
- Status view shows running state, detailed output, and in-app
success/error messages after actions.

- Style
- Minor theme adjustments to border colors; no layout changes expected.

- Chores
  - Updated UI text: “Unraid API” → “Unraid API Settings” in settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 13:04:01 -04:00
Eli Bosley
9714b21c5c fix: no sizeRootFs unless queried (#1710)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
  - sizeRootFs now reported in bytes as BigInt.
- Container listings include size only when requested; caching
distinguishes size vs no-size.
- New Docker-related fields: per-container update statuses,
rebuild/update indicators, spinning state, and a mutation to refresh
docker digests.
- **Tests**
- Added unit tests for GraphQL field inspection and container size/cache
behavior.
- **Chores**
  - Version bumped to 4.22.2.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 12:01:49 -04:00
Eli Bosley
44b4d77d80 fix: use virtual-modal-container (#1709)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- Ensured modals consistently render by using a dedicated container,
reducing cases where dialogs failed to open or appeared in the wrong
place.
- Improved reliability of modal mounting during page load and
navigation.

- Refactor
- Simplified the modal mounting mechanism to improve stability and
reduce reliance on DOM structure assumptions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-15 16:04:03 -04:00
renovate[bot]
3f5039c342 chore(deps): pin dependency node to 22.19.0 (#1706)
Coming soon: The Renovate bot (GitHub App) will be renamed to Mend. PRs
from Renovate will soon appear from 'Mend'. Learn more
[here](https://redirect.github.com/renovatebot/renovate/discussions/37842).

This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [node](https://redirect.github.com/actions/node-versions) | uses-with
| pin | `22` -> `22.19.0` |

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### 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 is behind base branch, 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-15 14:37:24 -04:00
Eli Bosley
1d2c6701ce fix(logging): remove colorized logs (#1705)
Move to simplified logging for PM2 (no more tables)
2025-09-15 13:34:07 -04:00
Eli Bosley
0ee09aefbb chore: prettier webhooks 2025-09-15 13:00:08 -04:00
Eli Bosley
c60a51dc1b chore: remove pnpm setup step from release workflow 2025-09-15 12:57:32 -04:00
Eli Bosley
c4fbf698b4 chore: specify node version in release workflow 2025-09-15 12:55:04 -04:00
Eli Bosley
00faa8f9d9 chore: update pnpm action to use the latest version 2025-09-15 12:52:19 -04:00
github-actions[bot]
45d9d65c13 chore(main): release 4.22.2 (#1699)
🤖 I have created a release *beep* *boop*
---


## [4.22.2](https://github.com/unraid/api/compare/v4.22.1...v4.22.2)
(2025-09-15)


### Bug Fixes

* **deps:** pin dependency conventional-changelog-conventionalcommits to
9.1.0 ([#1697](https://github.com/unraid/api/issues/1697))
([9a86c61](9a86c615da))
* **deps:** update dependency filenamify to v7
([#1703](https://github.com/unraid/api/issues/1703))
([b80988a](b80988aaab))
* **deps:** update graphqlcodegenerator monorepo (major)
([#1689](https://github.com/unraid/api/issues/1689))
([ba4a43a](ba4a43aec8))
* false positive on verify_install script being external shell
([#1704](https://github.com/unraid/api/issues/1704))
([31a255c](31a255c928))
* improve vue mount speed by 10x
([c855caa](c855caa9b2))

---
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-15 12:26:56 -04:00
Eli Bosley
771014b005 chore: fix timezone on pushed PRs to actually redirect correctly 2025-09-15 12:23:11 -04:00
Eli Bosley
31a255c928 fix: false positive on verify_install script being external shell (#1704)
- Introduced a new test script for shell detection logic in
`verify_install.sh`.
- Updated the `package.json` to include a new test command for shell
detection.
- Enhanced the `verify_install.sh` script to accurately check the
current shell interpreter using `/proc` and fallback methods.

This improves the testing framework and ensures better validation of
shell detection functionality.

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

- Bug Fixes
- Installer now detects the actual interpreter more robustly and
strictly requires Bash; non-Bash environments produce clear error
messages and abort installation.
- Tests
- Added automated shell-detection tests to validate behavior across
environments.
- Test suite expanded to run the new shell-detection checks as part of
the standard test command.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-15 12:17:50 -04:00
Eli Bosley
167857a323 chore: fix dev environment 2025-09-15 11:38:04 -04:00
renovate[bot]
b80988aaab fix(deps): update dependency filenamify to v7 (#1703)
This PR contains the following updates:

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

---

### Release Notes

<details>
<summary>sindresorhus/filenamify (filenamify)</summary>

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

[Compare
Source](https://redirect.github.com/sindresorhus/filenamify/compare/v6.0.0...v7.0.0)

##### Breaking

- Require Node.js 20
[`cc39041`](https://redirect.github.com/sindresorhus/filenamify/commit/cc39041)
- Fix Unicode normalization
[`ea8b961`](https://redirect.github.com/sindresorhus/filenamify/commit/ea8b961)
- Previously, it used the incorrect `NFD` Unicode normalization. It now
uses the correct `NFC` normalization. This could be a breaking change if
you stored the filenames and then compare, as they won't match. You
could run a `NFC` normalization on the stored once to fix that.

##### Improvements

- Use grapheme-safe truncation
[`98169ce`](https://redirect.github.com/sindresorhus/filenamify/commit/98169ce)
- Fix trailing spaces and periods handling
[`43eea4d`](https://redirect.github.com/sindresorhus/filenamify/commit/43eea4d)

***

</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-15 11:24:31 -04:00
Eli Bosley
fe4a6451f1 chore: update pnpm lock 2025-09-15 11:24:31 -04:00
renovate[bot]
9a86c615da fix(deps): pin dependency conventional-changelog-conventionalcommits to 9.1.0 (#1697)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[conventional-changelog-conventionalcommits](https://redirect.github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-conventionalcommits#readme)
([source](https://redirect.github.com/conventional-changelog/conventional-changelog/tree/HEAD/packages/conventional-changelog-conventionalcommits))
| dependencies | pin | [`^9.1.0` ->
`9.1.0`](https://renovatebot.com/diffs/npm/conventional-changelog-conventionalcommits/9.1.0/9.1.0)
|

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### 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 is behind base branch, 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-15 11:24:31 -04:00
renovate[bot]
25ff8992a5 chore(deps): update dependency type-fest to v5 (#1701)
This PR contains the following updates:

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

---

### Release Notes

<details>
<summary>sindresorhus/type-fest (type-fest)</summary>

###
[`v5.0.0`](https://redirect.github.com/sindresorhus/type-fest/releases/tag/v5.0.0)

[Compare
Source](https://redirect.github.com/sindresorhus/type-fest/compare/v4.41.0...v5.0.0)

##### Breaking

- This package is now pure ESM. **Please [read
this](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c).**
- Require TypeScript 5.9
[`b5b0214`](https://redirect.github.com/sindresorhus/type-fest/commit/b5b0214)
- Require Node.js 20
[`cc2b0f2`](https://redirect.github.com/sindresorhus/type-fest/commit/cc2b0f2)
- Reminder: `type-fest` requires `strict: true` in your tsconfig.
- `StringKeyOf`: Rename to
[`KeyAsString`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/key-as-string.d.ts)
[`e492c9c`](https://redirect.github.com/sindresorhus/type-fest/commit/e492c9c)
-
[`ArrayTail`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-tail.d.ts):
Enable
[`preserveReadonly`](68469722a9/source/array-tail.d.ts (L8-L30))
by default and remove the option
[`b34b1d8`](https://redirect.github.com/sindresorhus/type-fest/commit/b34b1d8)
-
[`CamelCase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-case.d.ts)
/
[`CamelCasedProperties`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-cased-properties.d.ts)
/
[`CamelCasedPropertiesDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/camel-cased-properties-deep.d.ts)
/
[`PascalCase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-case.d.ts)
/
[`PascalCasedProperties`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-cased-properties.d.ts)
/
[`PascalCasedPropertiesDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/pascal-cased-properties-deep.d.ts):
Disable `preserveConsecutiveUppercase` by default
[`8226c1b`](https://redirect.github.com/sindresorhus/type-fest/commit/8226c1b)
  - This aligns it with the general JavaScript naming convention.
-
[`PartialDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/partial-deep.d.ts):
Disable `allowUndefinedInNonTupleArrays` by default
[`b3c4524`](https://redirect.github.com/sindresorhus/type-fest/commit/b3c4524)
-
[`Split`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/split.d.ts):
Enable `strictLiteralChecks` by default
[`544a846`](https://redirect.github.com/sindresorhus/type-fest/commit/544a846)
-
[`Paths`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/paths.d.ts):
Default `maxRecursionDepth` 5 (was 10)
[`2ab5dec`](https://redirect.github.com/sindresorhus/type-fest/commit/2ab5dec)
-
[`ObservableLike`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/globals/observable-like.d.ts):
Move to sub-export
[`2a1072e`](https://redirect.github.com/sindresorhus/type-fest/commit/2a1072e)
- Deprecate `If*` types in favor of a single
[`If`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/if.d.ts)
[`4c2151a`](https://redirect.github.com/sindresorhus/type-fest/commit/4c2151a)

##### New types

-
[`Alphanumeric`](fc14f87e7f/source/characters.d.ts)
— Single alphanumeric character (`A–Z`, `a–z`, `0–9`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`AllExtend`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/all-extend.d.ts)
— Evaluates to `true` if every element of a tuple/array extends `U`.
[`c8c6d55`](https://redirect.github.com/sindresorhus/type-fest/commit/c8c6d55)
-
[`ConditionalSimplify`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-simplify.d.ts)
— Simplifies a type’s unions/intersections with opt-in controls.
[`b7a4771`](https://redirect.github.com/sindresorhus/type-fest/commit/b7a4771)
-
[`ConditionalSimplifyDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-simplify-deep.d.ts)
— Deep version of `ConditionalSimplify` that recurses into objects.
[`b7a4771`](https://redirect.github.com/sindresorhus/type-fest/commit/b7a4771)
-
[`DigitCharacter`](fc14f87e7f/source/characters.d.ts)
— Single ASCII digit character (`0–9`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`ExcludeStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/exclude-strict.d.ts)
— Non-distributive, stricter variant of `Exclude<T, U>`.
[`e6f62a2`](https://redirect.github.com/sindresorhus/type-fest/commit/e6f62a2)
-
[`ExtendsStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/extends-strict.d.ts)
— Non-distributive `A extends B` check.
[`d71242a`](https://redirect.github.com/sindresorhus/type-fest/commit/d71242a)
-
[`ExtractStrict`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/extract-strict.d.ts)
— Non-distributive, stricter variant of `Extract<T, U>`.
[`98d24fa`](https://redirect.github.com/sindresorhus/type-fest/commit/98d24fa)
-
[`IsLowercase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-lowercase.d.ts)
— Evaluates to `true` if a string literal is all lowercase.
[`afe132c`](https://redirect.github.com/sindresorhus/type-fest/commit/afe132c)
-
[`IsNullable`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-nullable.d.ts)
— Evaluates to `true` if `T` includes `null`.
[`5067e25`](https://redirect.github.com/sindresorhus/type-fest/commit/5067e25)
-
[`IsOptional`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-optional.d.ts)
— Evaluates to `true` if `T` includes `undefined`.
[`5067e25`](https://redirect.github.com/sindresorhus/type-fest/commit/5067e25)
-
[`IsOptionalKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-optional-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is optional.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsReadonlyKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-readonly-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is readonly.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsRequiredKeyOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-required-key-of.d.ts)
— Evaluates to `true` if property `K` of `T` is required.
[`93728b5`](https://redirect.github.com/sindresorhus/type-fest/commit/93728b5)
-
[`IsUnion`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-union.d.ts)
— Evaluates to `true` if `T` is a union type.
[`b3d92ed`](https://redirect.github.com/sindresorhus/type-fest/commit/b3d92ed)
-
[`IsUndefined`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-undefined.d.ts)
— Evaluates to `true` if the type is exactly `undefined`.
[`f7bc576`](https://redirect.github.com/sindresorhus/type-fest/commit/f7bc576)
-
[`IsUppercase`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-uppercase.d.ts)
— Evaluates to `true` if a string literal is all uppercase.
[`afe132c`](https://redirect.github.com/sindresorhus/type-fest/commit/afe132c)
-
[`LowercaseLetter`](fc14f87e7f/source/characters.d.ts)
— Single lowercase Latin letter (`a–z`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)
-
[`RemovePrefix`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/remove-prefix.d.ts)
— Removes a specified prefix from a string literal.
[`18a1c04`](https://redirect.github.com/sindresorhus/type-fest/commit/18a1c04)
-
[`UppercaseLetter`](fc14f87e7f/source/characters.d.ts)
— Single uppercase Latin letter (`A–Z`).
[`484e030`](https://redirect.github.com/sindresorhus/type-fest/commit/484e030)

##### Improvements

-
[`Jsonify`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/jsonify.d.ts):
Handle `unknown` as `JsonValue`
[`642bb13`](https://redirect.github.com/sindresorhus/type-fest/commit/642bb13)
-
[`SetRequired`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-required.d.ts)
/
[`SetOptional`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-optional.d.ts)
/
[`SetReadonly`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/set-readonly.d.ts):
Handle functions with properties
[`a5e45d4`](https://redirect.github.com/sindresorhus/type-fest/commit/a5e45d4)
-
[`Schema`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/schema.d.ts):
Preserve arrays/remove extraneous unions
[`8a96def`](https://redirect.github.com/sindresorhus/type-fest/commit/8a96def);
drop `undefined` for `recurseIntoArrays`
[`1cb955b`](https://redirect.github.com/sindresorhus/type-fest/commit/1cb955b)
-
[`ReadonlyKeysOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/readonly-keys-of.d.ts)
/
[`WritableKeysOf`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/writable-keys-of.d.ts):
Add `object` constraint
[`a6efbe0`](https://redirect.github.com/sindresorhus/type-fest/commit/a6efbe0)
-
[`TsConfigJson`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/tsconfig-json.d.ts):
Add TypeScript 5.9 fields
[`d2bda94`](https://redirect.github.com/sindresorhus/type-fest/commit/d2bda94)

##### Fixes

-
[`Or`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/or.d.ts):
Fix with `boolean`, `never`, `any`
[`42d6106`](https://redirect.github.com/sindresorhus/type-fest/commit/42d6106)
-
[`And`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/and.d.ts):
Fix with `boolean`, `never`, `any`
[`b38ac60`](https://redirect.github.com/sindresorhus/type-fest/commit/b38ac60)
-
[`IsStringLiteral`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/is-literal.d.ts):
Fix uncollapsed unions, and tagged types
[`eb37799`](https://redirect.github.com/sindresorhus/type-fest/commit/eb37799)
/
[`d1b35c7`](https://redirect.github.com/sindresorhus/type-fest/commit/d1b35c7)
-
[`Paths`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/paths.d.ts):
Fix behavior with index signatures
[`9926e5d`](https://redirect.github.com/sindresorhus/type-fest/commit/9926e5d)
-
[`ConditionalKeys`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/conditional-keys.d.ts):
Fix behavior with arrays and unions
[`4d7cc50`](https://redirect.github.com/sindresorhus/type-fest/commit/4d7cc50)
-
[`RequiredDeep`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/required-deep.d.ts):
Fix with `undefined`
[`bfcdbc4`](https://redirect.github.com/sindresorhus/type-fest/commit/bfcdbc4)
-
[`Split`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/split.d.ts):
Fix template strings ending with interpolation
[`853b881`](https://redirect.github.com/sindresorhus/type-fest/commit/853b881)
-
[`ArrayTail`](https://redirect.github.com/sindresorhus/type-fest/blob/main/source/array-tail.d.ts):
Fix fix non-tuple arrays
[`f3aabd8`](https://redirect.github.com/sindresorhus/type-fest/commit/f3aabd8)
- Fix `UnionMin` and `UnionMax`
[`d52d5e7`](https://redirect.github.com/sindresorhus/type-fest/commit/d52d5e7)

##### Meta

Huge thanks to all the contributors to this release, especially
[@&#8203;som-sm](https://redirect.github.com/som-sm) 🙌

***

</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-15 11:24:31 -04:00
renovate[bot]
45fb53d040 chore(deps): update actions/setup-node action to v5 (#1680)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [actions/setup-node](https://redirect.github.com/actions/setup-node) |
action | major | `v4` -> `v5` |

---

### Release Notes

<details>
<summary>actions/setup-node (actions/setup-node)</summary>

###
[`v5`](https://redirect.github.com/actions/setup-node/compare/v4...v5)

[Compare
Source](https://redirect.github.com/actions/setup-node/compare/v4...v5)

</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>
Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-09-15 11:24:31 -04:00
Eli Bosley
c855caa9b2 fix: improve vue mount speed by 10x
- Refactored teleport container management to be lazily created,
improving performance by avoiding unnecessary DOM manipulations.
- Updated `useTeleport` to dynamically determine the correct teleport
target based on mounted components.
- Removed the `ensureTeleportContainer` import from various components,
streamlining the mounting process.
- Adjusted the dropdown menu component to utilize a computed property
for teleport target management.
- Enhanced the component registry to support a unified app architecture,
replacing legacy mounting functions with a more efficient approach.

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

* **New Features**
  * Support bundles may include the GraphQL API log when present.
  * OS version data loads lazily when the header dropdown first opens.
* Many UI components now load on demand via a unified mounting approach.

* **Bug Fixes**
  * Dropdowns and modals consistently stack above other UI elements.
  * Server status layout fixes improve small-screen alignment.

* **Breaking Changes**
* Teleport/mounting APIs and public teleport helper were
consolidated/removed; integrations may need update.

* **Tests**
* Extensive new unit tests added for mounting, teleport, modals, and
REST log handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-15 11:24:31 -04:00
renovate[bot]
ba4a43aec8 fix(deps): update graphqlcodegenerator monorepo (major) (#1689) 2025-09-15 11:24:24 -04:00
github-actions[bot]
c4ca761dfc chore(main): release 4.22.1 (#1698)
🤖 I have created a release *beep* *boop*
---


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


### Bug Fixes

* set input color in SSO field rather than inside of the main.css
([01d353f](01d353fa08))

---
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 15:41:12 -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
github-actions[bot]
88a924c84f chore(main): release 4.21.0 (#1684)
🤖 I have created a release *beep* *boop*
---


## [4.21.0](https://github.com/unraid/api/compare/v4.20.4...v4.21.0)
(2025-09-10)


### Features

* add zsh shell detection to install script
([#1539](https://github.com/unraid/api/issues/1539))
([50ea2a3](50ea2a3ffb))
* **api:** determine if docker container has update
([#1582](https://github.com/unraid/api/issues/1582))
([e57d81e](e57d81e073))


### Bug Fixes

* white on white login text
([ae4d3ec](ae4d3ecbc4))

---
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-10 15:14:10 -04:00
Eli Bosley
ae4d3ecbc4 fix: white on white login text 2025-09-10 14:59:10 -04:00
Eli Bosley
c569043ab5 chore: rclone initialization version check (#1683)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Enforces a minimum RClone version (1.70.0) with clearer startup/log
messages for missing, too-old, or unparseable versions.
* Adjusted initialization timing to a later bootstrap phase for more
reliable startup.

* **Tests**
* Expanded and hardened tests: broader API endpoint coverage, enhanced
HTTP error scenarios, refined request assertions, and comprehensive
RClone version-detection tests (newer/older, missing, malformed,
beta/RC).

* **Chores**
* Simplified permissions configuration by replacing detailed rules with
an empty permissions object and removing a top-level flag.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 11:00:59 -04:00
Eli Bosley
50ea2a3ffb feat: add zsh shell detection to install script (#1539)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Installer now detects when the environment is using Zsh and halts with
clear error messages and guidance so users can move Zsh configuration to
interactive-only files.

* **Bug Fixes**
* Prevents running the installer under unsupported shell setups,
improving installation reliability and avoiding misconfigured runs.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-09-10 11:00:41 -04:00
Eli Bosley
b518131406 chore(docs): improve build:watch mode to be easier to use 2025-09-10 10:27:05 -04:00
Pujit Mehrotra
e57d81e073 feat(api): determine if docker container has update (#1582)
- Add a new utility class, `AsyncMutex` in `unraid-shared ->
processing.ts`, for ergonomically de-duplicating async operations.
- Add an `@OmitIf` decorator for omitting graphql queries, mutations, or
field resolvers from the runtime graphql schema.
- Add feature-flagging system
  - `FeatureFlags` export from `consts.ts`
  - `@UseFeatureFlag` decorator built upon `OmitIf`
- `checkFeatureFlag` for constructing & throwing a `ForbiddenError` if
the given feature flag evaluates to `false`.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Expose disk spinning state, per-container "update available" and
"rebuild ready" indicators, a structured per-container update-status
list, and a mutation to refresh Docker digests. Periodic and
post-startup digest refreshes added (feature-flag gated).

* **Chores**
  * Cron scheduling refactor and scheduler centralization.
  * Build now bundles a PHP wrapper asset.
  * Added feature-flag env var and .gitignore entry for local keys.

* **Documentation**
  * Added developer guide for feature flags.

* **Tests**
  * New concurrency, parser, decorator, config, and mutex test suites.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-09 16:25:32 -04:00
Eli Bosley
88baddd6c0 chore: add previous build cleanup scripts (#1682)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Chores
- Added automated cleanup of preview builds older than seven days to
reduce storage usage; runs on non-release pushes and won’t fail the
build if cleanup issues occur.
- Introduced a tool to optionally remove all pull request preview builds
with confirmation and clear summaries.
- Updated CI behavior to cancel in-progress runs only for pull requests;
pushes and releases are no longer auto-canceled.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-09 12:24:52 -04:00
github-actions[bot]
abc22bdb87 chore(main): release 4.20.4 (#1681)
🤖 I have created a release *beep* *boop*
---


## [4.20.4](https://github.com/unraid/api/compare/v4.20.3...v4.20.4)
(2025-09-09)


### Bug Fixes

* staging PR plugin fixes + UI issues on 7.2 beta
([b79b44e](b79b44e95c))

---
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-09 10:53:45 -04:00
Eli Bosley
6ed2f5ce8e chore: add comment when PR is merged 2025-09-09 10:42:57 -04:00
Eli Bosley
b79b44e95c fix: staging PR plugin fixes + UI issues on 7.2 beta 2025-09-09 10:39:48 -04:00
Eli Bosley
ca22285a26 chore: fix invalid user profile test (#1678)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* New Features
  * No user-facing changes in this release.
* Chores
* Streamlined release automation to run after successful build and test
stages on main, improving reliability of release tagging and downstream
usage.
  * Simplified job dependencies for related build pipelines.
* Tests
* Updated User Profile tests to align with revised DOM structure for the
description area; assertions unchanged and no functional impact for
users.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-09 10:29:47 -04:00
github-actions[bot]
838be2c52e chore(main): release 4.20.3 (#1677)
🤖 I have created a release *beep* *boop*
---


## [4.20.3](https://github.com/unraid/api/compare/v4.20.2...v4.20.3)
(2025-09-09)


### Bug Fixes

* header background color issues fixed on 7.2 - thanks Nick!
([73c1100](73c1100d0b))

---
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-09 09:31:14 -04:00
Eli Bosley
73c1100d0b fix: header background color issues fixed on 7.2 - thanks Nick! 2025-09-09 09:29:37 -04:00
github-actions[bot]
434e331384 chore(main): release 4.20.2 (#1676)
🤖 I have created a release *beep* *boop*
---


## [4.20.2](https://github.com/unraid/api/compare/v4.20.1...v4.20.2)
(2025-09-09)


### Bug Fixes

* trigger deployment
([a27453f](a27453fda8))

---
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-09 08:46:54 -04:00
Eli Bosley
a27453fda8 fix: trigger deployment 2025-09-09 08:45:02 -04:00
renovate[bot]
98e6058cd8 chore(deps): update actions/github-script action to v8 (#1671)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/github-script](https://redirect.github.com/actions/github-script)
| action | major | `v7` -> `v8` |

---

### Release Notes

<details>
<summary>actions/github-script (actions/github-script)</summary>

###
[`v8`](https://redirect.github.com/actions/github-script/compare/v7...v8)

[Compare
Source](https://redirect.github.com/actions/github-script/compare/v7...v8)

</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-09 08:30:02 -04:00
github-actions[bot]
6c2c51ae1d chore(main): release 4.20.1 (#1674)
🤖 I have created a release *beep* *boop*
---


## [4.20.1](https://github.com/unraid/api/compare/v4.20.0...v4.20.1)
(2025-09-09)


### Bug Fixes

* adjust header styles to fix flashing and width issues - thanks ZarZ
([4759b3d](4759b3d0b3))

---
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-09 08:28:56 -04:00
Eli Bosley
d10c12035e chore: Revert "fix(deps): update all non-major dependencies" (#1675)
Reverts unraid/api#1633
2025-09-09 08:25:30 -04:00
renovate[bot]
5dd6f42550 fix(deps): update all non-major dependencies (#1633)
This PR contains the following updates:

| Package | Change | Age | Confidence | Type | Update |
|---|---|---|---|---|---|
| [@eslint/js](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint/tree/HEAD/packages/js))
| [`9.34.0` ->
`9.35.0`](https://renovatebot.com/diffs/npm/@eslint%2fjs/9.34.0/9.35.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@eslint%2fjs/9.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@eslint%2fjs/9.34.0/9.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@graphql-codegen/typescript-resolvers](https://redirect.github.com/dotansimha/graphql-code-generator)
([source](https://redirect.github.com/dotansimha/graphql-code-generator/tree/HEAD/packages/plugins/typescript/resolvers))
| [`4.5.1` ->
`4.5.2`](https://renovatebot.com/diffs/npm/@graphql-codegen%2ftypescript-resolvers/4.5.1/4.5.2)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@graphql-codegen%2ftypescript-resolvers/4.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@graphql-codegen%2ftypescript-resolvers/4.5.1/4.5.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@ianvs/prettier-plugin-sort-imports](https://redirect.github.com/ianvs/prettier-plugin-sort-imports)
| [`4.6.3` ->
`4.7.0`](https://renovatebot.com/diffs/npm/@ianvs%2fprettier-plugin-sort-imports/4.6.3/4.7.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@ianvs%2fprettier-plugin-sort-imports/4.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@ianvs%2fprettier-plugin-sort-imports/4.6.3/4.7.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@manypkg/cli](https://redirect.github.com/Thinkmill/manypkg)
([source](https://redirect.github.com/Thinkmill/manypkg/tree/HEAD/packages/cli))
| [`0.25.0` ->
`0.25.1`](https://renovatebot.com/diffs/npm/@manypkg%2fcli/0.25.0/0.25.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@manypkg%2fcli/0.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@manypkg%2fcli/0.25.0/0.25.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@nuxt/ui](https://ui.nuxt.com)
([source](https://redirect.github.com/nuxt/ui)) | [`4.0.0-alpha.0` ->
`4.0.0-alpha.1`](https://renovatebot.com/diffs/npm/@nuxt%2fui/4.0.0-alpha.0/4.0.0-alpha.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@nuxt%2fui/4.0.0-alpha.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@nuxt%2fui/4.0.0-alpha.0/4.0.0-alpha.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@reduxjs/toolkit](https://redux-toolkit.js.org)
([source](https://redirect.github.com/reduxjs/redux-toolkit)) | [`2.8.2`
->
`2.9.0`](https://renovatebot.com/diffs/npm/@reduxjs%2ftoolkit/2.8.2/2.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@reduxjs%2ftoolkit/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@reduxjs%2ftoolkit/2.8.2/2.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@rollup/rollup-linux-x64-gnu](https://rollupjs.org/)
([source](https://redirect.github.com/rollup/rollup)) | [`4.49.0` ->
`4.50.1`](https://renovatebot.com/diffs/npm/@rollup%2frollup-linux-x64-gnu/4.49.0/4.50.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@rollup%2frollup-linux-x64-gnu/4.50.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@rollup%2frollup-linux-x64-gnu/4.49.0/4.50.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| optionalDependencies | minor |
|
[@storybook/addon-docs](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/docs)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/docs))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/@storybook%2faddon-docs/9.1.3/9.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-docs/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-docs/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/addon-links](https://redirect.github.com/storybookjs/storybook/tree/next/code/addons/links)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/addons/links))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/@storybook%2faddon-links/9.1.3/9.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2faddon-links/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2faddon-links/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/builder-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/builders/builder-vite/#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/builders/builder-vite))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/@storybook%2fbuilder-vite/9.1.3/9.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fbuilder-vite/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fbuilder-vite/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@storybook/vue3-vite](https://redirect.github.com/storybookjs/storybook/tree/next/code/frameworks/vue3-vite)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/frameworks/vue3-vite))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/@storybook%2fvue3-vite/9.1.3/9.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@storybook%2fvue3-vite/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@storybook%2fvue3-vite/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [@tailwindcss/cli](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-cli))
| [`4.1.12` ->
`4.1.13`](https://renovatebot.com/diffs/npm/@tailwindcss%2fcli/4.1.12/4.1.13)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@tailwindcss%2fcli/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tailwindcss%2fcli/4.1.12/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [@tailwindcss/vite](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite))
| [`4.1.12` ->
`4.1.13`](https://renovatebot.com/diffs/npm/@tailwindcss%2fvite/4.1.12/4.1.13)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@tailwindcss%2fvite/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@tailwindcss%2fvite/4.1.12/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/node](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node))
| [`22.18.0` ->
`22.18.1`](https://renovatebot.com/diffs/npm/@types%2fnode/22.18.0/22.18.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fnode/22.18.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fnode/22.18.0/22.18.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@types/semver](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/semver)
([source](https://redirect.github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver))
| [`7.7.0` ->
`7.7.1`](https://renovatebot.com/diffs/npm/@types%2fsemver/7.7.0/7.7.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@types%2fsemver/7.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@types%2fsemver/7.7.0/7.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[@typescript-eslint/eslint-plugin](https://typescript-eslint.io/packages/eslint-plugin)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin))
| [`8.41.0` ->
`8.43.0`](https://renovatebot.com/diffs/npm/@typescript-eslint%2feslint-plugin/8.41.0/8.43.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@typescript-eslint%2feslint-plugin/8.43.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@typescript-eslint%2feslint-plugin/8.41.0/8.43.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[@vueuse/components](https://redirect.github.com/vueuse/vueuse/tree/main/packages/components#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/components))
| [`13.8.0` ->
`13.9.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcomponents/13.8.0/13.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcomponents/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcomponents/13.8.0/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.8.0` ->
`13.9.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.8.0/13.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.8.0/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [@vueuse/core](https://redirect.github.com/vueuse/vueuse)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/core))
| [`13.8.0` ->
`13.9.0`](https://renovatebot.com/diffs/npm/@vueuse%2fcore/13.8.0/13.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fcore/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fcore/13.8.0/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
|
[@vueuse/integrations](https://redirect.github.com/vueuse/vueuse/tree/main/packages/integrations#readme)
([source](https://redirect.github.com/vueuse/vueuse/tree/HEAD/packages/integrations))
| [`13.8.0` ->
`13.9.0`](https://renovatebot.com/diffs/npm/@vueuse%2fintegrations/13.8.0/13.9.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vueuse%2fintegrations/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vueuse%2fintegrations/13.8.0/13.9.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [chalk](https://redirect.github.com/chalk/chalk) | [`5.6.0` ->
`5.6.2`](https://renovatebot.com/diffs/npm/chalk/5.6.0/5.6.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/chalk/5.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/chalk/5.6.0/5.6.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [dayjs](https://day.js.org)
([source](https://redirect.github.com/iamkun/dayjs)) | [`1.11.14` ->
`1.11.18`](https://renovatebot.com/diffs/npm/dayjs/1.11.14/1.11.18) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/dayjs/1.11.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dayjs/1.11.14/1.11.18?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [dockerode](https://redirect.github.com/apocas/dockerode) | [`4.0.7`
-> `4.0.8`](https://renovatebot.com/diffs/npm/dockerode/4.0.7/4.0.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/dockerode/4.0.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dockerode/4.0.7/4.0.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [dotenv](https://redirect.github.com/motdotla/dotenv) | [`17.2.1` ->
`17.2.2`](https://renovatebot.com/diffs/npm/dotenv/17.2.1/17.2.2) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/dotenv/17.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/dotenv/17.2.1/17.2.2?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [eslint](https://eslint.org)
([source](https://redirect.github.com/eslint/eslint)) | [`9.34.0` ->
`9.35.0`](https://renovatebot.com/diffs/npm/eslint/9.34.0/9.35.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint/9.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint/9.34.0/9.35.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[eslint-plugin-storybook](https://redirect.github.com/storybookjs/storybook/code/lib/eslint-plugin#readme)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/lib/eslint-plugin))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/eslint-plugin-storybook/9.1.3/9.1.5)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/eslint-plugin-storybook/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/eslint-plugin-storybook/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [fast-check](https://fast-check.dev/)
([source](https://redirect.github.com/dubzzz/fast-check/tree/HEAD/packages/fast-check))
| [`4.2.0` ->
`4.3.0`](https://renovatebot.com/diffs/npm/fast-check/4.2.0/4.3.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/fast-check/4.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fast-check/4.2.0/4.3.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [fastify](https://fastify.dev/)
([source](https://redirect.github.com/fastify/fastify)) | [`5.5.0` ->
`5.6.0`](https://renovatebot.com/diffs/npm/fastify/5.5.0/5.6.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/fastify/5.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/fastify/5.5.0/5.6.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [got](https://redirect.github.com/sindresorhus/got) | [`14.4.7` ->
`14.4.8`](https://renovatebot.com/diffs/npm/got/14.4.7/14.4.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/got/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/got/14.4.7/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
| [got](https://redirect.github.com/sindresorhus/got) | [`14.4.7` ->
`14.4.8`](https://renovatebot.com/diffs/npm/got/14.4.7/14.4.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/got/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/got/14.4.7/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [got](https://redirect.github.com/sindresorhus/got) | [`14.4.7` ->
`14.4.8`](https://renovatebot.com/diffs/npm/got/14.4.7/14.4.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/got/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/got/14.4.7/14.4.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.13` ->
`6.1.0`](https://renovatebot.com/diffs/npm/jose/6.0.13/6.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.13/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | minor |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.13` ->
`6.1.0`](https://renovatebot.com/diffs/npm/jose/6.0.13/6.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.13/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [jose](https://redirect.github.com/panva/jose) | [`6.0.13` ->
`6.1.0`](https://renovatebot.com/diffs/npm/jose/6.0.13/6.1.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/jose/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/jose/6.0.13/6.1.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [lint-staged](https://redirect.github.com/lint-staged/lint-staged) |
[`16.1.5` ->
`16.1.6`](https://renovatebot.com/diffs/npm/lint-staged/16.1.5/16.1.6) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/lint-staged/16.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lint-staged/16.1.5/16.1.6?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [lucide-vue-next](https://lucide.dev)
([source](https://redirect.github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-vue-next))
| [`0.542.0` ->
`0.543.0`](https://renovatebot.com/diffs/npm/lucide-vue-next/0.542.0/0.543.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/lucide-vue-next/0.543.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/lucide-vue-next/0.542.0/0.543.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [nest-commander](https://nest-commander.jaymcdoniel.dev)
([source](https://redirect.github.com/jmcdo29/nest-commander/tree/HEAD/pacakges/nest-commander))
| [`3.19.0` ->
`3.19.1`](https://renovatebot.com/diffs/npm/nest-commander/3.19.0/3.19.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/nest-commander/3.19.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/nest-commander/3.19.0/3.19.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [node](https://nodejs.org)
([source](https://redirect.github.com/nodejs/node)) | `22.18.0` ->
`22.19.0` |
[![age](https://developer.mend.io/api/mc/badges/age/node-version/node/v22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/node-version/node/v22.18.0/v22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| | minor |
| [node](https://redirect.github.com/actions/node-versions) | `22.18.0`
-> `22.19.0` |
[![age](https://developer.mend.io/api/mc/badges/age/github-releases/actions%2fnode-versions/22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/github-releases/actions%2fnode-versions/22.18.0/22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| uses-with | minor |
| [node](https://redirect.github.com/nodejs/node) |
`22.18.0-bookworm-slim` -> `22.19.0-bookworm-slim` |
[![age](https://developer.mend.io/api/mc/badges/age/docker/node/22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/docker/node/22.18.0/22.19.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| final | minor |
| [openid-client](https://redirect.github.com/panva/openid-client) |
[`6.6.4` ->
`6.7.1`](https://renovatebot.com/diffs/npm/openid-client/6.6.4/6.7.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/openid-client/6.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/openid-client/6.6.4/6.7.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | minor |
| [pino](https://getpino.io)
([source](https://redirect.github.com/pinojs/pino)) | [`9.9.0` ->
`9.9.4`](https://renovatebot.com/diffs/npm/pino/9.9.0/9.9.4) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pino/9.9.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pino/9.9.0/9.9.4?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [pm2](http://pm2.keymetrics.io/)
([source](https://redirect.github.com/Unitech/pm2)) | [`6.0.8` ->
`6.0.10`](https://renovatebot.com/diffs/npm/pm2/6.0.8/6.0.10) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pm2/6.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pm2/6.0.8/6.0.10?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`10.15.0` ->
`10.15.1`](https://renovatebot.com/diffs/npm/pnpm/10.15.0/10.15.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.15.0/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| packageManager | patch |
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
[`10.15.0` ->
`10.15.1`](https://renovatebot.com/diffs/npm/pnpm/10.15.0/10.15.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.15.0/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| engines | patch |
| [pnpm](https://pnpm.io)
([source](https://redirect.github.com/pnpm/pnpm/tree/HEAD/pnpm)) |
`10.15.0` -> `10.15.1` |
[![age](https://developer.mend.io/api/mc/badges/age/npm/pnpm/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/pnpm/10.15.0/10.15.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| uses-with | patch |
|
[rollup-plugin-node-externals](https://redirect.github.com/Septh/rollup-plugin-node-externals)
| [`8.1.0` ->
`8.1.1`](https://renovatebot.com/diffs/npm/rollup-plugin-node-externals/8.1.0/8.1.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/rollup-plugin-node-externals/8.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/rollup-plugin-node-externals/8.1.0/8.1.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [storybook](https://storybook.js.org)
([source](https://redirect.github.com/storybookjs/storybook/tree/HEAD/code/core))
| [`9.1.3` ->
`9.1.5`](https://renovatebot.com/diffs/npm/storybook/9.1.3/9.1.5) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/storybook/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/storybook/9.1.3/9.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [tailwindcss](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss))
| [`4.1.12` ->
`4.1.13`](https://renovatebot.com/diffs/npm/tailwindcss/4.1.12/4.1.13) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tailwindcss/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tailwindcss/4.1.12/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
| [tailwindcss](https://tailwindcss.com)
([source](https://redirect.github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss))
| [`4.1.12` ->
`4.1.13`](https://renovatebot.com/diffs/npm/tailwindcss/4.1.12/4.1.13) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tailwindcss/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tailwindcss/4.1.12/4.1.13?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
| [terser](https://terser.org)
([source](https://redirect.github.com/terser/terser)) | [`5.43.1` ->
`5.44.0`](https://renovatebot.com/diffs/npm/terser/5.43.1/5.44.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/terser/5.44.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/terser/5.43.1/5.44.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
|
[tw-animate-css](https://redirect.github.com/Wombosvideo/tw-animate-css)
| [`1.3.7` ->
`1.3.8`](https://renovatebot.com/diffs/npm/tw-animate-css/1.3.7/1.3.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tw-animate-css/1.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tw-animate-css/1.3.7/1.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[tw-animate-css](https://redirect.github.com/Wombosvideo/tw-animate-css)
| [`1.3.7` ->
`1.3.8`](https://renovatebot.com/diffs/npm/tw-animate-css/1.3.7/1.3.8) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/tw-animate-css/1.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/tw-animate-css/1.3.7/1.3.8?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
|
[typescript-eslint](https://typescript-eslint.io/packages/typescript-eslint)
([source](https://redirect.github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint))
| [`8.41.0` ->
`8.43.0`](https://renovatebot.com/diffs/npm/typescript-eslint/8.41.0/8.43.0)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/typescript-eslint/8.43.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/typescript-eslint/8.41.0/8.43.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |
| [vite](https://vite.dev)
([source](https://redirect.github.com/vitejs/vite/tree/HEAD/packages/vite))
| [`7.1.3` ->
`7.1.5`](https://renovatebot.com/diffs/npm/vite/7.1.3/7.1.5) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vite/7.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vite/7.1.3/7.1.5?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[vue](https://redirect.github.com/vuejs/core/tree/main/packages/vue#readme)
([source](https://redirect.github.com/vuejs/core)) | [`3.5.20` ->
`3.5.21`](https://renovatebot.com/diffs/npm/vue/3.5.20/3.5.21) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.20/3.5.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | patch |
|
[vue](https://redirect.github.com/vuejs/core/tree/main/packages/vue#readme)
([source](https://redirect.github.com/vuejs/core)) | [`3.5.20` ->
`3.5.21`](https://renovatebot.com/diffs/npm/vue/3.5.20/3.5.21) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue/3.5.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue/3.5.20/3.5.21?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| peerDependencies | patch |
|
[vue-i18n](https://redirect.github.com/intlify/vue-i18n/tree/master/packages/vue-i18n#readme)
([source](https://redirect.github.com/intlify/vue-i18n/tree/HEAD/packages/vue-i18n))
| [`11.1.11` ->
`11.1.12`](https://renovatebot.com/diffs/npm/vue-i18n/11.1.11/11.1.12) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vue-i18n/11.1.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vue-i18n/11.1.11/11.1.12?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [vuetify](https://vuetifyjs.com)
([source](https://redirect.github.com/vuetifyjs/vuetify/tree/HEAD/packages/vuetify))
| [`3.9.6` ->
`3.9.7`](https://renovatebot.com/diffs/npm/vuetify/3.9.6/3.9.7) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vuetify/3.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vuetify/3.9.6/3.9.7?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| dependencies | patch |
| [wrangler](https://redirect.github.com/cloudflare/workers-sdk)
([source](https://redirect.github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler))
| [`4.33.0` ->
`4.34.0`](https://renovatebot.com/diffs/npm/wrangler/4.33.0/4.34.0) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/wrangler/4.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/wrangler/4.33.0/4.34.0?slim=true)](https://docs.renovatebot.com/merge-confidence/)
| devDependencies | minor |

---

### Release Notes

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

###
[`v9.35.0`](https://redirect.github.com/eslint/eslint/compare/v9.34.0...af2a0870fdc646091d027516601888923e5bc202)

[Compare
Source](https://redirect.github.com/eslint/eslint/compare/v9.34.0...v9.35.0)

</details>

<details>
<summary>dotansimha/graphql-code-generator
(@&#8203;graphql-codegen/typescript-resolvers)</summary>

###
[`v4.5.2`](https://redirect.github.com/dotansimha/graphql-code-generator/blob/HEAD/packages/plugins/typescript/resolvers/CHANGELOG.md#452)

[Compare
Source](https://redirect.github.com/dotansimha/graphql-code-generator/compare/@graphql-codegen/typescript-resolvers@4.5.1...@graphql-codegen/typescript-resolvers@4.5.2)

##### Patch Changes

-
[#&#8203;10419](https://redirect.github.com/dotansimha/graphql-code-generator/pull/10419)
[`2fc3869`](2fc3869de2)
Thanks
[@&#8203;chdanielmueller](https://redirect.github.com/chdanielmueller)!
- Fix enum resolver for partially mapped enumValues

</details>

<details>
<summary>ianvs/prettier-plugin-sort-imports
(@&#8203;ianvs/prettier-plugin-sort-imports)</summary>

###
[`v4.7.0`](https://redirect.github.com/IanVS/prettier-plugin-sort-imports/releases/tag/v4.7.0)

[Compare
Source](https://redirect.github.com/ianvs/prettier-plugin-sort-imports/compare/v4.6.3...v4.7.0)

#### What's Changed

This project began as a fork because I wanted a plugin that would not
move side-effect imports around and mess with my CSS cascade. So its
first and most distinguishing feature is that side-effect imports do not
move, and other imports are not sorted across them.

This works fine in most cases, but some people have side-effect imports
that they know *can* be sorted safely. For those, there is now an
"escape hatch" option named `importOrderSafeSideEffects`. It is an array
of glob pattern strings (similar to `importOrder`) which, when they
match against a side-effect import, allow that import to be sorted as if
it were a standard import.

Suggestions for safe use:

- Use `^` at the start of your pattern and `$` at the end, to avoid
accidentally matching part of an import name. For example,
`"^server-only$"`, to avoid matching against `import "not-server-only"`.
- Use extreme caution if matching against relative files or CSS files.
If you decide to sort CSS imports and a file ever imports more than one
CSS file, your cascade may change.
- You can still use `// prettier-ignore` to stop sorting a particular
import that would otherwise be sorted.

Feedback on this feature is welcome.

##### Features

- Add `importOrderSafeSideEffects` option by
[@&#8203;IanVS](https://redirect.github.com/IanVS) in
[IanVS#240](https://redirect.github.com/IanVS/prettier-plugin-sort-imports/pull/240)

##### Internal

- Clean up options & remove explicit function types by
[@&#8203;IanVS](https://redirect.github.com/IanVS) in
[IanVS#239](https://redirect.github.com/IanVS/prettier-plugin-sort-imports/pull/239)

**Full Changelog**:
<https://github.com/IanVS/prettier-plugin-sort-imports/compare/v4.6.3...v4.7.0>

</details>

<details>
<summary>Thinkmill/manypkg (@&#8203;manypkg/cli)</summary>

###
[`v0.25.1`](https://redirect.github.com/Thinkmill/manypkg/blob/HEAD/packages/cli/CHANGELOG.md#0251)

[Compare
Source](https://redirect.github.com/Thinkmill/manypkg/compare/@manypkg/cli@0.25.0...@manypkg/cli@0.25.1)

##### Patch Changes

- [#&#8203;260](https://redirect.github.com/Thinkmill/manypkg/pull/260)
[`5854938`](585493847a)
Thanks [@&#8203;jasekiw](https://redirect.github.com/jasekiw)! - Keep
detected line endings flavor of `package.json` files on Windows when
updating those files

</details>

<details>
<summary>nuxt/ui (@&#8203;nuxt/ui)</summary>

###
[`v4.0.0-alpha.1`](https://redirect.github.com/nuxt/ui/blob/HEAD/CHANGELOG.md#400-alpha1-2025-09-01)

[Compare
Source](https://redirect.github.com/nuxt/ui/compare/v4.0.0-alpha.0...v4.0.0-alpha.1)

##### ⚠ BREAKING CHANGES

- **components:** rename `nullify` modifier to `nullable` and add
`optional`
([#&#8203;4838](https://redirect.github.com/nuxt/ui/issues/4838))
- **module:** update compatibility to nuxt 4
- **PageAccordion:** remove in favor of `Accordion`
([#&#8203;4734](https://redirect.github.com/nuxt/ui/issues/4734))
- **Marquee:** rename from `PageMarquee`
([#&#8203;4741](https://redirect.github.com/nuxt/ui/issues/4741))
- **FieldGroup:** rename from `ButtonGroup`
([#&#8203;4596](https://redirect.github.com/nuxt/ui/issues/4596))
- **components:** upgrade `ai-sdk` to v5
([#&#8203;4698](https://redirect.github.com/nuxt/ui/issues/4698))

##### Features

- **components:** rename `nullify` modifier to `nullable` and add
`optional`
([#&#8203;4838](https://redirect.github.com/nuxt/ui/issues/4838))
([83b0306](83b0306a30))
- **components:** upgrade `ai-sdk` to v5
([#&#8203;4698](https://redirect.github.com/nuxt/ui/issues/4698))
([de7822f](de7822f6a1))
- **FieldGroup:** rename from `ButtonGroup`
([#&#8203;4596](https://redirect.github.com/nuxt/ui/issues/4596))
([a0963eb](a0963eba82))
- **Icon:** allow passing a component instead of a name
([#&#8203;4766](https://redirect.github.com/nuxt/ui/issues/4766))
([61b603f](61b603fff4))
- import `@nuxt/ui-pro` components
([#&#8203;4675](https://redirect.github.com/nuxt/ui/issues/4675))
([5cb65cf](5cb65cfbd0))
- **Marquee:** rename from `PageMarquee`
([#&#8203;4741](https://redirect.github.com/nuxt/ui/issues/4741))
([b6edce2](b6edce2662))
- **module:** update compatibility to nuxt 4
([2aca598](2aca598792))
- **PageAccordion:** remove in favor of `Accordion`
([#&#8203;4734](https://redirect.github.com/nuxt/ui/issues/4734))
([f70a3ff](f70a3ff13f))

##### Bug Fixes

- **AuthForm:** use `error` from form field
([#&#8203;4738](https://redirect.github.com/nuxt/ui/issues/4738))
([00dfb6b](00dfb6b586))
- **BlogPost:** ensure date slot renders
([#&#8203;4743](https://redirect.github.com/nuxt/ui/issues/4743))
([4514880](4514880902))
- **ChangelogVersion/ChangelogVersions:** handle RTL mode
([#&#8203;4777](https://redirect.github.com/nuxt/ui/issues/4777))
([f91c408](f91c4081e5))
- **ContentSearch/DashboardSearch:** make `ui.modal` work
([946c2ec](946c2ec887))
- **module:** add `[@source](https://redirect.github.com/source)` on
components
([a16465f](a16465f3da)),
closes [#&#8203;4773](https://redirect.github.com/nuxt/ui/issues/4773)
- **PageCard:** improve keyboard accessibility
([#&#8203;4733](https://redirect.github.com/nuxt/ui/issues/4733))
([3029568](3029568465))
- **ProseImg:** ensure unique motion layout id for images
([#&#8203;4720](https://redirect.github.com/nuxt/ui/issues/4720))
([9480a0b](9480a0baa4))
- **unplugin:** handle components overrides in subdirectories
([#&#8203;4781](https://redirect.github.com/nuxt/ui/issues/4781))
([69ee75e](69ee75e5b2))

</details>

<details>
<summary>reduxjs/redux-toolkit (@&#8203;reduxjs/toolkit)</summary>

###
[`v2.9.0`](https://redirect.github.com/reduxjs/redux-toolkit/releases/tag/v2.9.0)

[Compare
Source](https://redirect.github.com/reduxjs/redux-toolkit/compare/v2.8.2...v2.9.0)

This **feature release** rewrites RTK Query's internal subscription and
polling systems and the `useStableQueryArgs` hook for better perf, adds
automatic `AbortSignal` handling to requests still in progress when a
cache entry is removed, fixes a bug with the `transformResponse` option
for queries, adds a new `builder.addAsyncThunk` method, and fixes
assorted other issues.

#### Changelog

##### RTK Query Performance Improvements

We had reports that [RTK Query could get very slow when there were
thousands of subscriptions to the same cache
entry](https://redirect.github.com/reduxjs/redux-toolkit/issues/5052).
After investigation, we found that the internal polling logic was
attempting to recalculate the minimum polling time after every new
subscription was added. This was highly inefficient, as most
subscriptions don't change polling settings, and it required repeated
O(n) iteration over the growing list of subscriptions. We've rewritten
that logic to debounce the update check and ensure a max of one polling
value update per tick for the entire API instance.

Related, while working on the request abort changes, testing showed that
use of plain `Record`s to hold subscription data was inefficient because
we have to iterate keys to check size. We've rewritten the subscription
handling internals to use `Map`s instead, as well as restructuring some
additional checks around in-flight requests.

These two improvements drastically improved runtime perf for the
thousands-of-subscriptions-one-cache-entry repro, eliminating RTK
methods as visible hotspots in the perf profiles. It likely also
improves perf for general usage as well.

We've also changed the implementation of our internal
`useStableQueryArgs` hook to avoid calling `serializeQueryArgs` on its
value, which can avoid potential perf issues when a query takes a very
large object as its cache key.

> \[!NOTE]
> The internal logic switched from serializing the query arg to doing
reference checks on nested values. This means that if you are passing a
non-POJO value in a query arg, such as `useSomeQuery({a: new Set()})`,
*and* you have `refetchOnMountOrArgChange` enabled, this will now
trigger refeteches each time as the `Set` references are now considered
different based on equality instead of serialization.

##### Abort Signal Handling on Cleanup

We've had numerous requests over time for various forms of "abort
in-progress requests when the data is no longer needed / params change /
component unmounts / some expensive request is taking too long". This is
a complex topic with multiple potential use cases, and our standard
answer has been that we *don't* want to abort those requests - after
all, cache entries default to staying in memory for 1 minute after the
last subscription is removed, so RTKQ's cache can still be updated when
the request completes. That also means that it doesn't make sense to
abort a request "on unmount".

However, it does then make sense to abort an in-progress request if the
cache entry itself is removed. Given that, we've updated our cache
handling to automatically call the existing `resPromise.abort()` method
in that case, triggering the `AbortSignal` attached to the `baseQuery`.
The handling at that point depends on your app - `fetchBaseQuery` should
handle that, a custom `baseQuery` or `queryFn` would need to listen to
the `AbortSignal`.

We do have [an open issue asking for further discussions of potential
abort / cancelation use
cases](https://redirect.github.com/reduxjs/redux-toolkit/issues/2444)
and would appreciate further feedback.

##### New Options

The builder callback used in `createReducer` and
`createSlice.extraReducers` now has `builder.addAsyncThunk` available,
which allows handling specific actions from a thunk in the same way that
you could define a thunk inside `createSlice.reducers`:

```ts
        const slice = createSlice({
          name: 'counter',
          initialState: {
            loading: false,
            errored: false,
            value: 0,
          },
          reducers: {},
          extraReducers: (builder) =>
            builder.addAsyncThunk(asyncThunk, {
              pending(state) {
                state.loading = true
              },
              fulfilled(state, action) {
                state.value = action.payload
              },
              rejected(state) {
                state.errored = true
              },
              settled(state) {
                state.loading = false
              },
            }),
        })
```

`createApi` and individual endpoint definitions now accept a
`skipSchemaValidation` option with an array of schema types to skip, or
`true` to skip validation entirely (in case you want to use a schema for
its types, but the actual validation is expensive).

##### Bug Fixes

The infinite query implementation accidentally changed the query
internals to *always* run `transformResponse` if provided, including if
you were using `upsertQueryData()`, which then broke. It's been fixed to
only run on an actual query request.

The internal changes to the structure of the `state.api.provided`
structure broke our handling of `extractRehydrationInfo` - we've updated
that to handle the changed structure.

The infinite query status fields like `hasNextPage` are now a looser
type of `boolean` initially, rather than strictly `false`.

##### TS Types

We now export Immer's `WritableDraft` type to fix another non-portable
types issue.

We've added an `api.endpoints.myEndpoint.types.RawResultType` types-only
field to match the other available fields.

#### What's Changed

- Add RawResultType as a type-only property on endpoints by
[@&#8203;EskiMojo14](https://redirect.github.com/EskiMojo14) in
[#&#8203;5037](https://redirect.github.com/reduxjs/redux-toolkit/pull/5037)
- allow passing an array of specific schemas to skip by
[@&#8203;EskiMojo14](https://redirect.github.com/EskiMojo14) in
[#&#8203;5042](https://redirect.github.com/reduxjs/redux-toolkit/pull/5042)
- fix(types): re-exporting WritableDraft from immer by
[@&#8203;marinsokol5](https://redirect.github.com/marinsokol5) in
[#&#8203;5015](https://redirect.github.com/reduxjs/redux-toolkit/pull/5015)
- Remove Serialisation from useStableQueryArgs by
[@&#8203;riqts](https://redirect.github.com/riqts) in
[#&#8203;4996](https://redirect.github.com/reduxjs/redux-toolkit/pull/4996)
- add addAsyncThunk method to reducer map builder by
[@&#8203;EskiMojo14](https://redirect.github.com/EskiMojo14) in
[#&#8203;5007](https://redirect.github.com/reduxjs/redux-toolkit/pull/5007)
- Only run `transformResponse` when a `query` is used by
[@&#8203;markerikson](https://redirect.github.com/markerikson) in
[#&#8203;5049](https://redirect.github.com/reduxjs/redux-toolkit/pull/5049)
- Assorted bugfixes for 2.8.3 by
[@&#8203;markerikson](https://redirect.github.com/markerikson) in
[#&#8203;5060](https://redirect.github.com/reduxjs/redux-toolkit/pull/5060)
- Abort pending requests if the cache entry is removed by
[@&#8203;markerikson](https://redirect.github.com/markerikson) in
[#&#8203;5061](https://redirect.github.com/reduxjs/redux-toolkit/pull/5061)
- Update TS CI config by
[@&#8203;markerikson](https://redirect.github.com/markerikson) in
[#&#8203;5065](https://redirect.github.com/reduxjs/redux-toolkit/pull/5065)
- Rewrite subscription handling and polling calculations for better perf
by [@&#8203;markerikson](https://redirect.github.com/markerikson) in
[#&#8203;5064](https://redirect.github.com/reduxjs/redux-toolkit/pull/5064)

**Full Changelog**:
<https://github.com/reduxjs/redux-toolkit/compare/v2.8.2...v2.9.0>

</details>

<details>
<summary>rollup/rollup (@&#8203;rollup/rollup-linux-x64-gnu)</summary>

###
[`v4.50.1`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4501)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.50.0...v4.50.1)

*2025-09-07*

##### Bug Fixes

- Resolve a situation where a destructuring default value was removed
([#&#8203;6090](https://redirect.github.com/rollup/rollup/issues/6090))

##### Pull Requests

- [#&#8203;6088](https://redirect.github.com/rollup/rollup/pull/6088):
feat(www): shorter repl shareables
([@&#8203;cyyynthia](https://redirect.github.com/cyyynthia),
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;6090](https://redirect.github.com/rollup/rollup/pull/6090):
Call includeNode for self or children nodes in
includeDestructuredIfNecessary
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi))
- [#&#8203;6091](https://redirect.github.com/rollup/rollup/pull/6091):
fix(deps): update rust crate swc\_compiler\_base to v33
([@&#8203;renovate](https://redirect.github.com/renovate)\[bot])
- [#&#8203;6092](https://redirect.github.com/rollup/rollup/pull/6092):
chore(deps): lock file maintenance minor/patch updates
([@&#8203;renovate](https://redirect.github.com/renovate)\[bot])
- [#&#8203;6094](https://redirect.github.com/rollup/rollup/pull/6094):
perf: replace startsWith with strict equality
([@&#8203;btea](https://redirect.github.com/btea))

###
[`v4.50.0`](https://redirect.github.com/rollup/rollup/blob/HEAD/CHANGELOG.md#4500)

[Compare
Source](https://redirect.github.com/rollup/rollup/compare/v4.49.0...v4.50.0)

*2025-08-31*

##### Features

- Support openharmony-arm64 platform
([#&#8203;6081](https://redirect.github.com/rollup/rollup/issues/6081))

##### Bug Fixes

- Fix loading of extensionless imports in config files
([#&#8203;6084](https://redirect.github.com/rollup/rollup/issues/6084))

##### Pull Requests

- [#&#8203;6081](https://redirect.github.com/rollup/rollup/pull/6081):
Add support for openharmony-arm64 platform
([@&#8203;hqzing](https://redirect.github.com/hqzing),
[@&#8203;lukastaegert](https://redirect.github.com/lukastaegert))
- [#&#8203;6084](https://redirect.github.com/rollup/rollup/pull/6084):
Return null to defer to the default resolution behavior
([@&#8203;TrickyPi](https://redirect.github.com/TrickyPi))

</details>

<details>
<summary>storybookjs/storybook (@&#8203;storybook/addon-docs)</summary>

###
[`v9.1.5`](https://redirect.github.com/storybookjs/storybook/blob/HEAD/CHANGELOG.md#915)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.1.4...v9.1.5)

- CSF: Support `satisfies x as y` syntax -
[#&#8203;32169](https://redirect.github.com/storybookjs/storybook/pull/32169),
thanks [@&#8203;diagramatics](https://redirect.github.com/diagramatics)!
- Vitest addon: Handle Playwright installation errors gracefully -
[#&#8203;32329](https://redirect.github.com/storybookjs/storybook/pull/32329),
thanks [@&#8203;ndelangen](https://redirect.github.com/ndelangen)!

###
[`v9.1.4`](https://redirect.github.com/storybookjs/storybook/blob/HEAD/CHANGELOG.md#914)

[Compare
Source](https://redirect.github.com/storybookjs/storybook/compare/v9.1.3...v9.1.4)

- Angular: Properly merge builder options and browserTarget options -
[#&#8203;32272](https://redirect.github.com/storybookjs/storybook/pull/32272),
thanks [@&#8203;kroeder](https://redirect.github.com/kroeder)!
- Core: Optimize bundlesize, by reusing internal/babel in mocking-utils
-
[#&#8203;32350](https://redirect.github.com/storybookjs/storybook/pull/32350),
thanks [@&#8203;ndelangen](https://redirect.github.com/ndelangen)!
- Svelte & Vue: Add framework-specific `docgen` option to disable docgen
processing -
[#&#8203;32319](https://redirect.github.com/storybookjs/storybook/pull/32319),
thanks
[@&#8203;copilot-swe-agent](https://redirect.github.com/copilot-swe-agent)!
- Svelte: Support `@sveltejs/vite-plugin-svelte` v6 -
[#&#8203;32320](https://redirect.github.com/storybookjs/storybook/pull/32320),
thanks [@&#8203;JReinhold](https://redirect.github.com/JReinhold)!

</details>

<details>
<summary>tailwindlabs/tailwindcss (@&#8203;tailwindcss/cli)</summary>

###
[`v4.1.13`](https://redirect.github.com/tailwindlabs/tailwindcss/blob/HEAD/CHANGELOG.md#4113---2025-09-03)

[Compare
Source](https://redirect.github.com/tailwindlabs/tailwindcss/compare/v4.1.12...v4.1.13)

##### Changed

- Drop warning from browser build
([#&#8203;18731](https://redirect.github.com/tailwindlabs/tailwindcss/issues/18731))
- Drop exact duplicate declarations when emitting CSS
([#&#8203;18809](https://redirect.github.com/tailwindlabs/tailwindcss/issues/18809))

##### Fixed

- Don't transition `visibility` when using `transition`
([#&#8203;18795](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18795))
- Discard matched variants with unknown named values
([#&#8203;18799](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18799))
- Discard matched variants with non-string values
([#&#8203;18799](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18799))
- Show suggestions for known `matchVariant` values
([#&#8203;18798](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18798))
- Replace deprecated `clip` with `clip-path` in `sr-only`
([#&#8203;18769](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18769))
- Hide internal fields from completions in `matchUtilities`
([#&#8203;18820](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18820))
- Ignore `.vercel` folders by default (can be overridden by `@source …`
rules)
([#&#8203;18855](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18855))
- Consider variants starting with `@-` to be invalid (e.g. `@-2xl:flex`)
([#&#8203;18869](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18869))
- Do not allow custom variants to start or end with a `-` or `_`
([#&#8203;18867](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18867),
[#&#8203;18872](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18872))
- Upgrade: Migrate `aria` theme keys to `@custom-variant`
([#&#8203;18815](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18815))
- Upgrade: Migrate `data` theme keys to `@custom-variant`
([#&#8203;18816](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18816))
- Upgrade: Migrate `supports` theme keys to `@custom-variant`
([#&#8203;18817](https://redirect.github.com/tailwindlabs/tailwindcss/pull/18817))

</details>

<details>
<summary>typescript-eslint/typescript-eslint
(@&#8203;typescript-eslint/eslint-plugin)</summary>

###
[`v8.43.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plugin/CHANGELOG.md#8430-2025-09-08)

[Compare
Source](https://redirect.github.com/typescript-eslint/typescript-eslint/compare/v8.42.0...v8.43.0)

##### 🚀 Features

- **typescript-estree:** disallow empty type parameter/argument lists
([#&#8203;11563](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11563))

##### 🩹 Fixes

- **eslint-plugin:** \[prefer-return-this-type] don't report an error
when returning a union type that includes a classType
([#&#8203;11432](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11432))
- **eslint-plugin:** \[no-deprecated] should report deprecated exports
and reexports
([#&#8203;11359](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11359))
- **eslint-plugin:** \[no-floating-promises] allowForKnownSafeCalls now
supports function names
([#&#8203;11423](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11423),
[#&#8203;11430](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11430))
- **eslint-plugin:** \[consistent-type-exports] fix declaration
shadowing
([#&#8203;11457](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11457))
- **eslint-plugin:** \[no-unnecessary-type-conversion] only report \~\~
on integer literal types
([#&#8203;11517](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11517))
- **scope-manager:** exclude Program from DefinitionBase node types
([#&#8203;11469](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11469))
- **eslint-plugin:** \[no-non-null-assertion] do not suggest optional
chain on LHS of assignment
([#&#8203;11489](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11489))
- **type-utils:** add union type support to TypeOrValueSpecifier
([#&#8203;11526](https://redirect.github.com/typescript-eslint/typescript-eslint/pull/11526))

##### ❤️ Thank You

- Dima [@&#8203;dbarabashh](https://redirect.github.com/dbarabashh)
- Kirk Waiblinger
[@&#8203;kirkwaiblinger](https://redirect.github.com/kirkwaiblinger)
- mdm317
- tao
- Victor Genaev
[@&#8203;mainframev](https://redirect.github.com/mainframev)
- Yukihiro Hasegawa [@&#8203;y-hsgw](https://redirect.github.com/y-hsgw)
- 민감자(Minji Kim)
[@&#8203;mouse0429](https://redirect.github.com/mouse0429)
- 송재욱

You can read about our [versioning
strategy](https://typescript-eslint.io/users/versioning) and
[releases](https://typescript-eslint.io/users/releases) on our website.

###
[`v8.42.0`](https://redirect.github.com/typescript-eslint/typescript-eslint/blob/HEAD/packages/eslint-plug

</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.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- 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:eyJjcmVhdGVkSW5WZXIiOiI0MS44Mi43IiwidXBkYXRlZEluVmVyIjoiNDEuOTcuMTAiLCJ0YXJnZXRCcmFuY2giOiJtYWluIiwibGFiZWxzIjpbXX0=-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 08:18:06 -04:00
Eli Bosley
4759b3d0b3 fix: adjust header styles to fix flashing and width issues - thanks ZarZ 2025-09-09 08:17:23 -04:00
Eli Bosley
daeeba8c1f chore: add public notice to unraid-components directory 2025-09-08 14:10:31 -04:00
github-actions[bot]
196bd52628 chore(main): release 4.20.0 (#1666)
🤖 I have created a release *beep* *boop*
---


## [4.20.0](https://github.com/unraid/api/compare/v4.19.1...v4.20.0)
(2025-09-08)


### Features

* **disks:** add isSpinning field to Disk type
([#1527](https://github.com/unraid/api/issues/1527))
([193be3d](193be3df36))


### Bug Fixes

* better component loading to prevent per-page strange behavior
([095c222](095c2221c9))
* **deps:** pin dependencies
([#1669](https://github.com/unraid/api/issues/1669))
([413db4b](413db4bd30))
* **plugin:** add fallback for unraid-api stop in deprecation cleanup
([#1668](https://github.com/unraid/api/issues/1668))
([797bf50](797bf50ec7))
* prepend 'v' to API version in workflow dispatch inputs
([f0cffbd](f0cffbdc7a))
* progress frame background color fix
([#1672](https://github.com/unraid/api/issues/1672))
([785f1f5](785f1f5eb1))
* properly override header values
([#1673](https://github.com/unraid/api/issues/1673))
([aecf70f](aecf70ffad))

---
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-08 13:57:47 -04:00
Eli Bosley
6c0061923a test(file-modification): add unit tests for version comparison methods
- Introduced a new test suite for the FileModification class to validate version comparison methods.
- Implemented tests for isUnraidVersionGreaterThanOrEqualTo and isUnraidVersionLessThanOrEqualTo, including scenarios for stable and prerelease versions.
- Enhanced the compareUnraidVersion method to streamline version comparison logic.
2025-09-08 13:45:54 -04:00
Eli Bosley
f33afe7ae5 refactor(auth-request): update asset file handling to include .css files
- Renamed method to getAssetFiles for clarity.
- Updated file search to include both .js and .css files in the specified directory.
- Adjusted logging to reflect the new asset file types found.
2025-09-08 13:19:13 -04:00
Eli Bosley
aecf70ffad fix: properly override header values (#1673) 2025-09-08 12:55:52 -04:00
Eli Bosley
785f1f5eb1 fix: progress frame background color fix (#1672)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Introduces scoped CSS resets to confine UI framework effects to
elements using a .unapi prefix, normalizing typography, buttons,
toggles, links, dialogs, and logos for consistent visuals.
* Updates Unraid UI integrations and dark mode visuals for more reliable
appearance.
* Keeps progress iframe background aligned to app theme for visual
consistency.

* **Chores**
* Moves shared styling imports to the root and extends resource scanning
to support the new scoped approach.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 12:53:10 -04:00
Eli Bosley
193be3df36 feat(disks): add isSpinning field to Disk type (#1527)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a new "isSpinning" status to disks, allowing users to see
whether each disk is currently spinning.

* **Bug Fixes**
* Improved accuracy of disk metadata by integrating external
configuration data.

* **Tests**
* Enhanced test setup to better simulate application state for
disk-related features.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-09-08 12:07:15 -04:00
Pujit Mehrotra
116ee88fcf refactor: add subscribe and enabled methods to ConfigFilePersister (#1670) 2025-09-08 11:05:22 -04:00
renovate[bot]
413db4bd30 fix(deps): pin dependencies (#1669)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [ajv-errors](https://redirect.github.com/epoberezkin/ajv-errors) |
dependencies | pin | [`^3.0.0` ->
`3.0.0`](https://renovatebot.com/diffs/npm/ajv-errors/3.0.0/3.0.0) |
| [ansi_up](https://redirect.github.com/drudru/ansi_up) | dependencies |
pin | [`^6.0.6` ->
`6.0.6`](https://renovatebot.com/diffs/npm/ansi_up/6.0.6/6.0.6) |
| [globals](https://redirect.github.com/sindresorhus/globals) |
devDependencies | pin | [`^16.3.0` ->
`16.3.0`](https://renovatebot.com/diffs/npm/globals/16.3.0/16.3.0) |
| [pify](https://redirect.github.com/sindresorhus/pify) |
devDependencies | pin | [`^6.1.0` ->
`6.1.0`](https://renovatebot.com/diffs/npm/pify/6.1.0/6.1.0) |
| [vue-router](https://redirect.github.com/vuejs/router) | dependencies
| pin | [`^4.5.1` ->
`4.5.1`](https://renovatebot.com/diffs/npm/vue-router/4.5.1/4.5.1) |

Add the preset `:preserveSemverRanges` to your config if you don't want
to pin your dependencies.

---

### 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 is behind base branch, or you tick the
rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get
[config
help](https://redirect.github.com/renovatebot/renovate/discussions) if
that's undesired.

---

- [ ] <!-- 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-08 10:35:44 -04:00
Eli Bosley
095c2221c9 fix: better component loading to prevent per-page strange behavior 2025-09-08 10:33:26 -04:00
Eli Bosley
dfe891ce38 refactor: update PM2_HOME path and enhance environment handling (#1667)
- Changed the default PM2_HOME path from the user's home directory to
'/var/log/.pm2' for consistency in production environments.
- Updated PM2 service to always set PM2_HOME in the environment for all
PM2 commands, ensuring proper execution context.
- Modified integration tests to use the home directory for PM2_HOME
during testing, improving test reliability.
- Refactored the UserProfile dropdown component to enhance styling and
accessibility features.

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

* Bug Fixes
* Improved reliability of PM2-based commands by consistently setting
PM2_HOME and conditionally updating PATH.
* Defaulted PM2_HOME to a writable system location to reduce permission
issues.
* Made log directory creation more robust: failures no longer crash the
process and are properly logged.
* Enhanced PM2 connection handling to avoid stale connections during
startup and error scenarios.

* Tests
* Added comprehensive unit and integration tests covering PM2 dependency
setup, error handling, and connection scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-08 10:27:45 -04:00
Pujit Mehrotra
797bf50ec7 fix(plugin): add fallback for unraid-api stop in deprecation cleanup (#1668) 2025-09-08 10:06:33 -04:00
Eli Bosley
af5ca11860 Feat/vue (#1655)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- New Features
- Introduced Docker management UI components: Overview, Logs, Console,
Preview, and Edit.
- Added responsive Card/Detail layouts with grouping, bulk actions, and
tabs.
  - New UnraidToaster component and global toaster configuration.
- Component auto-mounting improved with async loading and multi-selector
support.
- UI/UX
- Overhauled theme system (light/dark tokens, primary/orange accents)
and added theme variants.
  - Header OS version now includes integrated changelog modal.
- Registration displays warning states; multiple visual polish updates.
- API
  - CPU load now includes percentGuest and percentSteal metrics.
- Chores
  - Migrated web app to Vite; updated artifacts and manifests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
Co-authored-by: Michael Datelle <mdatelle@icloud.com>
2025-09-08 10:04:49 -04:00
Eli Bosley
f0cffbdc7a fix: prepend 'v' to API version in workflow dispatch inputs
- Updated the build-plugin.yml workflow to include a 'v' prefix in the version input for the release-production workflow, ensuring proper version formatting during production releases.
2025-09-04 20:28:15 -04:00
github-actions[bot]
16905dd3a6 chore(main): release 4.19.1 (#1665)
🤖 I have created a release *beep* *boop*
---


## [4.19.1](https://github.com/unraid/api/compare/v4.19.0...v4.19.1)
(2025-09-05)


### Bug Fixes

* custom path detection to fix setup issues
([#1664](https://github.com/unraid/api/issues/1664))
([2ecdb99](2ecdb99052))

---
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-04 20:19:36 -04:00
Eli Bosley
2ecdb99052 fix: custom path detection to fix setup issues (#1664)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- Improved reliability of background command execution by ensuring
common system binary paths are available, reducing PATH-related errors.
- Aligned environment handling for spawned processes with typical shell
behavior to prevent intermittent failures across different environments.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 20:03:28 -04:00
Eli Bosley
286f1be8ed chore: start job correctly when releasing (#1662)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Chores
- Updated build and release workflows to include an additional admin
token secret used for triggering production releases via workflow
dispatch.
- Expanded secret mapping for the production build job to pass the token
through the pipeline as needed.
  - No changes to application behavior, UI, or user workflows.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 16:48:28 -04:00
Eli Bosley
bcefdd5261 chore: add Discord notification action for release announcements
- Beat EDACerton to the punch
- Integrated a new action in the release workflow to send notifications to a Discord channel upon new releases.
- The notification includes the release version, a link to the release, and the changelog, enhancing communication with users.

This addition improves user engagement by providing timely updates on new releases directly in Discord.
2025-09-04 16:05:39 -04:00
github-actions[bot]
d3459ecbc6 chore(main): release 4.19.0 (#1650)
🤖 I have created a release *beep* *boop*
---


## [4.19.0](https://github.com/unraid/api/compare/v4.18.2...v4.19.0)
(2025-09-04)


### Features

* mount vue apps, not web components
([#1639](https://github.com/unraid/api/issues/1639))
([88087d5](88087d5201))


### Bug Fixes

* api version json response
([#1653](https://github.com/unraid/api/issues/1653))
([292bc0f](292bc0fc81))
* enhance DOM validation and cleanup in vue-mount-app
([6cf7c88](6cf7c88242))
* enhance getKeyFile function to handle missing key file gracefully
([#1659](https://github.com/unraid/api/issues/1659))
([728b38a](728b38ac11))
* info alert docker icon
([#1661](https://github.com/unraid/api/issues/1661))
([239cdd6](239cdd6133))
* oidc cache busting issues fixed
([#1656](https://github.com/unraid/api/issues/1656))
([e204eb8](e204eb80a0))
* **plugin:** restore cleanup behavior for unsupported unraid versions
([#1658](https://github.com/unraid/api/issues/1658))
([534a077](534a07788b))
* UnraidToaster component and update dialog close button
([#1657](https://github.com/unraid/api/issues/1657))
([44774d0](44774d0acd))
* vue mounting logic with tests
([#1651](https://github.com/unraid/api/issues/1651))
([33774aa](33774aa596))

---
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-04 15:42:35 -04:00
Pujit Mehrotra
534a07788b fix(plugin): restore cleanup behavior for unsupported unraid versions (#1658)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Adds guided warnings and an explicit cleanup/uninstall workflow for
unsupported Unraid versions, with safer removal paths by OS release.

* **Bug Fixes**
* Detects and removes both new and legacy Connect configurations,
ensuring proper sign-out and web-server reload.
* Strengthens version gating to avoid problematic pre-release builds and
advises uninstall/upgrade when needed.

* **Chores**
  * Lowers declared minimum Unraid version to broaden compatibility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 15:28:20 -04:00
Eli Bosley
239cdd6133 fix: info alert docker icon (#1661) 2025-09-04 15:18:07 -04:00
Eli Bosley
77cfc07dda refactor: enhance CSS structure with @layer for component styles (#1660)
- Introduced @layer directive to ensure base styles have lower priority
than Tailwind utilities.
- Organized CSS resets for box-sizing, figures, headings, paragraphs,
and unordered lists under a single @layer base block for improved
maintainability.

These changes streamline the CSS structure and enhance compatibility
with Tailwind CSS utilities.

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

## Summary by CodeRabbit

- Style
- Wrapped core resets in a base style layer, adjusting cascade with
utility classes.
  - Applied global box-sizing within the base layer.
  - Consolidated heading and paragraph resets into the layer.
- Added a reset for unordered lists to remove default bullets and
padding.
  - Retained the logo figure reset within the layer.
- Updated formatting and header comments to reflect the layering
approach.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 14:36:25 -04:00
Eli Bosley
728b38ac11 fix: enhance getKeyFile function to handle missing key file gracefully (#1659)
- Updated the getKeyFile function to catch ENOENT errors when the
specified key file does not exist, returning an empty string instead of
throwing an error.
- Added new tests to verify the behavior of getKeyFile when the key file
is missing and when it exists, ensuring robust error handling and
correct functionality.

These changes improve the reliability of the key file retrieval process
in the application.

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

## Summary by CodeRabbit

- New Features
  - None

- Bug Fixes
- Prevents errors when the key file is missing by returning an empty
value instead of failing, while preserving existing behaviors in other
states.

- Tests
  - Refactored tests to use a mocked filesystem with better isolation.
  - Added scenarios for missing key files and correctly decoded keys.
  - Improved assertions for clearer, deterministic outcomes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-04 14:23:42 -04:00
Eli Bosley
44774d0acd fix: UnraidToaster component and update dialog close button (#1657)
- Introduced a new UnraidToaster component for displaying notifications
with customizable positions.
- Updated the DialogClose component to use a span element for better
semantic structure.
- Enhanced CSS for the sonner component to ensure proper layout and
styling.

These changes improve user feedback through notifications and refine the
dialog close button's implementation.

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

* **New Features**
* Added a toaster notifications component with configurable screen
position, rich colors, and a close button; programmatic and legacy
mounting helpers exposed.

* **Style**
  * Updated toast close-button spacing and min-width behavior.
* Simplified dialog close-button rendering and removed redundant style
resets.
* Reduced SSO provider icon size and added SSO button font-size tokens.

* **Tests**
  * Added unit tests covering component mounting and global exports.

* **Chores**
  * Deployment now performs broader remote cleanup before syncing.
* **Chores**
* Type declarations and tsconfig updated for global mount/utility
typings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 12:34:16 -04:00
Eli Bosley
e204eb80a0 fix: oidc cache busting issues fixed (#1656)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- Bug Fixes
- SSO/OIDC provider changes now take effect immediately by clearing
caches on updates, deletes, and settings changes.
  - Updating a provider’s issuer no longer requires an API restart.

- Tests
- Added extensive test coverage for OIDC config caching, including
per‑provider and global invalidation and manual/automatic configuration
paths.

- Chores
- Updated internal module wiring to resolve circular dependencies; no
user-facing changes.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 12:29:13 -04:00
Eli Bosley
0c727c37f4 refactor: update UserProfile positioning and clean up unraid components
- Adjusted the positioning of the UserProfile component to be absolute, ensuring it aligns correctly within its parent container.
- Modified the clean-unraid.sh script to remove the entire components directory instead of just the standalone apps directory, enhancing cleanup efficiency.
- Added a cleanup step in deploy-dev.sh to clear the remote standalone directory before deployment, ensuring a fresh setup.

These changes improve the layout of the UserProfile component and streamline the deployment process by ensuring no residual files remain.
2025-09-04 07:24:46 -04:00
Eli Bosley
292bc0fc81 fix: api version json response (#1653)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Version command now supports JSON output via a -j/--json flag,
returning version, build (when available), and a combined value. Default
human-readable output remains unchanged.
* **Tests**
* Added comprehensive tests for version command behavior across
human-readable and JSON modes, including scenarios with and without
build metadata.
* **Chores**
  * Bumped API configuration version from 4.18.1 to 4.18.2.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 21:09:37 -04:00
Eli Bosley
53f501e1a7 refactor: update test for hidden element mounting in vue-mount-app
- Renamed test case to clarify that the component should mount even when the element is hidden.
- Adjusted assertions to ensure that hidden elements can still mount successfully without triggering warnings.

This change enhances the clarity and reliability of the test suite for the vue-mount-app component.
2025-09-03 17:31:52 -04:00
Eli Bosley
6cf7c88242 fix: enhance DOM validation and cleanup in vue-mount-app
- Improved validation logic for mounted elements to ensure stable DOM connections and prevent manipulation issues.
- Added cleanup step to clear existing unraid-components directory before installation, ensuring a clean setup.

This update aims to enhance the reliability of component mounting and reduce potential UI issues.
2025-09-03 17:29:51 -04:00
Eli Bosley
33774aa596 fix: vue mounting logic with tests (#1651)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* Bug Fixes
* Prevents duplicate modal instances and remounts, improving stability
across pages.
* Improves auto-mount reliability with better DOM validation and
recovery from mount errors.
* Enhances cleanup during unmounts to avoid residual artifacts and
intermittent UI issues.
* More robust handling of shadow DOM and problematic DOM structures,
reducing crashes.

* Style
* Adds extra top margin to the OS version controls for improved spacing.

* Tests
* Introduces a comprehensive test suite covering mounting, unmounting,
error recovery, i18n, and global state behaviors.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 17:10:21 -04:00
Eli Bosley
88087d5201 feat: mount vue apps, not web components (#1639)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Standalone web bundle with auto-mount utilities and a self-contained
test page.
* New responsive modal components for consistent mobile/desktop dialogs.
  * Header actions to copy OS/API versions.

* **Improvements**
* Refreshed UI styles (muted borders), accessibility and animation
refinements.
  * Theming updates and Tailwind v4–aligned, component-scoped styles.
  * Runtime GraphQL endpoint override and CSRF header support.

* **Bug Fixes**
* Safer network fetching and improved manifest/asset loading with
duplicate protection.

* **Tests/Chores**
* Parallel plugin tests, new extractor test suite, and updated
build/test scripts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 15:42:21 -04:00
github-actions[bot]
5d89682a3f chore(main): release 4.18.2 (#1643)
🤖 I have created a release *beep* *boop*
---


## [4.18.2](https://github.com/unraid/api/compare/v4.18.1...v4.18.2)
(2025-09-03)


### Bug Fixes

* add missing CPU guest metrics to CPU responses
([#1644](https://github.com/unraid/api/issues/1644))
([99dbad5](99dbad57d5))
* **plugin:** raise minimum unraid os version to 6.12.15
([#1649](https://github.com/unraid/api/issues/1649))
([bc15bd3](bc15bd3d70))
* update GitHub Actions token for workflow trigger
([4d8588b](4d8588b173))
* update OIDC URL validation and add tests
([#1646](https://github.com/unraid/api/issues/1646))
([c7c3bb5](c7c3bb57ea))
* use shared bg & border color for styled toasts
([#1647](https://github.com/unraid/api/issues/1647))
([7c3aee8](7c3aee8f3f))

---
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-03 15:23:02 -04:00
Pujit Mehrotra
bc15bd3d70 fix(plugin): raise minimum unraid os version to 6.12.15 (#1649) 2025-09-03 15:20:24 -04:00
Pujit Mehrotra
7c3aee8f3f fix: use shared bg & border color for styled toasts (#1647)
Addresses user complaints about light colored notifications in dark
themes.

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

## Summary by CodeRabbit

- New Features
- Introduced type-specific toast color tokens (success, info, warning,
error) for richer, clearer toast styling.
- Applied consistent theming across light, inverted, and dark modes to
improve readability and contrast.
  - Enabled compatibility with rich color settings for toasts.
- Bug Fixes
- Corrected a CSS comment block to ensure styles compile and apply
reliably.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 12:36:18 -04:00
Eli Bosley
c7c3bb57ea fix: update OIDC URL validation and add tests (#1646)
- Updated the OIDC issuer URL validation to prevent trailing slashes and
whitespace.
- Introduced a utility class `OidcUrlPatterns` for managing URL patterns
and validation logic.
- Added comprehensive tests for the new URL validation logic and
examples to ensure correctness.
- Bumped version to 4.18.1 in the configuration file.

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

## Summary by CodeRabbit

- New Features
- Added strict validation for OIDC issuer URLs in the SSO configuration
form, with clearer guidance to avoid trailing slashes.
- Bug Fixes
- Prevented misconfiguration by rejecting issuer URLs with trailing
slashes (e.g., Google issuer), avoiding double slashes in discovery
URLs.
- Tests
- Introduced comprehensive unit tests covering issuer URL validation,
patterns, and real-world scenarios to ensure reliability.
- Chores
  - Bumped version to 4.18.1.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 11:56:30 -04:00
Eli Bosley
99dbad57d5 fix: add missing CPU guest metrics to CPU responses (#1644)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- New Features
- CPU load metrics now include guest runtime and hypervisor steal time
percentages, exposed as additional fields in CPU load responses
(per‑CPU).
- Tests
- Added comprehensive unit tests for CPU info and load generation,
including edge cases and validation of the new guest and steal metrics.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-03 11:06:51 -04:00
Eli Bosley
c42f79d406 chore: add code coverage monitoring (#1645) 2025-09-03 11:06:05 -04:00
Eli Bosley
4d8588b173 fix: update GitHub Actions token for workflow trigger
Replaced the token used for triggering workflows in the build-plugin.yml file from WORKFLOW_TRIGGER_PAT to UNRAID_BOT_GITHUB_ADMIN_TOKEN for improved security and access control.
2025-09-03 10:04:54 -04:00
github-actions[bot]
0d1d27064e chore(main): release 4.18.1 (#1641)
🤖 I have created a release *beep* *boop*
---


## [4.18.1](https://github.com/unraid/api/compare/v4.18.0...v4.18.1)
(2025-09-03)


### Bug Fixes

* OIDC and API Key management issues
([#1642](https://github.com/unraid/api/issues/1642))
([0fe2c2c](0fe2c2c1c8))
* rm redundant emission to `$HOME/.pm2/logs`
([#1640](https://github.com/unraid/api/issues/1640))
([a8e4119](a8e4119270))

---
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-03 09:47:55 -04:00
Eli Bosley
0fe2c2c1c8 fix: OIDC and API Key management issues (#1642) 2025-09-03 09:47:11 -04:00
664 changed files with 52085 additions and 15835 deletions

View File

@@ -1,123 +1,3 @@
{
"permissions": {
"allow": [
"# Development Commands",
"Bash(pnpm install)",
"Bash(pnpm dev)",
"Bash(pnpm build)",
"Bash(pnpm test)",
"Bash(pnpm test:*)",
"Bash(pnpm lint)",
"Bash(pnpm lint:fix)",
"Bash(pnpm type-check)",
"Bash(pnpm codegen)",
"Bash(pnpm storybook)",
"Bash(pnpm --filter * dev)",
"Bash(pnpm --filter * build)",
"Bash(pnpm --filter * test)",
"Bash(pnpm --filter * lint)",
"Bash(pnpm --filter * codegen)",
"# Git Commands (read-only)",
"Bash(git status)",
"Bash(git diff)",
"Bash(git log)",
"Bash(git branch)",
"Bash(git remote -v)",
"# Search Commands",
"Bash(rg *)",
"# File System (read-only)",
"Bash(ls)",
"Bash(ls -la)",
"Bash(pwd)",
"Bash(find . -name)",
"Bash(find . -type)",
"# Node/NPM Commands",
"Bash(node --version)",
"Bash(pnpm --version)",
"Bash(npx --version)",
"# Environment Commands",
"Bash(echo $*)",
"Bash(which *)",
"# Process Commands",
"Bash(ps aux | grep)",
"Bash(lsof -i)",
"# Documentation Domains",
"WebFetch(domain:tailwindcss.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:reka-ui.com)",
"WebFetch(domain:nodejs.org)",
"WebFetch(domain:pnpm.io)",
"WebFetch(domain:vitejs.dev)",
"WebFetch(domain:nuxt.com)",
"WebFetch(domain:nestjs.com)",
"# IDE Integration",
"mcp__ide__getDiagnostics",
"# Browser MCP (for testing)",
"mcp__browsermcp__browser_navigate",
"mcp__browsermcp__browser_click",
"mcp__browsermcp__browser_screenshot"
],
"deny": [
"# Dangerous Commands",
"Bash(rm -rf)",
"Bash(chmod 777)",
"Bash(curl)",
"Bash(wget)",
"Bash(ssh)",
"Bash(scp)",
"Bash(sudo)",
"Bash(su)",
"Bash(pkill)",
"Bash(kill)",
"Bash(killall)",
"Bash(python)",
"Bash(python3)",
"Bash(pip)",
"Bash(npm)",
"Bash(yarn)",
"Bash(apt)",
"Bash(brew)",
"Bash(systemctl)",
"Bash(service)",
"Bash(docker)",
"Bash(docker-compose)",
"# File Modification (use Edit/Write tools instead)",
"Bash(sed)",
"Bash(awk)",
"Bash(perl)",
"Bash(echo > *)",
"Bash(echo >> *)",
"Bash(cat > *)",
"Bash(cat >> *)",
"Bash(tee)",
"# Git Write Commands (require explicit user action)",
"Bash(git add)",
"Bash(git commit)",
"Bash(git push)",
"Bash(git pull)",
"Bash(git merge)",
"Bash(git rebase)",
"Bash(git checkout)",
"Bash(git reset)",
"Bash(git clean)",
"# Package Management Write Commands",
"Bash(pnpm add)",
"Bash(pnpm remove)",
"Bash(pnpm update)",
"Bash(pnpm upgrade)"
]
},
"enableAllProjectMcpServers": false
"permissions": {}
}

201
.github/workflows/build-artifacts.yml vendored Normal file
View File

@@ -0,0 +1,201 @@
name: Build Artifacts
on:
workflow_call:
inputs:
ref:
type: string
required: false
description: "Git ref to checkout (commit SHA, branch, or tag)"
version_override:
type: string
required: false
description: "Override version (for manual releases)"
outputs:
build_number:
description: "Build number for the artifacts"
value: ${{ jobs.build-api.outputs.build_number }}
secrets:
VITE_ACCOUNT:
required: true
VITE_CONNECT:
required: true
VITE_UNRAID_NET:
required: true
VITE_CALLBACK_KEY:
required: true
UNRAID_BOT_GITHUB_ADMIN_TOKEN:
required: false
jobs:
build-api:
name: Build API
runs-on: ubuntu-latest
outputs:
build_number: ${{ steps.buildnumber.outputs.build_number }}
defaults:
run:
working-directory: api
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: PNPM Install
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile
- name: Get Git Short Sha and API version
id: vars
run: |
GIT_SHA=$(git rev-parse --short HEAD)
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
API_VERSION=${{ inputs.version_override && format('"{0}"', inputs.version_override) || '${PACKAGE_LOCK_VERSION}' }}
if [ -z "${{ inputs.version_override }}" ] && [ -z "$IS_TAGGED" ]; then
API_VERSION="${PACKAGE_LOCK_VERSION}+${GIT_SHA}"
fi
export API_VERSION
echo "API_VERSION=${API_VERSION}" >> $GITHUB_ENV
echo "PACKAGE_LOCK_VERSION=${PACKAGE_LOCK_VERSION}" >> $GITHUB_OUTPUT
- name: Generate build number
id: buildnumber
uses: onyxmueller/build-tag-number@v1
with:
token: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN || github.token }}
prefix: ${{ inputs.version_override || steps.vars.outputs.PACKAGE_LOCK_VERSION }}
- name: Build
run: |
pnpm run build:release
tar -czf deploy/unraid-api.tgz -C deploy/pack/ .
- name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v4
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version)
defaults:
run:
working-directory: unraid-ui
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.ref || github.ref }}
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: Install dependencies
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/ui
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build:wc
- name: Upload Artifact to Github
uses: actions/upload-artifact@v4
with:
name: unraid-wc-ui
path: unraid-ui/dist-wc/
build-web:
name: Build Web App
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.ref || github.ref }}
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ secrets.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ secrets.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ secrets.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ secrets.VITE_CALLBACK_KEY }} >> .env
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Install Node
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: PNPM Install
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/web --filter @unraid/ui
- name: Build Unraid UI
run: |
cd ${{ github.workspace }}/unraid-ui
pnpm run build
- name: Lint files
run: pnpm run lint
- name: Type Check
run: pnpm run type-check
- name: Build
run: pnpm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v4
with:
name: unraid-wc-rich
path: web/dist

View File

@@ -27,6 +27,15 @@ on:
type: string
required: true
description: "Build number for the plugin builds"
ref:
type: string
required: false
description: "Git ref (commit SHA, branch, or tag) to checkout"
TRIGGER_PRODUCTION_RELEASE:
type: boolean
required: false
default: false
description: "Whether to automatically trigger the release-production workflow (default: false)"
secrets:
CF_ACCESS_KEY_ID:
required: true
@@ -36,6 +45,8 @@ on:
required: true
CF_ENDPOINT:
required: true
UNRAID_BOT_GITHUB_ADMIN_TOKEN:
required: false
jobs:
build-plugin:
name: Build and Deploy Plugin
@@ -47,23 +58,19 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT
- name: Install Node
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: Get API Version
id: vars
@@ -74,14 +81,6 @@ jobs:
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
echo "API_VERSION=${API_VERSION}" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: |
cd ${{ github.workspace }}
@@ -97,7 +96,7 @@ jobs:
uses: actions/download-artifact@v5
with:
pattern: unraid-wc-rich
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/nuxt
path: ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/standalone
merge-multiple: true
- name: Download Unraid API
uses: actions/download-artifact@v5
@@ -147,12 +146,12 @@ jobs:
done
- name: Workflow Dispatch and wait
if: inputs.RELEASE_CREATED == 'true'
if: inputs.RELEASE_CREATED == 'true' && inputs.TRIGGER_PRODUCTION_RELEASE == true
uses: the-actions-org/workflow-dispatch@v4.0.0
with:
workflow: release-production.yml
inputs: '{ "version": "${{ steps.vars.outputs.API_VERSION }}" }'
token: ${{ secrets.WORKFLOW_TRIGGER_PAT }}
inputs: '{ "version": "v${{ steps.vars.outputs.API_VERSION }}" }'
token: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
- name: Upload to Cloudflare
if: inputs.RELEASE_CREATED == 'false'
@@ -181,3 +180,40 @@ jobs:
```
${{ inputs.BASE_URL }}/tag/${{ inputs.TAG }}/dynamix.unraid.net.plg
```
- name: Clean up old preview builds
if: inputs.RELEASE_CREATED == 'false' && github.event_name == 'push'
continue-on-error: true
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
run: |
echo "🧹 Cleaning up old preview builds (keeping last 7 days)..."
# Calculate cutoff date (7 days ago)
CUTOFF_DATE=$(date -d "7 days ago" +"%Y.%m.%d")
echo "Deleting builds older than: ${CUTOFF_DATE}"
# List and delete old timestamped .txz files
OLD_FILES=$(aws s3 ls "s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/" \
--endpoint-url ${{ secrets.CF_ENDPOINT }} --recursive | \
grep -E "dynamix\.unraid\.net-[0-9]{4}\.[0-9]{2}\.[0-9]{2}\.[0-9]{4}\.txz" | \
awk '{print $4}' || true)
DELETED_COUNT=0
if [ -n "$OLD_FILES" ]; then
while IFS= read -r file; do
if [[ $file =~ ([0-9]{4}\.[0-9]{2}\.[0-9]{2})\.[0-9]{4}\.txz ]]; then
FILE_DATE="${BASH_REMATCH[1]}"
if [[ "$FILE_DATE" < "$CUTOFF_DATE" ]]; then
echo "Deleting old build: $(basename "$file")"
aws s3 rm "s3://${{ secrets.CF_BUCKET_PREVIEW }}/${file}" \
--endpoint-url ${{ secrets.CF_ENDPOINT }} || true
((DELETED_COUNT++))
fi
fi
done <<< "$OLD_FILES"
fi
echo "✅ Deleted ${DELETED_COUNT} old builds"

View File

@@ -1,103 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Skip reviews for non-code changes
paths-ignore:
- "**/*.md"
- "**/package-lock.json"
- "**/pnpm-lock.yaml"
- "**/.gitignore"
- "**/LICENSE"
- "**/*.config.js"
- "**/*.config.ts"
- "**/tsconfig.json"
- "**/.github/workflows/*.yml"
- "**/docs/**"
jobs:
claude-review:
# Skip review for bot PRs and WIP/skip-review PRs
# Only run if changes are significant (>10 lines)
if: |
(github.event.pull_request.additions > 10 || github.event.pull_request.deletions > 10) &&
!contains(github.event.pull_request.title, '[skip-review]') &&
!contains(github.event.pull_request.title, '[WIP]') &&
!endsWith(github.event.pull_request.user.login, '[bot]') &&
github.event.pull_request.user.login != 'dependabot' &&
github.event.pull_request.user.login != 'renovate'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Direct prompt for automated review (no @claude mention needed)
direct_prompt: |
IMPORTANT: Review ONLY the DIFF/CHANGESET - the actual lines that were added or modified in this PR.
DO NOT review the entire file context, only analyze the specific changes being made.
Look for HIGH-PRIORITY issues in the CHANGED LINES ONLY:
1. CRITICAL BUGS: Logic errors, null pointer issues, infinite loops, race conditions
2. SECURITY: SQL injection, XSS, authentication bypass, exposed secrets, unsafe operations
3. BREAKING CHANGES: API contract violations, removed exports, changed function signatures
4. DATA LOSS RISKS: Destructive operations without safeguards, missing data validation
DO NOT comment on:
- Code that wasn't changed in this PR
- Style, formatting, or documentation
- Test coverage (unless tests are broken by the changes)
- Minor optimizations or best practices
- Existing code issues that weren't introduced by this PR
If you find no critical issues in the DIFF, respond with: "✅ No critical issues found in changes"
Keep response under 10 lines. Reference specific line numbers from the diff when reporting issues.
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
use_sticky_comment: true
# Context-aware review based on PR characteristics
# Uncomment to enable different review strategies based on context
# direct_prompt: |
# ${{
# (github.event.pull_request.additions > 500) &&
# 'Large PR detected. Focus only on architectural issues and breaking changes. Skip minor issues.' ||
# contains(github.event.pull_request.title, 'fix') &&
# 'Bug fix PR: Verify the fix addresses the root cause and check for regression risks.' ||
# contains(github.event.pull_request.title, 'deps') &&
# 'Dependency update: Check for breaking changes and security advisories only.' ||
# contains(github.event.pull_request.title, 'refactor') &&
# 'Refactor PR: Verify no behavior changes and check for performance regressions.' ||
# contains(github.event.pull_request.title, 'feat') &&
# 'New feature: Check for security issues, edge cases, and integration problems only.' ||
# 'Standard review: Check for critical bugs, security issues, and breaking changes only.'
# }}
# Optional: Add specific tools for running tests or linting
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
# Optional: Skip review for certain conditions
# if: |
# !contains(github.event.pull_request.title, '[skip-review]') &&
# !contains(github.event.pull_request.title, '[WIP]')

View File

@@ -1,64 +0,0 @@
name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4)
# model: "claude-opus-4-20250514"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test

View File

@@ -1,82 +0,0 @@
name: Update API Documentation
on:
push:
branches:
- main
paths:
- 'api/docs/**'
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Add permissions for GITHUB_TOKEN
permissions:
contents: write
pull-requests: write
jobs:
create-docs-pr:
runs-on: ubuntu-latest
steps:
- name: Checkout source repository
uses: actions/checkout@v5
with:
path: source-repo
- name: Checkout docs repository
uses: actions/checkout@v5
with:
repository: unraid/docs
path: docs-repo
token: ${{ secrets.DOCS_PAT_UNRAID_BOT }}
- name: Copy and process docs
run: |
if [ ! -d "source-repo/api/docs" ]; then
echo "Source directory does not exist!"
exit 1
fi
# Remove old API docs but preserve other folders
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API
# Copy all markdown files and maintain directory structure
cp -r source-repo/api/docs/public/. docs-repo/docs/API/
# Copy images to Docusaurus static directory
mkdir -p docs-repo/static/img/api
# Copy images from public/images if they exist
if [ -d "source-repo/api/docs/public/images" ]; then
cp -r source-repo/api/docs/public/images/. docs-repo/static/img/api/
fi
# Also copy any images from the parent docs/images directory
if [ -d "source-repo/api/docs/images" ]; then
cp -r source-repo/api/docs/images/. docs-repo/static/img/api/
fi
# Update image paths in markdown files
# Replace relative image paths with absolute paths pointing to /img/api/
find docs-repo/docs/API -name "*.md" -type f -exec sed -i 's|!\[\([^]]*\)\](\./images/\([^)]*\))|![\1](/img/api/\2)|g' {} \;
find docs-repo/docs/API -name "*.md" -type f -exec sed -i 's|!\[\([^]]*\)\](images/\([^)]*\))|![\1](/img/api/\2)|g' {} \;
find docs-repo/docs/API -name "*.md" -type f -exec sed -i 's|!\[\([^]]*\)\](../images/\([^)]*\))|![\1](/img/api/\2)|g' {} \;
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.DOCS_PAT_UNRAID_BOT }}
path: docs-repo
commit-message: 'docs: update API documentation'
title: 'Update API Documentation'
body: |
This PR updates the API documentation based on changes from the main repository.
Changes were automatically generated from api/docs/* directory.
@coderabbitai ignore
reviewers: ljm42, elibosley
branch: update-api-docs
base: main
delete-branch: true

View File

@@ -22,16 +22,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v5
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.18.0'
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
@@ -65,7 +66,7 @@ jobs:
- name: Comment PR with deployment URL
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({

View File

@@ -0,0 +1,210 @@
name: Generate Release Notes
on:
workflow_call:
inputs:
version:
description: 'Version number (e.g., 4.25.3)'
required: true
type: string
target_commitish:
description: 'Commit SHA or branch (leave empty for current HEAD)'
required: false
type: string
release_notes:
description: 'Custom release notes (leave empty to auto-generate)'
required: false
type: string
outputs:
release_notes:
description: 'Generated or provided release notes'
value: ${{ jobs.generate.outputs.release_notes }}
secrets:
UNRAID_BOT_GITHUB_ADMIN_TOKEN:
required: true
jobs:
generate:
name: Generate Release Notes
runs-on: ubuntu-latest
outputs:
release_notes: ${{ steps.generate_notes.outputs.release_notes }}
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.target_commitish || github.ref }}
fetch-depth: 0
token: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Generate Release Notes
id: generate_notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_NAME="v${{ inputs.version }}"
VERSION="${{ inputs.version }}"
if [ -n "${{ inputs.release_notes }}" ]; then
NOTES="${{ inputs.release_notes }}"
else
CHANGELOG_PATH="api/CHANGELOG.md"
if [ -f "$CHANGELOG_PATH" ]; then
echo "Extracting release notes from CHANGELOG.md for version ${VERSION}"
NOTES=$(awk -v ver="$VERSION" '
BEGIN {
found=0; capture=0; output="";
gsub(/\./, "\\.", ver);
}
/^## \[/ {
if (capture) exit;
if ($0 ~ "\\[" ver "\\]") {
found=1;
capture=1;
}
}
capture {
if (output != "") output = output "\n";
output = output $0;
}
END {
if (found) print output;
else exit 1;
}
' "$CHANGELOG_PATH") || EXTRACTION_STATUS=$?
if [ ${EXTRACTION_STATUS:-0} -eq 0 ] && [ -n "$NOTES" ]; then
echo "✓ Found release notes in CHANGELOG.md"
else
echo "⚠ Version ${VERSION} not found in CHANGELOG.md, generating with conventional-changelog"
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
CHANGELOG_GENERATED=false
if [ -n "$PREV_TAG" ]; then
echo "Generating changelog from ${PREV_TAG}..HEAD using conventional-changelog"
npm install -g conventional-changelog-cli
TEMP_NOTES=$(mktemp)
conventional-changelog -p conventionalcommits \
--release-count 1 \
--output-unreleased \
> "$TEMP_NOTES" 2>/dev/null || true
if [ -s "$TEMP_NOTES" ]; then
NOTES=$(cat "$TEMP_NOTES")
if [ -n "$NOTES" ]; then
echo "✓ Generated changelog with conventional-changelog"
CHANGELOG_GENERATED=true
TEMP_CHANGELOG=$(mktemp)
{
if [ -f "$CHANGELOG_PATH" ]; then
head -n 1 "$CHANGELOG_PATH"
echo ""
echo "$NOTES"
echo ""
tail -n +2 "$CHANGELOG_PATH"
else
echo "# Changelog"
echo ""
echo "$NOTES"
fi
} > "$TEMP_CHANGELOG"
mv "$TEMP_CHANGELOG" "$CHANGELOG_PATH"
echo "✓ Updated CHANGELOG.md with generated notes"
else
echo "⚠ conventional-changelog produced empty output, using GitHub auto-generation"
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${TAG_NAME}" \
-f target_commitish="${{ inputs.target_commitish || github.sha }}" \
-f previous_tag_name="${PREV_TAG}" \
--jq '.body')
fi
else
echo "⚠ conventional-changelog failed, using GitHub auto-generation"
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${TAG_NAME}" \
-f target_commitish="${{ inputs.target_commitish || github.sha }}" \
-f previous_tag_name="${PREV_TAG}" \
--jq '.body')
fi
rm -f "$TEMP_NOTES"
else
echo "⚠ No previous tag found, using GitHub auto-generation"
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${TAG_NAME}" \
-f target_commitish="${{ inputs.target_commitish || github.sha }}" \
--jq '.body' || echo "Release ${VERSION}")
fi
if [ "$CHANGELOG_GENERATED" = true ]; then
BRANCH_OR_SHA="${{ inputs.target_commitish || github.ref }}"
if git show-ref --verify --quiet "refs/heads/${BRANCH_OR_SHA}"; then
echo ""
echo "=========================================="
echo "CHANGELOG GENERATED AND COMMITTED"
echo "=========================================="
echo ""
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BEFORE_SHA=$(git rev-parse HEAD)
git add "$CHANGELOG_PATH"
git commit -m "chore: add changelog for version ${VERSION}"
git push origin "HEAD:${BRANCH_OR_SHA}"
AFTER_SHA=$(git rev-parse HEAD)
echo "✓ Changelog committed and pushed successfully"
echo ""
echo "Previous SHA: ${BEFORE_SHA}"
echo "New SHA: ${AFTER_SHA}"
echo ""
echo "⚠️ CRITICAL: A new commit was created, but github.sha is immutable."
echo "⚠️ github.sha = ${BEFORE_SHA} (original workflow trigger)"
echo "⚠️ The release tag must point to ${AFTER_SHA} (with changelog)"
echo ""
echo "Re-run this workflow to create the release with the correct commit."
echo ""
exit 1
else
echo "⚠ Target is a commit SHA, not a branch. Cannot push changelog updates."
echo "Changelog was generated but not committed."
fi
fi
fi
else
echo "⚠ CHANGELOG.md not found, using GitHub auto-generation"
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
NOTES=$(gh api repos/${{ github.repository }}/releases/generate-notes \
-f tag_name="${TAG_NAME}" \
-f target_commitish="${{ inputs.target_commitish || github.sha }}" \
-f previous_tag_name="${PREV_TAG}" \
--jq '.body')
else
NOTES="Release ${VERSION}"
fi
fi
fi
echo "release_notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT

View File

@@ -6,29 +6,15 @@ on:
branches:
- main
permissions:
contents: write
pull-requests: write
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v5
# Only run release-please on pushes to main
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- id: release
uses: googleapis/release-please-action@v4
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
releases_created: ${{ steps.release.outputs.releases_created || 'false' }}
tag_name: ${{ steps.release.outputs.tag_name || '' }}
test-api:
name: Test API
defaults:
@@ -38,36 +24,25 @@ jobs:
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
version: 1.0
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
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
- name: Install Node
uses: actions/setup-node@v5
with:
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
node-version-file: ".nvmrc"
cache: 'pnpm'
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system php-cli
version: 1.0
- name: PNPM Install
run: pnpm install --frozen-lockfile
@@ -117,265 +92,113 @@ jobs:
# Verify libvirt is running using sudo to bypass group membership delays
sudo virsh list --all || true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Build UI Package First
run: |
echo "🔧 Building UI package for web tests dependency..."
cd ../unraid-ui && pnpm run build
- name: Run Tests Concurrently
run: |
set -e
# Run all tests in parallel with labeled output
# Run all tests in parallel with labeled output and coverage generation
echo "🚀 Starting API coverage tests..."
pnpm run coverage > api-test.log 2>&1 &
API_PID=$!
echo "🚀 Starting Connect plugin tests..."
(cd ../packages/unraid-api-plugin-connect && pnpm test) > connect-test.log 2>&1 &
(cd ../packages/unraid-api-plugin-connect && pnpm test --coverage 2>/dev/null || pnpm test) > connect-test.log 2>&1 &
CONNECT_PID=$!
echo "🚀 Starting Shared package tests..."
(cd ../packages/unraid-shared && pnpm test) > shared-test.log 2>&1 &
(cd ../packages/unraid-shared && pnpm test --coverage 2>/dev/null || pnpm test) > shared-test.log 2>&1 &
SHARED_PID=$!
echo "🚀 Starting Web package coverage tests..."
(cd ../web && (pnpm test --coverage || pnpm test)) > web-test.log 2>&1 &
WEB_PID=$!
echo "🚀 Starting UI package coverage tests..."
(cd ../unraid-ui && pnpm test --coverage 2>/dev/null || pnpm test) > ui-test.log 2>&1 &
UI_PID=$!
echo "🚀 Starting Plugin tests..."
(cd ../plugin && pnpm test) > plugin-test.log 2>&1 &
PLUGIN_PID=$!
# Wait for all processes and capture exit codes
wait $API_PID && echo "✅ API tests completed" || { echo "❌ API tests failed"; API_EXIT=1; }
wait $CONNECT_PID && echo "✅ Connect tests completed" || { echo "❌ Connect tests failed"; CONNECT_EXIT=1; }
wait $SHARED_PID && echo "✅ Shared tests completed" || { echo "❌ Shared tests failed"; SHARED_EXIT=1; }
wait $WEB_PID && echo "✅ Web tests completed" || { echo "❌ Web tests failed"; WEB_EXIT=1; }
wait $UI_PID && echo "✅ UI tests completed" || { echo "❌ UI tests failed"; UI_EXIT=1; }
wait $PLUGIN_PID && echo "✅ Plugin tests completed" || { echo "❌ Plugin tests failed"; PLUGIN_EXIT=1; }
# Display all outputs
echo "📋 API Test Results:" && cat api-test.log
echo "📋 Connect Plugin Test Results:" && cat connect-test.log
echo "📋 Shared Package Test Results:" && cat shared-test.log
echo "📋 Web Package Test Results:" && cat web-test.log
echo "📋 UI Package Test Results:" && cat ui-test.log
echo "📋 Plugin Test Results:" && cat plugin-test.log
# Exit with error if any test failed
if [[ ${API_EXIT:-0} -eq 1 || ${CONNECT_EXIT:-0} -eq 1 || ${SHARED_EXIT:-0} -eq 1 ]]; then
if [[ ${API_EXIT:-0} -eq 1 || ${CONNECT_EXIT:-0} -eq 1 || ${SHARED_EXIT:-0} -eq 1 || ${WEB_EXIT:-0} -eq 1 || ${UI_EXIT:-0} -eq 1 || ${PLUGIN_EXIT:-0} -eq 1 ]]; then
exit 1
fi
build-api:
name: Build API
- name: Upload all coverage reports to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/coverage-final.json,../web/coverage/coverage-final.json,../unraid-ui/coverage/coverage-final.json,../packages/unraid-api-plugin-connect/coverage/coverage-final.json,../packages/unraid-shared/coverage/coverage-final.json
fail_ci_if_error: false
build-artifacts:
name: Build All Artifacts
uses: ./.github/workflows/build-artifacts.yml
secrets:
VITE_ACCOUNT: ${{ secrets.VITE_ACCOUNT }}
VITE_CONNECT: ${{ secrets.VITE_CONNECT }}
VITE_UNRAID_NET: ${{ secrets.VITE_UNRAID_NET }}
VITE_CALLBACK_KEY: ${{ secrets.VITE_CALLBACK_KEY }}
UNRAID_BOT_GITHUB_ADMIN_TOKEN: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
release-please:
name: Release Please
runs-on: ubuntu-latest
# Only run on pushes to main AND after tests pass
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- test-api
- build-artifacts
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- id: release
uses: googleapis/release-please-action@v4
outputs:
build_number: ${{ steps.buildnumber.outputs.build_number }}
defaults:
run:
working-directory: api
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
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('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: PNPM Install
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile
- name: Build
run: pnpm run build
- name: Get Git Short Sha and API version
id: vars
run: |
GIT_SHA=$(git rev-parse --short HEAD)
IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '')
PACKAGE_LOCK_VERSION=$(jq -r '.version' package.json)
API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")
export API_VERSION
echo "API_VERSION=${API_VERSION}" >> $GITHUB_ENV
echo "PACKAGE_LOCK_VERSION=${PACKAGE_LOCK_VERSION}" >> $GITHUB_OUTPUT
- name: Generate build number
id: buildnumber
uses: onyxmueller/build-tag-number@v1
with:
token: ${{secrets.github_token}}
prefix: ${{steps.vars.outputs.PACKAGE_LOCK_VERSION}}
- name: Build
run: |
pnpm run build:release
tar -czf deploy/unraid-api.tgz -C deploy/pack/ .
- name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v4
with:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/unraid-api.tgz
build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version)
defaults:
run:
working-directory: unraid-ui
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
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('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: Install dependencies
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/ui
- name: Lint
run: pnpm run lint
- name: Build
run: pnpm run build:wc
- name: Upload Artifact to Github
uses: actions/upload-artifact@v4
with:
name: unraid-wc-ui
path: unraid-ui/dist-wc/
build-web:
# needs: [build-unraid-ui]
name: Build Web App
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ secrets.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ secrets.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ secrets.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ secrets.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
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('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: PNPM Install
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/web --filter @unraid/ui
- name: Build Unraid UI
run: |
cd ${{ github.workspace }}/unraid-ui
pnpm run build
- name: Lint files
run: pnpm run lint
- name: Type Check
run: pnpm run type-check
- name: Test
run: pnpm run test:ci
- name: Build
run: pnpm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v4
with:
name: unraid-wc-rich
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
releases_created: ${{ steps.release.outputs.releases_created || 'false' }}
tag_name: ${{ steps.release.outputs.tag_name || '' }}
build-plugin-staging-pr:
name: Build and Deploy Plugin
needs:
- release-please
- build-api
- build-web
- build-unraid-ui-webcomponents
- build-artifacts
- test-api
uses: ./.github/workflows/build-plugin.yml
with:
RELEASE_CREATED: false
RELEASE_CREATED: 'false'
TAG: ${{ github.event.pull_request.number && format('PR{0}', github.event.pull_request.number) || '' }}
BUCKET_PATH: ${{ github.event.pull_request.number && format('unraid-api/tag/PR{0}', github.event.pull_request.number) || 'unraid-api' }}
BASE_URL: "https://preview.dl.unraid.net/unraid-api"
BUILD_NUMBER: ${{ needs.build-api.outputs.build_number }}
BUILD_NUMBER: ${{ needs.build-artifacts.outputs.build_number }}
secrets:
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
@@ -387,20 +210,19 @@ jobs:
name: Build and Deploy Production Plugin
needs:
- release-please
- build-api
- build-web
- build-unraid-ui-webcomponents
- test-api
- build-artifacts
uses: ./.github/workflows/build-plugin.yml
with:
RELEASE_CREATED: true
RELEASE_CREATED: 'true'
RELEASE_TAG: ${{ needs.release-please.outputs.tag_name }}
TAG: ""
BUCKET_PATH: unraid-api
BASE_URL: "https://stable.dl.unraid.net/unraid-api"
BUILD_NUMBER: ${{ needs.build-api.outputs.build_number }}
BUILD_NUMBER: ${{ needs.build-artifacts.outputs.build_number }}
TRIGGER_PRODUCTION_RELEASE: true
secrets:
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }}
CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
UNRAID_BOT_GITHUB_ADMIN_TOKEN: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}

239
.github/workflows/manual-release.yml vendored Normal file
View File

@@ -0,0 +1,239 @@
name: Manual Release
on:
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., 4.25.3)'
required: true
type: string
target_commitish:
description: 'Commit SHA or branch (leave empty for current HEAD)'
required: false
type: string
release_notes:
description: 'Release notes/changelog (leave empty to auto-generate from commits)'
required: false
type: string
prerelease:
description: 'Mark as prerelease'
required: false
type: boolean
default: false
permissions:
contents: write
pull-requests: write
jobs:
validate-version:
name: Validate and Update Package Versions
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.target_commitish || github.ref }}
fetch-depth: 0
token: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Check and Update Package Versions
run: |
EXPECTED_VERSION="${{ inputs.version }}"
MISMATCHES_FOUND=false
PACKAGE_JSONS=(
"package.json"
"api/package.json"
"web/package.json"
"unraid-ui/package.json"
"plugin/package.json"
"packages/unraid-shared/package.json"
"packages/unraid-api-plugin-health/package.json"
"packages/unraid-api-plugin-generator/package.json"
"packages/unraid-api-plugin-connect/package.json"
)
echo "Checking package.json versions against expected version: ${EXPECTED_VERSION}"
for pkg in "${PACKAGE_JSONS[@]}"; do
if [ -f "$pkg" ]; then
CURRENT_VERSION=$(node -p "require('./$pkg').version")
if [ "$CURRENT_VERSION" != "$EXPECTED_VERSION" ]; then
echo "❌ Version mismatch in $pkg: $CURRENT_VERSION != $EXPECTED_VERSION"
MISMATCHES_FOUND=true
# Detect indentation by checking the first property line
INDENT_SPACES=$(head -10 "$pkg" | grep '^ *"' | head -1 | sed 's/".*//g' | wc -c)
INDENT_SPACES=$((INDENT_SPACES - 1))
jq --indent "$INDENT_SPACES" --arg version "$EXPECTED_VERSION" '.version = $version' "$pkg" > "$pkg.tmp" && mv "$pkg.tmp" "$pkg"
echo "✓ Updated $pkg to version $EXPECTED_VERSION"
else
echo "✓ $pkg version matches: $CURRENT_VERSION"
fi
fi
done
if [ "$MISMATCHES_FOUND" = true ]; then
echo ""
echo "=========================================="
echo "Version mismatches found!"
echo "=========================================="
echo ""
BRANCH_OR_SHA="${{ inputs.target_commitish || github.ref }}"
if git show-ref --verify --quiet "refs/heads/${BRANCH_OR_SHA}"; then
echo "Creating commit with version updates and pushing to branch: ${BRANCH_OR_SHA}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BEFORE_SHA=$(git rev-parse HEAD)
git add ${PACKAGE_JSONS[@]}
git commit -m "chore: update package versions to ${{ inputs.version }}"
git push origin "HEAD:${BRANCH_OR_SHA}"
AFTER_SHA=$(git rev-parse HEAD)
echo ""
echo "=========================================="
echo "WORKFLOW MUST BE RE-RUN"
echo "=========================================="
echo ""
echo "✓ Version updates committed and pushed successfully"
echo ""
echo "Previous SHA: ${BEFORE_SHA}"
echo "New SHA: ${AFTER_SHA}"
echo ""
echo "⚠️ CRITICAL: A new commit was created, but github.sha is immutable."
echo "⚠️ github.sha = ${BEFORE_SHA} (original workflow trigger)"
echo "⚠️ The release tag must point to ${AFTER_SHA} (with version updates)"
echo ""
echo "Re-run this workflow to create the release with the correct commit."
echo ""
exit 1
else
echo "Target is a commit SHA, not a branch. Cannot push version updates."
echo "Please update the package.json versions manually and re-run the workflow."
exit 1
fi
fi
echo ""
echo "✓ All package.json versions match the expected version: ${EXPECTED_VERSION}"
build-artifacts:
name: Build All Artifacts
needs:
- validate-version
uses: ./.github/workflows/build-artifacts.yml
with:
ref: ${{ inputs.target_commitish || github.ref }}
version_override: ${{ inputs.version }}
secrets:
VITE_ACCOUNT: ${{ secrets.VITE_ACCOUNT }}
VITE_CONNECT: ${{ secrets.VITE_CONNECT }}
VITE_UNRAID_NET: ${{ secrets.VITE_UNRAID_NET }}
VITE_CALLBACK_KEY: ${{ secrets.VITE_CALLBACK_KEY }}
UNRAID_BOT_GITHUB_ADMIN_TOKEN: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
generate-release-notes:
name: Generate Release Notes
needs:
- build-artifacts
uses: ./.github/workflows/generate-release-notes.yml
with:
version: ${{ inputs.version }}
target_commitish: ${{ inputs.target_commitish || github.ref }}
release_notes: ${{ inputs.release_notes }}
secrets:
UNRAID_BOT_GITHUB_ADMIN_TOKEN: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
create-release:
name: Create GitHub Release (Draft)
runs-on: ubuntu-latest
needs:
- generate-release-notes
outputs:
tag_name: ${{ steps.create_release.outputs.tag_name }}
release_notes: ${{ needs.generate-release-notes.outputs.release_notes }}
steps:
- name: Checkout repo
uses: actions/checkout@v5
with:
ref: ${{ inputs.target_commitish || github.ref }}
fetch-depth: 0
- name: Create or Update Release as Draft
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_NAME="v${{ inputs.version }}"
TARGET="${{ inputs.target_commitish || github.sha }}"
echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT
if gh release view "${TAG_NAME}" > /dev/null 2>&1; then
echo "Release ${TAG_NAME} already exists, updating as draft..."
gh release edit "${TAG_NAME}" \
--draft \
--notes "${{ needs.generate-release-notes.outputs.release_notes }}" \
${{ inputs.prerelease && '--prerelease' || '' }}
else
echo "Creating new draft release ${TAG_NAME}..."
git tag "${TAG_NAME}" "${TARGET}" || true
git push origin "${TAG_NAME}" || true
gh release create "${TAG_NAME}" \
--draft \
--title "${{ inputs.version }}" \
--notes "${{ needs.generate-release-notes.outputs.release_notes }}" \
--target "${TARGET}" \
${{ inputs.prerelease && '--prerelease' || '' }}
fi
build-plugin-production:
name: Build and Deploy Production Plugin
needs:
- create-release
- build-artifacts
uses: ./.github/workflows/build-plugin.yml
with:
RELEASE_CREATED: 'true'
RELEASE_TAG: ${{ needs.create-release.outputs.tag_name }}
TAG: ""
BUCKET_PATH: unraid-api
BASE_URL: "https://stable.dl.unraid.net/unraid-api"
BUILD_NUMBER: ${{ needs.build-artifacts.outputs.build_number }}
ref: ${{ inputs.target_commitish || github.ref }}
secrets:
CF_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
CF_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
CF_BUCKET_PREVIEW: ${{ secrets.CF_BUCKET_PREVIEW }}
CF_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
UNRAID_BOT_GITHUB_ADMIN_TOKEN: ${{ secrets.UNRAID_BOT_GITHUB_ADMIN_TOKEN }}
publish-release:
name: Publish Release
runs-on: ubuntu-latest
needs:
- create-release
- build-plugin-production
steps:
- name: Publish Release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG_NAME="${{ needs.create-release.outputs.tag_name }}"
echo "Publishing release ${TAG_NAME}..."
gh release edit "${TAG_NAME}" --draft=false --repo ${{ github.repository }}

30
.github/workflows/publish-schema.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Publish GraphQL Schema
on:
push:
branches:
- main
paths:
- 'api/generated-schema.graphql'
workflow_dispatch:
jobs:
publish-schema:
name: Publish Schema to Apollo Studio
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v5
- name: Install Apollo Rover CLI
run: |
curl -sSL https://rover.apollo.dev/nix/latest | sh
echo "$HOME/.rover/bin" >> $GITHUB_PATH
- name: Publish schema to Apollo Studio
env:
APOLLO_KEY: ${{ secrets.APOLLO_KEY }}
run: |
rover graph publish Unraid-API@current \
--schema api/generated-schema.graphql

View File

@@ -1,4 +1,9 @@
name: Push Staging Plugin on PR Close
name: Replace PR Plugin with Staging Redirect on Merge
# This workflow runs when a PR is merged and replaces the PR-specific plugin
# with a redirect version that points to the main staging URL.
# This ensures users who installed the PR version will automatically
# update to the staging version on their next update check.
on:
pull_request:
@@ -17,18 +22,13 @@ on:
default: true
jobs:
push-staging:
push-staging-redirect:
if: (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && inputs.pr_merged == true)
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- name: Set Timezone
uses: szenius/set-timezone@v2.0
with:
timezoneLinux: "America/Los_Angeles"
- name: Set PR number
id: pr_number
run: |
@@ -45,11 +45,12 @@ jobs:
name: unraid-plugin-.*
path: connect-files
pr: ${{ steps.pr_number.outputs.pr_number }}
workflow: main.yml
workflow_conclusion: success
workflow_search: true
search_artifacts: true
if_no_artifact_found: fail
- name: Update Downloaded Staging Plugin to New Date
- name: Update Downloaded Plugin to Redirect to Staging
run: |
# Find the .plg file in the downloaded artifact
plgfile=$(find connect-files -name "*.plg" -type f | head -1)
@@ -60,23 +61,82 @@ jobs:
fi
echo "Found plugin file: $plgfile"
version=$(date +"%Y.%m.%d.%H%M")
sed -i -E "s#(<!ENTITY version \").*(\">)#\1${version}\2#g" "${plgfile}" || exit 1
# Get current version and bump it with current timestamp
current_version=$(grep '<!ENTITY version' "${plgfile}" | sed -E 's/.*"(.*)".*/\1/')
echo "Current version: ${current_version}"
# Create new version with current timestamp (ensures it's newer)
new_version=$(date +"%Y.%m.%d.%H%M")
echo "New redirect version: ${new_version}"
# Update version to trigger update
sed -i -E "s#(<!ENTITY version \").*(\">)#\1${new_version}\2#g" "${plgfile}" || exit 1
# Change the plugin url to point to staging
# Change the plugin url to point to staging - users will switch to staging on next update
url="https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg"
sed -i -E "s#(<!ENTITY plugin_url \").*?(\">)#\1${url}\2#g" "${plgfile}" || exit 1
cat "${plgfile}"
echo "Modified plugin to redirect to: ${url}"
echo "Version bumped from ${current_version} to ${new_version}"
mkdir -p pr-release
mv "${plgfile}" pr-release/dynamix.unraid.net.plg
- name: Upload to Cloudflare
uses: jakejarvis/s3-sync-action@v0.5.1
- name: Clean up old PR artifacts from Cloudflare
env:
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: "auto"
SOURCE_DIR: pr-release
DEST_DIR: unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}
AWS_DEFAULT_REGION: auto
run: |
# Delete all existing files in the PR directory first (txz, plg, etc.)
aws s3 rm s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/ \
--recursive \
--endpoint-url ${{ secrets.CF_ENDPOINT }}
echo "✅ Cleaned up old PR artifacts"
- name: Upload PR Redirect Plugin to Cloudflare
env:
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: auto
run: |
# Upload only the redirect plugin file
aws s3 cp pr-release/dynamix.unraid.net.plg \
s3://${{ secrets.CF_BUCKET_PREVIEW }}/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/dynamix.unraid.net.plg \
--endpoint-url ${{ secrets.CF_ENDPOINT }} \
--content-encoding none \
--acl public-read
echo "✅ Uploaded redirect plugin"
- name: Output redirect information
run: |
echo "✅ PR plugin replaced with staging redirect version"
echo "PR URL remains: https://preview.dl.unraid.net/unraid-api/tag/PR${{ steps.pr_number.outputs.pr_number }}/dynamix.unraid.net.plg"
echo "Redirects users to staging: https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg"
echo "Users updating from this PR version will automatically switch to staging"
- name: Comment on PR about staging redirect
if: github.event_name == 'pull_request'
uses: thollander/actions-comment-pull-request@v3
with:
comment-tag: pr-closed-staging
mode: recreate
message: |
## 🔄 PR Merged - Plugin Redirected to Staging
This PR has been merged and the preview plugin has been updated to redirect to the staging version.
**For users testing this PR:**
- Your plugin will automatically update to the staging version on the next update check
- The staging version includes all merged changes from this PR
- No manual intervention required
**Staging URL:**
```
https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg
```
Thank you for testing! 🚀

View File

@@ -28,16 +28,16 @@ jobs:
with:
latest: true
prerelease: false
- uses: actions/setup-node@v4
- uses: actions/setup-node@v5
with:
node-version: '22.18.0'
node-version: 22.19.0
- run: |
cat << 'EOF' > release-notes.txt
${{ steps.release-info.outputs.body }}
EOF
- run: npm install html-escaper@2 xml2js
- name: Update Plugin Changelog
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
@@ -124,3 +124,22 @@ jobs:
--no-guess-mime-type \
--content-encoding none \
--acl public-read
- name: Discord Webhook Notification
uses: tsickert/discord-webhook@v7.0.0
with:
webhook-url: ${{ secrets.PUBLIC_DISCORD_RELEASE_ENDPOINT }}
username: "Unraid API Bot"
avatar-url: "https://craftassets.unraid.net/uploads/logos/un-mark-gradient.png"
embed-title: "🚀 Unraid API ${{ inputs.version }} Released!"
embed-url: "https://github.com/${{ github.repository }}/releases/tag/${{ inputs.version }}"
embed-description: |
A new version of Unraid API has been released!
**Version:** `${{ inputs.version }}`
**Release Page:** [View on GitHub](https://github.com/${{ github.repository }}/releases/tag/${{ inputs.version }})
**📋 Changelog:**
${{ steps.release-info.outputs.body }}
embed-color: 16734296
embed-footer-text: "Unraid API • Automated Release"

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

8
.gitignore vendored
View File

@@ -29,6 +29,10 @@ unraid-ui/node_modules/
# TypeScript v1 declaration files
typings/
# Auto-generated type declarations for Nuxt UI
auto-imports.d.ts
components.d.ts
# Optional npm cache directory
.npm
@@ -118,3 +122,7 @@ api/dev/Unraid.net/myservers.cfg
# local Mise settings
.mise.toml
# Compiled test pages (generated from Nunjucks templates)
web/public/test-pages/*.html

View File

@@ -1 +1 @@
{".":"4.18.0"}
{".":"4.26.2"}

View File

@@ -1,7 +1,8 @@
@custom-variant dark (&:where(.dark, .dark *));
@layer utilities {
:host {
/* Utility defaults for web components (when we were using shadow DOM) */
:host,
.unapi {
--tw-divide-y-reverse: 0;
--tw-border-style: solid;
--tw-font-weight: initial;
@@ -48,21 +49,20 @@
--tw-drop-shadow: initial;
--tw-duration: initial;
--tw-ease: initial;
}
}
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: hsl(var(--border));
}
/* Global border color - this is what's causing the issue! */
/* Commenting out since it affects all elements globally
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: hsl(var(--border));
}
*/
body {
.unapi {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
@@ -74,8 +74,25 @@
--ring-shadow: 0 0 var(--color-beta);
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
}
.unapi button:not(:disabled),
.unapi [role='button']:not(:disabled) {
cursor: pointer;
}
/* Font size overrides for SSO button component */
.unapi unraid-sso-button,
unraid-sso-button.unapi {
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--text-4xl: 2.25rem;
--text-5xl: 3rem;
--text-6xl: 3.75rem;
--text-7xl: 4.5rem;
--text-8xl: 6rem;
--text-9xl: 8rem;
}

View File

@@ -1,7 +1,61 @@
/* Hybrid theme system: Native CSS + Theme Store fallback */
@layer base {
/* Light mode defaults */
:root {
/* Light mode defaults */
:root {
/* Nuxt UI Color System - Primary (Orange for Unraid) */
--ui-color-primary-50: #fff7ed;
--ui-color-primary-100: #ffedd5;
--ui-color-primary-200: #fed7aa;
--ui-color-primary-300: #fdba74;
--ui-color-primary-400: #fb923c;
--ui-color-primary-500: #ff8c2f;
--ui-color-primary-600: #ea580c;
--ui-color-primary-700: #c2410c;
--ui-color-primary-800: #9a3412;
--ui-color-primary-900: #7c2d12;
--ui-color-primary-950: #431407;
/* Nuxt UI Color System - Neutral (True Gray) */
--ui-color-neutral-50: #fafafa;
--ui-color-neutral-100: #f5f5f5;
--ui-color-neutral-200: #e5e5e5;
--ui-color-neutral-300: #d4d4d4;
--ui-color-neutral-400: #a3a3a3;
--ui-color-neutral-500: #737373;
--ui-color-neutral-600: #525252;
--ui-color-neutral-700: #404040;
--ui-color-neutral-800: #262626;
--ui-color-neutral-900: #171717;
--ui-color-neutral-950: #0a0a0a;
/* Nuxt UI Default color shades */
--ui-primary: var(--ui-color-primary-500);
--ui-secondary: var(--ui-color-neutral-500);
/* Nuxt UI Design Tokens - Text */
--ui-text-dimmed: var(--ui-color-neutral-400);
--ui-text-muted: var(--ui-color-neutral-500);
--ui-text-toned: var(--ui-color-neutral-600);
--ui-text: var(--ui-color-neutral-700);
--ui-text-highlighted: var(--ui-color-neutral-900);
--ui-text-inverted: white;
/* Nuxt UI Design Tokens - Background */
--ui-bg: white;
--ui-bg-muted: var(--ui-color-neutral-50);
--ui-bg-elevated: var(--ui-color-neutral-100);
--ui-bg-accented: var(--ui-color-neutral-200);
--ui-bg-inverted: var(--ui-color-neutral-900);
/* Nuxt UI Design Tokens - Border */
--ui-border: var(--ui-color-neutral-200);
--ui-border-muted: var(--ui-color-neutral-200);
--ui-border-accented: var(--ui-color-neutral-300);
--ui-border-inverted: var(--ui-color-neutral-900);
/* Nuxt UI Radius */
--ui-radius: 0.5rem;
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--muted: 0 0% 96.1%;
@@ -12,7 +66,7 @@
--card-foreground: 0 0% 3.9%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--primary: 0 0% 9%;
--primary: 24 100% 50%; /* Orange #ff8c2f in HSL */
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
@@ -20,7 +74,7 @@
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
--ring: 24 100% 50%; /* Orange ring to match primary */
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
@@ -30,6 +84,31 @@
/* Dark mode */
.dark {
/* Nuxt UI Default color shades - Dark mode */
--ui-primary: var(--ui-color-primary-400);
--ui-secondary: var(--ui-color-neutral-400);
/* Nuxt UI Design Tokens - Text (Dark) */
--ui-text-dimmed: var(--ui-color-neutral-500);
--ui-text-muted: var(--ui-color-neutral-400);
--ui-text-toned: var(--ui-color-neutral-300);
--ui-text: var(--ui-color-neutral-200);
--ui-text-highlighted: white;
--ui-text-inverted: var(--ui-color-neutral-900);
/* Nuxt UI Design Tokens - Background (Dark) */
--ui-bg: var(--ui-color-neutral-900);
--ui-bg-muted: var(--ui-color-neutral-800);
--ui-bg-elevated: var(--ui-color-neutral-800);
--ui-bg-accented: var(--ui-color-neutral-700);
--ui-bg-inverted: white;
/* Nuxt UI Design Tokens - Border (Dark) */
--ui-border: var(--ui-color-neutral-800);
--ui-border-muted: var(--ui-color-neutral-700);
--ui-border-accented: var(--ui-color-neutral-700);
--ui-border-inverted: white;
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
@@ -40,15 +119,15 @@
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--primary: 24 100% 50%; /* Orange #ff8c2f in HSL */
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--ring: 24 100% 50%; /* Orange ring to match primary */
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
@@ -62,69 +141,4 @@
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--border: 0 0% 14.9%;
}
/* For web components: inherit CSS variables from the host */
:host {
--background: inherit;
--foreground: inherit;
--muted: inherit;
--muted-foreground: inherit;
--popover: inherit;
--popover-foreground: inherit;
--card: inherit;
--card-foreground: inherit;
--border: inherit;
--input: inherit;
--primary: inherit;
--primary-foreground: inherit;
--secondary: inherit;
--secondary-foreground: inherit;
--accent: inherit;
--accent-foreground: inherit;
--destructive: inherit;
--destructive-foreground: inherit;
--ring: inherit;
--chart-1: inherit;
--chart-2: inherit;
--chart-3: inherit;
--chart-4: inherit;
--chart-5: inherit;
}
/* Class-based dark mode support for web components using :host-context */
:host-context(.dark) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--popover: 0 0% 3.9%;
--popover-foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
/* Alternative class-based dark mode support for specific Unraid themes */
:host-context(.dark[data-theme='black']),
:host-context(.dark[data-theme='gray']) {
--background: 0 0% 3.9%;
--foreground: 0 0% 98%;
--border: 0 0% 14.9%;
}
}
}

View File

@@ -1,5 +1,5 @@
/* Tailwind Shared Styles - Single entry point for all shared CSS */
@import './css-variables.css';
@import './unraid-theme.css';
@import './theme-variants.css';
@import './base-utilities.css';
@import './sonner.css';

View File

@@ -1,665 +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;
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(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(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%);
}

View File

@@ -0,0 +1,97 @@
/**
* Tailwind v4 Theme Variants
* Defines theme-specific CSS variables that can be switched via classes
* These are applied dynamically based on the theme selected in GraphQL
*/
/* Default/White Theme */
.Theme--white {
--header-text-primary: #ffffff;
--header-text-secondary: #999999;
--header-background-color: #1c1b1b;
--header-gradient-start: rgba(28, 27, 27, 0);
--header-gradient-end: rgba(28, 27, 27, 0.7);
--color-border: #383735;
--color-alpha: #ff8c2f;
--color-beta: #1c1b1b;
--color-gamma: #ffffff;
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
}
/* Black Theme */
.Theme--black,
.Theme--black.dark {
--header-text-primary: #1c1b1b;
--header-text-secondary: #999999;
--header-background-color: #f2f2f2;
--header-gradient-start: rgba(242, 242, 242, 0);
--header-gradient-end: rgba(242, 242, 242, 0.7);
--color-border: #e0e0e0;
--color-alpha: #ff8c2f;
--color-beta: #f2f2f2;
--color-gamma: #1c1b1b;
--color-gamma-opaque: rgba(28, 27, 27, 0.3);
}
/* Gray Theme */
.Theme--gray {
--header-text-primary: #ffffff;
--header-text-secondary: #999999;
--header-background-color: #1c1b1b;
--header-gradient-start: rgba(28, 27, 27, 0);
--header-gradient-end: rgba(28, 27, 27, 0.7);
--color-border: #383735;
--color-alpha: #ff8c2f;
--color-beta: #383735;
--color-gamma: #ffffff;
--color-gamma-opaque: rgba(255, 255, 255, 0.3);
}
/* Azure Theme */
.Theme--azure {
--header-text-primary: #1c1b1b;
--header-text-secondary: #999999;
--header-background-color: #f2f2f2;
--header-gradient-start: rgba(242, 242, 242, 0);
--header-gradient-end: rgba(242, 242, 242, 0.7);
--color-border: #5a8bb8;
--color-alpha: #ff8c2f;
--color-beta: #e7f2f8;
--color-gamma: #336699;
--color-gamma-opaque: rgba(51, 102, 153, 0.3);
}
/* Dark Mode Overrides */
.dark {
--color-border: #383735;
}
/*
* Dynamic color variables for user overrides from GraphQL
* These are set via JavaScript and override the theme defaults
* Using :root with class for higher specificity to override theme classes
*/
:root.has-custom-header-text {
--header-text-primary: var(--custom-header-text-primary);
--color-header-text-primary: var(--custom-header-text-primary);
}
:root.has-custom-header-meta {
--header-text-secondary: var(--custom-header-text-secondary);
--color-header-text-secondary: var(--custom-header-text-secondary);
}
:root.has-custom-header-bg,
.has-custom-header-bg.Theme--black,
.has-custom-header-bg.Theme--black.dark,
.has-custom-header-bg.Theme--white,
.has-custom-header-bg.Theme--white.dark,
.has-custom-header-bg.Theme--gray,
.has-custom-header-bg.Theme--azure {
--header-background-color: var(--custom-header-background-color);
--color-header-background: var(--custom-header-background-color);
--header-gradient-start: var(--custom-header-gradient-start);
--header-gradient-end: var(--custom-header-gradient-end);
--color-header-gradient-start: var(--custom-header-gradient-start);
--color-header-gradient-end: var(--custom-header-gradient-end);
}

View File

@@ -84,23 +84,23 @@
--color-primary-900: #7c2d12;
--color-primary-950: #431407;
/* Header colors */
--color-header-text-primary: var(--header-text-primary);
--color-header-text-secondary: var(--header-text-secondary);
--color-header-background-color: var(--header-background-color);
/* Header colors - defaults will be overridden by theme */
--color-header-text-primary: var(--header-text-primary, #1c1c1c);
--color-header-text-secondary: var(--header-text-secondary, #999999);
--color-header-background: var(--header-background-color, #f2f2f2);
/* Legacy colors */
--color-alpha: var(--color-alpha);
--color-beta: var(--color-beta);
--color-gamma: var(--color-gamma);
--color-gamma-opaque: var(--color-gamma-opaque);
--color-customgradient-start: var(--color-customgradient-start);
--color-customgradient-end: var(--color-customgradient-end);
/* Legacy colors - defaults (overridden by theme-variants.css) */
--color-alpha: #ff8c2f;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-gamma-opaque: rgba(153, 153, 153, 0.5);
--color-customgradient-start: rgba(242, 242, 242, 0);
--color-customgradient-end: rgba(242, 242, 242, 0.85);
/* Gradients */
--color-header-gradient-start: var(--header-gradient-start);
--color-header-gradient-end: var(--header-gradient-end);
--color-banner-gradient: var(--banner-gradient);
/* Gradients - defaults (overridden by theme-variants.css) */
--color-header-gradient-start: rgba(242, 242, 242, 0);
--color-header-gradient-end: rgba(242, 242, 242, 0.85);
--color-banner-gradient: none;
/* Font sizes */
--font-10px: 10px;
@@ -167,6 +167,27 @@
--max-width-800px: 800px;
--max-width-1024px: 1024px;
/* Container sizes adjusted for 10px base font size (1.6x scale) */
--container-xs: 32rem;
--container-sm: 38.4rem;
--container-md: 44.8rem;
--container-lg: 51.2rem;
--container-xl: 57.6rem;
--container-2xl: 67.2rem;
--container-3xl: 76.8rem;
--container-4xl: 89.6rem;
--container-5xl: 102.4rem;
--container-6xl: 115.2rem;
--container-7xl: 128rem;
/* Extended width scale for max-w-* utilities */
--width-5xl: 102.4rem;
--width-6xl: 115.2rem;
--width-7xl: 128rem;
--width-8xl: 140.8rem;
--width-9xl: 153.6rem;
--width-10xl: 166.4rem;
/* Animations */
--animate-mark-2: mark-2 1.5s ease infinite;
--animate-mark-3: mark-3 1.5s ease infinite;

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

@@ -31,3 +31,5 @@ BYPASS_CORS_CHECKS=true
CHOKIDAR_USEPOLLING=true
LOG_TRANSPORT=console
LOG_LEVEL=trace
ENABLE_NEXT_DOCKER_RELEASE=true
SKIP_CONNECT_PLUGIN_CHECK=true

View File

@@ -42,7 +42,10 @@ export default tseslint.config(
'ignorePackages',
{
js: 'always',
ts: 'always',
mjs: 'always',
cjs: 'always',
ts: 'never',
tsx: 'never',
},
],
'no-restricted-globals': [

3
api/.gitignore vendored
View File

@@ -93,3 +93,6 @@ dev/local-session
# local OIDC config for testing - contains secrets
dev/configs/oidc.local.json
# local api keys
dev/keys/*

View File

@@ -1,5 +1,252 @@
# Changelog
## [4.26.2](https://github.com/unraid/api/compare/v4.26.1...v4.26.2) (2025-11-19)
### Bug Fixes
* **theme:** Missing header background color ([e2fdf6c](https://github.com/unraid/api/commit/e2fdf6cadbd816559b8c82546c2bc771a81ffa9e))
## [4.26.1](https://github.com/unraid/api/compare/v4.26.0...v4.26.1) (2025-11-18)
### Bug Fixes
* **theme:** update theme class naming and scoping logic ([b28ef1e](https://github.com/unraid/api/commit/b28ef1ea334cb4842f01fa992effa7024185c6c9))
## [4.26.0](https://github.com/unraid/api/compare/v4.25.3...v4.26.0) (2025-11-17)
### Features
* add cpu power query & subscription ([#1745](https://github.com/unraid/api/issues/1745)) ([d7aca81](https://github.com/unraid/api/commit/d7aca81c60281bfa47fb9113929c1ead6ed3361b))
* add schema publishing to apollo studio ([#1772](https://github.com/unraid/api/issues/1772)) ([7e13202](https://github.com/unraid/api/commit/7e13202aa1c02803095bb72bb1bcb2472716f53a))
* add workflow_dispatch trigger to schema publishing workflow ([818e7ce](https://github.com/unraid/api/commit/818e7ce997059663e07efcf1dab706bf0d7fc9da))
* apollo studio readme link ([c4cd0c6](https://github.com/unraid/api/commit/c4cd0c63520deec15d735255f38811f0360fe3a1))
* **cli:** make `unraid-api plugins remove` scriptable ([#1774](https://github.com/unraid/api/issues/1774)) ([64eb9ce](https://github.com/unraid/api/commit/64eb9ce9b5d1ff4fb1f08d9963522c5d32221ba7))
* use persisted theme css to fix flashes on header ([#1784](https://github.com/unraid/api/issues/1784)) ([854b403](https://github.com/unraid/api/commit/854b403fbd85220a3012af58ce033cf0b8418516))
### Bug Fixes
* **api:** decode html entities before parsing notifications ([#1768](https://github.com/unraid/api/issues/1768)) ([42406e7](https://github.com/unraid/api/commit/42406e795da1e5b95622951a467722dde72d51a8))
* **connect:** disable api plugin if unraid plugin is absent ([#1773](https://github.com/unraid/api/issues/1773)) ([c264a18](https://github.com/unraid/api/commit/c264a1843cf115e8cc1add1ab4f12fdcc932405a))
* detection of flash backup activation state ([#1769](https://github.com/unraid/api/issues/1769)) ([d18eaf2](https://github.com/unraid/api/commit/d18eaf2364e0c04992c52af38679ff0a0c570440))
* re-add missing header gradient styles ([#1787](https://github.com/unraid/api/issues/1787)) ([f8a6785](https://github.com/unraid/api/commit/f8a6785e9c92f81acaef76ac5eb78a4a769e69da))
* respect OS safe mode in plugin loader ([#1775](https://github.com/unraid/api/issues/1775)) ([92af3b6](https://github.com/unraid/api/commit/92af3b61156cabae70368cf5222a2f7ac5b4d083))
## [4.25.3](https://github.com/unraid/unraid-api/compare/v4.25.2...v4.25.3) (2025-10-22)
### Bug Fixes
* flaky watch on boot drive's dynamix config ([ec7aa06](https://github.com/unraid/unraid-api/commit/ec7aa06d4a5fb1f0e84420266b0b0d7ee09a3663))
## [4.25.2](https://github.com/unraid/api/compare/v4.25.1...v4.25.2) (2025-09-30)
### Bug Fixes
* enhance activation code modal visibility logic ([#1733](https://github.com/unraid/api/issues/1733)) ([e57ec00](https://github.com/unraid/api/commit/e57ec00627e54ce76d903fd0fa8686ad02b393f3))
## [4.25.1](https://github.com/unraid/api/compare/v4.25.0...v4.25.1) (2025-09-30)
### Bug Fixes
* add cache busting to web component extractor ([#1731](https://github.com/unraid/api/issues/1731)) ([0d165a6](https://github.com/unraid/api/commit/0d165a608740505bdc505dcf69fb615225969741))
* Connect won't appear within Apps - Previous Apps ([#1727](https://github.com/unraid/api/issues/1727)) ([d73953f](https://github.com/unraid/api/commit/d73953f8ff3d7425c0aed32d16236ededfd948e1))
## [4.25.0](https://github.com/unraid/api/compare/v4.24.1...v4.25.0) (2025-09-26)
### Features
* add Tailwind scoping plugin and integrate into Vite config ([#1722](https://github.com/unraid/api/issues/1722)) ([b7afaf4](https://github.com/unraid/api/commit/b7afaf463243b073e1ab1083961a16a12ac6c4a3))
* notification filter controls pill buttons ([#1718](https://github.com/unraid/api/issues/1718)) ([661865f](https://github.com/unraid/api/commit/661865f97611cf802f239fde8232f3109281dde6))
### Bug Fixes
* enable auth guard for nested fields - thanks [@ingel81](https://github.com/ingel81) ([7bdeca8](https://github.com/unraid/api/commit/7bdeca8338a3901f15fde06fd7aede3b0c16e087))
* enhance user context validation in auth module ([#1726](https://github.com/unraid/api/issues/1726)) ([cd5eff1](https://github.com/unraid/api/commit/cd5eff11bcb4398581472966cb7ec124eac7ad0a))
## [4.24.1](https://github.com/unraid/api/compare/v4.24.0...v4.24.1) (2025-09-23)
### Bug Fixes
* cleanup leftover removed packages on upgrade ([#1719](https://github.com/unraid/api/issues/1719)) ([9972a5f](https://github.com/unraid/api/commit/9972a5f178f9a251e6c129d85c5f11cfd25e6281))
* enhance version comparison logic in installation script ([d9c561b](https://github.com/unraid/api/commit/d9c561bfebed0c553fe4bfa26b088ae71ca59755))
* issue with incorrect permissions on viewer / other roles ([378cdb7](https://github.com/unraid/api/commit/378cdb7f102f63128dd236c13f1a3745902d5a2c))
## [4.24.0](https://github.com/unraid/api/compare/v4.23.1...v4.24.0) (2025-09-18)
### Features
* improve dom content loading by being more efficient about component mounting ([#1716](https://github.com/unraid/api/issues/1716)) ([d8b166e](https://github.com/unraid/api/commit/d8b166e4b6a718e07783d9c8ac8393b50ec89ae3))
## [4.23.1](https://github.com/unraid/api/compare/v4.23.0...v4.23.1) (2025-09-17)
### Bug Fixes
* cleanup ini parser logic with better fallbacks ([#1713](https://github.com/unraid/api/issues/1713)) ([1691362](https://github.com/unraid/api/commit/16913627de9497a5d2f71edb710cec6e2eb9f890))
## [4.23.0](https://github.com/unraid/api/compare/v4.22.2...v4.23.0) (2025-09-16)
### Features
* add unraid api status manager ([#1708](https://github.com/unraid/api/issues/1708)) ([1d9ce0a](https://github.com/unraid/api/commit/1d9ce0aa3d067726c2c880929408c68f53e13e0d))
### Bug Fixes
* **logging:** remove colorized logs ([#1705](https://github.com/unraid/api/issues/1705)) ([1d2c670](https://github.com/unraid/api/commit/1d2c6701ce56b1d40afdb776065295e9273d08e9))
* no sizeRootFs unless queried ([#1710](https://github.com/unraid/api/issues/1710)) ([9714b21](https://github.com/unraid/api/commit/9714b21c5c07160b92a11512e8b703908adb0620))
* use virtual-modal-container ([#1709](https://github.com/unraid/api/issues/1709)) ([44b4d77](https://github.com/unraid/api/commit/44b4d77d803aa724968307cfa463f7c440791a10))
## [4.22.2](https://github.com/unraid/api/compare/v4.22.1...v4.22.2) (2025-09-15)
### Bug Fixes
* **deps:** pin dependency conventional-changelog-conventionalcommits to 9.1.0 ([#1697](https://github.com/unraid/api/issues/1697)) ([9a86c61](https://github.com/unraid/api/commit/9a86c615da2e975f568922fa012cc29b3f9cde0e))
* **deps:** update dependency filenamify to v7 ([#1703](https://github.com/unraid/api/issues/1703)) ([b80988a](https://github.com/unraid/api/commit/b80988aaabebc4b8dbf2bf31f0764bf2f28e1575))
* **deps:** update graphqlcodegenerator monorepo (major) ([#1689](https://github.com/unraid/api/issues/1689)) ([ba4a43a](https://github.com/unraid/api/commit/ba4a43aec863fc30c47dd17370d74daed7f84703))
* false positive on verify_install script being external shell ([#1704](https://github.com/unraid/api/issues/1704)) ([31a255c](https://github.com/unraid/api/commit/31a255c9281b29df983d0f5d0475cd5a69790a48))
* improve vue mount speed by 10x ([c855caa](https://github.com/unraid/api/commit/c855caa9b2d4d63bead1a992f5c583e00b9ba843))
## [4.22.1](https://github.com/unraid/api/compare/v4.22.0...v4.22.1) (2025-09-12)
### Bug Fixes
* set input color in SSO field rather than inside of the main.css ([01d353f](https://github.com/unraid/api/commit/01d353fa08a3df688b37a495a204605138f7f71d))
## [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)
### Features
* add zsh shell detection to install script ([#1539](https://github.com/unraid/api/issues/1539)) ([50ea2a3](https://github.com/unraid/api/commit/50ea2a3ffb82b30152fb85e0fb9b0d178d596efe))
* **api:** determine if docker container has update ([#1582](https://github.com/unraid/api/issues/1582)) ([e57d81e](https://github.com/unraid/api/commit/e57d81e0735772758bb85e0b3c89dce15c56635e))
### Bug Fixes
* white on white login text ([ae4d3ec](https://github.com/unraid/api/commit/ae4d3ecbc417454ae3c6e02018f8e4c49bbfc902))
## [4.20.4](https://github.com/unraid/api/compare/v4.20.3...v4.20.4) (2025-09-09)
### Bug Fixes
* staging PR plugin fixes + UI issues on 7.2 beta ([b79b44e](https://github.com/unraid/api/commit/b79b44e95c65a124313814ab55b0d0a745a799c7))
## [4.20.3](https://github.com/unraid/api/compare/v4.20.2...v4.20.3) (2025-09-09)
### Bug Fixes
* header background color issues fixed on 7.2 - thanks Nick! ([73c1100](https://github.com/unraid/api/commit/73c1100d0ba396fe4342f8ce7561017ab821e68b))
## [4.20.2](https://github.com/unraid/api/compare/v4.20.1...v4.20.2) (2025-09-09)
### Bug Fixes
* trigger deployment ([a27453f](https://github.com/unraid/api/commit/a27453fda81e4eeb07f257e60516bebbbc27cf7a))
## [4.20.1](https://github.com/unraid/api/compare/v4.20.0...v4.20.1) (2025-09-09)
### Bug Fixes
* adjust header styles to fix flashing and width issues - thanks ZarZ ([4759b3d](https://github.com/unraid/api/commit/4759b3d0b3fb6bc71636f75f807cd6f4f62305d1))
## [4.20.0](https://github.com/unraid/api/compare/v4.19.1...v4.20.0) (2025-09-08)
### Features
* **disks:** add isSpinning field to Disk type ([#1527](https://github.com/unraid/api/issues/1527)) ([193be3d](https://github.com/unraid/api/commit/193be3df3672514be9904e3d4fbdff776470afc0))
### Bug Fixes
* better component loading to prevent per-page strange behavior ([095c222](https://github.com/unraid/api/commit/095c2221c94f144f8ad410a69362b15803765531))
* **deps:** pin dependencies ([#1669](https://github.com/unraid/api/issues/1669)) ([413db4b](https://github.com/unraid/api/commit/413db4bd30a06aa69d3ca86e793782854f822589))
* **plugin:** add fallback for unraid-api stop in deprecation cleanup ([#1668](https://github.com/unraid/api/issues/1668)) ([797bf50](https://github.com/unraid/api/commit/797bf50ec702ebc8244ff71a8ef1a80ea5cd2169))
* prepend 'v' to API version in workflow dispatch inputs ([f0cffbd](https://github.com/unraid/api/commit/f0cffbdc7ac36e7037ab60fe9dddbb2cab4a5e10))
* progress frame background color fix ([#1672](https://github.com/unraid/api/issues/1672)) ([785f1f5](https://github.com/unraid/api/commit/785f1f5eb1a1cc8b41f6eb502e4092d149cfbd80))
* properly override header values ([#1673](https://github.com/unraid/api/issues/1673)) ([aecf70f](https://github.com/unraid/api/commit/aecf70ffad60c83074347d3d6ec23f73acbd1aee))
## [4.19.1](https://github.com/unraid/api/compare/v4.19.0...v4.19.1) (2025-09-05)
### Bug Fixes
* custom path detection to fix setup issues ([#1664](https://github.com/unraid/api/issues/1664)) ([2ecdb99](https://github.com/unraid/api/commit/2ecdb99052f39d89af21bbe7ad3f80b83bb1eaa9))
## [4.19.0](https://github.com/unraid/api/compare/v4.18.2...v4.19.0) (2025-09-04)
### Features
* mount vue apps, not web components ([#1639](https://github.com/unraid/api/issues/1639)) ([88087d5](https://github.com/unraid/api/commit/88087d5201992298cdafa791d5d1b5bb23dcd72b))
### Bug Fixes
* api version json response ([#1653](https://github.com/unraid/api/issues/1653)) ([292bc0f](https://github.com/unraid/api/commit/292bc0fc810a0d0f0cce6813b0631ff25099cc05))
* enhance DOM validation and cleanup in vue-mount-app ([6cf7c88](https://github.com/unraid/api/commit/6cf7c88242f2f4fe9f83871560039767b5b90273))
* enhance getKeyFile function to handle missing key file gracefully ([#1659](https://github.com/unraid/api/issues/1659)) ([728b38a](https://github.com/unraid/api/commit/728b38ac11faeacd39ce9d0157024ad140e29b36))
* info alert docker icon ([#1661](https://github.com/unraid/api/issues/1661)) ([239cdd6](https://github.com/unraid/api/commit/239cdd6133690699348e61f68e485d2b54fdcbdb))
* oidc cache busting issues fixed ([#1656](https://github.com/unraid/api/issues/1656)) ([e204eb8](https://github.com/unraid/api/commit/e204eb80a00ab9242e3dca4ccfc3e1b55a7694b7))
* **plugin:** restore cleanup behavior for unsupported unraid versions ([#1658](https://github.com/unraid/api/issues/1658)) ([534a077](https://github.com/unraid/api/commit/534a07788b76de49e9ba14059a9aed0bf16e02ca))
* UnraidToaster component and update dialog close button ([#1657](https://github.com/unraid/api/issues/1657)) ([44774d0](https://github.com/unraid/api/commit/44774d0acdd25aa33cb60a5d0b4f80777f4068e5))
* vue mounting logic with tests ([#1651](https://github.com/unraid/api/issues/1651)) ([33774aa](https://github.com/unraid/api/commit/33774aa596124a031a7452b62ca4c43743a09951))
## [4.18.2](https://github.com/unraid/api/compare/v4.18.1...v4.18.2) (2025-09-03)
### Bug Fixes
* add missing CPU guest metrics to CPU responses ([#1644](https://github.com/unraid/api/issues/1644)) ([99dbad5](https://github.com/unraid/api/commit/99dbad57d55a256f5f3f850f9a47a6eaa6348065))
* **plugin:** raise minimum unraid os version to 6.12.15 ([#1649](https://github.com/unraid/api/issues/1649)) ([bc15bd3](https://github.com/unraid/api/commit/bc15bd3d7008acb416ac3c6fb1f4724c685ec7e7))
* update GitHub Actions token for workflow trigger ([4d8588b](https://github.com/unraid/api/commit/4d8588b17331afa45ba8caf84fcec8c0ea03591f))
* update OIDC URL validation and add tests ([#1646](https://github.com/unraid/api/issues/1646)) ([c7c3bb5](https://github.com/unraid/api/commit/c7c3bb57ea482633a7acff064b39fbc8d4e07213))
* use shared bg & border color for styled toasts ([#1647](https://github.com/unraid/api/issues/1647)) ([7c3aee8](https://github.com/unraid/api/commit/7c3aee8f3f9ba82ae8c8ed3840c20ab47f3cb00f))
## [4.18.1](https://github.com/unraid/api/compare/v4.18.0...v4.18.1) (2025-09-03)
### Bug Fixes
* OIDC and API Key management issues ([#1642](https://github.com/unraid/api/issues/1642)) ([0fe2c2c](https://github.com/unraid/api/commit/0fe2c2c1c85dcc547e4b1217a3b5636d7dd6d4b4))
* rm redundant emission to `$HOME/.pm2/logs` ([#1640](https://github.com/unraid/api/issues/1640)) ([a8e4119](https://github.com/unraid/api/commit/a8e4119270868a1dabccd405853a7340f8dcd8a5))
## [4.18.0](https://github.com/unraid/api/compare/v4.17.0...v4.18.0) (2025-09-02)

View File

@@ -71,6 +71,10 @@ unraid-api report -vv
If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).
## Internationalization
- Run `pnpm --filter @unraid/api i18n:extract` to scan the Nest.js source for translation helper usages and update `src/i18n/en.json` with any new keys. The extractor keeps existing translations intact and appends new keys with their English source text.
## License
Copyright Lime Technology Inc. All rights reserved.

View File

@@ -17,6 +17,7 @@ const config: CodegenConfig = {
URL: 'URL',
Port: 'number',
UUID: 'string',
BigInt: 'number',
},
scalarSchemas: {
URL: 'z.instanceof(URL)',
@@ -24,6 +25,7 @@ const config: CodegenConfig = {
JSON: 'z.record(z.string(), z.any())',
Port: 'z.number()',
UUID: 'z.string()',
BigInt: 'z.number()',
},
},
generates: {

View File

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

View File

@@ -0,0 +1,6 @@
timestamp=1730937600
event=Hashtag Test
subject=Warning [UNRAID] - #1 OS is cooking
description=Disk 1 temperature has reached #epic # levels of proportion
importance=warning

View File

@@ -0,0 +1,6 @@
timestamp=1730937600
event=Temperature Test
subject=Warning [UNRAID] - High disk temperature detected: 45&#8201;&#176;C
description=Disk 1 temperature has reached 45&#8201;&#176;C (threshold: 40&#8201;&#176;C)<br><br>Current temperatures:<br>Parity - 32&#8201;&#176;C [OK]<br>Disk 1 - 45&#8201;&#176;C [WARNING]<br>Disk 2 - 38&#8201;&#176;C [OK]<br>Cache - 28&#8201;&#176;C [OK]<br><br>Please check cooling system.
importance=warning

View File

@@ -0,0 +1,247 @@
# Feature Flags
Feature flags allow you to conditionally enable or disable functionality in the Unraid API. This is useful for gradually rolling out new features, A/B testing, or keeping experimental code behind flags during development.
## Setting Up Feature Flags
### 1. Define the Feature Flag
Feature flags are defined as environment variables and collected in `src/consts.ts`:
```typescript
// src/environment.ts
export const ENABLE_MY_NEW_FEATURE = process.env.ENABLE_MY_NEW_FEATURE === 'true';
// src/consts.ts
export const FeatureFlags = Object.freeze({
ENABLE_NEXT_DOCKER_RELEASE,
ENABLE_MY_NEW_FEATURE, // Add your new flag here
});
```
### 2. Set the Environment Variable
Set the environment variable when running the API:
```bash
ENABLE_MY_NEW_FEATURE=true unraid-api start
```
Or add it to your `.env` file:
```env
ENABLE_MY_NEW_FEATURE=true
```
## Using Feature Flags in GraphQL
### Method 1: @UseFeatureFlag Decorator (Schema-Level)
The `@UseFeatureFlag` decorator conditionally includes or excludes GraphQL fields, queries, and mutations from the schema based on feature flags. When a feature flag is disabled, the field won't appear in the GraphQL schema at all.
```typescript
import { UseFeatureFlag } from '@app/unraid-api/decorators/use-feature-flag.decorator.js';
import { Query, Mutation, ResolveField } from '@nestjs/graphql';
@Resolver()
export class MyResolver {
// Conditionally include a query
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@Query(() => String)
async experimentalQuery() {
return 'This query only exists when ENABLE_MY_NEW_FEATURE is true';
}
// Conditionally include a mutation
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@Mutation(() => Boolean)
async experimentalMutation() {
return true;
}
// Conditionally include a field resolver
@UseFeatureFlag('ENABLE_MY_NEW_FEATURE')
@ResolveField(() => String)
async experimentalField() {
return 'This field only exists when the flag is enabled';
}
}
```
**Benefits:**
- Clean schema - disabled features don't appear in GraphQL introspection
- No runtime overhead for disabled features
- Clear feature boundaries
**Use when:**
- You want to completely hide features from the GraphQL schema
- The feature is experimental or in beta
- You're doing a gradual rollout
### Method 2: checkFeatureFlag Function (Runtime)
The `checkFeatureFlag` function provides runtime feature flag checking within resolver methods. It throws a `ForbiddenException` if the feature is disabled.
```typescript
import { checkFeatureFlag } from '@app/unraid-api/utils/feature-flag.helper.js';
import { FeatureFlags } from '@app/consts.js';
import { Query, ResolveField } from '@nestjs/graphql';
@Resolver()
export class MyResolver {
@Query(() => String)
async myQuery(
@Args('useNewAlgorithm', { nullable: true }) useNewAlgorithm?: boolean
) {
// Conditionally use new logic based on feature flag
if (useNewAlgorithm) {
checkFeatureFlag(FeatureFlags, 'ENABLE_MY_NEW_FEATURE');
return this.newAlgorithm();
}
return this.oldAlgorithm();
}
@ResolveField(() => String)
async dataField() {
// Check flag at the start of the method
checkFeatureFlag(FeatureFlags, 'ENABLE_MY_NEW_FEATURE');
// Feature-specific logic here
return this.computeExperimentalData();
}
}
```
**Benefits:**
- More granular control within methods
- Can conditionally execute parts of a method
- Useful for A/B testing scenarios
- Good for gradual migration strategies
**Use when:**
- You need conditional logic within a method
- The field should exist but behavior changes based on the flag
- You're migrating from old to new implementation gradually
## Feature Flag Patterns
### Pattern 1: Complete Feature Toggle
Hide an entire feature behind a flag:
```typescript
@UseFeatureFlag('ENABLE_DOCKER_TEMPLATES')
@Resolver(() => DockerTemplate)
export class DockerTemplateResolver {
// All resolvers in this class are toggled by the flag
}
```
### Pattern 2: Gradual Migration
Migrate from old to new implementation:
```typescript
@Query(() => [Container])
async getContainers(@Args('version') version?: string) {
if (version === 'v2') {
checkFeatureFlag(FeatureFlags, 'ENABLE_CONTAINERS_V2');
return this.getContainersV2();
}
return this.getContainersV1();
}
```
### Pattern 3: Beta Features
Mark features as beta:
```typescript
@UseFeatureFlag('ENABLE_BETA_FEATURES')
@ResolveField(() => BetaMetrics, {
description: 'BETA: Advanced metrics (requires ENABLE_BETA_FEATURES flag)'
})
async betaMetrics() {
return this.computeBetaMetrics();
}
```
### Pattern 4: Performance Optimizations
Toggle expensive operations:
```typescript
@ResolveField(() => Statistics)
async statistics() {
const basicStats = await this.getBasicStats();
try {
checkFeatureFlag(FeatureFlags, 'ENABLE_ADVANCED_ANALYTICS');
const advancedStats = await this.getAdvancedStats();
return { ...basicStats, ...advancedStats };
} catch {
// Feature disabled, return only basic stats
return basicStats;
}
}
```
## Testing with Feature Flags
When writing tests for feature-flagged code, create a mock to control feature flag values:
```typescript
import { vi } from 'vitest';
// Mock the entire consts module
vi.mock('@app/consts.js', async () => {
const actual = await vi.importActual('@app/consts.js');
return {
...actual,
FeatureFlags: {
ENABLE_MY_NEW_FEATURE: true, // Set your test value
ENABLE_NEXT_DOCKER_RELEASE: false,
}
};
});
describe('MyResolver', () => {
it('should execute new logic when feature is enabled', async () => {
// Test new behavior with mocked flag
});
});
```
## Best Practices
1. **Naming Convention**: Use `ENABLE_` prefix for boolean feature flags
2. **Environment Variables**: Always use uppercase with underscores
3. **Documentation**: Document what each feature flag controls
4. **Cleanup**: Remove feature flags once features are stable and fully rolled out
5. **Default State**: New features should default to `false` (disabled)
6. **Granularity**: Keep feature flags focused on a single feature or capability
7. **Testing**: Always test both enabled and disabled states
## Common Use Cases
- **Experimental Features**: Hide unstable features in production
- **Gradual Rollouts**: Enable features for specific environments first
- **A/B Testing**: Toggle between different implementations
- **Performance**: Disable expensive operations when not needed
- **Breaking Changes**: Provide migration path with both old and new behavior
- **Debug Features**: Enable additional logging or debugging tools
## Checking Active Feature Flags
To see which feature flags are currently active:
```typescript
// Log all feature flags on startup
console.log('Active Feature Flags:', FeatureFlags);
```
Or check via GraphQL introspection to see which fields are available based on current flags.

View File

@@ -1,4 +0,0 @@
{
"label": "Unraid API",
"position": 4
}

View File

@@ -1,100 +0,0 @@
# API Key Authorization Flow
This document describes the self-service API key creation flow for third-party applications.
## Overview
Applications can request API access to an Unraid server by redirecting users to a special authorization page where users can review requested permissions and create an API key with one click.
## Flow
1. **Application initiates request**: The app redirects the user to:
```
https://[unraid-server]/ApiKeyAuthorize?name=MyApp&scopes=docker:read,vm:*&redirect_uri=https://myapp.com/callback&state=abc123
```
2. **User authentication**: If not already logged in, the user is redirected to login first (standard Unraid auth)
3. **Consent screen**: User sees:
- Application name and description
- Requested permissions (with checkboxes to approve/deny specific scopes)
- API key name field (pre-filled)
- Authorize & Cancel buttons
4. **API key creation**: Upon authorization:
- API key is created with approved scopes
- Key is displayed to the user
- If `redirect_uri` is provided, user is redirected back with the key
5. **Callback**: App receives the API key:
```
https://myapp.com/callback?api_key=xxx&state=abc123
```
## Query Parameters
- `name` (required): Name of the requesting application
- `description` (optional): Description of the application
- `scopes` (required): Comma-separated list of requested scopes
- `redirect_uri` (optional): URL to redirect after authorization
- `state` (optional): Opaque value for maintaining state
## Scope Format
Scopes follow the pattern: `resource:action`
### Examples:
- `docker:read` - Read access to Docker
- `vm:*` - Full access to VMs
- `system:update` - Update access to system
- `role:viewer` - Viewer role access
- `role:admin` - Admin role access
### Available Resources:
- `docker`, `vm`, `system`, `share`, `user`, `network`, `disk`, etc.
### Available Actions:
- `create`, `read`, `update`, `delete` or `*` for all
## Security Considerations
1. **HTTPS required**: Redirect URIs must use HTTPS (except localhost for development)
2. **User consent**: Users explicitly approve each permission
3. **Session-based**: Uses existing Unraid authentication session
4. **One-time display**: API keys are shown once and must be saved securely
## Example Integration
```javascript
// JavaScript example
const unraidServer = 'tower.local';
const appName = 'My Docker Manager';
const scopes = 'docker:*,system:read';
const redirectUri = 'https://myapp.com/unraid/callback';
const state = generateRandomState();
// Store state for verification
sessionStorage.setItem('oauth_state', state);
// Redirect user to authorization page
window.location.href =
`https://${unraidServer}/ApiKeyAuthorize?` +
`name=${encodeURIComponent(appName)}&` +
`scopes=${encodeURIComponent(scopes)}&` +
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
`state=${encodeURIComponent(state)}`;
// Handle callback
const urlParams = new URLSearchParams(window.location.search);
const apiKey = urlParams.get('api_key');
const returnedState = urlParams.get('state');
if (returnedState === sessionStorage.getItem('oauth_state')) {
// Save API key securely
saveApiKey(apiKey);
}
```

View File

@@ -1,210 +0,0 @@
---
title: CLI Reference
description: Complete reference for all Unraid API CLI commands
sidebar_position: 4
---
# CLI Commands
:::info[Command Structure]
All commands follow the pattern: `unraid-api <command> [options]`
:::
## 🚀 Service Management
### Start
```bash
unraid-api start [--log-level <level>]
```
Starts the Unraid API service.
Options:
- `--log-level`: Set logging level (trace|debug|info|warn|error|fatal)
Alternative: You can also set the log level using the `LOG_LEVEL` environment variable:
```bash
LOG_LEVEL=trace unraid-api start
```
### Stop
```bash
unraid-api stop [--delete]
```
Stops the Unraid API service.
- `--delete`: Optional. Delete the PM2 home directory
### Restart
```bash
unraid-api restart [--log-level <level>]
```
Restarts the Unraid API service.
Options:
- `--log-level`: Set logging level (trace|debug|info|warn|error|fatal)
Alternative: You can also set the log level using the `LOG_LEVEL` environment variable:
```bash
LOG_LEVEL=trace unraid-api restart
```
### Logs
```bash
unraid-api logs [-l <lines>]
```
View the API logs.
- `-l, --lines`: Optional. Number of lines to tail (default: 100)
## ⚙️ Configuration Commands
### Config
```bash
unraid-api config
```
Displays current configuration values.
### Switch Environment
```bash
unraid-api switch-env [-e <environment>]
```
Switch between production and staging environments.
- `-e, --environment`: Optional. Target environment (production|staging)
### Developer Mode
:::tip Web GUI Management
You can also manage developer options through the web interface at **Settings****Management Access****Developer Options**
:::
```bash
unraid-api developer # Interactive prompt for tools
unraid-api developer --sandbox true # Enable GraphQL sandbox
unraid-api developer --sandbox false # Disable GraphQL sandbox
unraid-api developer --enable-modal # Enable modal testing tool
unraid-api developer --disable-modal # Disable modal testing tool
```
Configure developer features for the API:
- **GraphQL Sandbox**: Enable/disable Apollo GraphQL sandbox at `/graphql`
- **Modal Testing Tool**: Enable/disable UI modal testing in the Unraid menu
## API Key Management
:::tip Web GUI Management
You can also manage API keys through the web interface at **Settings****Management Access****API Keys**
:::
### API Key Commands
```bash
unraid-api apikey [options]
```
Create and manage API keys via CLI.
Options:
- `--name <name>`: Name of the key
- `--create`: Create a new key
- `-r, --roles <roles>`: Comma-separated list of roles
- `-p, --permissions <permissions>`: Comma-separated list of permissions
- `-d, --description <description>`: Description for the key
## SSO (Single Sign-On) Management
:::info OIDC Configuration
For OIDC/SSO provider configuration, see the web interface at **Settings****Management Access****API****OIDC** or refer to the [OIDC Provider Setup](./oidc-provider-setup.md) guide.
:::
### SSO Base Command
```bash
unraid-api sso
```
#### Add SSO User
```bash
unraid-api sso add-user
# or
unraid-api sso add
# or
unraid-api sso a
```
Add a new user for SSO authentication.
#### Remove SSO User
```bash
unraid-api sso remove-user
# or
unraid-api sso remove
# or
unraid-api sso r
```
Remove a user (or all users) from SSO.
#### List SSO Users
```bash
unraid-api sso list-users
# or
unraid-api sso list
# or
unraid-api sso l
```
List all configured SSO users.
#### Validate SSO Token
```bash
unraid-api sso validate-token <token>
# or
unraid-api sso validate
# or
unraid-api sso v
```
Validates an SSO token and returns its status.
## Report Generation
### Generate Report
```bash
unraid-api report [-r] [-j]
```
Generate a system report.
- `-r, --raw`: Display raw command output
- `-j, --json`: Display output in JSON format
## Notes
1. Most commands require appropriate permissions to modify system state
2. Some commands require the API to be running or stopped
3. Store API keys securely as they provide system access
4. SSO configuration changes may require a service restart

View File

@@ -1,255 +0,0 @@
---
title: Using the Unraid API
description: Learn how to interact with your Unraid server through the GraphQL API
sidebar_position: 2
---
# Using the Unraid API
:::tip[Quick Start]
The Unraid API provides a powerful GraphQL interface for managing your server. This guide covers authentication, common queries, and best practices.
:::
The Unraid API provides a GraphQL interface that allows you to interact with your Unraid server. This guide will help you get started with exploring and using the API.
## 🎮 Enabling the GraphQL Sandbox
### Web GUI Method (Recommended)
:::info[Preferred Method]
Using the Web GUI is the easiest way to enable the GraphQL sandbox.
:::
1. Navigate to **Settings****Management Access****Developer Options**
2. Enable the **GraphQL Sandbox** toggle
3. Access the GraphQL playground by navigating to:
```txt
http://YOUR_SERVER_IP/graphql
```
### CLI Method
Alternatively, you can enable developer mode using the CLI:
```bash
unraid-api developer --sandbox true
```
Or use the interactive mode:
```bash
unraid-api developer
```
## 🔑 Authentication
:::warning[Required for Most Operations]
Most queries and mutations require authentication. Always include appropriate credentials in your requests.
:::
You can authenticate using:
1. **API Keys** - For programmatic access
2. **Cookies** - Automatic when signed into the WebGUI
3. **SSO/OIDC** - When configured with external providers
### Managing API Keys
<tabs>
<tabItem value="gui" label="Web GUI (Recommended)" default>
Navigate to **Settings** → **Management Access** → **API Keys** in your Unraid web interface to:
- View existing API keys
- Create new API keys
- Manage permissions and roles
- Revoke or regenerate keys
</tabItem>
<tabItem value="cli" label="CLI Method">
You can also use the CLI to create an API key:
```bash
unraid-api apikey --create
```
Follow the prompts to set:
- Name
- Description
- Roles
- Permissions
</tabItem>
</tabs>
### Using API Keys
The generated API key should be included in your GraphQL requests as a header:
```json
{
"x-api-key": "YOUR_API_KEY"
}
```
## 📊 Available Schemas
The API provides access to various aspects of your Unraid server:
### System Information
- Query system details including CPU, memory, and OS information
- Monitor system status and health
- Access baseboard and hardware information
### Array Management
- Query array status and configuration
- Manage array operations (start/stop)
- Monitor disk status and health
- Perform parity checks
### Docker Management
- List and manage Docker containers
- Monitor container status
- Manage Docker networks
### Remote Access
- Configure and manage remote access settings
- Handle SSO configuration
- Manage allowed origins
### 💻 Example Queries
#### Check System Status
```graphql
query {
info {
os {
platform
distro
release
uptime
}
cpu {
manufacturer
brand
cores
threads
}
}
}
```
#### Monitor Array Status
```graphql
query {
array {
state
capacity {
disks {
free
used
total
}
}
disks {
name
size
status
temp
}
}
}
```
#### List Docker Containers
```graphql
query {
dockerContainers {
id
names
state
status
autoStart
}
}
```
## 🏗️ Schema Types
The API includes several core types:
### Base Types
- `Node`: Interface for objects with unique IDs - please see [Object Identification](https://graphql.org/learn/global-object-identification/)
- `JSON`: For complex JSON data
- `DateTime`: For timestamp values
- `Long`: For 64-bit integers
### Resource Types
- `Array`: Array and disk management
- `Docker`: Container and network management
- `Info`: System information
- `Config`: Server configuration
- `Connect`: Remote access settings
### Role-Based Access
Available roles:
- `admin`: Full access
- `connect`: Remote access features
- `guest`: Limited read access
## ✨ Best Practices
:::tip[Pro Tips]
1. Use the Apollo Sandbox to explore the schema and test queries
2. Start with small queries and gradually add fields as needed
3. Monitor your query complexity to maintain performance
4. Use appropriate roles and permissions for your API keys
5. Keep your API keys secure and rotate them periodically
:::
## ⏱️ Rate Limiting
:::caution[Rate Limits]
The API implements rate limiting to prevent abuse. Ensure your applications handle rate limit responses appropriately.
:::
## 🚨 Error Handling
The API returns standard GraphQL errors in the following format:
```json
{
"errors": [
{
"message": "Error description",
"locations": [...],
"path": [...]
}
]
}
```
## 📚 Additional Resources
:::info[Learn More]
- Use the Apollo Sandbox's schema explorer to browse all available types and fields
- Check the documentation tab in Apollo Sandbox for detailed field descriptions
- Monitor the API's health using `unraid-api status`
- Generate reports using `unraid-api report` for troubleshooting
For more information about specific commands and configuration options, refer to the [CLI documentation](/cli) or run `unraid-api --help`.
:::

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

View File

@@ -1,94 +0,0 @@
---
title: Welcome to Unraid API
description: The official GraphQL API for Unraid Server management and automation
sidebar_position: 1
---
# Welcome to Unraid API
:::tip[What's New]
Starting with Unraid OS v7.2, the API comes built into the operating system - no plugin installation required!
:::
The Unraid API provides a GraphQL interface for programmatic interaction with your Unraid server. It enables automation, monitoring, and integration capabilities.
## 📦 Availability
### ✨ Native Integration (Unraid OS v7.2+)
Starting with Unraid OS v7.2, the API is integrated directly into the operating system:
- No plugin installation required
- Automatically available on system startup
- Deep system integration
- Access through **Settings****Management Access****API**
### 🔌 Plugin Installation (Pre-7.2 and Advanced Users)
For Unraid versions prior to v7.2 or to access newer API features:
1. Install the Unraid Connect Plugin from Community Apps
2. [Configure the plugin](./how-to-use-the-api.md#enabling-the-graphql-sandbox)
3. Access API functionality through the [GraphQL Sandbox](./how-to-use-the-api.md)
:::info Important Notes
- The Unraid Connect plugin provides the API for pre-7.2 versions
- You do NOT need to sign in to Unraid Connect to use the API locally
- Installing the plugin on 7.2+ gives you access to newer API features before they're included in OS releases
:::
## 📚 Documentation Sections
<cards>
<card title="CLI Commands" icon="terminal" href="./cli">
Complete reference for all CLI commands
</card>
<card title="Using the API" icon="code" href="./how-to-use-the-api">
Learn how to interact with the GraphQL API
</card>
<card title="OIDC Setup" icon="shield" href="./oidc-provider-setup">
Configure SSO authentication providers
</card>
<card title="Upcoming Features" icon="rocket" href="./upcoming-features">
See what's coming next
</card>
</cards>
## 🌟 Key Features
:::info[Core Capabilities]
The API provides:
- **GraphQL Interface**: Modern, flexible API with strong typing
- **Authentication**: Multiple methods including API keys, session cookies, and SSO/OIDC
- **Comprehensive Coverage**: Access to system information, array management, and Docker operations
- **Developer Tools**: Built-in GraphQL sandbox configurable via web interface or CLI
- **Role-Based Access**: Granular permission control
- **Web Management**: Manage API keys and settings through the web interface
:::
## 🚀 Get Started
<tabs>
<tabItem value="v72" label="Unraid OS v7.2+" default>
1. The API is already installed and running
2. Access settings at **Settings****Management Access****API**
3. Enable the GraphQL Sandbox for development
4. Create your first API key
5. Start making GraphQL queries!
</tabItem>
<tabItem value="older" label="Pre-7.2 Versions">
1. Install the Unraid Connect plugin from Community Apps
2. No Unraid Connect login required for local API access
3. Configure the plugin settings
4. Enable the GraphQL Sandbox
5. Start exploring the API!
</tabItem>
</tabs>
For detailed usage instructions, see the [CLI Commands](./cli) reference.

View File

@@ -0,0 +1 @@
# All Content Here has been permanently moved to [Unraid Docs](https://github.com/unraid/docs)

View File

@@ -1,420 +0,0 @@
---
title: OIDC Provider Setup
description: Configure OIDC (OpenID Connect) providers for SSO authentication in Unraid API
sidebar_position: 3
---
# OIDC Provider Setup
:::info[What is OIDC?]
OpenID Connect (OIDC) is an authentication protocol that allows users to sign in using their existing accounts from providers like Google, Microsoft, or your corporate identity provider. It enables Single Sign-On (SSO) for seamless and secure authentication.
:::
This guide walks you through configuring OIDC (OpenID Connect) providers for SSO authentication in the Unraid API using the web interface.
## 🚀 Quick Start
<details open>
<summary><strong>Getting to OIDC Settings</strong></summary>
1. Navigate to your Unraid server's web interface
2. Go to **Settings****Management Access****API****OIDC**
3. You'll see tabs for different providers - click the **+** button to add a new provider
</details>
### OIDC Providers Interface Overview
![Login Page with SSO Options](./images/sso-with-options.png)
*Login page showing traditional login form with SSO options - "Login With Unraid.net" and "Sign in with Google" buttons*
The interface includes:
- **Provider tabs**: Each configured provider (Unraid.net, Google, etc.) appears as a tab
- **Add Provider button**: Click the **+** button to add new providers
- **Authorization Mode dropdown**: Toggle between "simple" and "advanced" modes
- **Simple Authorization section**: Configure allowed email domains and specific addresses
- **Add Item buttons**: Click to add multiple authorization rules
## Understanding Authorization Modes
The interface provides two authorization modes:
### Simple Mode (Recommended)
Simple mode is the easiest way to configure authorization. You can:
- Allow specific email domains (e.g., @company.com)
- Allow specific email addresses
- Configure who can access your Unraid server with minimal setup
**When to use Simple Mode:**
- You want to allow all users from your company domain
- You have a small list of specific users
- You're new to OIDC configuration
<details>
<summary><strong>Advanced Mode</strong></summary>
Advanced mode provides granular control using claim-based rules. You can:
- Create complex authorization rules based on JWT claims
- Use operators like equals, contains, endsWith, startsWith
- Combine multiple conditions with OR/AND logic
- Choose whether ANY rule must pass (OR mode) or ALL rules must pass (AND mode)
**When to use Advanced Mode:**
- You need to check group memberships
- You want to verify multiple claims (e.g., email domain AND verified status)
- You have complex authorization requirements
- You need fine-grained control over how rules are evaluated
</details>
## Authorization Rules
![Authorization Rules Configuration](./images/advanced-rules.png)
*Advanced authorization rules showing JWT claim configuration with email endsWith operator for domain-based access control*
### Simple Mode Examples
#### Allow Company Domain
In Simple Authorization:
- **Allowed Email Domains**: Enter `company.com`
- This allows anyone with @company.com email
#### Allow Specific Users
- **Specific Email Addresses**: Add individual emails
- Click **Add Item** to add multiple addresses
<details>
<summary><strong>Advanced Mode Examples</strong></summary>
#### Authorization Rule Mode
When using multiple rules, you can choose how they're evaluated:
- **OR Mode** (default): User is authorized if ANY rule passes
- **AND Mode**: User is authorized only if ALL rules pass
#### Email Domain with Verification (AND Mode)
To require both email domain AND verification:
1. Set **Authorization Rule Mode** to `AND`
2. Add two rules:
- Rule 1:
- **Claim**: `email`
- **Operator**: `endsWith`
- **Value**: `@company.com`
- Rule 2:
- **Claim**: `email_verified`
- **Operator**: `equals`
- **Value**: `true`
This ensures users must have both a company email AND a verified email address.
#### Group-Based Access (OR Mode)
To allow access to multiple groups:
1. Set **Authorization Rule Mode** to `OR` (default)
2. Add rules for each group:
- **Claim**: `groups`
- **Operator**: `contains`
- **Value**: `admins`
Or add another rule:
- **Claim**: `groups`
- **Operator**: `contains`
- **Value**: `developers`
Users in either `admins` OR `developers` group will be authorized.
#### Multiple Domains
- **Claim**: `email`
- **Operator**: `endsWith`
- **Values**: Add multiple domains (e.g., `company.com`, `subsidiary.com`)
#### Complex Authorization (AND Mode)
For strict security requiring multiple conditions:
1. Set **Authorization Rule Mode** to `AND`
2. Add multiple rules that ALL must pass:
- Email must be from company domain
- Email must be verified
- User must be in specific group
- Account must have 2FA enabled (if claim available)
</details>
<details>
<summary><strong>Configuration Interface Details</strong></summary>
### Provider Tabs
- Each configured provider appears as a tab at the top
- Click a tab to switch between provider configurations
- The **+** button on the right adds a new provider
### Authorization Mode Dropdown
- **simple**: Best for email-based authorization (recommended for most users)
- **advanced**: For complex claim-based rules using JWT claims
### Simple Authorization Fields
When "simple" mode is selected, you'll see:
- **Allowed Email Domains**: Enter domains without @ (e.g., `company.com`)
- Helper text: "Users with emails ending in these domains can login"
- **Specific Email Addresses**: Add individual email addresses
- Helper text: "Only these exact email addresses can login"
- **Add Item** buttons to add multiple entries
### Advanced Authorization Fields
When "advanced" mode is selected, you'll see:
- **Authorization Rule Mode**: Choose `OR` (any rule passes) or `AND` (all rules must pass)
- **Authorization Rules**: Add multiple claim-based rules
- **For each rule**:
- **Claim**: The JWT claim to check
- **Operator**: How to compare (equals, contains, endsWith, startsWith)
- **Value**: What to match against
### Additional Interface Elements
- **Enable Developer Sandbox**: Toggle to enable GraphQL sandbox at `/graphql`
- The interface uses a dark theme for better visibility
- Field validation indicators help ensure correct configuration
</details>
### Required Redirect URI
:::caution[Important Configuration]
All providers must be configured with this exact redirect URI format:
:::
```bash
http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback
```
:::tip
Replace `YOUR_UNRAID_IP` with your actual server IP address (e.g., `192.168.1.100` or `tower.local`).
:::
### Issuer URL Format
The **Issuer URL** field accepts both formats, but **base URL is strongly recommended** for security:
- **Base URL** (recommended): `https://accounts.google.com`
- **Full discovery URL**: `https://accounts.google.com/.well-known/openid-configuration`
**⚠️ Security Note**: Always use the base URL format when possible. The system automatically appends `/.well-known/openid-configuration` for OIDC discovery. Using the full discovery URL directly disables important issuer validation checks and is not recommended by the OpenID Connect specification.
**Examples of correct base URLs:**
- Google: `https://accounts.google.com`
- Microsoft/Azure: `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0`
- Keycloak: `https://keycloak.example.com/realms/YOUR_REALM`
- Authelia: `https://auth.yourdomain.com`
## ✅ Testing Your Configuration
![Login Page with SSO Buttons](./images/sso-with-options.png)
*Unraid login page displaying both traditional username/password authentication and SSO options with customized provider buttons*
1. Save your provider configuration
2. Log out (if logged in)
3. Navigate to the login page
4. Your configured provider button should appear
5. Click to test the login flow
## 🔧 Troubleshooting
### Common Issues
#### "Provider not found" error
- Ensure the Issuer URL is correct
- Check that the provider supports OIDC discovery (/.well-known/openid-configuration)
#### "Authorization failed"
- In Simple Mode: Check email domains are entered correctly (without @)
- In Advanced Mode:
- Verify claim names match exactly what your provider sends
- Check if Authorization Rule Mode is set correctly (OR vs AND)
- Ensure all required claims are present in the token
- Enable debug logging to see actual claims and rule evaluation
#### "Invalid redirect URI"
- Ensure the redirect URI in your provider matches exactly
- Include the correct port if using a non-standard configuration
- Verify the redirect URI protocol matches your server's configuration (HTTP or HTTPS)
#### Cannot see login button
- Check that at least one authorization rule is configured
- Verify the provider is enabled/saved
### Debug Mode
To troubleshoot issues:
1. Enable debug logging:
```bash
LOG_LEVEL=debug unraid-api start --debug
```
2. Check logs for:
- Received claims from provider
- Authorization rule evaluation
- Token validation errors
## 🔐 Security Best Practices
1. **Use Simple Mode for authorization** - Prevents overly accepting configurations and reduces misconfiguration risks
2. **Be specific with authorization** - Don't use overly broad rules
3. **Rotate secrets regularly** - Update client secrets periodically
4. **Test thoroughly** - Verify only intended users can access
## 💡 Need Help?
- Check provider's OIDC documentation
- Review Unraid API logs for detailed error messages
- Ensure your provider supports standard OIDC discovery
- Verify network connectivity between Unraid and provider
## 🏢 Provider-Specific Setup
### Unraid.net Provider
The Unraid.net provider is built-in and pre-configured. You only need to configure authorization rules in the interface.
**Configuration:**
- **Issuer URL**: Pre-configured (built-in provider)
- **Client ID/Secret**: Pre-configured (built-in provider)
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
:::tip[Redirect URI Protocol]
**Match the protocol to your server setup:** Use `http://` if accessing your Unraid server without SSL/TLS (typical for local network access). Use `https://` if you've configured SSL/TLS on your server. Some OIDC providers (like Google) require HTTPS and won't accept HTTP redirect URIs.
:::
Configure authorization rules using Simple Mode (allowed email domains/addresses) or Advanced Mode for complex requirements.
### Google
<details>
<summary><strong>📋 Setup Steps</strong></summary>
Set up OAuth 2.0 credentials in [Google Cloud Console](https://console.cloud.google.com/):
1. Go to **APIs & Services****Credentials**
2. Click **Create Credentials****OAuth client ID**
3. Choose **Web application** as the application type
4. Add your redirect URI to **Authorized redirect URIs**
5. Configure the OAuth consent screen if prompted
</details>
**Configuration:**
- **Issuer URL**: `https://accounts.google.com`
- **Client ID/Secret**: From your OAuth 2.0 client credentials
- **Required Scopes**: `openid`, `profile`, `email`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
:::warning[Google Domain Requirements]
**Google requires valid domain names for OAuth redirect URIs.** Local IP addresses and `.local` domains are not accepted. To use Google OAuth with your Unraid server, you'll need:
- **Option 1: Reverse Proxy** - Set up a reverse proxy (like NGINX Proxy Manager or Traefik) with a valid domain name pointing to your Unraid API
- **Option 2: Tailscale** - Use Tailscale to get a valid `*.ts.net` domain that Google will accept
- **Option 3: Dynamic DNS** - Use a DDNS service to get a public domain name for your server
Remember to update your redirect URI in both Google Cloud Console and your Unraid OIDC configuration to use the valid domain.
:::
For Google Workspace domains, use Advanced Mode with the `hd` claim to restrict access to your organization's domain.
### Authelia
Configure OIDC client in your Authelia `configuration.yml` with client ID `unraid-api` and generate a hashed secret using the Authelia hash-password command.
**Configuration:**
- **Issuer URL**: `https://auth.yourdomain.com`
- **Client ID**: `unraid-api` (or as configured in Authelia)
- **Client Secret**: Your unhashed secret
- **Required Scopes**: `openid`, `profile`, `email`, `groups`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
Use Advanced Mode with `groups` claim for group-based authorization.
### Microsoft/Azure AD
Register a new app in [Azure Portal](https://portal.azure.com/) under Azure Active Directory → App registrations. Note the Application ID, create a client secret, and note your tenant ID.
**Configuration:**
- **Issuer URL**: `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0`
- **Client ID**: Your Application (client) ID
- **Client Secret**: Generated client secret
- **Required Scopes**: `openid`, `profile`, `email`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
Authorization rules can be configured in the interface using email domains or advanced claims.
### Keycloak
Create a new confidential client in Keycloak Admin Console with `openid-connect` protocol and copy the client secret from the Credentials tab.
**Configuration:**
- **Issuer URL**: `https://keycloak.example.com/realms/YOUR_REALM`
- **Client ID**: `unraid-api` (or as configured in Keycloak)
- **Client Secret**: From Keycloak Credentials tab
- **Required Scopes**: `openid`, `profile`, `email`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
For role-based authorization, use Advanced Mode with `realm_access.roles` or `resource_access` claims.
### Authentik
Create a new OAuth2/OpenID Provider in Authentik, then create an Application and link it to the provider.
**Configuration:**
- **Issuer URL**: `https://authentik.example.com/application/o/<application_slug>/`
- **Client ID**: From Authentik provider configuration
- **Client Secret**: From Authentik provider configuration
- **Required Scopes**: `openid`, `profile`, `email`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
Authorization rules can be configured in the interface.
### Okta
Create a new OIDC Web Application in Okta Admin Console and assign appropriate users or groups.
**Configuration:**
- **Issuer URL**: `https://YOUR_DOMAIN.okta.com`
- **Client ID**: From Okta application configuration
- **Client Secret**: From Okta application configuration
- **Required Scopes**: `openid`, `profile`, `email`
- **Redirect URI**: `http://YOUR_UNRAID_IP/graphql/api/auth/oidc/callback`
Authorization rules can be configured in the interface using email domains or advanced claims.

View File

@@ -1,172 +0,0 @@
---
title: Roadmap & Features
description: Current status and upcoming features for the Unraid API
sidebar_position: 10
---
# Roadmap & Features
:::info Development Status
This roadmap outlines completed and planned features for the Unraid API. Features and timelines may change based on development priorities and community feedback.
:::
## Feature Status Legend
| Status | Description |
|--------|-------------|
| ✅ **Done** | Feature is complete and available |
| 🚧 **In Progress** | Currently under active development |
| 📅 **Planned** | Scheduled for future development |
| 💡 **Under Consideration** | Being evaluated for future inclusion |
## Core Infrastructure
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **API Development Environment Improvements** | v4.0.0 |
| **Include API in Unraid OS** | Unraid v7.2-beta.1 |
| **Separate API from Connect Plugin** | Unraid v7.2-beta.1 |
### Upcoming Features 📅
| Feature | Target Timeline |
|---------|-----------------|
| **Make API Open Source** | Q1 2025 |
| **Developer Tools for Plugins** | Q2 2025 |
## Security & Authentication
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **Permissions System Rewrite** | v4.0.0 |
| **OIDC/SSO Support** | Unraid v7.2-beta.1 |
### In Development 🚧
- **User Interface Component Library** - Enhanced security components for the UI
## User Interface Improvements
### Planned Features 📅
| Feature | Target Timeline | Description |
|---------|-----------------|-------------|
| **New Settings Pages** | Q2 2025 | Modernized settings interface with improved UX |
| **Custom Theme Creator** | Q2-Q3 2025 | Allow users to create and share custom themes |
| **New Connect Settings Interface** | Q1 2025 | Redesigned Unraid Connect configuration |
## Array Management
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **Array Status Monitoring** | v4.0.0 |
### Planned Features 📅
| Feature | Target Timeline | Description |
|---------|-----------------|-------------|
| **Storage Pool Creation Interface** | Q2 2025 | Simplified pool creation workflow |
| **Storage Pool Status Interface** | Q2 2025 | Real-time pool health monitoring |
## Docker Integration
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **Docker Container Status Monitoring** | v4.0.0 |
### Planned Features 📅
| Feature | Target Timeline | Description |
|---------|-----------------|-------------|
| **New Docker Status Interface Design** | Q3 2025 | Modern container management UI |
| **New Docker Status Interface** | Q3 2025 | Implementation of new design |
| **Docker Container Setup Interface** | Q3 2025 | Streamlined container deployment |
| **Docker Compose Support** | TBD | Native docker-compose.yml support |
## Share Management
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **Array/Cache Share Status Monitoring** | v4.0.0 |
### Under Consideration 💡
- **Storage Share Creation & Settings** - Enhanced share configuration options
- **Storage Share Management Interface** - Unified share management dashboard
## Plugin System
### Planned Features 📅
| Feature | Target Timeline | Description |
|---------|-----------------|-------------|
| **New Plugins Interface** | Q3 2025 | Redesigned plugin management UI |
| **Plugin Management Interface** | TBD | Advanced plugin configuration |
| **Plugin Development Tools** | TBD | SDK and tooling for developers |
## Notifications
### Completed Features ✅
| Feature | Available Since |
|---------|-----------------|
| **Notifications System** | v4.0.0 |
| **Notifications Interface** | v4.0.0 |
---
## Recent Releases
:::info Full Release History
For a complete list of all releases, changelogs, and download links, visit the [Unraid API GitHub Releases](https://github.com/unraid/api/releases) page.
:::
### Unraid v7.2-beta.1 Highlights
- 🎉 **API included in Unraid OS** - Native integration
- 🔐 **OIDC/SSO Support** - Enterprise authentication
- 📦 **Standalone API** - Separated from Connect plugin
### v4.0.0 Highlights
- 🛡️ **Permissions System Rewrite** - Enhanced security
- 📊 **Comprehensive Monitoring** - Array, Docker, and Share status
- 🔔 **Notifications System** - Real-time alerts and notifications
- 🛠️ **Developer Environment** - Improved development tools
## Community Feedback
:::tip Have a Feature Request?
We value community input! Please submit feature requests and feedback through:
- [Unraid Forums](https://forums.unraid.net)
- [GitHub Issues](https://github.com/unraid/api/issues) - API is open source!
:::
## Version Support
| Unraid Version | API Version | Support Status |
|----------------|-------------|----------------|
| Unraid v7.2-beta.1+ | Latest | ✅ Active |
| 7.0 - 7.1.x | v4.x via Plugin | ⚠️ Limited |
| 6.12.x | v4.x via Plugin | ⚠️ Limited |
| < 6.12 | Not Supported | ❌ EOL |
:::warning Legacy Support
Versions prior to Unraid 7.2 require the API to be installed through the Unraid Connect plugin. Some features may not be available on older versions.
:::
:::tip Pre-release Versions
You can always install the Unraid Connect plugin to access pre-release versions of the API and get early access to new features before they're included in Unraid OS releases.
:::

View File

@@ -139,6 +139,9 @@ type ArrayDisk implements Node {
"""ata | nvme | usb | (others)"""
transport: String
color: ArrayDiskFsColor
"""Whether the disk is currently spinning"""
isSpinning: Boolean
}
interface Node {
@@ -346,6 +349,9 @@ type Disk implements Node {
"""The partitions on the disk"""
partitions: [DiskPartition!]!
"""Whether the disk is spinning or not"""
isSpinning: Boolean!
}
"""The type of interface the disk uses to connect to the system"""
@@ -1044,6 +1050,19 @@ enum ThemeName {
white
}
type ExplicitStatusItem {
name: String!
updateStatus: UpdateStatus!
}
"""Update status of a container."""
enum UpdateStatus {
UP_TO_DATE
UPDATE_AVAILABLE
REBUILD_READY
UNKNOWN
}
type ContainerPort {
ip: String
privatePort: Port
@@ -1074,8 +1093,8 @@ type DockerContainer implements Node {
created: Int!
ports: [ContainerPort!]!
"""Total size of all the files in the container"""
sizeRootFs: Int
"""Total size of all files in the container (in bytes)"""
sizeRootFs: BigInt
labels: JSON
state: ContainerState!
status: String!
@@ -1083,6 +1102,8 @@ type DockerContainer implements Node {
networkSettings: JSON
mounts: [JSON!]
autoStart: Boolean!
isUpdateAvailable: Boolean
isRebuildReady: Boolean
}
enum ContainerState {
@@ -1113,6 +1134,7 @@ type Docker implements Node {
containers(skipCache: Boolean! = false): [DockerContainer!]!
networks(skipCache: Boolean! = false): [DockerNetwork!]!
organizer: ResolvedOrganizerV1!
containerUpdateStatuses: [ExplicitStatusItem!]!
}
type ResolvedOrganizerView {
@@ -1361,6 +1383,25 @@ type CpuLoad {
"""The percentage of time the CPU spent servicing hardware interrupts."""
percentIrq: Float!
"""The percentage of time the CPU spent running virtual machines (guest)."""
percentGuest: Float!
"""The percentage of CPU time stolen by the hypervisor."""
percentSteal: Float!
}
type CpuPackages implements Node {
id: PrefixedID!
"""Total CPU package power draw (W)"""
totalPower: Float!
"""Power draw per package (W)"""
power: [Float!]!
"""Temperature per package (°C)"""
temp: [Float!]!
}
type CpuUtilization implements Node {
@@ -1426,6 +1467,12 @@ type InfoCpu implements Node {
"""CPU feature flags"""
flags: [String!]
"""
Per-package array of core/thread pairs, e.g. [[[0,1],[2,3]], [[4,5],[6,7]]]
"""
topology: [[[Int!]!]!]!
packages: CpuPackages!
}
type MemoryLayout implements Node {
@@ -2407,6 +2454,7 @@ type Mutation {
setDockerFolderChildren(folderId: String, childrenIds: [String!]!): ResolvedOrganizerV1!
deleteDockerEntries(entryIds: [String!]!): ResolvedOrganizerV1!
moveDockerEntriesToFolder(sourceEntryIds: [String!]!, destinationFolderId: String!): ResolvedOrganizerV1!
refreshDockerDigests: Boolean!
"""Initiates a flash drive backup using a configured remote."""
initiateFlashBackup(input: InitiateFlashBackupInput!): FlashBackupStatus!
@@ -2613,6 +2661,7 @@ type Subscription {
arraySubscription: UnraidArray!
logFile(path: String!): LogFileContent!
systemMetricsCpu: CpuUtilization!
systemMetricsCpuTelemetry: CpuPackages!
systemMetricsMemory: MemoryUtilization!
upsUpdates: UPSDevice!
}

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.18.0",
"version": "4.26.2",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -30,6 +30,8 @@
"// GraphQL Codegen": "",
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch",
"// Internationalization": "",
"i18n:extract": "node ./scripts/extract-translations.mjs",
"// Code Quality": "",
"lint": "eslint --config .eslintrc.ts src/",
"lint:fix": "eslint --fix --config .eslintrc.ts src/",
@@ -56,7 +58,7 @@
"@as-integrations/fastify": "2.1.1",
"@fastify/cookie": "11.0.2",
"@fastify/helmet": "13.0.1",
"@graphql-codegen/client-preset": "4.8.3",
"@graphql-codegen/client-preset": "5.0.0",
"@graphql-tools/load-files": "7.0.1",
"@graphql-tools/merge": "9.1.1",
"@graphql-tools/schema": "10.0.25",
@@ -84,7 +86,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",
@@ -94,7 +96,7 @@
"command-exists": "1.2.9",
"convert": "5.12.0",
"cookie": "1.0.2",
"cron": "4.3.3",
"cron": "4.3.0",
"cross-fetch": "4.1.0",
"diff": "8.0.2",
"dockerode": "4.0.7",
@@ -103,7 +105,7 @@
"execa": "9.6.0",
"exit-hook": "4.0.0",
"fastify": "5.5.0",
"filenamify": "6.0.0",
"filenamify": "7.0.0",
"fs-extra": "11.3.1",
"glob": "11.0.3",
"global-agent": "3.0.0",
@@ -114,6 +116,7 @@
"graphql-subscriptions": "3.0.0",
"graphql-tag": "2.12.6",
"graphql-ws": "6.0.6",
"html-entities": "^2.6.0",
"ini": "5.0.0",
"ip": "2.0.1",
"jose": "6.0.13",
@@ -127,7 +130,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 +144,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"
@@ -156,14 +159,14 @@
},
"devDependencies": {
"@eslint/js": "9.34.0",
"@graphql-codegen/add": "5.0.3",
"@graphql-codegen/cli": "5.0.7",
"@graphql-codegen/fragment-matcher": "5.1.0",
"@graphql-codegen/add": "6.0.0",
"@graphql-codegen/cli": "6.0.0",
"@graphql-codegen/fragment-matcher": "6.0.0",
"@graphql-codegen/import-types-preset": "3.0.1",
"@graphql-codegen/typed-document-node": "5.1.2",
"@graphql-codegen/typescript": "4.1.6",
"@graphql-codegen/typescript-operations": "4.6.1",
"@graphql-codegen/typescript-resolvers": "4.5.1",
"@graphql-codegen/typed-document-node": "6.0.0",
"@graphql-codegen/typescript": "5.0.0",
"@graphql-codegen/typescript-operations": "5.0.0",
"@graphql-codegen/typescript-resolvers": "5.0.0",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.6.3",
"@nestjs/testing": "11.1.6",
@@ -190,7 +193,7 @@
"@types/stoppable": "1.1.3",
"@types/strftime": "0.9.8",
"@types/supertest": "6.0.3",
"@types/uuid": "10.0.0",
"@types/uuid": "11.0.0",
"@types/ws": "8.18.1",
"@types/wtfnode": "0.10.0",
"@vitest/coverage-v8": "3.2.4",
@@ -205,7 +208,7 @@
"rollup-plugin-node-externals": "8.1.0",
"supertest": "7.1.4",
"tsx": "4.20.5",
"type-fest": "4.41.0",
"type-fest": "5.0.0",
"typescript": "5.9.2",
"typescript-eslint": "8.41.0",
"unplugin-swc": "1.5.7",

View File

@@ -0,0 +1,162 @@
#!/usr/bin/env node
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { glob } from 'glob';
import ts from 'typescript';
const projectRoot = process.cwd();
const sourcePatterns = 'src/**/*.{ts,js}';
const ignorePatterns = [
'**/__tests__/**',
'**/__test__/**',
'**/*.spec.ts',
'**/*.spec.js',
'**/*.test.ts',
'**/*.test.js',
];
const englishLocaleFile = path.resolve(projectRoot, 'src/i18n/en.json');
const identifierTargets = new Set(['t', 'translate']);
const propertyTargets = new Set([
'i18n.t',
'i18n.translate',
'ctx.t',
'this.translate',
'this.i18n.translate',
'this.i18n.t',
]);
function getPropertyChain(node) {
if (ts.isIdentifier(node)) {
return node.text;
}
if (ts.isPropertyAccessExpression(node)) {
const left = getPropertyChain(node.expression);
if (!left) return undefined;
return `${left}.${node.name.text}`;
}
return undefined;
}
function extractLiteral(node) {
if (ts.isStringLiteralLike(node)) {
return node.text;
}
if (ts.isNoSubstitutionTemplateLiteral(node)) {
return node.text;
}
return undefined;
}
function collectKeysFromSource(sourceFile) {
const keys = new Set();
function visit(node) {
if (ts.isCallExpression(node)) {
const expr = node.expression;
let matches = false;
if (ts.isIdentifier(expr) && identifierTargets.has(expr.text)) {
matches = true;
} else if (ts.isPropertyAccessExpression(expr)) {
const chain = getPropertyChain(expr);
if (chain && propertyTargets.has(chain)) {
matches = true;
}
}
if (matches) {
const [firstArg] = node.arguments;
if (firstArg) {
const literal = extractLiteral(firstArg);
if (literal) {
keys.add(literal);
}
}
}
}
ts.forEachChild(node, visit);
}
visit(sourceFile);
return keys;
}
async function loadEnglishCatalog() {
try {
const raw = await readFile(englishLocaleFile, 'utf8');
const parsed = raw.trim() ? JSON.parse(raw) : {};
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('English locale file must contain a JSON object.');
}
return parsed;
} catch (error) {
if (error && error.code === 'ENOENT') {
return {};
}
throw error;
}
}
async function ensureEnglishCatalog(keys) {
const existingCatalog = await loadEnglishCatalog();
const existingKeys = new Set(Object.keys(existingCatalog));
let added = 0;
const combinedKeys = new Set([...existingKeys, ...keys]);
const sortedKeys = Array.from(combinedKeys).sort((a, b) => a.localeCompare(b));
const nextCatalog = {};
for (const key of sortedKeys) {
if (Object.prototype.hasOwnProperty.call(existingCatalog, key)) {
nextCatalog[key] = existingCatalog[key];
} else {
nextCatalog[key] = key;
added += 1;
}
}
const nextJson = `${JSON.stringify(nextCatalog, null, 2)}\n`;
const existingJson = JSON.stringify(existingCatalog, null, 2) + '\n';
if (nextJson !== existingJson) {
await writeFile(englishLocaleFile, nextJson, 'utf8');
}
return added;
}
async function main() {
const files = await glob(sourcePatterns, {
cwd: projectRoot,
ignore: ignorePatterns,
absolute: true,
});
const collectedKeys = new Set();
await Promise.all(
files.map(async (file) => {
const content = await readFile(file, 'utf8');
const sourceFile = ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true);
const keys = collectKeysFromSource(sourceFile);
keys.forEach((key) => collectedKeys.add(key));
}),
);
const added = await ensureEnglishCatalog(collectedKeys);
if (added === 0) {
console.log('[i18n] No new backend translation keys detected.');
} else {
console.log(`[i18n] Added ${added} key(s) to src/i18n/en.json.`);
}
}
main().catch((error) => {
console.error('[i18n] Failed to extract backend translations.', error);
process.exitCode = 1;
});

View File

@@ -4,23 +4,18 @@ import {
getBannerPathIfPresent,
getCasePathIfPresent,
} from '@app/core/utils/images/image-file-helpers.js';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { store } from '@app/store/index.js';
import { loadDynamixConfig } from '@app/store/index.js';
test('get case path returns expected result', async () => {
await expect(getCasePathIfPresent()).resolves.toContain('/dev/dynamix/case-model.png');
});
test('get banner path returns null (state unloaded)', async () => {
await expect(getBannerPathIfPresent()).resolves.toMatchInlineSnapshot('null');
});
test('get banner path returns the banner (state loaded)', async () => {
await store.dispatch(loadDynamixConfigFile()).unwrap();
loadDynamixConfig();
await expect(getBannerPathIfPresent()).resolves.toContain('/dev/dynamix/banner.png');
});
test('get banner path returns null when no banner (state loaded)', async () => {
await store.dispatch(loadDynamixConfigFile()).unwrap();
loadDynamixConfig();
await expect(getBannerPathIfPresent('notabanner.png')).resolves.toMatchInlineSnapshot('null');
});

View File

@@ -1,11 +1,12 @@
import { expect, test } from 'vitest';
import { expect, test, vi } from 'vitest';
import { store } from '@app/store/index.js';
import { FileLoadStatus, StateFileKey } from '@app/store/types.js';
import '@app/core/utils/misc/get-key-file.js';
import '@app/store/modules/emhttp.js';
vi.mock('fs/promises');
test('Before loading key returns null', async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
const { status } = store.getState().registration;
@@ -48,21 +49,70 @@ test('Returns empty key if key location is empty', async () => {
await expect(getKeyFile()).resolves.toBe('');
});
test(
'Returns decoded key file if key location exists',
async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
const { loadStateFiles } = await import('@app/store/modules/emhttp.js');
const { loadRegistrationKey } = await import('@app/store/modules/registration.js');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
await expect(getKeyFile()).resolves.toMatchInlineSnapshot(
'"hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w"'
);
},
{ timeout: 10000 }
);
test('Returns empty string when key file does not exist (ENOENT)', async () => {
const { readFile } = await import('fs/promises');
// Mock readFile to throw ENOENT error
const readFileMock = vi.mocked(readFile);
readFileMock.mockRejectedValueOnce(
Object.assign(new Error('ENOENT: no such file or directory'), { code: 'ENOENT' })
);
// Clear the module cache and re-import to get fresh module with mock
vi.resetModules();
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
const { updateEmhttpState } = await import('@app/store/modules/emhttp.js');
const { store: freshStore } = await import('@app/store/index.js');
// Set key file location to a non-existent file
freshStore.dispatch(
updateEmhttpState({
field: StateFileKey.var,
state: {
regFile: '/boot/config/Pro.key',
},
})
);
// Should return empty string when file doesn't exist
await expect(getKeyFile()).resolves.toBe('');
// Clear mock
readFileMock.mockReset();
vi.resetModules();
});
test('Returns decoded key file if key location exists', async () => {
const { readFile } = await import('fs/promises');
// Mock a valid key file content
const mockKeyContent =
'hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w==';
const binaryContent = Buffer.from(mockKeyContent, 'base64').toString('binary');
const readFileMock = vi.mocked(readFile);
readFileMock.mockResolvedValue(binaryContent);
// Clear the module cache and re-import to get fresh module with mock
vi.resetModules();
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file.js');
const { loadStateFiles } = await import('@app/store/modules/emhttp.js');
const { loadRegistrationKey } = await import('@app/store/modules/registration.js');
const { store: freshStore } = await import('@app/store/index.js');
// Load state files into store
await freshStore.dispatch(loadStateFiles());
await freshStore.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status } = freshStore.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
const result = await getKeyFile();
expect(result).toBe(
'hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w'
);
// Clear mock
readFileMock.mockReset();
vi.resetModules();
}, 10000);

View File

@@ -0,0 +1,178 @@
import { describe, expect, test } from 'vitest';
import {
iniBooleanOrAutoToJsBoolean,
iniBooleanToJsBoolean,
} from '@app/core/utils/parsers/ini-boolean-parser.js';
describe('iniBooleanToJsBoolean', () => {
describe('valid boolean values', () => {
test('returns false for "no"', () => {
expect(iniBooleanToJsBoolean('no')).toBe(false);
});
test('returns false for "false"', () => {
expect(iniBooleanToJsBoolean('false')).toBe(false);
});
test('returns true for "yes"', () => {
expect(iniBooleanToJsBoolean('yes')).toBe(true);
});
test('returns true for "true"', () => {
expect(iniBooleanToJsBoolean('true')).toBe(true);
});
});
describe('malformed values', () => {
test('handles "no*" as false', () => {
expect(iniBooleanToJsBoolean('no*')).toBe(false);
});
test('handles "yes*" as true', () => {
expect(iniBooleanToJsBoolean('yes*')).toBe(true);
});
test('handles "true*" as true', () => {
expect(iniBooleanToJsBoolean('true*')).toBe(true);
});
test('handles "false*" as false', () => {
expect(iniBooleanToJsBoolean('false*')).toBe(false);
});
test('returns undefined for "n0!" (cleans to "n" which is invalid)', () => {
expect(iniBooleanToJsBoolean('n0!')).toBe(undefined);
});
test('returns undefined for "y3s!" (cleans to "ys" which is invalid)', () => {
expect(iniBooleanToJsBoolean('y3s!')).toBe(undefined);
});
test('handles mixed case with extra chars "YES*" as true', () => {
expect(iniBooleanToJsBoolean('YES*')).toBe(true);
});
test('handles mixed case with extra chars "NO*" as false', () => {
expect(iniBooleanToJsBoolean('NO*')).toBe(false);
});
});
describe('default values', () => {
test('returns default value for invalid input when provided', () => {
expect(iniBooleanToJsBoolean('invalid', true)).toBe(true);
expect(iniBooleanToJsBoolean('invalid', false)).toBe(false);
});
test('returns default value for empty string when provided', () => {
expect(iniBooleanToJsBoolean('', true)).toBe(true);
expect(iniBooleanToJsBoolean('', false)).toBe(false);
});
});
describe('undefined fallback cases', () => {
test('returns undefined for invalid input without default', () => {
expect(iniBooleanToJsBoolean('invalid')).toBe(undefined);
});
test('returns undefined for empty string without default', () => {
expect(iniBooleanToJsBoolean('')).toBe(undefined);
});
test('returns undefined for numeric string without default', () => {
expect(iniBooleanToJsBoolean('123')).toBe(undefined);
});
});
});
describe('iniBooleanOrAutoToJsBoolean', () => {
describe('valid boolean values', () => {
test('returns false for "no"', () => {
expect(iniBooleanOrAutoToJsBoolean('no')).toBe(false);
});
test('returns false for "false"', () => {
expect(iniBooleanOrAutoToJsBoolean('false')).toBe(false);
});
test('returns true for "yes"', () => {
expect(iniBooleanOrAutoToJsBoolean('yes')).toBe(true);
});
test('returns true for "true"', () => {
expect(iniBooleanOrAutoToJsBoolean('true')).toBe(true);
});
});
describe('auto value', () => {
test('returns null for "auto"', () => {
expect(iniBooleanOrAutoToJsBoolean('auto')).toBe(null);
});
});
describe('malformed values', () => {
test('handles "no*" as false', () => {
expect(iniBooleanOrAutoToJsBoolean('no*')).toBe(false);
});
test('handles "yes*" as true', () => {
expect(iniBooleanOrAutoToJsBoolean('yes*')).toBe(true);
});
test('handles "auto*" as null', () => {
expect(iniBooleanOrAutoToJsBoolean('auto*')).toBe(null);
});
test('handles "true*" as true', () => {
expect(iniBooleanOrAutoToJsBoolean('true*')).toBe(true);
});
test('handles "false*" as false', () => {
expect(iniBooleanOrAutoToJsBoolean('false*')).toBe(false);
});
test('handles "n0!" as undefined fallback (cleans to "n" which is invalid)', () => {
expect(iniBooleanOrAutoToJsBoolean('n0!')).toBe(undefined);
});
test('handles "a1ut2o!" as null (removes non-alphabetic chars)', () => {
expect(iniBooleanOrAutoToJsBoolean('a1ut2o!')).toBe(null);
});
test('handles mixed case "AUTO*" as null', () => {
expect(iniBooleanOrAutoToJsBoolean('AUTO*')).toBe(null);
});
});
describe('fallback behavior', () => {
test('returns undefined for completely invalid input', () => {
expect(iniBooleanOrAutoToJsBoolean('invalid123')).toBe(undefined);
});
test('returns undefined for empty string', () => {
expect(iniBooleanOrAutoToJsBoolean('')).toBe(undefined);
});
test('returns undefined for numeric string', () => {
expect(iniBooleanOrAutoToJsBoolean('123')).toBe(undefined);
});
test('returns undefined for special characters only', () => {
expect(iniBooleanOrAutoToJsBoolean('!@#$')).toBe(undefined);
});
});
describe('edge cases', () => {
test('handles undefined gracefully', () => {
expect(iniBooleanOrAutoToJsBoolean(undefined as any)).toBe(undefined);
});
test('handles null gracefully', () => {
expect(iniBooleanOrAutoToJsBoolean(null as any)).toBe(undefined);
});
test('handles non-string input gracefully', () => {
expect(iniBooleanOrAutoToJsBoolean(123 as any)).toBe(undefined);
});
});
});

View File

@@ -1,10 +1,11 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { execa } from 'execa';
import pm2 from 'pm2';
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running.js';
@@ -17,11 +18,6 @@ const TEST_PROCESS_NAME = 'test-unraid-api';
// Shared PM2 connection state
let pm2Connected = false;
// Helper function to run CLI command (assumes CLI is built)
async function runCliCommand(command: string, options: any = {}) {
return await execa('node', [CLI_PATH, command], options);
}
// Helper to ensure PM2 connection is established
async function ensurePM2Connection() {
if (pm2Connected) return;
@@ -57,7 +53,7 @@ async function deleteTestProcesses() {
}
const processName = processNames[deletedCount];
pm2.delete(processName, (deleteErr) => {
pm2.delete(processName, () => {
// Ignore errors, process might not exist
deletedCount++;
deleteNext();
@@ -92,7 +88,7 @@ async function cleanupAllPM2Processes() {
}
// Kill the daemon to ensure fresh state
pm2.killDaemon((killErr) => {
pm2.killDaemon(() => {
pm2.disconnect();
pm2Connected = false;
// Small delay to let PM2 fully shutdown
@@ -104,6 +100,9 @@ async function cleanupAllPM2Processes() {
describe.skipIf(!!process.env.CI)('PM2 integration tests', () => {
beforeAll(async () => {
// Set PM2_HOME to use home directory for testing (not /var/log)
process.env.PM2_HOME = join(homedir(), '.pm2');
// Build the CLI if it doesn't exist (only for CLI tests)
if (!existsSync(CLI_PATH)) {
console.log('Building CLI for integration tests...');
@@ -198,6 +197,13 @@ describe.skipIf(!!process.env.CI)('PM2 integration tests', () => {
}, 30000);
it('should handle PM2 connection errors gracefully', async () => {
// Disconnect PM2 first to ensure we're testing fresh connection
await new Promise<void>((resolve) => {
pm2.disconnect();
pm2Connected = false;
setTimeout(resolve, 100);
});
// Set an invalid PM2_HOME to force connection failure
const originalPM2Home = process.env.PM2_HOME;
process.env.PM2_HOME = '/invalid/path/that/does/not/exist';

View File

@@ -12,7 +12,22 @@ import {
UpdateRCloneRemoteDto,
} from '@app/unraid-api/graph/resolvers/rclone/rclone.model.js';
vi.mock('got');
vi.mock('got', () => {
const mockPost = vi.fn();
const gotMock = {
post: mockPost,
};
return {
default: gotMock,
HTTPError: class HTTPError extends Error {
response?: any;
constructor(response?: any) {
super('HTTP Error');
this.response = response;
}
},
};
});
vi.mock('execa');
vi.mock('p-retry');
vi.mock('node:fs', () => ({
@@ -60,7 +75,7 @@ vi.mock('@nestjs/common', async (importOriginal) => {
describe('RCloneApiService', () => {
let service: RCloneApiService;
let mockGot: any;
let mockGotPost: any;
let mockExeca: any;
let mockPRetry: any;
let mockExistsSync: any;
@@ -68,19 +83,19 @@ describe('RCloneApiService', () => {
beforeEach(async () => {
vi.clearAllMocks();
const { default: got } = await import('got');
const got = await import('got');
const { execa } = await import('execa');
const pRetry = await import('p-retry');
const { existsSync } = await import('node:fs');
const { fileExists } = await import('@app/core/utils/files/file-exists.js');
mockGot = vi.mocked(got);
mockGotPost = vi.mocked(got.default.post);
mockExeca = vi.mocked(execa);
mockPRetry = vi.mocked(pRetry.default);
mockExistsSync = vi.mocked(existsSync);
// Mock successful RClone API response for socket check
mockGot.post = vi.fn().mockResolvedValue({ body: { pid: 12345 } });
mockGotPost.mockResolvedValue({ body: { pid: 12345 } });
// Mock RClone binary exists check
vi.mocked(fileExists).mockResolvedValue(true);
@@ -97,10 +112,10 @@ describe('RCloneApiService', () => {
mockPRetry.mockResolvedValue(undefined);
service = new RCloneApiService();
await service.onModuleInit();
await service.onApplicationBootstrap();
// Reset the mock after initialization to prepare for test-specific responses
mockGot.post.mockClear();
mockGotPost.mockClear();
});
describe('getProviders', () => {
@@ -109,15 +124,15 @@ describe('RCloneApiService', () => {
{ name: 'aws', prefix: 's3', description: 'Amazon S3' },
{ name: 'google', prefix: 'drive', description: 'Google Drive' },
];
mockGot.post.mockResolvedValue({
mockGotPost.mockResolvedValue({
body: { providers: mockProviders },
});
const result = await service.getProviders();
expect(result).toEqual(mockProviders);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/providers',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/providers$/),
expect.objectContaining({
json: {},
responseType: 'json',
@@ -130,7 +145,7 @@ describe('RCloneApiService', () => {
});
it('should return empty array when no providers', async () => {
mockGot.post.mockResolvedValue({ body: {} });
mockGotPost.mockResolvedValue({ body: {} });
const result = await service.getProviders();
@@ -141,15 +156,15 @@ describe('RCloneApiService', () => {
describe('listRemotes', () => {
it('should return list of remotes', async () => {
const mockRemotes = ['backup-s3', 'drive-storage'];
mockGot.post.mockResolvedValue({
mockGotPost.mockResolvedValue({
body: { remotes: mockRemotes },
});
const result = await service.listRemotes();
expect(result).toEqual(mockRemotes);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/listremotes',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/listremotes$/),
expect.objectContaining({
json: {},
responseType: 'json',
@@ -162,7 +177,7 @@ describe('RCloneApiService', () => {
});
it('should return empty array when no remotes', async () => {
mockGot.post.mockResolvedValue({ body: {} });
mockGotPost.mockResolvedValue({ body: {} });
const result = await service.listRemotes();
@@ -174,13 +189,13 @@ describe('RCloneApiService', () => {
it('should return remote details', async () => {
const input: GetRCloneRemoteDetailsDto = { name: 'test-remote' };
const mockConfig = { type: 's3', provider: 'AWS' };
mockGot.post.mockResolvedValue({ body: mockConfig });
mockGotPost.mockResolvedValue({ body: mockConfig });
const result = await service.getRemoteDetails(input);
expect(result).toEqual(mockConfig);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/get',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/get$/),
expect.objectContaining({
json: { name: 'test-remote' },
responseType: 'json',
@@ -197,7 +212,7 @@ describe('RCloneApiService', () => {
it('should return remote configuration', async () => {
const input: GetRCloneRemoteConfigDto = { name: 'test-remote' };
const mockConfig = { type: 's3', access_key_id: 'AKIA...' };
mockGot.post.mockResolvedValue({ body: mockConfig });
mockGotPost.mockResolvedValue({ body: mockConfig });
const result = await service.getRemoteConfig(input);
@@ -213,13 +228,13 @@ describe('RCloneApiService', () => {
parameters: { access_key_id: 'AKIA...', secret_access_key: 'secret' },
};
const mockResponse = { success: true };
mockGot.post.mockResolvedValue({ body: mockResponse });
mockGotPost.mockResolvedValue({ body: mockResponse });
const result = await service.createRemote(input);
expect(result).toEqual(mockResponse);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/create',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/create$/),
expect.objectContaining({
json: {
name: 'new-remote',
@@ -243,13 +258,13 @@ describe('RCloneApiService', () => {
parameters: { access_key_id: 'NEW_AKIA...' },
};
const mockResponse = { success: true };
mockGot.post.mockResolvedValue({ body: mockResponse });
mockGotPost.mockResolvedValue({ body: mockResponse });
const result = await service.updateRemote(input);
expect(result).toEqual(mockResponse);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/update',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/update$/),
expect.objectContaining({
json: {
name: 'existing-remote',
@@ -269,13 +284,13 @@ describe('RCloneApiService', () => {
it('should delete a remote', async () => {
const input: DeleteRCloneRemoteDto = { name: 'remote-to-delete' };
const mockResponse = { success: true };
mockGot.post.mockResolvedValue({ body: mockResponse });
mockGotPost.mockResolvedValue({ body: mockResponse });
const result = await service.deleteRemote(input);
expect(result).toEqual(mockResponse);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/config/delete',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/config\/delete$/),
expect.objectContaining({
json: { name: 'remote-to-delete' },
responseType: 'json',
@@ -296,13 +311,13 @@ describe('RCloneApiService', () => {
options: { delete_on: 'dst' },
};
const mockResponse = { jobid: 'job-123' };
mockGot.post.mockResolvedValue({ body: mockResponse });
mockGotPost.mockResolvedValue({ body: mockResponse });
const result = await service.startBackup(input);
expect(result).toEqual(mockResponse);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/sync/copy',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/sync\/copy$/),
expect.objectContaining({
json: {
srcFs: '/source/path',
@@ -323,13 +338,13 @@ describe('RCloneApiService', () => {
it('should return job status', async () => {
const input: GetRCloneJobStatusDto = { jobId: 'job-123' };
const mockStatus = { status: 'running', progress: 0.5 };
mockGot.post.mockResolvedValue({ body: mockStatus });
mockGotPost.mockResolvedValue({ body: mockStatus });
const result = await service.getJobStatus(input);
expect(result).toEqual(mockStatus);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/job/status',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/job\/status$/),
expect.objectContaining({
json: { jobid: 'job-123' },
responseType: 'json',
@@ -348,13 +363,13 @@ describe('RCloneApiService', () => {
{ id: 'job-1', status: 'running' },
{ id: 'job-2', status: 'finished' },
];
mockGot.post.mockResolvedValue({ body: mockJobs });
mockGotPost.mockResolvedValue({ body: mockJobs });
const result = await service.listRunningJobs();
expect(result).toEqual(mockJobs);
expect(mockGot.post).toHaveBeenCalledWith(
'http://unix:/tmp/rclone.sock:/job/list',
expect(mockGotPost).toHaveBeenCalledWith(
expect.stringMatching(/\/job\/list$/),
expect.objectContaining({
json: {},
responseType: 'json',
@@ -378,7 +393,7 @@ describe('RCloneApiService', () => {
},
};
Object.setPrototypeOf(httpError, HTTPError.prototype);
mockGot.post.mockRejectedValue(httpError);
mockGotPost.mockRejectedValue(httpError);
await expect(service.getProviders()).rejects.toThrow(
'Rclone API Error (config/providers, HTTP 500): Rclone Error: Internal server error'
@@ -395,7 +410,7 @@ describe('RCloneApiService', () => {
},
};
Object.setPrototypeOf(httpError, HTTPError.prototype);
mockGot.post.mockRejectedValue(httpError);
mockGotPost.mockRejectedValue(httpError);
await expect(service.getProviders()).rejects.toThrow(
'Rclone API Error (config/providers, HTTP 404): Failed to process error response body. Raw body:'
@@ -412,7 +427,7 @@ describe('RCloneApiService', () => {
},
};
Object.setPrototypeOf(httpError, HTTPError.prototype);
mockGot.post.mockRejectedValue(httpError);
mockGotPost.mockRejectedValue(httpError);
await expect(service.getProviders()).rejects.toThrow(
'Rclone API Error (config/providers, HTTP 400): Failed to process error response body. Raw body: invalid json'
@@ -421,17 +436,108 @@ describe('RCloneApiService', () => {
it('should handle non-HTTP errors', async () => {
const networkError = new Error('Network connection failed');
mockGot.post.mockRejectedValue(networkError);
mockGotPost.mockRejectedValue(networkError);
await expect(service.getProviders()).rejects.toThrow('Network connection failed');
});
it('should handle unknown errors', async () => {
mockGot.post.mockRejectedValue('unknown error');
mockGotPost.mockRejectedValue('unknown error');
await expect(service.getProviders()).rejects.toThrow(
'Unknown error calling RClone API (config/providers) with params {}: unknown error'
);
});
});
describe('checkRcloneBinaryExists', () => {
beforeEach(() => {
// Create a new service instance without initializing for these tests
service = new RCloneApiService();
});
it('should return true when rclone version is 1.70.0', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone v1.70.0\n- os/version: darwin 14.0 (64 bit)\n- os/kernel: 23.0.0 (arm64)',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(true);
});
it('should return true when rclone version is newer than 1.70.0', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone v1.75.2\n- os/version: darwin 14.0 (64 bit)\n- os/kernel: 23.0.0 (arm64)',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(true);
});
it('should return false when rclone version is older than 1.70.0', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone v1.69.0\n- os/version: darwin 14.0 (64 bit)\n- os/kernel: 23.0.0 (arm64)',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(false);
});
it('should return false when rclone version is much older', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone v1.50.0\n- os/version: darwin 14.0 (64 bit)\n- os/kernel: 23.0.0 (arm64)',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(false);
});
it('should return false when version cannot be parsed', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone unknown version format',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(false);
});
it('should return false when rclone binary is not found', async () => {
const error = new Error('Command not found') as any;
error.code = 'ENOENT';
mockExeca.mockRejectedValueOnce(error);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(false);
});
it('should return false and log error for other exceptions', async () => {
mockExeca.mockRejectedValueOnce(new Error('Some other error'));
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(false);
});
it('should handle beta/rc versions correctly', async () => {
mockExeca.mockResolvedValueOnce({
stdout: 'rclone v1.70.0-beta.1\n- os/version: darwin 14.0 (64 bit)\n- os/kernel: 23.0.0 (arm64)',
stderr: '',
} as any);
const result = await (service as any).checkRcloneBinaryExists();
expect(result).toBe(true);
});
});
});

View File

@@ -211,6 +211,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": null,
"id": "ST18000NM000J-2TV103_ZR585CPY",
"idx": 0,
"isSpinning": true,
"name": "parity",
"numErrors": 0,
"numReads": 0,
@@ -235,6 +236,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": 4116003021,
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
"idx": 1,
"isSpinning": true,
"name": "disk1",
"numErrors": 0,
"numReads": 0,
@@ -259,6 +261,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": 11904860828,
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
"idx": 2,
"isSpinning": true,
"name": "disk2",
"numErrors": 0,
"numReads": 0,
@@ -283,6 +286,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": 6478056481,
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
"idx": 3,
"isSpinning": true,
"name": "disk3",
"numErrors": 0,
"numReads": 0,
@@ -307,6 +311,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": 137273827,
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
"idx": 30,
"isSpinning": true,
"name": "cache",
"numErrors": 0,
"numReads": 0,
@@ -331,6 +336,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": null,
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
"idx": 31,
"isSpinning": true,
"name": "cache2",
"numErrors": 0,
"numReads": 0,
@@ -355,6 +361,7 @@ test('After init returns values from cfg file for all fields', { timeout: 30000
"fsUsed": 851325,
"id": "Cruzer",
"idx": 32,
"isSpinning": true,
"name": "flash",
"numErrors": 0,
"numReads": 0,

View File

@@ -28,6 +28,7 @@ test('Returns parsed state file', async () => {
"fsUsed": null,
"id": "ST18000NM000J-2TV103_ZR585CPY",
"idx": 0,
"isSpinning": true,
"name": "parity",
"numErrors": 0,
"numReads": 0,
@@ -52,6 +53,7 @@ test('Returns parsed state file', async () => {
"fsUsed": 4116003021,
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
"idx": 1,
"isSpinning": true,
"name": "disk1",
"numErrors": 0,
"numReads": 0,
@@ -76,6 +78,7 @@ test('Returns parsed state file', async () => {
"fsUsed": 11904860828,
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
"idx": 2,
"isSpinning": true,
"name": "disk2",
"numErrors": 0,
"numReads": 0,
@@ -100,6 +103,7 @@ test('Returns parsed state file', async () => {
"fsUsed": 6478056481,
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
"idx": 3,
"isSpinning": true,
"name": "disk3",
"numErrors": 0,
"numReads": 0,
@@ -124,6 +128,7 @@ test('Returns parsed state file', async () => {
"fsUsed": 137273827,
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
"idx": 30,
"isSpinning": true,
"name": "cache",
"numErrors": 0,
"numReads": 0,
@@ -148,6 +153,7 @@ test('Returns parsed state file', async () => {
"fsUsed": null,
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
"idx": 31,
"isSpinning": true,
"name": "cache2",
"numErrors": 0,
"numReads": 0,
@@ -172,6 +178,7 @@ test('Returns parsed state file', async () => {
"fsUsed": 851325,
"id": "Cruzer",
"idx": 32,
"isSpinning": true,
"name": "flash",
"numErrors": 0,
"numReads": 0,

View File

@@ -2,7 +2,7 @@ import { join } from 'path';
import type { JSONWebKeySet } from 'jose';
import { PORT } from '@app/environment.js';
import { ENABLE_NEXT_DOCKER_RELEASE, PORT } from '@app/environment.js';
export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
const envPort = PORT;
@@ -79,3 +79,14 @@ export const KEYSERVER_VALIDATION_ENDPOINT = 'https://keys.lime-technology.com/v
/** Set the max retries for the GraphQL Client */
export const MAX_RETRIES_FOR_LINEAR_BACKOFF = 100;
/**
* Feature flags are used to conditionally enable or disable functionality in the Unraid API.
*
* Keys are human readable feature flag names -- will be used to construct error messages.
*
* Values are boolean/truthy values.
*/
export const FeatureFlags = Object.freeze({
ENABLE_NEXT_DOCKER_RELEASE,
});

View File

@@ -1,7 +1,7 @@
import pino from 'pino';
import pretty from 'pino-pretty';
import { API_VERSION, LOG_LEVEL, LOG_TYPE, PATHS_LOGS_FILE, SUPPRESS_LOGS } from '@app/environment.js';
import { API_VERSION, LOG_LEVEL, LOG_TYPE, SUPPRESS_LOGS } from '@app/environment.js';
export const levels = ['trace', 'debug', 'info', 'warn', 'error', 'fatal'] as const;
@@ -17,30 +17,27 @@ const nullDestination = pino.destination({
export const logDestination =
process.env.SUPPRESS_LOGS === 'true' ? nullDestination : pino.destination();
const localFileDestination = pino.destination({
dest: PATHS_LOGS_FILE,
sync: true,
});
// Since PM2 captures stdout and writes to the log file, we should not colorize stdout
// to avoid ANSI escape codes in the log file
const stream = SUPPRESS_LOGS
? nullDestination
: LOG_TYPE === 'pretty'
? pretty({
singleLine: true,
hideObject: false,
colorize: true,
colorizeObjects: true,
colorize: false, // No colors since PM2 writes stdout to file
colorizeObjects: false,
levelFirst: false,
ignore: 'hostname,pid',
destination: logDestination,
translateTime: 'HH:mm:ss',
customPrettifiers: {
time: (timestamp: string | object) => `[${timestamp}`,
level: (logLevel: string | object, key: string, log: any, extras: any) => {
// Use labelColorized which preserves the colors
const { labelColorized } = extras;
level: (_logLevel: string | object, _key: string, log: any, extras: any) => {
// Use label instead of labelColorized for non-colored output
const { label } = extras;
const context = log.context || log.logger || 'app';
return `${labelColorized} ${context}]`;
return `${label} ${context}]`;
},
},
messageFormat: (log: any, messageKey: string) => {
@@ -98,7 +95,7 @@ export const keyServerLogger = logger.child({ logger: 'key-server' });
export const remoteAccessLogger = logger.child({ logger: 'remote-access' });
export const remoteQueryLogger = logger.child({ logger: 'remote-query' });
export const apiLogger = logger.child({ logger: 'api' });
export const pluginLogger = logger.child({ logger: 'plugin', stream: localFileDestination });
export const pluginLogger = logger.child({ logger: 'plugin' });
export const loggers = [
internalLogger,

View File

@@ -0,0 +1,66 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import { isSafeModeEnabled } from '@app/core/utils/safe-mode.js';
import { store } from '@app/store/index.js';
import * as stateFileLoader from '@app/store/services/state-file-loader.js';
describe('isSafeModeEnabled', () => {
afterEach(() => {
vi.restoreAllMocks();
});
it('returns the safe mode flag already present in the store', () => {
const baseState = store.getState();
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
emhttp: {
...baseState.emhttp,
var: {
...(baseState.emhttp?.var ?? {}),
safeMode: true,
},
},
});
const loaderSpy = vi.spyOn(stateFileLoader, 'loadStateFileSync');
expect(isSafeModeEnabled()).toBe(true);
expect(loaderSpy).not.toHaveBeenCalled();
});
it('falls back to the synchronous loader when store state is missing', () => {
const baseState = store.getState();
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
emhttp: {
...baseState.emhttp,
var: {
...(baseState.emhttp?.var ?? {}),
safeMode: undefined as unknown as boolean,
} as typeof baseState.emhttp.var,
} as typeof baseState.emhttp,
} as typeof baseState);
vi.spyOn(stateFileLoader, 'loadStateFileSync').mockReturnValue({
...(baseState.emhttp?.var ?? {}),
safeMode: true,
} as any);
expect(isSafeModeEnabled()).toBe(true);
});
it('defaults to false when loader cannot provide state', () => {
const baseState = store.getState();
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
emhttp: {
...baseState.emhttp,
var: {
...(baseState.emhttp?.var ?? {}),
safeMode: undefined as unknown as boolean,
} as typeof baseState.emhttp.var,
} as typeof baseState.emhttp,
} as typeof baseState);
vi.spyOn(stateFileLoader, 'loadStateFileSync').mockReturnValue(null);
expect(isSafeModeEnabled()).toBe(false);
});
});

View File

@@ -16,11 +16,22 @@ export const getKeyFile = async function (appStore: RootState = store.getState()
const keyFileName = basename(emhttp.var?.regFile);
const registrationKeyFilePath = join(paths['keyfile-base'], keyFileName);
const keyFile = await readFile(registrationKeyFilePath, 'binary');
return Buffer.from(keyFile, 'binary')
.toString('base64')
.trim()
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
try {
const keyFile = await readFile(registrationKeyFilePath, 'binary');
return Buffer.from(keyFile, 'binary')
.toString('base64')
.trim()
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
} catch (error) {
// Handle ENOENT error when Pro.key file doesn't exist
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
// Return empty string when key file is missing (ENOKEYFILE state)
return '';
}
// Re-throw other errors
throw error;
}
};

View File

@@ -0,0 +1,86 @@
import { type IniStringBoolean, type IniStringBooleanOrAuto } from '@app/core/types/ini.js';
/**
* Converts INI boolean string values to JavaScript boolean values.
* Handles malformed values by cleaning them of non-alphabetic characters.
*
* @param value - The string value to parse ("yes", "no", "true", "false", etc.)
* @returns boolean value or undefined if parsing fails
*/
export function iniBooleanToJsBoolean(value: string): boolean | undefined;
/**
* Converts INI boolean string values to JavaScript boolean values.
* Handles malformed values by cleaning them of non-alphabetic characters.
*
* @param value - The string value to parse ("yes", "no", "true", "false", etc.)
* @param defaultValue - Default value to return if parsing fails
* @returns boolean value or defaultValue if parsing fails (never undefined when defaultValue is provided)
*/
export function iniBooleanToJsBoolean(value: string, defaultValue: boolean): boolean;
export function iniBooleanToJsBoolean(value: string, defaultValue?: boolean): boolean | undefined {
if (value === 'no' || value === 'false') {
return false;
}
if (value === 'yes' || value === 'true') {
return true;
}
// Handle malformed values by cleaning them first
if (typeof value === 'string') {
const cleanValue = value.replace(/[^a-zA-Z]/g, '').toLowerCase();
if (cleanValue === 'no' || cleanValue === 'false') {
return false;
}
if (cleanValue === 'yes' || cleanValue === 'true') {
return true;
}
}
// Always return defaultValue when provided (even if undefined)
if (arguments.length >= 2) {
return defaultValue;
}
// Return undefined only when no default was provided
return undefined;
}
/**
* Converts INI boolean or auto string values to JavaScript boolean or null values.
* Handles malformed values by cleaning them of non-alphabetic characters.
*
* @param value - The string value to parse ("yes", "no", "auto", "true", "false", etc.)
* @returns boolean value for yes/no/true/false, null for auto, or undefined as fallback
*/
export const iniBooleanOrAutoToJsBoolean = (
value: IniStringBooleanOrAuto | string
): boolean | null | undefined => {
// Handle auto first
if (value === 'auto') {
return null;
}
// Try to parse as boolean
const boolResult = iniBooleanToJsBoolean(value as IniStringBoolean);
if (boolResult !== undefined) {
return boolResult;
}
// Handle malformed values like "auto*" by extracting the base value
if (typeof value === 'string') {
const cleanValue = value.replace(/[^a-zA-Z]/g, '').toLowerCase();
if (cleanValue === 'auto') {
return null;
}
if (cleanValue === 'no' || cleanValue === 'false') {
return false;
}
if (cleanValue === 'yes' || cleanValue === 'true') {
return true;
}
}
// Return undefined as fallback instead of throwing to prevent API crash
return undefined;
};

View File

@@ -0,0 +1,17 @@
import { store } from '@app/store/index.js';
import { loadStateFileSync } from '@app/store/services/state-file-loader.js';
import { StateFileKey } from '@app/store/types.js';
export const isSafeModeEnabled = (): boolean => {
const safeModeFromStore = store.getState().emhttp?.var?.safeMode;
if (typeof safeModeFromStore === 'boolean') {
return safeModeFromStore;
}
const varState = loadStateFileSync(StateFileKey.var);
if (varState) {
return Boolean(varState.safeMode);
}
return false;
};

View File

@@ -2,7 +2,6 @@
// Non-function exports from this module are loaded into the NestJS Config at runtime.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -99,7 +98,7 @@ export const MOTHERSHIP_GRAPHQL_LINK = process.env.MOTHERSHIP_GRAPHQL_LINK
? 'https://staging.mothership.unraid.net/ws'
: 'https://mothership.unraid.net/ws';
export const PM2_HOME = process.env.PM2_HOME ?? join(homedir(), '.pm2');
export const PM2_HOME = process.env.PM2_HOME ?? '/var/log/.pm2';
export const PM2_PATH = join(import.meta.dirname, '../../', 'node_modules', 'pm2', 'bin', 'pm2');
export const ECOSYSTEM_PATH = join(import.meta.dirname, '../../', 'ecosystem.config.json');
export const PATHS_LOGS_DIR =
@@ -111,3 +110,6 @@ export const PATHS_CONFIG_MODULES =
export const PATHS_LOCAL_SESSION_FILE =
process.env.PATHS_LOCAL_SESSION_FILE ?? '/var/run/unraid-api/local-session';
/** feature flag for the upcoming docker release */
export const ENABLE_NEXT_DOCKER_RELEASE = process.env.ENABLE_NEXT_DOCKER_RELEASE === 'true';

1
api/src/i18n/ar.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/bn.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/ca.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/cs.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/da.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/de.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/en.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/es.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/fr.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/hi.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/hr.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/hu.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/it.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/ja.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/ko.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/lv.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/nl.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/no.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/pl.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/pt.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/ro.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/ru.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/sv.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/uk.json Normal file
View File

@@ -0,0 +1 @@
{}

1
api/src/i18n/zh.json Normal file
View File

@@ -0,0 +1 @@
{}

View File

@@ -18,13 +18,11 @@ import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
import { getServerIdentifier } from '@app/core/utils/server-identifier.js';
import { environment, PATHS_CONFIG_MODULES, PORT } from '@app/environment.js';
import * as envVars from '@app/environment.js';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { shutdownApiEvent } from '@app/store/actions/shutdown-api-event.js';
import { store } from '@app/store/index.js';
import { loadDynamixConfig, store } from '@app/store/index.js';
import { startMiddlewareListeners } from '@app/store/listeners/listener-middleware.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import { loadRegistrationKey } from '@app/store/modules/registration.js';
import { setupDynamixConfigWatch } from '@app/store/watch/dynamix-config-watch.js';
import { setupRegistrationKeyWatch } from '@app/store/watch/registration-watch.js';
import { StateManager } from '@app/store/watch/state-watch.js';
@@ -76,7 +74,7 @@ export const viteNodeApp = async () => {
await store.dispatch(loadRegistrationKey());
// Load my dynamix config file into store
await store.dispatch(loadDynamixConfigFile());
loadDynamixConfig();
// Start listening to file updates
StateManager.getInstance();
@@ -84,9 +82,6 @@ export const viteNodeApp = async () => {
// Start listening to key file changes
setupRegistrationKeyWatch();
// Start listening to dynamix config file changes
setupDynamixConfigWatch();
// If port is unix socket, delete old socket before starting http server
unlinkUnixPort();

View File

@@ -1,12 +1,9 @@
import { F_OK } from 'constants';
import { access } from 'fs/promises';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { createTtlMemoizedLoader } from '@unraid/shared';
import type { RecursivePartial } from '@app/types/index.js';
import { type DynamixConfig } from '@app/core/types/ini.js';
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
import { parseConfig } from '@app/core/utils/misc/parse-config.js';
import { type RecursiveNullable, type RecursivePartial } from '@app/types/index.js';
import { batchProcess } from '@app/utils.js';
/**
* Loads a configuration file from disk, parses it to a RecursivePartial of the provided type, and returns it.
@@ -16,11 +13,8 @@ import { batchProcess } from '@app/utils.js';
* @param path The path to the configuration file on disk.
* @returns A parsed RecursivePartial of the provided type.
*/
async function loadConfigFile<ConfigType>(path: string): Promise<RecursivePartial<ConfigType>> {
const fileIsAccessible = await access(path, F_OK)
.then(() => true)
.catch(() => false);
return fileIsAccessible
function loadConfigFileSync<ConfigType>(path: string): RecursivePartial<ConfigType> {
return fileExistsSync(path)
? parseConfig<RecursivePartial<ConfigType>>({
filePath: path,
type: 'ini',
@@ -28,21 +22,40 @@ async function loadConfigFile<ConfigType>(path: string): Promise<RecursivePartia
: {};
}
/**
* Load the dynamix.cfg into the store.
*
* Note: If the file doesn't exist this will fallback to default values.
*/
export const loadDynamixConfigFile = createAsyncThunk<
RecursiveNullable<RecursivePartial<DynamixConfig>>,
string | undefined
>('config/load-dynamix-config-file', async (filePath) => {
if (filePath) {
return loadConfigFile<DynamixConfig>(filePath);
}
const store = await import('@app/store/index.js');
const paths = store.getters.paths()['dynamix-config'];
const { data: configs } = await batchProcess(paths, (path) => loadConfigFile<DynamixConfig>(path));
const [defaultConfig = {}, customConfig = {}] = configs;
return { ...defaultConfig, ...customConfig };
type ConfigPaths = readonly (string | undefined | null)[];
const CACHE_WINDOW_MS = 250;
const memoizedConfigLoader = createTtlMemoizedLoader<
RecursivePartial<DynamixConfig>,
ConfigPaths,
string
>({
ttlMs: CACHE_WINDOW_MS,
getCacheKey: (configPaths: ConfigPaths): string => JSON.stringify(configPaths),
load: (configPaths: ConfigPaths) => {
const validPaths = configPaths.filter((path): path is string => Boolean(path));
if (validPaths.length === 0) {
return {};
}
const configFiles = validPaths.map((path) => loadConfigFileSync<DynamixConfig>(path));
return configFiles.reduce<RecursivePartial<DynamixConfig>>(
(accumulator, configFile) => ({
...accumulator,
...configFile,
}),
{}
);
},
});
/**
* Loads dynamix config from disk with TTL caching.
*
* @param configPaths - Array of config file paths to load and merge
* @returns Merged config object from all valid paths
*/
export const loadDynamixConfigFromDiskSync = (
configPaths: readonly (string | undefined | null)[]
): RecursivePartial<DynamixConfig> => {
return memoizedConfigLoader.get(configPaths);
};

View File

@@ -1,7 +1,11 @@
import { configureStore } from '@reduxjs/toolkit';
import { logger } from '@app/core/log.js';
import { loadDynamixConfigFromDiskSync } from '@app/store/actions/load-dynamix-config-file.js';
import { listenerMiddleware } from '@app/store/listeners/listener-middleware.js';
import { updateDynamixConfig } from '@app/store/modules/dynamix.js';
import { rootReducer } from '@app/store/root-reducer.js';
import { FileLoadStatus } from '@app/store/types.js';
export const store = configureStore({
reducer: rootReducer,
@@ -15,8 +19,36 @@ export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export type ApiStore = typeof store;
// loadDynamixConfig is located here and not in the actions/load-dynamix-config-file.js file because it needs to access the store,
// and injecting it seemed circular and convoluted for this use case.
/**
* Loads the dynamix config into the store.
* Can be called multiple times - uses TTL caching internally.
* @returns The loaded dynamix config.
*/
export const loadDynamixConfig = () => {
const configPaths = store.getState().paths['dynamix-config'] ?? [];
try {
const config = loadDynamixConfigFromDiskSync(configPaths);
store.dispatch(
updateDynamixConfig({
...config,
status: FileLoadStatus.LOADED,
})
);
} catch (error) {
logger.error(error, 'Failed to load dynamix config from disk');
store.dispatch(
updateDynamixConfig({
status: FileLoadStatus.FAILED_LOADING,
})
);
}
return store.getState().dynamix;
};
export const getters = {
dynamix: () => store.getState().dynamix,
dynamix: () => loadDynamixConfig(),
emhttp: () => store.getState().emhttp,
paths: () => store.getState().paths,
registration: () => store.getState().registration,

View File

@@ -2,7 +2,6 @@ import type { PayloadAction } from '@reduxjs/toolkit';
import { createSlice } from '@reduxjs/toolkit';
import { type DynamixConfig } from '@app/core/types/ini.js';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { FileLoadStatus } from '@app/store/types.js';
import { RecursivePartial } from '@app/types/index.js';
@@ -22,24 +21,6 @@ export const dynamix = createSlice({
return Object.assign(state, action.payload);
},
},
extraReducers(builder) {
builder.addCase(loadDynamixConfigFile.pending, (state) => {
state.status = FileLoadStatus.LOADING;
});
builder.addCase(loadDynamixConfigFile.fulfilled, (state, action) => {
return {
...(action.payload as DynamixConfig),
status: FileLoadStatus.LOADED,
};
});
builder.addCase(loadDynamixConfigFile.rejected, (state, action) => {
Object.assign(state, action.payload, {
status: FileLoadStatus.FAILED_LOADING,
});
});
},
});
export const { updateDynamixConfig } = dynamix.actions;

View File

@@ -163,6 +163,18 @@ export const loadStateFiles = createAsyncThunk<
return state;
});
const stateFieldKeyMap: Record<StateFileKey, keyof SliceState> = {
[StateFileKey.var]: 'var',
[StateFileKey.devs]: 'devices',
[StateFileKey.network]: 'networks',
[StateFileKey.nginx]: 'nginx',
[StateFileKey.shares]: 'shares',
[StateFileKey.disks]: 'disks',
[StateFileKey.users]: 'users',
[StateFileKey.sec]: 'smbShares',
[StateFileKey.sec_nfs]: 'nfsShares',
};
export const emhttp = createSlice({
name: 'emhttp',
initialState,
@@ -175,7 +187,8 @@ export const emhttp = createSlice({
}>
) {
const { field } = action.payload;
return Object.assign(state, { [field]: action.payload.state });
const targetField = stateFieldKeyMap[field] ?? (field as keyof SliceState);
return Object.assign(state, { [targetField]: action.payload.state });
},
},
extraReducers(builder) {

View File

@@ -0,0 +1,81 @@
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { store } from '@app/store/index.js';
import { loadStateFileSync } from '@app/store/services/state-file-loader.js';
import { StateFileKey } from '@app/store/types.js';
const VAR_FIXTURE = readFileSync(new URL('../../../../dev/states/var.ini', import.meta.url), 'utf-8');
const writeVarFixture = (dir: string, safeMode: 'yes' | 'no') => {
const content = VAR_FIXTURE.replace(/safeMode="(yes|no)"/, `safeMode="${safeMode}"`);
writeFileSync(join(dir, `${StateFileKey.var}.ini`), content);
};
describe('loadStateFileSync', () => {
let tempDir: string;
let baseState: ReturnType<typeof store.getState>;
beforeEach(() => {
tempDir = mkdtempSync(join(tmpdir(), 'state-file-'));
baseState = store.getState();
});
afterEach(() => {
vi.restoreAllMocks();
rmSync(tempDir, { recursive: true, force: true });
});
it('loads var.ini, updates the store, and returns the parsed state', () => {
writeVarFixture(tempDir, 'yes');
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
paths: {
...baseState.paths,
states: tempDir,
},
});
const dispatchSpy = vi.spyOn(store, 'dispatch').mockImplementation((action) => action as any);
const result = loadStateFileSync(StateFileKey.var);
expect(result?.safeMode).toBe(true);
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
type: 'emhttp/updateEmhttpState',
payload: {
field: StateFileKey.var,
state: expect.objectContaining({ safeMode: true }),
},
})
);
});
it('returns null when the states path is missing', () => {
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
paths: undefined,
} as any);
const dispatchSpy = vi.spyOn(store, 'dispatch');
expect(loadStateFileSync(StateFileKey.var)).toBeNull();
expect(dispatchSpy).not.toHaveBeenCalled();
});
it('returns null when the requested state file cannot be found', () => {
vi.spyOn(store, 'getState').mockReturnValue({
...baseState,
paths: {
...baseState.paths,
states: tempDir,
},
});
const dispatchSpy = vi.spyOn(store, 'dispatch');
expect(loadStateFileSync(StateFileKey.var)).toBeNull();
expect(dispatchSpy).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,81 @@
import { join } from 'node:path';
import type { SliceState } from '@app/store/modules/emhttp.js';
import type { StateFileToIniParserMap } from '@app/store/types.js';
import { parseConfig } from '@app/core/utils/misc/parse-config.js';
import { store } from '@app/store/index.js';
import { updateEmhttpState } from '@app/store/modules/emhttp.js';
import { parse as parseDevices } from '@app/store/state-parsers/devices.js';
import { parse as parseNetwork } from '@app/store/state-parsers/network.js';
import { parse as parseNfs } from '@app/store/state-parsers/nfs.js';
import { parse as parseNginx } from '@app/store/state-parsers/nginx.js';
import { parse as parseShares } from '@app/store/state-parsers/shares.js';
import { parse as parseSlots } from '@app/store/state-parsers/slots.js';
import { parse as parseSmb } from '@app/store/state-parsers/smb.js';
import { parse as parseUsers } from '@app/store/state-parsers/users.js';
import { parse as parseVar } from '@app/store/state-parsers/var.js';
import { StateFileKey } from '@app/store/types.js';
type ParserReturnMap = {
[StateFileKey.var]: ReturnType<typeof parseVar>;
[StateFileKey.devs]: ReturnType<typeof parseDevices>;
[StateFileKey.network]: ReturnType<typeof parseNetwork>;
[StateFileKey.nginx]: ReturnType<typeof parseNginx>;
[StateFileKey.shares]: ReturnType<typeof parseShares>;
[StateFileKey.disks]: ReturnType<typeof parseSlots>;
[StateFileKey.users]: ReturnType<typeof parseUsers>;
[StateFileKey.sec]: ReturnType<typeof parseSmb>;
[StateFileKey.sec_nfs]: ReturnType<typeof parseNfs>;
};
const PARSER_MAP: { [K in StateFileKey]: StateFileToIniParserMap[K] } = {
[StateFileKey.var]: parseVar,
[StateFileKey.devs]: parseDevices,
[StateFileKey.network]: parseNetwork,
[StateFileKey.nginx]: parseNginx,
[StateFileKey.shares]: parseShares,
[StateFileKey.disks]: parseSlots,
[StateFileKey.users]: parseUsers,
[StateFileKey.sec]: parseSmb,
[StateFileKey.sec_nfs]: parseNfs,
};
/**
* Synchronously loads an emhttp state file, updates the Redux store slice, and returns the parsed state.
*
* Designed for bootstrap contexts (CLI, plugin loading, etc.) where dispatching the async thunks is
* impractical but we still need authoritative emhttp state from disk.
*/
export const loadStateFileSync = <K extends StateFileKey>(
stateFileKey: K
): ParserReturnMap[K] | null => {
const state = store.getState();
const statesDirectory = state.paths?.states;
if (!statesDirectory) {
return null;
}
const filePath = join(statesDirectory, `${stateFileKey}.ini`);
try {
const parser = PARSER_MAP[stateFileKey] as StateFileToIniParserMap[K];
const rawConfig = parseConfig<Record<string, unknown>>({
filePath,
type: 'ini',
});
const config = rawConfig as Parameters<StateFileToIniParserMap[K]>[0];
const parsed = (parser as (input: any) => ParserReturnMap[K])(config);
store.dispatch(
updateEmhttpState({
field: stateFileKey,
state: parsed as Partial<SliceState[keyof SliceState]>,
})
);
return parsed;
} catch (error) {
return null;
}
};

View File

@@ -36,6 +36,7 @@ export type IniSlot = {
size: string;
sizeSb: string;
slots: string;
spundown: string;
status: SlotStatus;
temp: string;
type: SlotType;
@@ -82,6 +83,7 @@ export const parse: StateFileToIniParserMap['disks'] = (disksIni) =>
fsType: slot.fsType ?? null,
format: slot.format === '-' ? null : slot.format,
transport: slot.transport ?? null,
isSpinning: slot.spundown ? slot.spundown === '0' : null,
};
// @TODO Zod Parse This
return result;

View File

@@ -1,6 +1,10 @@
import type { StateFileToIniParserMap } from '@app/store/types.js';
import { type IniStringBoolean, type IniStringBooleanOrAuto } from '@app/core/types/ini.js';
import { toNumber } from '@app/core/utils/index.js';
import {
iniBooleanOrAutoToJsBoolean,
iniBooleanToJsBoolean,
} from '@app/core/utils/parsers/ini-boolean-parser.js';
import { ArrayState } from '@app/unraid-api/graph/resolvers/array/array.model.js';
import { DiskFsType } from '@app/unraid-api/graph/resolvers/disks/disks.model.js';
import {
@@ -157,36 +161,6 @@ export type VarIni = {
useUpnp: IniStringBoolean;
};
const iniBooleanToJsBoolean = (value: string, defaultValue?: boolean) => {
if (value === 'no' || value === 'false') {
return false;
}
if (value === 'yes' || value === 'true') {
return true;
}
if (defaultValue !== undefined) {
return defaultValue;
}
throw new Error(`Value "${value}" is not false/true or no/yes.`);
};
const iniBooleanOrAutoToJsBoolean = (value: IniStringBooleanOrAuto) => {
try {
// Either it'll return true/false or throw
return iniBooleanToJsBoolean(value as IniStringBoolean);
} catch {
// Auto or null
if (value === 'auto') {
return null;
}
}
throw new Error(`Value "${value as string}" is not auto/no/yes.`);
};
const safeParseMdState = (mdState: string | undefined): ArrayState => {
if (!mdState || typeof mdState !== 'string') {
return ArrayState.STOPPED;
@@ -210,7 +184,7 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
...iniFile,
defaultFsType: DiskFsType[iniFile.defaultFsType] || DiskFsType.XFS,
mdState: safeParseMdState(iniFile.mdState),
bindMgt: iniBooleanOrAutoToJsBoolean(iniFile.bindMgt),
bindMgt: iniBooleanOrAutoToJsBoolean(iniFile.bindMgt) ?? null,
cacheNumDevices: toNumber(iniFile.cacheNumDevices),
cacheSbNumDisks: toNumber(iniFile.cacheSbNumDisks),
configValid: iniBooleanToJsBoolean(iniFile.configValid, false),
@@ -221,8 +195,8 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
fsCopyPrcnt: toNumber(iniFile.fsCopyPrcnt),
fsNumMounted: toNumber(iniFile.fsNumMounted),
fsNumUnmountable: toNumber(iniFile.fsNumUnmountable),
hideDotFiles: iniBooleanToJsBoolean(iniFile.hideDotFiles),
localMaster: iniBooleanToJsBoolean(iniFile.localMaster),
hideDotFiles: iniBooleanToJsBoolean(iniFile.hideDotFiles, false),
localMaster: iniBooleanToJsBoolean(iniFile.localMaster, false),
maxArraysz: toNumber(iniFile.maxArraysz),
maxCachesz: toNumber(iniFile.maxCachesz),
mdNumDisabled: toNumber(iniFile.mdNumDisabled),
@@ -254,34 +228,34 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
regState:
RegistrationState[(iniFile.regCheck || iniFile.regTy || '').toUpperCase()] ??
RegistrationState.EGUID,
safeMode: iniBooleanToJsBoolean(iniFile.safeMode),
sbClean: iniBooleanToJsBoolean(iniFile.sbClean),
safeMode: iniBooleanToJsBoolean(iniFile.safeMode, false),
sbClean: iniBooleanToJsBoolean(iniFile.sbClean, false),
sbEvents: toNumber(iniFile.sbEvents),
sbNumDisks: toNumber(iniFile.sbNumDisks),
sbSynced: toNumber(iniFile.sbSynced),
sbSynced2: toNumber(iniFile.sbSynced2),
sbSyncErrs: toNumber(iniFile.sbSyncErrs),
shareAvahiEnabled: iniBooleanToJsBoolean(iniFile.shareAvahiEnabled),
shareCacheEnabled: iniBooleanToJsBoolean(iniFile.shareCacheEnabled),
shareAvahiEnabled: iniBooleanToJsBoolean(iniFile.shareAvahiEnabled, false),
shareCacheEnabled: iniBooleanToJsBoolean(iniFile.shareCacheEnabled, false),
shareCount: toNumber(iniFile.shareCount),
shareMoverActive: iniBooleanToJsBoolean(iniFile.shareMoverActive),
shareMoverLogging: iniBooleanToJsBoolean(iniFile.shareMoverLogging),
shareMoverActive: iniBooleanToJsBoolean(iniFile.shareMoverActive, false),
shareMoverLogging: iniBooleanToJsBoolean(iniFile.shareMoverLogging, false),
shareNfsCount: toNumber(iniFile.shareNfsCount),
shareNfsEnabled: iniBooleanToJsBoolean(iniFile.shareNfsEnabled),
shareNfsEnabled: iniBooleanToJsBoolean(iniFile.shareNfsEnabled, false),
shareSmbCount: toNumber(iniFile.shareSmbCount),
shareSmbEnabled: ['yes', 'ads'].includes(iniFile.shareSmbEnabled),
shareSmbMode: iniFile.shareSmbEnabled === 'ads' ? 'active-directory' : 'workgroup',
shutdownTimeout: toNumber(iniFile.shutdownTimeout),
spindownDelay: toNumber(iniFile.spindownDelay),
spinupGroups: iniBooleanToJsBoolean(iniFile.spinupGroups),
startArray: iniBooleanToJsBoolean(iniFile.startArray),
spinupGroups: iniBooleanToJsBoolean(iniFile.spinupGroups, false),
startArray: iniBooleanToJsBoolean(iniFile.startArray, false),
sysArraySlots: toNumber(iniFile.sysArraySlots),
sysCacheSlots: toNumber(iniFile.sysCacheSlots),
sysFlashSlots: toNumber(iniFile.sysFlashSlots),
useNtp: iniBooleanToJsBoolean(iniFile.useNtp),
useSsh: iniBooleanToJsBoolean(iniFile.useSsh),
useSsl: iniBooleanOrAutoToJsBoolean(iniFile.useSsl),
useTelnet: iniBooleanToJsBoolean(iniFile.useTelnet),
useUpnp: iniBooleanToJsBoolean(iniFile.useUpnp),
useNtp: iniBooleanToJsBoolean(iniFile.useNtp, false),
useSsh: iniBooleanToJsBoolean(iniFile.useSsh, false),
useSsl: iniBooleanOrAutoToJsBoolean(iniFile.useSsl) ?? null,
useTelnet: iniBooleanToJsBoolean(iniFile.useTelnet, false),
useUpnp: iniBooleanToJsBoolean(iniFile.useUpnp, false),
};
};

View File

@@ -1,17 +0,0 @@
import { watch } from 'chokidar';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { getters, store } from '@app/store/index.js';
export const setupDynamixConfigWatch = () => {
const configPath = getters.paths()?.['dynamix-config'];
// Update store when cfg changes
watch(configPath, {
persistent: true,
ignoreInitial: true,
}).on('change', async () => {
// Load updated dynamix config file into store
await store.dispatch(loadDynamixConfigFile());
});
};

40
api/src/types/jsonforms-i18n.d.ts vendored Normal file
View File

@@ -0,0 +1,40 @@
import '@jsonforms/core/lib/models/jsonSchema4';
import '@jsonforms/core/lib/models/jsonSchema7';
import '@jsonforms/core/src/models/jsonSchema4';
import '@jsonforms/core/src/models/jsonSchema7';
declare module '@jsonforms/core/lib/models/jsonSchema4' {
interface JsonSchema4 {
i18n?: string;
}
}
declare module '@jsonforms/core/lib/models/jsonSchema7' {
interface JsonSchema7 {
i18n?: string;
}
}
declare module '@jsonforms/core/src/models/jsonSchema4' {
interface JsonSchema4 {
i18n?: string;
}
}
declare module '@jsonforms/core/src/models/jsonSchema7' {
interface JsonSchema7 {
i18n?: string;
}
}
declare module '@jsonforms/core/lib/models/jsonSchema4.js' {
interface JsonSchema4 {
i18n?: string;
}
}
declare module '@jsonforms/core/lib/models/jsonSchema7.js' {
interface JsonSchema7 {
i18n?: string;
}
}

View File

@@ -6,8 +6,7 @@ import { AuthZGuard } from 'nest-authz';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file.js';
import { store } from '@app/store/index.js';
import { loadDynamixConfig, store } from '@app/store/index.js';
import { loadStateFiles } from '@app/store/modules/emhttp.js';
import { AppModule } from '@app/unraid-api/app/app.module.js';
import { AuthService } from '@app/unraid-api/auth/auth.service.js';
@@ -111,8 +110,8 @@ describe('AppModule Integration Tests', () => {
beforeAll(async () => {
// Initialize the dynamix config and state files before creating the module
await store.dispatch(loadDynamixConfigFile());
await store.dispatch(loadStateFiles());
loadDynamixConfig();
// Debug: Log the CSRF token from the store
const { getters } = await import('@app/store/index.js');

Some files were not shown because too many files have changed in this diff Show More