Compare commits

...

259 Commits

Author SHA1 Message Date
Eli Bosley
b9bb346ab9 chore(release): 4.1.1 2025-02-20 11:32:38 -05:00
Eli Bosley
2b1e3076b0 fix: main.yml release issue 2025-02-20 11:32:30 -05:00
Eli Bosley
be6dbe587f chore(release): 4.1.0 2025-02-20 11:26:39 -05:00
Michael Datelle
fb2472399a fix: connect breaks default css of header (#1155)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced theme customization with new options for custom gradients,
delivering dynamic and visually cohesive banner effects.
- Improved header styling with refined color handling to adapt
seamlessly across both dark and light modes.
- Added new CSS variables for gradient management in theme definitions.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-02-20 11:18:12 -05:00
Eli Bosley
6084b9df93 Update renovate.json 2025-02-20 10:38:00 -05:00
Eli Bosley
c74bdd8890 feat: attempt to resolve performance issues with rm earlier in build … (#1152)
…process

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

- **Refactor**
- Enhanced file integrity validation during installation for more
reliable performance.

- **Chores**
- Streamlined the setup process by adding a cleanup step to remove
outdated components post-installation.
- Improved error handling and validation logic for checksum
verification.
	- Adjusted command sequence for better clarity and control flow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-20 10:36:52 -05:00
Eli Bosley
5a3e8df003 fix: storybook resolution issue (#1153)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Enhanced the Storybook configuration by integrating improvements for
build efficiency.
- Updated build settings to optimize dependency handling during
Storybook execution.
- Updated several Storybook-related package versions and added new
dependencies.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-20 10:13:21 -05:00
Michael Datelle
62dc8294e8 refactor: make stepper responsive (#1144)
This PR adds a responsive layout for the Stepper component on small
screens. There's a vertical orientation version but the changes here
won't be compatible. If we need a verticle version we can just create a
separate component folder for the vertical only version.

<img width="171" alt="image"
src="https://github.com/user-attachments/assets/4e38ac68-ca17-400a-b07b-2bfcb2c0a192"
/>

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

## Summary by CodeRabbit

- **Style**
- Enhanced the visual design of the stepper components with improved
responsive layouts—displaying vertically on smaller screens and
horizontally on medium and larger screens.
- **New Features**
- Added an interactive demo showcasing the stepper workflow in
Storybook.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-02-19 14:40:11 -05:00
Pujit Mehrotra
7588e0e3cf feat(web): improve notification count syncing (#1148)
## Summary by CodeRabbit

- **New Features**
- Added a refresh button in the notifications sidebar, allowing users to
update notification counts on demand.
- Introduced real-time updates for notification counts through a new
subscription.
- Enhanced GraphQL functionality to support recalculating notification
counts for archived and unread notifications.
  - Added a new mutation for recalculating the notifications overview.
- Implemented a new subscription to receive updates on notification
counts.
- Minor formatting update to the notifications title for improved
readability.
2025-02-19 14:25:04 -05:00
Michael Datelle
6378047bc4 feat: add unraid-ui documentation (#1142)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Improved repository management to prevent unnecessary tracking of the
`.pnpm-store` directory.
- **Documentation**
- Updated installation and configuration guidelines for the UI component
library.
- Refined instructions for Tailwind configuration, now utilizing
TypeScript.
- Expanded guidance for component development and Storybook best
practices, providing clearer examples and workflows.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-02-19 14:23:52 -05:00
Eli Bosley
ad6b6589db feat: convert to pnpm monorepo (#1137)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced enhanced project management scripts for building, testing,
and deploying the monorepo.
- Added an automated testing pipeline for improved reliability of the
Libvirt functionality.
- Provided a new plugin installation script that ensures thorough
cleanup during removal.

- **Improvements**
- Updated container mappings and dependency configurations for more
stable and efficient operations.
- Refined web application settings and build commands for smoother
performance.
- Streamlined continuous integration workflows with optimized caching
and dependency management.
  - Updated allowed origins in configuration for enhanced security.

- **Chores/Refactor**
- Removed outdated configuration files to simplify maintenance and
enhance consistency.
- Enhanced event listener management in the web application for better
error handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-19 13:41:23 -05:00
Pujit Mehrotra
93980f929d fix(web): name of toaster component
changed `unraid-toaster` to `uui-toaster`
2025-02-19 12:04:03 -05:00
Pujit Mehrotra
19208e5fab fix(web): broken modals 2025-02-19 12:04:03 -05:00
Pujit Mehrotra
b970fd9e6c fix(api): logrotate modification & permissions (#1145)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Chores**
- Updated deployment and build commands to use a more efficient package
manager.
- **Refactor**
- Improved the internal file modification structure for enhanced
flexibility and maintainability.
- **New Features**
- Enhanced log management by adding functionality for proper permission
handling and cleanup after operations.
- Introduced a new log rotation configuration for managing log files
effectively.
- Updated timestamps for various components to reflect the latest
download times.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-19 09:13:43 -05:00
renovate[bot]
7539a3ed75 chore(deps): update dependency vite-plugin-vue-devtools to v7.7.2 (#1134)
This PR contains the following updates:

<details>
<summary>vuejs/devtools (vite-plugin-vue-devtools)</summary>

###
[`v7.7.2`](https://redirect.github.com/vuejs/devtools/releases/tag/v7.7.2)

[Compare
Source](https://redirect.github.com/vuejs/devtools/compare/v7.7.1...v7.7.2)

🐞 Bug Fixes

- **api**: Allow treeshaking  -  by
[@&#8203;posva](https://redirect.github.com/posva) in
[https://github.com/vuejs/devtools/issues/795](https://redirect.github.com/vuejs/devtools/issues/795)
[<samp>(81cac)</samp>](https://redirect.github.com/vuejs/devtools/commit/81cacec)

#####     [View changes on
GitHub](https://redirect.github.com/vuejs/devtools/compare/v7.7.1...v7.7.2)

</details>

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 09:12:01 -05:00
Pujit Mehrotra
0b8df2a43e chore(web): add pinia store and select dropdown for dummy server state (#1143)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced an interactive server selector that lets users toggle
between different server modes (e.g., Default and OEM Activation) via a
dropdown.
- Integrated reactive state management across key pages, ensuring
dynamic UI updates.
  - Added new popover components for enhanced UI interactions.
- Introduced a settings interface for developers, allowing access to
server selection within a popover.

- **Bug Fixes**
- Restored functionality for the downgrade feature that was previously
removed.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1209127325997642
2025-02-19 08:53:54 -05:00
Pujit Mehrotra
9bc8060a83 fix(api): change log output location for diagnostic compatibility (#1130)
now outputs logs to `/var/log/graphql-api.log` instead of
`/var/log/unraid-api/unraid-api.log`

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

- **Chores**
  - Updated the logging file destination to `/var/log/graphql-api.log`.
  - Streamlined configuration formatting for enhanced clarity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-02-18 09:47:42 -05:00
Eli Bosley
2b163b361a fix: revert dockerode upgrade (#1140)
Reverts unraid/api#830
2025-02-12 21:12:04 -05:00
Michael Datelle
741e8532ab refactor: unraid-ui-web-migration (#1106)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced enhanced stepper components for smoother multi-step
interactions.
- Added new loading indicators and improved the loading experience with
customizable variants.
  
- **UI Improvements**
- Refreshed the global color palette and updated styling across buttons,
badges, and loading indicators for a more modern, consistent experience.
- Improved the organization and readability of templates and styles
across various components.

- **Code & Dependency Updates**
- Updated key dependencies and revised the theme and configuration
settings to improve performance and maintainability.
- Introduced new environment variables for better configuration
management.

- **Legacy Cleanup**
- Removed deprecated components and streamlined registrations to
simplify the codebase without affecting end-user functionality.
- Eliminated unused utility functions and legacy code to enhance overall
code quality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
Co-authored-by: Eli Bosley <ekbosley@gmail.com>
2025-02-12 18:00:06 -05:00
Eli Bosley
f76c0f05fb fix: upload to correct tag directory on build 2025-02-12 16:31:16 -05:00
renovate[bot]
d439fcc7bb chore(deps): update dependency @graphql-codegen/client-preset to v4.6.2 (#1131) 2025-02-12 11:29:04 -05:00
renovate[bot]
45006a1e4c chore(deps): update dependency @types/node to v22.13.1 (#1110) 2025-02-12 11:28:35 -05:00
renovate[bot]
4ee85eb121 chore(deps): update dependency postcss to v8.5.2 (#1115) 2025-02-12 11:27:41 -05:00
renovate[bot]
eff7507605 chore(deps): update dependency typescript-eslint to v8.24.0 (#1117) 2025-02-12 11:26:59 -05:00
renovate[bot]
c311a89aee fix(deps): update dependency dockerode to v4 (#830) 2025-02-12 11:26:37 -05:00
renovate[bot]
b7b4a3974b chore(deps): update dependency nuxt to v3.15.4 (#1114) 2025-02-12 11:26:07 -05:00
renovate[bot]
b4d48335c4 chore(deps): update dependency vue-tsc to v2.2.0 (#1118) 2025-02-12 11:25:45 -05:00
renovate[bot]
797695535e chore(deps): update storybook monorepo to v8.5.4 (#1119) 2025-02-12 11:25:27 -05:00
renovate[bot]
16620a249b chore(deps): update vitest monorepo (#1109)
This PR contains the following updates:

| Package | Change | Age | Adoption | Passing | Confidence |
|---|---|---|---|---|---|
|
[@vitest/coverage-v8](https://redirect.github.com/vitest-dev/vitest/tree/main/packages/coverage-v8#readme)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8))
| [`1.6.0` ->
`1.6.1`](https://renovatebot.com/diffs/npm/@vitest%2fcoverage-v8/1.6.0/1.6.1)
|
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fcoverage-v8/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vitest%2fcoverage-v8/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vitest%2fcoverage-v8/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fcoverage-v8/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
|
[@vitest/ui](https://redirect.github.com/vitest-dev/vitest/tree/main/packages/ui#readme)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/ui))
| [`1.6.0` ->
`1.6.1`](https://renovatebot.com/diffs/npm/@vitest%2fui/1.6.0/1.6.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/@vitest%2fui/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@vitest%2fui/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@vitest%2fui/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@vitest%2fui/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [vitest](https://redirect.github.com/vitest-dev/vitest)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest))
| [`2.1.8` ->
`2.1.9`](https://renovatebot.com/diffs/npm/vitest/2.1.8/2.1.9) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/2.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vitest/2.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vitest/2.1.8/2.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/2.1.8/2.1.9?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
| [vitest](https://redirect.github.com/vitest-dev/vitest)
([source](https://redirect.github.com/vitest-dev/vitest/tree/HEAD/packages/vitest))
| [`1.6.0` ->
`1.6.1`](https://renovatebot.com/diffs/npm/vitest/1.6.0/1.6.1) |
[![age](https://developer.mend.io/api/mc/badges/age/npm/vitest/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/vitest/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/vitest/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|
[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/vitest/1.6.0/1.6.1?slim=true)](https://docs.renovatebot.com/merge-confidence/)
|

---

### Release Notes

<details>
<summary>vitest-dev/vitest (@&#8203;vitest/coverage-v8)</summary>

###
[`v1.6.1`](https://redirect.github.com/vitest-dev/vitest/releases/tag/v1.6.1)

[Compare
Source](https://redirect.github.com/vitest-dev/vitest/compare/v1.6.0...v1.6.1)

This release includes security patches for:

- [Remote Code Execution when accessing a malicious website while Vitest
API server is listening |
CVE-2025-24964](https://redirect.github.com/vitest-dev/vitest/security/advisories/GHSA-9crc-q9x8-hgqq)

#####    🐞 Bug Fixes

- backport
[https://github.com/vitest-dev/vitest/issues/7317](https://redirect.github.com/vitest-dev/vitest/issues/7317)
to v1 - by [@&#8203;hi-ogawa](https://redirect.github.com/hi-ogawa) in
[https://github.com/vitest-dev/vitest/pull/7319](https://redirect.github.com/vitest-dev/vitest/pull/7319)

#####     [View changes on
GitHub](https://redirect.github.com/vitest-dev/vitest/compare/v1.6.0...v1.6.1)

</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 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:eyJjcmVhdGVkSW5WZXIiOiIzOS4xNDUuMCIsInVwZGF0ZWRJblZlciI6IjM5LjE2NC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-12 11:25:04 -05:00
Eli Bosley
cc18239748 feat: contributing guide 2025-02-12 10:39:22 -05:00
Eli Bosley
8374af8ee8 feat: work intent process 2025-02-12 10:35:11 -05:00
Eli Bosley
5b2403ad04 fix: shorten work intent form 2025-02-12 10:32:40 -05:00
Eli Bosley
c519ba28e4 feat: work intent 2025-02-12 10:31:21 -05:00
Eli Bosley
0c0a63b525 feat: feature request template 2025-02-12 10:28:18 -05:00
Eli Bosley
d6fde34365 feat: bug report template 2025-02-12 10:25:49 -05:00
Eli Bosley
a4b3f8c6c3 feat: reorder index 2025-02-12 09:23:10 -05:00
Eli Bosley
5f29e6d5e7 fix: simplify api setup index 2025-02-12 09:16:09 -05:00
Eli Bosley
1e4a4f0745 feat: simplify docs 2025-02-11 21:32:50 -05:00
Eli Bosley
608151d84c fix: simplify upcoming features 2025-02-11 19:08:24 -05:00
Eli Bosley
8cbb3c4718 feat: public index 2025-02-11 15:56:54 -05:00
Eli Bosley
e784391ac3 feat: add category.json 2025-02-11 15:48:45 -05:00
Eli Bosley
84611d7691 fix: make public not a part of folder structure in PR 2025-02-11 15:40:37 -05:00
Eli Bosley
dabe334072 fix: create PR ignored 2025-02-11 15:36:15 -05:00
Eli Bosley
59e48ad85c feat: upgrade workflow and auto-assign reviewers 2025-02-11 15:34:56 -05:00
Eli Bosley
13b501a342 fix: docs creation workflow 2025-02-11 15:26:17 -05:00
Eli Bosley
75474bde47 feat: add developer docs (#1128)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Documentation**
- Improved the API documentation update process to enhance clarity and
maintain consistent content.
- Introduced a comprehensive guide outlining the API's repository
organization and system architecture.
- Streamlined the documentation by removing outdated developer guides on
API introspection and feature implementation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-11 15:14:59 -05:00
Eli Bosley
7a19c9331f feat: fix docusaurus build + update snapshot 2025-02-11 14:47:34 -05:00
Eli Bosley
8a575765a9 feat: auto-docusaurus-prs (#1127)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
- Expanded the Unraid API guides with comprehensive CLI command details
for managing services, logs, configuration, and authentication.
- Updated the API usage instructions to include steps for enabling a
GraphQL sandbox, example queries, error handling, and best practices.
- Added an implementation overview detailing component capabilities and
a release roadmap outlining upcoming improvements across core
infrastructure, security, and user interface.

- **Chores**
- Introduced automation that streamlines the updating of API
documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-11 14:45:03 -05:00
Eli Bosley
753f1588b8 Feat/local-plugin (#1125)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit


• New Features  
 - Enhanced the login experience with improved session management and
two-factor authentication.
 - Introduced a comprehensive README for the Unraid Plugin Builder,
detailing development workflows and commands.

• Chores  
 - Streamlined build, packaging, and deployment processes with updated
dependency and environment configurations.
 - Updated Docker configurations to support pnpm as the package manager.
 - Added new environment variables for better configuration management.
 - Introduced new scripts for improved build and packaging processes.  

• Tests  
 - Removed outdated test cases and simplified test setups.  

• Refactor  
 - Modernized internal code structure and asynchronous handling for
improved overall performance.
 - Transitioned imports from lodash to lodash-es for better module
handling.
 - Updated environment variable management and configuration settings.  
 - Enhanced the build script for improved deployment processes.  
 - Updated the notification handling structure to improve efficiency.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-11 11:45:26 -05:00
ljm42
59d6c1b678 fix: PHP Warning in state.php (#1126)
PHP 8.4.4 (Unraid 7.1.0): Implicitly marking parameter $subkey as
nullable is deprecated

Related: https://github.com/unraid/webgui/pull/2009

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

## Summary by CodeRabbit

- **Refactor**
- Improved internal handling of optional details, enhancing system
robustness and flexibility when certain inputs are omitted. This update
contributes to smoother, more reliable operations without affecting
visible functionality.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-10 10:58:30 -08:00
Eli Bosley
6abddd85d2 chore(release): 4.0.1 2025-02-06 16:18:02 -05:00
Eli Bosley
29fd61be6c Update release-production.yml 2025-02-06 16:17:05 -05:00
Eli Bosley
92f72e33ec chore(release): 4.0.0 2025-02-06 16:02:28 -05:00
Michael Datelle
26639d5139 refactor: update config and scripts to ensure production mode (#1122)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Refactor**
- Updated the user profile’s loading indicator for a smoother visual
experience during system restart.
- **Chores**
- Improved environment-specific configurations and asset management,
streamlining production builds and deployment.
	- Enhanced the development tools behavior based on the environment.
- Updated scripts for building and serving the application to align with
production settings.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-02-06 13:39:52 -05:00
Eli Bosley
19f9261025 chore: add missing descriptions on commands (#1124)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Documentation**
	- Enhanced CLI help text by adding and refining command descriptions:
		• Logs command now clearly displays “View logs.”
		• Restart command now focuses solely on restarting the API.
		• Start command now shows “Start the Unraid API.”
		• Stop command now explicitly states its stopping action.
• Environment switch command now indicates its role in switching the API
environment.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-06 13:39:30 -05:00
Eli Bosley
e7b7caae43 feat: checkout correct branch on close (#1123)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Chores**
- Refined our deployment process to ensure staging updates now reflect
the fully merged changes.
	- Made minor formatting tweaks for improved clarity.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-02-06 12:53:00 -05:00
Eli Bosley
4f5c367fdf feat: begin building plugin with node instead of bash (#1120)
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Enhanced automated build and release processes with containerized
builds, improved caching, and refined artifact handling.
- Introduced new configuration options to strengthen versioning,
integrity checks, and pull request tracking.
	- Added a new Dockerfile for building the Node.js application.
- Added new environment variables for API versioning and validation
control.
	- Implemented comprehensive management of PM2 processes and state.
- Introduced a new GitHub Actions workflow for automating staging plugin
deployment upon pull request closure.
	- Updated logic for handling plugin installation and error feedback.
	- Added new asynchronous methods for managing PM2 processes.
	- Updated logging configurations for better control over log outputs.
	- Added Prettier configuration for consistent code formatting.
- Introduced a configuration to prevent the application from watching
for file changes.

- **Bug Fixes**
- Improved error handling and user feedback during the installation of
staging versions.

- **Documentation**
- Removed outdated introductory documentation to streamline project
information.

- **Chores**
- Updated deployment routines and validation steps to improve release
consistency and error handling.
- Simplified packaging and build scripts for smoother staging and
production workflows.
	- Excluded sensitive files from the Docker build context.
- Updated the `.gitignore` file to prevent unnecessary files from being
tracked.
- Adjusted the test timeout configuration for improved test reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-06 12:32:41 -05:00
Pujit Mehrotra
321703e907 fix(web): track 'notification seen' state across tabs & page loads (#1121)
**New Features**
	- Enhanced notifications tracking that updates seen status in real time.
	- Improved notification indicators provide a more consistent and responsive experience.
	- Persistent state management ensures your viewed notifications remain accurately reflected across sessions.
	- New composable functions introduced for better management of notification visibility and interaction.
	- Streamlined notification handling by simplifying state management processes.
2025-02-06 12:00:53 -05:00
Pujit Mehrotra
a21f39d617 fix(api): improve defaults in PM2 service (#1116)
* change default execa opts in pm2 service (disable `extendEnv` and add default bash shell)

* change default log level of `pm2.run` to `trace`

* add tsdoc for `pm2.run`
2025-02-05 09:57:33 -05:00
Eli Bosley
3c357e7e95 fix: use batchProcess 2025-02-04 14:36:39 -05:00
Eli Bosley
f22b262830 feat: async disk mapping 2025-02-04 14:36:39 -05:00
Eli Bosley
e16763b49b fix: do not process.exit on restart or stop command 2025-02-04 13:57:51 -05:00
Eli Bosley
133c8e0d70 fix: lint 2025-02-04 13:57:51 -05:00
Eli Bosley
1392bdeecb feat: allow deletion and creation of files with patches 2025-02-04 13:57:51 -05:00
Eli Bosley
2fce2e9a28 fix: install as-integrations/fastify 2025-02-04 13:20:08 -05:00
renovate[bot]
430656f6af chore(deps): update dependency graphql-codegen-typescript-validation-schema to ^0.17.0 2025-02-04 13:10:53 -05:00
renovate[bot]
d7887c2183 chore(deps): update dependency @rollup/rollup-linux-x64-gnu to v4.34.2 2025-02-04 13:10:41 -05:00
renovate[bot]
ebd1a391b6 chore(deps): update dependency @types/node to v20.17.17 2025-02-04 13:10:26 -05:00
renovate[bot]
1f42bbb4aa chore(deps): update dependency zx to v8.3.2 2025-02-04 13:10:18 -05:00
renovate[bot]
5f59d31ab3 chore(deps): update dependency @swc/core to v1.10.14 2025-02-04 13:10:10 -05:00
Eli Bosley
d8478152e9 fix: remove devDependencies from output package json 2025-02-04 13:06:56 -05:00
Eli Bosley
63fcde8243 fix: staging build issues 2025-02-04 13:01:56 -05:00
renovate[bot]
2bc9af2578 fix(deps): update graphqlcodegenerator monorepo 2025-02-04 12:15:54 -05:00
renovate[bot]
5b14be6b0f fix(deps): update dependency @apollo/client to v3.12.9 2025-02-04 12:15:33 -05:00
renovate[bot]
9ef56d8c05 chore(deps): update dependency eslint to v9.19.0 2025-02-04 12:15:21 -05:00
renovate[bot]
c4204d89aa chore(deps): update dependency @nuxt/eslint to v0.7.6 2025-02-04 12:15:10 -05:00
renovate[bot]
048a0a88dc chore(deps): update dependency prettier-plugin-tailwindcss to v0.6.11 2025-02-04 12:14:57 -05:00
Eli Bosley
7b3834ca1f fix: resource busy when removing all subdirectories 2025-02-04 12:14:41 -05:00
Eli Bosley
0e9c91af86 feat: ignore generated code 2025-02-04 12:14:41 -05:00
Eli Bosley
a6f67060b4 feat: log size and only tar files 2025-02-04 12:14:41 -05:00
Eli Bosley
3c61a615f0 feat: improve packing 2025-02-04 12:14:41 -05:00
Eli Bosley
073a51572a feat: shared call to createPatch 2025-02-04 12:05:56 -05:00
Eli Bosley
c00789865c fix: allow concurrent testing with a shared patcher instance 2025-02-04 12:05:56 -05:00
Eli Bosley
9d1442b2ee fix: sequential test execution for generic-modification 2025-02-04 12:05:56 -05:00
Eli Bosley
935318dda6 feat: docstrings 2025-02-04 12:05:56 -05:00
Eli Bosley
dfb006e696 feat: move fixtures into __test__ folder 2025-02-04 12:05:56 -05:00
Eli Bosley
445f3b50b1 feat: better patch application 2025-02-04 12:05:56 -05:00
Eli Bosley
a12181a5e0 feat: rollback if patch exists before applying 2025-02-04 12:05:56 -05:00
Eli Bosley
0cb0fc9881 fix: unused imports 2025-02-04 12:05:56 -05:00
Eli Bosley
42610d290d fix: lint 2025-02-04 12:05:56 -05:00
Eli Bosley
9e12407565 fix: paths now correct, better download logic 2025-02-04 12:05:56 -05:00
Eli Bosley
bb92c3f9f8 fix: better js file handling 2025-02-04 12:05:56 -05:00
Eli Bosley
5b0971ea8d feat: logrotate test 2025-02-04 12:05:56 -05:00
Eli Bosley
7f997663f9 Update api/src/unraid-api/unraid-file-modifier/modifications/auth-request.modification.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-02-04 12:05:56 -05:00
Eli Bosley
8b75d6cc99 fix: format authrequest mod as other files 2025-02-04 12:05:56 -05:00
Eli Bosley
b1a993a8e9 fix: lint 2025-02-04 12:05:56 -05:00
Eli Bosley
b1a1779a8b feat: add patch for auth-request.php 2025-02-04 12:05:56 -05:00
Eli Bosley
36d8399045 feat: fallback to local 2025-02-04 12:05:56 -05:00
Eli Bosley
6beafbe8ed fix: lint 2025-02-04 12:05:56 -05:00
Eli Bosley
a502134c0a fix: better loader functionality and error handling 2025-02-04 12:05:56 -05:00
Eli Bosley
fa16dcd801 feat: add logging around fixture downloads 2025-02-04 12:05:56 -05:00
Eli Bosley
d38f3ef49b fix: lint 2025-02-04 12:05:56 -05:00
Eli Bosley
ce92cb06b7 fix: remove unused constructor 2025-02-04 12:05:56 -05:00
Eli Bosley
1d5c2c8338 fix: patch-utils unused 2025-02-04 12:05:56 -05:00
Eli Bosley
0163acb7f3 fix: type for generic test 2025-02-04 12:05:56 -05:00
Eli Bosley
5347d54b11 fix: test simplification to ensure no redownloads 2025-02-04 12:05:56 -05:00
Eli Bosley
547ae180dd fix: delete .original files 2025-02-04 12:05:56 -05:00
Eli Bosley
05f661e0e5 feat: download fixtures from the web 2025-02-04 12:05:56 -05:00
Eli Bosley
5d909a856b feat: rename modification file 2025-02-04 12:05:56 -05:00
Eli Bosley
4d45caf258 feat: extensive file checking 2025-02-04 12:05:56 -05:00
Eli Bosley
805bc5bfc0 feat: initial patcher implementation using the diff tool 2025-02-04 12:05:56 -05:00
Pujit Mehrotra
81d33f6b3a refactor(api): pm2 usage in cli (#1104)
* invoke pm2 via PM2Service

* fix `unraid-api logs` command

* default to LOG_LEVEL=debug in non-production envs

* rm pm2 dump file after `pm2 update`

* add PM2_HOME to `@app/environment`
2025-02-04 11:39:29 -05:00
Pujit Mehrotra
2a82ea4765 fix(api): make cookie recognition during websocket connection more
robust
2025-02-04 11:30:57 -05:00
Pujit Mehrotra
6bb3d55e3c chore(web): suppress activation code view in dev server startup 2025-02-04 11:30:57 -05:00
Pujit Mehrotra
8251c6f2d3 fix: only toast unread notifications, not archived ones 2025-02-04 11:30:57 -05:00
Eli Bosley
ad32cffd75 feat: force linting on build 2025-01-31 11:02:55 -05:00
Eli Bosley
2b213619db chore: lint api codebase 2025-01-31 11:02:55 -05:00
Pujit Mehrotra
61ee689658 feat(ui): webgui-compatible web component library (#1075)
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Eli Bosley <ekbosley@gmail.com>

- **CI/CD**
	- Updated GitHub Actions workflow to build Unraid UI Web Components.
	- Adjusted artifact naming and download configurations.

- **Web Components**
	- Added new web components and registration mechanism.
	- Implemented toast notifications.
	- Enhanced UI component library.

- **Notifications**
	- Added real-time notification subscription.
	- Created notification settings page.
	- Implemented notification toast system.

- **API Improvements**
	- Refactored GraphQL schema loading.
	- Updated authentication and cookie handling.
	- Improved error logging and server initialization.

- **Development Tools**
	- Updated ESLint configuration.
	- Enhanced import path management.
	- Added new development dependencies.
2025-01-31 10:47:03 -05:00
renovate[bot]
904cd466db chore(deps): update dependency vite to v5.4.14 2025-01-30 17:42:47 -05:00
renovate[bot]
6cb28d5f8f fix(deps): update dependency @floating-ui/vue to v1.1.6 2025-01-30 15:21:39 -05:00
renovate[bot]
c180728696 fix(deps): update dependency radix-vue to v1.9.13 2025-01-30 15:21:26 -05:00
renovate[bot]
84752043e5 fix(deps): update dependency focus-trap to v7.6.4 2025-01-30 15:21:04 -05:00
renovate[bot]
c112f19c95 fix(deps): update dependency graphql-ws to v5.16.2 2025-01-30 15:20:44 -05:00
renovate[bot]
9acb2926da chore(deps): update dependency @types/node to v20.17.16 2025-01-30 15:20:30 -05:00
renovate[bot]
29d216ece7 chore(deps): update dependency @types/node to v22.12.0 2025-01-30 15:20:16 -05:00
renovate[bot]
5f597f9d4c fix(deps): update dependency @graphql-tools/load-files to v7.0.1 2025-01-30 15:19:56 -05:00
Eli Bosley
d26ddef33e fix: sandbox defaults in dev mode wrong 2025-01-30 15:05:34 -05:00
Eli Bosley
21208bfcf6 feat: enable sandbox in dev mode 2025-01-30 15:05:34 -05:00
Eli Bosley
6c46f9413f fix: tests and validate token clears screen 2025-01-30 15:05:34 -05:00
Eli Bosley
b56b2157fa fix: extra log line 2025-01-30 15:05:34 -05:00
Eli Bosley
2108ed0ecd fix: use an enum and defaults for sandbox value 2025-01-30 15:05:34 -05:00
Eli Bosley
9c5e418872 feat: enable sandbox with developer command 2025-01-30 15:05:34 -05:00
Eli Bosley
c4d731401c feat: codeowners 2025-01-30 14:41:10 -05:00
Eli Bosley
7e5dd07d4a fix: revert myservers.cfg 2025-01-30 14:37:33 -05:00
Eli Bosley
62824ba76f fix: default overwrite false test 2025-01-30 14:37:33 -05:00
Eli Bosley
e58410bd57 fix: remove memory key generation 2025-01-30 14:37:33 -05:00
Eli Bosley
e88593620b feat: automatic session setup for dev 2025-01-30 14:37:33 -05:00
Eli Bosley
8026ef53e8 feat: session issues 2025-01-30 14:37:33 -05:00
Zack Spear
1ecac5ee4e fix(web): theme header differences (#1085)
* feat(theme): add default header colors for theme differences

* refactor(theme): update UserProfile component colors to use theme variables

* fix(theme): safely handle default header colors for themes
2025-01-30 11:14:30 -08:00
Zack Spear
e7d15ee5ec fix(web): remove warn and error console log removal (#1086)
* fix(web): remove warn and error console log removal

* chore: comment explaining VITE_ALLOW_CONSOLE_LOGS
2025-01-30 11:14:10 -08:00
Eli Bosley
c3f4cf53c1 fix: more verbose logging for node install to find issues 2025-01-29 15:50:07 -05:00
Zack Spear
d8a5b1711a feat(web): activation modal steps, updated copy (#1079)
* feat(stepper): add shadcn stepper components

* chore(serverState): remove partnerLogo property from server state configuration

* refactor(web): modal add subfooter slot

- adds ability to display content below the modal's content box

* feat(modal): add ActivationSteps component to subFooter slot in WelcomeModal and ActivationModal

* refactor: improve activation modal buttons responsiveness

* refactor: update activation flow messaging and UI

* feat: web/deploy-dev.sh add dynamic web component JS file whitelisting in auth-request.php

* fix: remove test UTM parameters from Unraid docs links in activation modal

* refactor: improve konami code handling and add type safety to activation steps

* chore: remove extra semicolon in serverState.ts

* Apply suggestions from code review

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-29 11:08:23 -08:00
Eli Bosley
8481c9a9fb fix: length 2025-01-29 13:53:59 -05:00
Eli Bosley
ddfc36fd73 fix: more generic test 2025-01-29 13:53:59 -05:00
Eli Bosley
3cc3f27dae feat: coderabbit suggestion 2025-01-29 13:53:59 -05:00
Eli Bosley
097415f6b8 feat: add logrotate cron again 2025-01-29 13:53:59 -05:00
Eli Bosley
b1d9ad7ef1 feat: add log rotation 2025-01-29 13:53:59 -05:00
Eli Bosley
c7d4e39287 feat: swap to async exit hook 2025-01-29 13:33:19 -05:00
Eli Bosley
0c6f44da35 feat: kill timeout extended 2025-01-29 13:33:19 -05:00
Eli Bosley
4655d72fbb feat: more pm2 fixes 2025-01-29 13:33:19 -05:00
Eli Bosley
4b3d6a7ba3 fix: report issues + pm2 issues 2025-01-29 13:33:19 -05:00
Eli Bosley
ed18945088 fix: tests 2025-01-29 12:57:20 -05:00
Eli Bosley
69cd92f974 fix: create api key for connect on startup 2025-01-29 12:55:10 -05:00
Eli Bosley
74b9fd0159 fix: unit test issues 2025-01-28 16:19:04 -05:00
Eli Bosley
ff63535b00 fix: watch all events to load keys 2025-01-28 16:19:04 -05:00
Eli Bosley
76711be3e8 fix: apply and rollback error handling 2025-01-28 16:19:04 -05:00
Eli Bosley
961bcc5db6 fix: remove line from or in button 2025-01-28 16:19:04 -05:00
Eli Bosley
0cfdd5a61b fix: backup restore formatting 2025-01-28 16:19:04 -05:00
Eli Bosley
8d905974be Update api/src/utils.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-28 16:19:04 -05:00
Eli Bosley
132840b0ef Update api/src/utils.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-28 16:19:04 -05:00
Eli Bosley
e4ebfc8a13 fix: file modification service fixes 2025-01-28 16:19:04 -05:00
Eli Bosley
8483143a40 Update api/src/unraid-api/unraid-file-modifier/modifications/sso.modification.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-28 16:19:04 -05:00
Eli Bosley
d6fa35cdee feat: service tests for modifier service 2025-01-28 16:19:04 -05:00
Eli Bosley
8a374b5b27 feat: properly read log level from environment 2025-01-28 16:19:04 -05:00
Eli Bosley
b73623e72a feat: configure PM2 on startup 2025-01-28 16:19:04 -05:00
Eli Bosley
bddda823e1 fix: basic test fixed 2025-01-28 16:19:04 -05:00
Eli Bosley
bb37140d40 feat: initial version of modification service 2025-01-28 16:19:04 -05:00
Eli Bosley
4d8f2ddac6 fix: test issues 2025-01-28 14:58:34 -05:00
Eli Bosley
5afa76043f Update api/src/unraid-api/graph/resolvers/vms/vms.resolver.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-28 14:58:34 -05:00
Eli Bosley
14fe30e925 fix: create api key permissions 2025-01-28 14:58:34 -05:00
Eli Bosley
eb1c62d3d9 fix: cleaner logs for starting API 2025-01-28 14:58:34 -05:00
Eli Bosley
f62f0d3a0f fix: don't check code for execa 2025-01-28 14:58:34 -05:00
Eli Bosley
e7b689c546 fix: code review feedback 2025-01-28 14:58:34 -05:00
Eli Bosley
b9249544fc fix: restart command elegant 2025-01-28 14:58:34 -05:00
Eli Bosley
cdfb3c772b feat: async hypervisor and FIXED vm listing 2025-01-28 14:58:34 -05:00
Eli Bosley
f1e53831c8 feat: hypervisor async imports 2025-01-28 14:58:34 -05:00
Eli Bosley
32f9c50227 Update api/src/unraid-api/cli/start.command.ts
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-28 09:12:54 -05:00
Eli Bosley
a8211cef7d feat: style improvements 2025-01-28 09:12:54 -05:00
Eli Bosley
e338eb9788 fix: completion script registration 2025-01-27 13:34:32 -05:00
Eli Bosley
dae57389c6 feat: restart the API when an SSO user is added 2025-01-27 13:34:32 -05:00
Eli Bosley
44d3d939a7 fix: shell path to unraid-api 2025-01-27 13:34:32 -05:00
Eli Bosley
6bfd8a2687 feat: revert local api key value 2025-01-27 13:34:32 -05:00
Eli Bosley
923e929878 fix: unneeded await on api-key service 2025-01-27 13:34:32 -05:00
Eli Bosley
2ad612cef8 fix: properly log error with template string 2025-01-27 13:34:32 -05:00
Eli Bosley
1756cc5b4b fix: pull token from query not params 2025-01-27 13:34:32 -05:00
Eli Bosley
3a8c9b13ee feat: allow csrf passing through querystring 2025-01-27 13:34:32 -05:00
Eli Bosley
daf904bc1b feat: remove sso if disabled on Unraid-API start 2025-01-27 13:34:32 -05:00
Eli Bosley
632775e435 feat: default value for option 2025-01-27 13:34:32 -05:00
Eli Bosley
e33c7583f7 fix: remove isNaN in favor of number.isNaN 2025-01-27 13:34:32 -05:00
Eli Bosley
55100daed4 feat: try catch restart 2025-01-27 13:34:32 -05:00
Eli Bosley
b9d9105e3e feat: validate token format in both PHP and CLI 2025-01-27 13:34:32 -05:00
Eli Bosley
3734730bf7 feat: state using crypto 2025-01-27 13:34:32 -05:00
Eli Bosley
c1fe95fcb6 feat: warning on missing fields 2025-01-27 13:34:32 -05:00
Eli Bosley
a1351b0469 feat: add api key creation logic 2025-01-27 13:34:32 -05:00
Eli Bosley
f0395bdf47 chore: update readme 2025-01-27 13:34:32 -05:00
Eli Bosley
76cf6f35dc fix: missing server type 2025-01-27 13:34:32 -05:00
Eli Bosley
ca94cc8602 Update web/components/SsoButton.ce.vue
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-27 13:34:32 -05:00
Eli Bosley
f560df0270 feat: unnecessary comment 2025-01-27 13:34:32 -05:00
Eli Bosley
bb95795a31 feat: address log level feedback 2025-01-27 13:34:32 -05:00
Eli Bosley
cbb42dc85e feat: secondary changes 2025-01-27 13:34:32 -05:00
Eli Bosley
060a1992c4 fix: initial feedback about report addressed 2025-01-27 13:34:32 -05:00
Eli Bosley
a1cf44162a feat: error state outside of button 2025-01-27 13:34:32 -05:00
Eli Bosley
1e6fb7e3e3 fix: thorw on invalid token body 2025-01-27 13:34:32 -05:00
Eli Bosley
336478c2e0 feat: Update plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-01-27 13:34:32 -05:00
Eli Bosley
895d5857f9 feat: remove apiKey from server 2025-01-27 13:34:32 -05:00
Eli Bosley
5ca225fe7a feat: exit cli after running command 2025-01-27 13:34:32 -05:00
Eli Bosley
6faef27d7c feat: zod config no longer any 2025-01-27 13:34:32 -05:00
Eli Bosley
bc04129342 feat: CLI options for adding and deleting users 2025-01-27 13:34:32 -05:00
Eli Bosley
b63720a6f2 feat: add line about recommendation for sso command 2025-01-27 13:34:32 -05:00
Eli Bosley
29a8689ad8 fix: lowercase or 2025-01-27 13:34:32 -05:00
Eli Bosley
3042ffa37e feat: restoring sso error 2025-01-27 13:34:32 -05:00
Eli Bosley
942b143fba fix: remove unused login entries 2025-01-27 13:34:32 -05:00
Eli Bosley
75d7e08824 feat: remove sso user options 2025-01-27 13:34:32 -05:00
Eli Bosley
f30292484d feat: remove sso user command 2025-01-27 13:34:32 -05:00
Eli Bosley
3867dfacb2 feat: cleanup disclaimer and command to add users 2025-01-27 13:34:32 -05:00
Eli Bosley
0da77d7119 feat: disable button on submit 2025-01-27 13:34:32 -05:00
Eli Bosley
09f741557b feat: sso testing page and form disable on submit 2025-01-27 13:34:32 -05:00
Eli Bosley
ae753d6bea fix: padding and glob function issues 2025-01-27 13:34:32 -05:00
Eli Bosley
56cfa84794 fix: oauth2 api prefix 2025-01-27 13:34:32 -05:00
Eli Bosley
065211413d feat: glob for files 2025-01-27 13:34:32 -05:00
Eli Bosley
1854aa9f28 fix: dont remove login file without a backup presetn 2025-01-27 13:34:32 -05:00
Eli Bosley
cb59090698 feat: add user with cli 2025-01-27 13:34:32 -05:00
Eli Bosley
81f051e02c feat:sso login boolean 2025-01-27 13:34:32 -05:00
Eli Bosley
11ff890bcc feat: or button on sign in page 2025-01-27 13:34:32 -05:00
Eli Bosley
3abf20b347 feat: sso button token exchange 2025-01-27 13:34:32 -05:00
Eli Bosley
6f5edb2406 feat: move ssoenabled to a boolean flag rather than ids 2025-01-27 13:34:32 -05:00
Eli Bosley
428ad15ec7 feat: back to callbackUrl 2025-01-27 13:34:32 -05:00
Eli Bosley
bd584902e0 feat: use state passing to validate requests 2025-01-27 13:34:32 -05:00
Eli Bosley
aae38e3404 fix: dev mode 2025-01-27 13:34:32 -05:00
Eli Bosley
e5d1146613 feat: inject after form 2025-01-27 13:34:32 -05:00
Eli Bosley
b3551a1b69 fix: further resolve sso sub ids issues 2025-01-27 13:34:32 -05:00
Eli Bosley
76a9ae9386 fix: pass ssoSubIds only 2025-01-27 13:34:32 -05:00
Eli Bosley
92799312c9 fix: pass token to password field 2025-01-27 13:34:32 -05:00
Eli Bosley
b969f3a9ab feat: dont pass entire server state for privacy 2025-01-27 13:34:32 -05:00
Eli Bosley
3419837eb5 feat: enable PR releases on non-mainline merges 2025-01-27 13:34:32 -05:00
Eli Bosley
2b25537e26 feat: unraid single sign on with account app 2025-01-27 13:34:32 -05:00
Eli Bosley
2d3892deb8 feat: remove unused fields 2025-01-27 13:34:32 -05:00
Eli Bosley
0ab40fefda fix: unit tests updated 2025-01-27 13:34:32 -05:00
Eli Bosley
58f65eabba fix: stop command exits 2025-01-27 13:34:32 -05:00
Eli Bosley
89d756ef4e feat: csv validation 2025-01-27 13:34:32 -05:00
Eli Bosley
a1a046f900 fix: back to default configs 2025-01-27 13:34:32 -05:00
Eli Bosley
d844903d78 fix: reset config to be closer to default 2025-01-27 13:34:32 -05:00
Eli Bosley
29ca5829ff feat: only write config when a specific config update action occurs 2025-01-27 13:34:32 -05:00
Eli Bosley
27049d9d91 fix: start command simplification 2025-01-27 13:34:32 -05:00
Eli Bosley
03e336b72f feat: remove unused config sections 2025-01-27 13:34:32 -05:00
Eli Bosley
c2e29dfb5f feat: cleanup config entries 2025-01-27 13:34:32 -05:00
Eli Bosley
e9bd18a409 feat: enable token sign in with comma separated subs in myservers.config 2025-01-27 13:34:32 -05:00
Eli Bosley
02c197f244 feat: use zod to parse config 2025-01-27 13:34:32 -05:00
Eli Bosley
6f9977eea0 feat: remove unused vars 2025-01-27 13:34:32 -05:00
Eli Bosley
05e77a4bc6 feat: use execa for start and stop 2025-01-27 13:34:32 -05:00
Eli Bosley
a892a3ce35 fix: deprecated version warning 2025-01-27 13:34:32 -05:00
Eli Bosley
33dd90af04 feat: better pm2 calls, log lines 2025-01-27 13:34:32 -05:00
Eli Bosley
7fa849d2a0 feat: cli Commands 2025-01-27 13:34:32 -05:00
Eli Bosley
7ceac1b184 feat: switch to nest-commander 2025-01-27 13:34:32 -05:00
mdatelle
3348a47470 feat: add command to package.json scripts 2025-01-27 13:34:32 -05:00
mdatelle
85cdb8f525 feat: add description flag, remove console log, and update readme 2025-01-27 13:34:32 -05:00
mdatelle
796cb09c61 feat: create key cli command logic and add to index command list 2025-01-27 13:34:32 -05:00
Eli Bosley
a554bde5c2 feat: initial setup of permissions on keys (#1068)
* feat: initial setup of permissions on keys

* fix: remove API keys

* test: update me resolver, findByIdWithSecret, findByKey and saveApiKey tests

* test: update and fix the rest of the failing api key tests

* fix: add reflect-metadata to test setup in vite config

* fix: revert myservers.cfg to original

* fix: update User type on me resolver

* fix: make permissions nullable and rerun codegen

* fix: update import syntax in me resolver

* refactor: move create-local-connect-api-key to api key service and handle in onModuleInit

* test: add tests for createLocalApiKeyForConnectIfNecessary

* refactor: add validation to me resolver

* refactor: address code rabbit suggestions

* refactor: update me resolver tests and fix hasOwnProperty error

* refactor: remove console log

* test: add additional coverage for me resolver tests

* test: fix failing test

* refactor: address review comments, add new api-key service test, and remove deprecated keys

* refactor: address review comments

---------

Co-authored-by: mdatelle <mike@datelle.net>
2025-01-23 15:37:15 -05:00
Pujit Mehrotra
3acc0dc9c0 fix: integration of unraid-ui tailwind config in web (#1074)
* fix: integration of unraid-ui tailwind config in web

* chore(ci): inline unraid-ui build
2025-01-21 15:48:25 -05:00
655 changed files with 47364 additions and 50857 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
@elibosley @pujitm @mdatelle @zackspear

45
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,45 @@
---
name: Bug Report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
<!--
IMPORTANT: If your issue is related to Unraid Connect features (Flash Backup, connect.myunraid.net, mothership errors with connectivity, etc.) please submit a ticket here: [LINK TO FRESHDESK FORM FOR CONNECT] and choose Unraid Connect in the dropdown.
-->
## Environment
**Unraid OS Version:**
<!-- Please specify your Unraid version (e.g. 7.0.0) -->
**Are you using a reverse proxy?**
<!-- Please answer Yes/No. If yes, have you tested the issue by accessing your server directly? -->
<!-- Note: Reverse proxies are not officially supported by Unraid and can cause issues with various components of Unraid OS -->
## Pre-submission Checklist
<!-- Please check all that apply by replacing [ ] with [x] -->
- [ ] I have verified that my Unraid OS is up to date
- [ ] I have tested this issue by accessing my server directly (not through a reverse proxy)
- [ ] This is not an Unraid Connect related issue (if it is, please submit via the support form instead)
## Issue Description
<!-- Please provide a clear and concise description of the issue -->
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
<!-- What did you expect to happen? -->
## Actual Behavior
<!-- What actually happened? -->
## Additional Context
<!-- Add any other context, screenshots, or error messages about the problem here -->

View File

@@ -0,0 +1,34 @@
---
name: Feature Request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
<!--
IMPORTANT: If your feature request is related to Unraid Connect features (Flash Backup, connect.myunraid.net, etc.) please submit it here: [LINK TO FRESHDESK FORM FOR CONNECT] and choose Unraid Connect in the dropdown.
-->
## Is your feature request related to a problem?
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
## Describe the solution you'd like
<!-- A clear and concise description of what you want to happen -->
## Describe alternatives you've considered
<!-- A clear and concise description of any alternative solutions or features you've considered -->
## Additional context
<!-- Add any other context, mockups, or screenshots about the feature request here -->
## Environment (if relevant)
**Unraid OS Version:**
<!-- Please specify your Unraid version (e.g. 7.0.0) if the feature request is version-specific -->
## Pre-submission Checklist
<!-- Please check all that apply by replacing [ ] with [x] -->
- [ ] I have searched existing issues to ensure this feature hasn't already been requested
- [ ] This is not an Unraid Connect related feature (if it is, please submit via the support form instead)
- [ ] I have provided clear examples or use cases for the feature

41
.github/ISSUE_TEMPLATE/work_intent.md vendored Normal file
View File

@@ -0,0 +1,41 @@
---
name: Work Intent
about: Request approval for planned development work (must be approved before starting)
title: 'Work Intent: '
labels: work-intent, unapproved
assignees: ''
---
<!--
IMPORTANT: This work intent must be approved by a core developer before beginning any development work.
The 'unapproved' label will be removed once approved.
-->
## Overview
<!-- Provide a high-level description of what you want to work on and why -->
## Technical Approach
<!-- Brief description of how you plan to implement this -->
## Scope
<!-- Check components that will be modified -->
- [ ] API
- [ ] Plugin
- [ ] Web UI
- [ ] Build/Deploy Process
- [ ] Documentation
## Timeline & Impact
<!-- Quick details about timing and effects -->
- Estimated time needed:
- Potential impacts:
## Pre-submission Checklist
<!-- Please check all that apply -->
- [ ] I have searched for similar work/issues
- [ ] I understand this needs approval before starting
- [ ] I am willing to make adjustments based on feedback
<!--
For Reviewers: Remove 'unapproved' label and add 'approved' label if accepted
-->

View File

@@ -0,0 +1,59 @@
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@v4
with:
path: source-repo
- name: Checkout docs repository
uses: actions/checkout@v4
with:
repository: unraid/docs
path: docs-repo
token: ${{ secrets.DOCS_PAT_UNRAID_BOT }}
- name: Copy updated docs
run: |
if [ ! -d "source-repo/api/docs" ]; then
echo "Source directory does not exist!"
exit 1
fi
rm -rf docs-repo/docs/API/
mkdir -p docs-repo/docs/API
cp -r source-repo/api/docs/public/. docs-repo/docs/API/
- 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, pujitm, mdatelle
branch: update-api-docs
base: main
delete-branch: true

View File

@@ -34,66 +34,122 @@ jobs:
- name: Validate branch and tag
run: exit 0
build-test-api:
test-api:
defaults:
run:
working-directory: api
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
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: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
- name: Test
run: pnpm run coverage
build-api:
name: Build and Test API
runs-on: ubuntu-latest
defaults:
run:
working-directory: api
outputs:
API_VERSION: ${{ steps.vars.outputs.API_VERSION }}
API_MD5: ${{ steps.set-hashes.outputs.API_MD5 }}
API_SHA256: ${{ steps.set-hashes.outputs.API_SHA256 }}
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Build with Buildx
uses: docker/setup-buildx-action@v3
with:
install: true
platforms: linux/amd64
- name: Build Builder
uses: docker/build-push-action@v6
with:
context: ./api
push: false
tags: builder:latest
cache-from: type=gha,ref=builder:latest
cache-to: type=gha,mode=max,ref=builder:latest
load: true
- name: Lint inside of the docker container
continue-on-error: true
run: |
docker run --rm builder npm run lint
- name: Test inside of the docker container
- name: Install Node
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
run_install: false
- name: Get pnpm store directory
id: pnpm-cache
shell: bash
run: |
git fetch --depth=2 origin main
if git diff --name-only --relative=api origin/main HEAD | grep -q '.'; then
docker run --rm builder npm run coverage
else
echo "No changes in /api folder, skipping coverage."
fi
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.4.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: Lint
run: pnpm run lint
- name: Type Check
run: pnpm run type-check
continue-on-error: true
- 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-lock.json)
echo "GIT_SHA=$GIT_SHA" >> $GITHUB_OUTPUT
echo "IS_TAGGED=$IS_TAGGED" >> $GITHUB_OUTPUT
echo "PACKAGE_LOCK_VERSION=$PACKAGE_LOCK_VERSION" >> $GITHUB_OUTPUT
echo "API_VERSION=$([[ -n "$IS_TAGGED" ]] && echo "$PACKAGE_LOCK_VERSION" || echo "${PACKAGE_LOCK_VERSION}+${GIT_SHA}")" >> $GITHUB_OUTPUT
- name: Build inside of the docker container
id: build-pack-binary
run: |
docker run --rm -v ${{ github.workspace }}/api/deploy/release:/app/deploy/release -e API_VERSION=${{ steps.vars.outputs.API_VERSION }} builder npm run build-and-pack
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
- name: Set Hashes
id: set-hashes
run: |
echo "API_MD5=$(md5sum ${{ github.workspace }}/api/deploy/release/*.tgz | awk '{ print $1 }')" >> $GITHUB_OUTPUT
echo "API_SHA256=$(sha256sum ${{ github.workspace }}/api/deploy/release/*.tgz | awk '{ print $1 }')" >> $GITHUB_OUTPUT
- name: Build
run: pnpm run build-and-pack
- name: Upload tgz to Github artifacts
uses: actions/upload-artifact@v4
@@ -101,8 +157,8 @@ jobs:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz
build-unraid-ui:
name: Build Unraid UI Library
build-unraid-ui-webcomponents:
name: Build Unraid UI Library (Webcomponent Version)
defaults:
run:
working-directory: unraid-ui
@@ -111,35 +167,53 @@ jobs:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install node
- name: Install Node
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: |
unraid-ui/package-lock.json
node-version-file: ".nvmrc"
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
version: 10
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.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
- name: Install dependencies
run: npm install
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/ui
- name: Build
run: npm run build
- name: Make Built Node Artifact
run: |
mkdir unraid-ui-dist
mv dist/ unraid-ui-dist/dist/
mv package.json unraid-ui-dist/package.json
ls unraid-ui-dist
run: pnpm run build:wc
- name: Upload Artifact to Github
uses: actions/upload-artifact@v4
with:
name: unraid-ui
path: unraid-ui/unraid-ui-dist
name: unraid-wc-ui
path: unraid-ui/dist/
build-web:
needs: [build-unraid-ui]
# needs: [build-unraid-ui]
name: Build Web App
environment:
name: production
@@ -160,45 +234,60 @@ jobs:
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
- name: Install Node
uses: actions/setup-node@v4
with:
cache: "npm"
cache-dependency-path: |
web/package-lock.json
node-version-file: "web/.nvmrc"
node-version-file: ".nvmrc"
- name: Remove Existing Unraid UI folder
run: |
rm -r ../unraid-ui
- name: Download Artifact for Unraid UI
uses: actions/download-artifact@v4
- uses: pnpm/action-setup@v4
name: Install pnpm
with:
name: unraid-ui
path: unraid-ui
version: 10
run_install: false
- name: Installing node deps
run: npm install
- 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
continue-on-error: true
run: npm run lint
run: pnpm run lint
- name: Test
run: npm run test:ci
run: pnpm run test:ci
- name: Build
run: npm run build
run: pnpm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v4
with:
name: unraid-web
name: unraid-wc-rich
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
build-plugin:
needs: [build-test-api, build-web]
needs: [build-api, build-web, build-unraid-ui-webcomponents]
outputs:
tag: ${{ steps.build-plugin.outputs.tag }}
defaults:
run:
working-directory: plugin
@@ -210,42 +299,78 @@ jobs:
timezoneLinux: "America/Los_Angeles"
- name: Checkout repo
uses: actions/checkout@v4
- name: Download unraid web components
with:
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:
version: 10
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: Install dependencies
run: |
cd ${{ github.workspace }}
pnpm install --frozen-lockfile --filter @unraid/connect-plugin
- name: Download Unraid Web Components
uses: actions/download-artifact@v4
with:
name: unraid-web
pattern: unraid-wc-*
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
- name: Build Plugin
merge-multiple: true
- name: Download Unraid API
uses: actions/download-artifact@v4
with:
name: unraid-api
path: /tmp/unraid-api/
- name: Extract Unraid API and Build Plugin
id: build-plugin
run: |
cd source/dynamix.unraid.net
export API_VERSION=${{needs.build-test-api.outputs.API_VERSION}}
export API_MD5=${{needs.build-test-api.outputs.API_MD5}}
export API_SHA256=${{needs.build-test-api.outputs.API_SHA256}}
if [ -z "${API_VERSION}" ] ||
[ -z "${API_MD5}" ] ||
[ -z "${API_SHA256}" ]; then
echo "Error: One or more required variables are not set."
exit 1
tar -xzf /tmp/unraid-api/unraid-api.tgz -C ${{ github.workspace }}/plugin/source/dynamix.unraid.net/usr/local/unraid-api
cd ${{ github.workspace }}/plugin
if [ -n "${{ github.event.pull_request.number }}" ]; then
export TAG=PR${{ github.event.pull_request.number }}
# Put tag into github env
echo "TAG=${TAG}" >> $GITHUB_OUTPUT
fi
bash ./pkg_build.sh s ${{github.event.pull_request.number}}
bash ./pkg_build.sh p
pnpm run build
- name: Upload binary txz and plg to Github artifacts
uses: actions/upload-artifact@v4
with:
name: connect-files
path: |
${{ github.workspace }}/plugin/archive/*.txz
${{ github.workspace }}/plugin/plugins/*.plg
plugin/deploy/release/plugins/
plugin/deploy/release/archive/*.txz
retention-days: 5
if-no-files-found: error
release-pull-request:
if: |
github.event_name == 'pull_request' &&
github.event.pull_request.base.ref == 'main'
github.event_name == 'pull_request'
runs-on: ubuntu-latest
needs: [build-plugin]
needs: [test-api, build-plugin]
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -253,35 +378,15 @@ jobs:
- name: Make PR Release Folder
run: mkdir pr-release/
- name: Download unraid-api binary tgz
uses: actions/download-artifact@v4
with:
name: unraid-api
path: pr-release
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Write Changelog to Plugin XML
run: |
# Capture the pull request number and latest commit message
pr_number="${{ github.event.pull_request.number }}"
commit_message=$(git log -1 --pretty=%B)
# Clean up newlines, escape special characters, and handle line breaks
notes=$(echo -e "Pull Request Build: ${pr_number}\n${commit_message}" | \
sed ':a;N;$!ba;s/\n/\\n/g' | \
sed -e 's/[&\\/]/\\&/g')
# Replace <CHANGES> tag content in the file
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${notes}\n<\/CHANGES>/g" "plugins/dynamix.unraid.net.staging.plg"
- name: Copy other release files to pr-release
run: |
cp archive/*.txz pr-release/
cp plugins/dynamix.unraid.net.staging.plg pr-release/
cp plugins/pr/dynamix.unraid.net.plg pr-release/dynamix.unraid.net.plg
- name: Upload to Cloudflare
uses: jakejarvis/s3-sync-action@v0.5.1
@@ -292,13 +397,18 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: "auto"
SOURCE_DIR: pr-release
DEST_DIR: unraid-api/pr/${{ github.event.pull_request.number }}
DEST_DIR: unraid-api/tag/${{ needs.build-plugin.outputs.tag }}
- name: Comment URL
uses: thollander/actions-comment-pull-request@v3
with:
comment-tag: prlink
mode: recreate
message: |
This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL: [https://preview.dl.unraid.net/unraid-api/pr/${{ github.event.pull_request.number }}/dynamix.unraid.net.staging.plg](https://preview.dl.unraid.net/unraid-api/pr/${{ github.event.pull_request.number }}/dynamix.unraid.net.staging.plg)
Download it at this URL:
```
https://preview.dl.unraid.net/unraid-api/tag/${{ needs.build-plugin.outputs.tag }}/dynamix.unraid.net.plg
```
release-staging:
environment:
@@ -306,7 +416,7 @@ jobs:
# Only release if this is a push to the main branch
if: startsWith(github.ref, 'refs/heads/main')
runs-on: ubuntu-latest
needs: [build-plugin]
needs: [test-api, build-plugin]
steps:
- name: Checkout repo
@@ -315,40 +425,17 @@ jobs:
- name: Make Staging Release Folder
run: mkdir staging-release/
- name: Download unraid-api binary tgz
uses: actions/download-artifact@v4
with:
name: unraid-api
path: staging-release
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Parse Changelog
id: changelog
uses: ocavue/changelog-parser-action@v1
with:
removeMarkdown: false
filePath: "./api/CHANGELOG.md"
- name: Copy Files for Staging Release
run: |
cp archive/*.txz staging-release/
cp plugins/dynamix.unraid.net.staging.plg staging-release/
cp plugins/staging/dynamix.unraid.net.plg staging-release/dynamix.unraid.net.plg
ls -al staging-release
- name: Upload Staging Plugin to DO Spaces
uses: BetaHuhn/do-spaces-action@v2
with:
access_key: ${{ secrets.DO_ACCESS_KEY }}
secret_key: ${{ secrets.DO_SECRET_KEY }}
space_name: ${{ secrets.DO_SPACE_NAME }}
space_region: ${{ secrets.DO_SPACE_REGION }}
source: staging-release
out_dir: unraid-api
- name: Upload Staging Plugin to Cloudflare Bucket
uses: jakejarvis/s3-sync-action@v0.5.1
env:
@@ -365,30 +452,28 @@ jobs:
if: |
startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: [build-plugin]
needs: [test-api, build-plugin]
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Download unraid-api binary tgz
uses: actions/download-artifact@v4
with:
name: unraid-api
- name: Download plugin binary tgz
uses: actions/download-artifact@v4
with:
name: connect-files
- name: Move Files to Release Folder
run: |
mkdir -p release/
mv plugins/production/dynamix.unraid.net.plg release/
mv archive/*.txz release/
- name: Create Github release
uses: softprops/action-gh-release@v1
with:
draft: true
prerelease: false
files: |
unraid-api-*.tgz
plugins/dynamix.unraid.net*
archive/*
release/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,57 @@
name: Push Staging Plugin on PR Close
on:
pull_request:
types:
- closed
jobs:
push-staging:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Set Timezone
uses: szenius/set-timezone@v1.2
with:
timezoneLinux: "America/Los_Angeles"
- name: Checkout repo
uses: actions/checkout@v4
with:
ref: refs/pull/${{ github.event.pull_request.number }}/merge
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: connect-files
path: connect-files
- name: Update Downloaded Staging Plugin to New Date
run: |
if [ ! -f "connect-files/plugins/dynamix.unraid.net.pr.plg" ]; then
echo "ERROR: dynamix.unraid.net.pr.plg not found"
exit 1
fi
plgfile="connect-files/plugins/dynamix.unraid.net.pr.plg"
version=$(date +"%Y.%m.%d.%H%M")
sed -i -E "s#(<!ENTITY version \").*(\">)#\1${version}\2#g" "${plgfile}" || exit 1
# Change the plugin url to point to staging
url="https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg"
sed -i -E "s#(<!ENTITY pluginURL \").*(\">)#\1${url}\2#g" "${plgfile}" || exit 1
cat "${plgfile}"
mkdir -p pr-release
mv "${plgfile}" pr-release/dynamix.unraid.net.plg
- name: Upload to Cloudflare
uses: jakejarvis/s3-sync-action@v0.5.1
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/pr/${{ github.event.pull_request.number }}

View File

@@ -33,7 +33,6 @@ jobs:
)
escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$notes")
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.plg"
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.staging.plg"
- name: Upload All Release Files to DO Spaces
uses: BetaHuhn/do-spaces-action@v2
@@ -54,4 +53,4 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
AWS_REGION: 'auto'
SOURCE_DIR: "."
DEST_DIR: unraid-api
DEST_DIR: unraid-api

71
.github/workflows/test-libvirt.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
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@v4
with:
submodules: recursive
- uses: actions/setup-python@v5
with:
python-version: "3.10"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.4.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
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

3
.gitignore vendored
View File

@@ -92,3 +92,6 @@ deploy/*
!.env.example
fb_keepalive
# pnpm store
.pnpm-store

View File

@@ -1,8 +1,6 @@
{
"recommendations": [
"natizyskunk.sftp",
"davidanson.vscode-markdownlint",
"bmewburn.vscode-intelephense-client",
"foxundermoon.shell-format",
"timonwong.shellcheck",
"esbenp.prettier-vscode"

61
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,61 @@
# Contributing to Unraid Connect
Thank you for your interest in contributing to Unraid Connect! We want to make contributing to this project as easy and transparent as possible, whether it's:
- Reporting a bug
- Discussing the current state of the code
- Submitting a fix
- Proposing new features
## Development Process
We use GitHub to host code, to track issues and feature requests, as well as accept pull requests.
### 1. Work Intent Process
**Before starting any development work**, you must submit a Work Intent and have it approved:
1. **Create a Work Intent**
- Go to [Issues → New Issue → Work Intent](https://github.com/unraid/api/issues/new?template=work_intent.md)
- Fill out the brief template describing what you want to work on
- The issue will be automatically labeled as `work-intent` and `unapproved`
2. **Wait for Approval**
- A core developer will review your Work Intent
- They may ask questions or suggest changes
- Once approved, the `unapproved` label will be removed
3. **Begin Development**
- Only start coding after your Work Intent is approved
- Follow the approach outlined in your approved Work Intent
- Reference the Work Intent in your future PR
### 2. Making Changes
1. Fork the repo and create your branch from `main`
2. Make your changes
3. Ensure your commits are clear and descriptive
4. Keep your changes focused - solve one thing at a time
### 3. Pull Request Process
1. Create a pull request from your fork to our `main` branch
2. Reference the approved Work Intent in your PR description
3. Ensure the PR description clearly describes the problem and solution
4. Include screenshots or examples if applicable
5. Wait for review from the core team
**Note:** Direct pushes to the main branch are not allowed. All changes must go through the PR process.
## Bug Reports and Feature Requests
We use GitHub issues to track bugs and feature requests:
- **Bug Report**: Use the [Bug Report Template](https://github.com/unraid/api/issues/new?template=bug_report.md)
- **Feature Request**: Use the [Feature Request Template](https://github.com/unraid/api/issues/new?template=feature_request.md)
For Unraid Connect specific issues (Flash Backup, connect.myunraid.net, mothership connectivity), please submit through our support portal instead.
## License
By contributing, you agree that your contributions will be licensed under the same terms as the main project.

View File

@@ -1,11 +1,13 @@
VERSION="THIS_WILL_BE_REPLACED_WHEN_BUILT"
PATHS_UNRAID_DATA=./dev/data # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=./dev/states # Where .ini files live (e.g. vars.ini)
PATHS_AUTH_SESSIONS=./dev/sessions # Where user sessions live
PATHS_AUTH_KEY=./dev/keys # Auth key directory
PATHS_DYNAMIX_BASE=./dev/dynamix # Dynamix's data directory
PATHS_DYNAMIX_CONFIG_DEFAULT=./dev/dynamix/default.cfg # Dynamix's default config file, which ships with unraid
PATHS_DYNAMIX_CONFIG=./dev/dynamix/dynamix.cfg # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file
PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PATHS_MACHINE_ID=./dev/data/machine-id
PORT=5000
NODE_ENV=test
NODE_ENV="test"

View File

@@ -1,9 +1,13 @@
import type { Linter } from 'eslint';
import eslint from '@eslint/js';
import noRelativeImportPaths from 'eslint-plugin-no-relative-import-paths';
import prettier from 'eslint-plugin-prettier';
import tseslint from 'typescript-eslint';
export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.recommended, {
plugins: {
'no-relative-import-paths': noRelativeImportPaths,
prettier: prettier,
},
rules: {
'@typescript-eslint/no-redundant-type-constituents': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
@@ -17,5 +21,15 @@ export default tseslint.config(eslint.configs.recommended, ...tseslint.configs.r
'no-multiple-empty-lines': ['error', { max: 1, maxBOF: 0, maxEOF: 1 }],
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'import/no-unresolved': 'off',
'import/extensions': 'off',
'import/no-absolute-path': 'off',
'import/prefer-default-export': 'off',
'no-relative-import-paths/no-relative-import-paths': [
'error',
{ allowSameFolder: false, rootDir: 'src', prefix: '@app' },
],
'prettier/prettier': 'error',
},
ignores: ['src/graphql/generated/client/**/*'],
});

2
api/.gitignore vendored
View File

@@ -80,3 +80,5 @@ deploy/*
# IDE Settings Files
.idea
!**/*.login.*

View File

@@ -1 +0,0 @@
v20

7
api/.prettierignore Normal file
View File

@@ -0,0 +1,7 @@
!src/*
# Downloaded Fixtures (For File Modifications)
src/unraid-api/unraid-file-modifier/modifications/__fixtures__/downloaded/*
# Generated Types
src/graphql/generated/client/*.ts

View File

@@ -2,6 +2,588 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [4.1.1](https://github.com/unraid/api/compare/v4.1.0...v4.1.1) (2025-02-20)
### Bug Fixes
* main.yml release issue ([8a2a24e](https://github.com/unraid/api/commit/8a2a24eb22762034d44995580d6057186521dae5))
## [4.1.0](https://github.com/unraid/api/compare/v4.0.1...v4.1.0) (2025-02-20)
### Features
* add category.json ([c9e87e2](https://github.com/unraid/api/commit/c9e87e2e5b47a8801b7865ed586c803d0b470915))
* add developer docs ([#1128](https://github.com/unraid/api/issues/1128)) ([bb2e340](https://github.com/unraid/api/commit/bb2e340b68268d5121db650b27e8b2580c7966bb))
* add unraid-ui documentation ([#1142](https://github.com/unraid/api/issues/1142)) ([c557806](https://github.com/unraid/api/commit/c55780680ae905558b79dfefa91b116aef22b105))
* attempt to resolve performance issues with rm earlier in build … ([#1152](https://github.com/unraid/api/issues/1152)) ([2a1aa95](https://github.com/unraid/api/commit/2a1aa95bd62ebfe42b62b8e7105c7a92b00cfca9))
* auto-docusaurus-prs ([#1127](https://github.com/unraid/api/issues/1127)) ([1147e76](https://github.com/unraid/api/commit/1147e762ae2fed6dea198fa38d6bcc514a1e66fb))
* bug report template ([f1ee8b2](https://github.com/unraid/api/commit/f1ee8b27b11fa969d0e6891590e44047c76eedb5))
* contributing guide ([c912476](https://github.com/unraid/api/commit/c912476b431750834c64bdec80a61fda23e6c490))
* convert to pnpm monorepo ([#1137](https://github.com/unraid/api/issues/1137)) ([8d89f8b](https://github.com/unraid/api/commit/8d89f8b20d6f3983d4e85b33827a857aa862db37))
* feature request template ([72a042c](https://github.com/unraid/api/commit/72a042c4fab295cf561807102c9eb9a78273bd83))
* fix docusaurus build + update snapshot ([23b27bd](https://github.com/unraid/api/commit/23b27bd63ea99f4137538eab40501daa67d7e3f5))
* public index ([f0641ea](https://github.com/unraid/api/commit/f0641ea7ca0919884dc3b8642c2e6694398e3246))
* reorder index ([858553f](https://github.com/unraid/api/commit/858553f0debb6424ae0614640b82a050c33f175a))
* simplify docs ([d428030](https://github.com/unraid/api/commit/d428030b806f55b62421559d434fc723786b03ad))
* upgrade workflow and auto-assign reviewers ([58a419e](https://github.com/unraid/api/commit/58a419ed36926d121e405a3de37bcb39f26f50b1))
* **web:** improve notification count syncing ([#1148](https://github.com/unraid/api/issues/1148)) ([af2057c](https://github.com/unraid/api/commit/af2057c643640270e3e152ff8e08c3045e622437))
* work intent ([feee4be](https://github.com/unraid/api/commit/feee4bebfe97620c73e6a6093065f22ea26ee8b9))
* work intent process ([b04a97a](https://github.com/unraid/api/commit/b04a97a493f06c450949c674629e8a787164464b))
### Bug Fixes
* **api:** change log output location for diagnostic compatibility ([#1130](https://github.com/unraid/api/issues/1130)) ([cba1551](https://github.com/unraid/api/commit/cba155138379d47bc3151c7c27d745ba6a345d83))
* **api:** logrotate modification & permissions ([#1145](https://github.com/unraid/api/issues/1145)) ([5209df2](https://github.com/unraid/api/commit/5209df2776e1a985e82bedc655fe28acf1fd0bde))
* connect breaks default css of header ([#1155](https://github.com/unraid/api/issues/1155)) ([4ac9aa3](https://github.com/unraid/api/commit/4ac9aa3e409d0d89f2be61bfbafb8d7b5a5b3b00))
* create PR ignored ([bdfefa8](https://github.com/unraid/api/commit/bdfefa808f5f1d85ff957a78a624edcef3afb47a))
* **deps:** update dependency dockerode to v4 ([#830](https://github.com/unraid/api/issues/830)) ([c331ecd](https://github.com/unraid/api/commit/c331ecd50c4910fd6c35e5ad92b3f676d552febc))
* docs creation workflow ([86134c6](https://github.com/unraid/api/commit/86134c60856c130dab9f96b718d9afa5bbab1e50))
* make public not a part of folder structure in PR ([099a88e](https://github.com/unraid/api/commit/099a88eb4970da48e57dafbc3807e16f1987d7fc))
* PHP Warning in state.php ([#1126](https://github.com/unraid/api/issues/1126)) ([c154b4e](https://github.com/unraid/api/commit/c154b4e0ad2d0627b1541a7f9ee5e55235d4dd5e))
* revert dockerode upgrade ([#1140](https://github.com/unraid/api/issues/1140)) ([a74a379](https://github.com/unraid/api/commit/a74a379a93fd15a315e31191de1bf69c5879f8a6)), closes [unraid/api#830](https://github.com/unraid/api/issues/830)
* shorten work intent form ([95fe671](https://github.com/unraid/api/commit/95fe671717ab856518f5b4893dfbcbade0d0f2ed))
* simplify api setup index ([701b1fb](https://github.com/unraid/api/commit/701b1fbd9096c9675475062eaf32a2cbfb0567b9))
* simplify upcoming features ([8af79b2](https://github.com/unraid/api/commit/8af79b27501b42e1c1f7697756a56a9001000d8f))
* storybook resolution issue ([#1153](https://github.com/unraid/api/issues/1153)) ([52c70b9](https://github.com/unraid/api/commit/52c70b9d85469008894d44788429ba298b082ac7))
* upload to correct tag directory on build ([c5fe723](https://github.com/unraid/api/commit/c5fe723a0abee0d0fc494a5b512c995001ae0615))
* **web:** broken modals ([aebf339](https://github.com/unraid/api/commit/aebf3392595d45c84a84668f461c632a2d62e7dd))
* **web:** name of toaster component ([e093242](https://github.com/unraid/api/commit/e093242d20ddd72567396f4a53238250f2199a64))
### [4.0.1](https://github.com/unraid/api/compare/v4.0.0...v4.0.1) (2025-02-06)
## [4.0.0](https://github.com/unraid/api/compare/v3.11.0...v4.0.0) (2025-02-06)
### Features
* actual install url ([89d667e](https://github.com/unraid/api/commit/89d667e33bffb17df43c768f12c21302571270ff))
* actually exit on stop and start ([bce5fde](https://github.com/unraid/api/commit/bce5fde64278dd853e71c022c03b9f6888dccfcf))
* add api key creation logic ([81382bc](https://github.com/unraid/api/commit/81382bcf1d26364ad9c5445530f648209101cf91))
* add command to package.json scripts ([0dfb07f](https://github.com/unraid/api/commit/0dfb07f9eb519e60441f4123423f65acfdffca3b))
* add csrf support to api & web components ([#999](https://github.com/unraid/api/issues/999)) ([19241ed](https://github.com/unraid/api/commit/19241ed55f5112f878b9890d8695badf7eb1c3eb))
* add date formatting helper ([#938](https://github.com/unraid/api/issues/938)) ([b8c8b00](https://github.com/unraid/api/commit/b8c8b005410bbb612014f34ada51ca23cae67a30))
* add deletion & update methods to NotificationService ([ac82b08](https://github.com/unraid/api/commit/ac82b08a9e865cd095cfb5c484404f9e7383391e))
* add description flag, remove console log, and update readme ([c416c30](https://github.com/unraid/api/commit/c416c30951de4ed6b8d7a8c014403772db1c2015))
* add deviceCount to serverAccountPayload for callbacks ([0fb8b87](https://github.com/unraid/api/commit/0fb8b87ff2645ec642d2f038e5f941a880274817))
* add ecosystem.config.json to files ([913febc](https://github.com/unraid/api/commit/913febc0e461bfe052fe116e76d9871e54584aa2))
* add exclude to vite.config ([e64dde7](https://github.com/unraid/api/commit/e64dde7a23414a2e649bef999de8e2164c7b507f))
* add ID prefix plugin to prefix IDs with server identifier ([066e93a](https://github.com/unraid/api/commit/066e93a52afa17f53df2f238065d853ce2945a1e))
* add line about recommendation for sso command ([44727a8](https://github.com/unraid/api/commit/44727a8d1a7c16c566678da43119b17a6303e375))
* add log rotation ([f5c7ad9](https://github.com/unraid/api/commit/f5c7ad9221f80e4630e69f78d57f08f4c7252719))
* add logging around fixture downloads ([a1ce27b](https://github.com/unraid/api/commit/a1ce27b17c970657f52635600f0d13116523f928))
* add logrotate cron again ([4f85f66](https://github.com/unraid/api/commit/4f85f6687f920dae50277e726e2db2c3d946e867))
* add patch for auth-request.php ([ec6ec56](https://github.com/unraid/api/commit/ec6ec562f43aac9947de2e9c269181303f42b2db))
* add user with cli ([37458cd](https://github.com/unraid/api/commit/37458cd7408a1ad8aedca66a55ff13ac19ee30db))
* add validation step to ensure that variables are set ([e3e9b2b](https://github.com/unraid/api/commit/e3e9b2bf404cb6f3bcae83db0395be272e4b79e3))
* add web gitignore ([8b49190](https://github.com/unraid/api/commit/8b491900947c9a7a63b7ad61e7d355ff2fd1f801))
* address log level feedback ([49774aa](https://github.com/unraid/api/commit/49774aae459797f04ef2866ca064050aa476ae91))
* allow csrf passing through querystring ([dba38c0](https://github.com/unraid/api/commit/dba38c0d149a77e4104c718c53d426330a17f2fa))
* allow deletion and creation of files with patches ([32c9524](https://github.com/unraid/api/commit/32c952402c25e8340b1c628b4d0fdc4816b28ade))
* almost working ([df1fc6d](https://github.com/unraid/api/commit/df1fc6dffaa242d85d8ab79f2bfe9e9b1de4b261))
* also copy in other files ([599b365](https://github.com/unraid/api/commit/599b365e8b668d9fba9d88f3d0d03fb7f63244cb))
* always ensureDirectory for keys exists ([c6e9f80](https://github.com/unraid/api/commit/c6e9f804c58e44b46bce9f0da2260888544354cd))
* always start the API and run npm link from script path ([30133ac](https://github.com/unraid/api/commit/30133acb0514a480177f563d4aee364a8a3fab1b))
* **api:** add default dynamix config to dev docker container ([0aeea34](https://github.com/unraid/api/commit/0aeea34427805a9b61a762efbd01c938016be28c))
* **api:** graphql sandbox on unraid servers ([#1047](https://github.com/unraid/api/issues/1047)) ([ec504f3](https://github.com/unraid/api/commit/ec504f39297c92b64d9d3cc2f8f482cc1f3a2e44))
* **api:** omit tz from sys time date format by default ([b2acde3](https://github.com/unraid/api/commit/b2acde3351d7afe18a2902e90b672537aadabffd))
* **api:** rm 2fa & t2fa from myservers config type ([#996](https://github.com/unraid/api/issues/996)) ([89e791a](https://github.com/unraid/api/commit/89e791ad2e6f0395bee05e3f8bdcb2c8d72305dd))
* **api:** sort notifications file listing by date (latest first) ([cae8d0b](https://github.com/unraid/api/commit/cae8d0bc07a465d73f0242a8d68816fd0a6042c7))
* array iteration for restoring files ([036e97b](https://github.com/unraid/api/commit/036e97bb02e463872b3c2f4b5f1aa3b4bf525d1e))
* async disk mapping ([bbb27e6](https://github.com/unraid/api/commit/bbb27e686897e4f9a0c926553d75aa046d7a8323))
* async hypervisor and FIXED vm listing ([e79f4dd](https://github.com/unraid/api/commit/e79f4ddbc7061c249efb8214a311bb629628f669))
* attempt to fix pm2 ([ab67717](https://github.com/unraid/api/commit/ab67717d5954ae3965c1e4082605af5e42f73ca2))
* attempt to start unraid-api with background task ([2a102fc](https://github.com/unraid/api/commit/2a102fc9944f3080af66a8ebadee35059bce2009))
* **Auth:** add cookie guard to check for valid sessions ([3dffc0c](https://github.com/unraid/api/commit/3dffc0c663cfbe8c4368ab98c834baf611a8910a))
* **auth:** make cors aware of authenticated sessions ([f9c23aa](https://github.com/unraid/api/commit/f9c23aa8852a8335640bbd16caa783e9a38b449c))
* automatic session setup for dev ([36d630e](https://github.com/unraid/api/commit/36d630e89bbf9bc7e3ae64bdf5cf73a8536d44ab))
* back to callbackUrl ([e39b120](https://github.com/unraid/api/commit/e39b1203a315889c5b5232ecfd32c7377ae04800))
* begin building plugin with node instead of bash ([#1120](https://github.com/unraid/api/issues/1120)) ([253b65a](https://github.com/unraid/api/commit/253b65a85ab9c5f53d53ef265b41aa132678f278))
* begin fixing dark mode in the webcomponents ([5f7dcdb](https://github.com/unraid/api/commit/5f7dcdb1a7e7bce87b29add7849c94a0353c2c96))
* begin nuking alpha beta gamma ([25acd4b](https://github.com/unraid/api/commit/25acd4b39fff9a0cb573f9e90c52830fef41d737))
* better patch application ([a3e7daa](https://github.com/unraid/api/commit/a3e7daa6a6565ac81004ffd13da35d8b95b429cf))
* better pm2 calls, log lines ([338ce30](https://github.com/unraid/api/commit/338ce3061310dfc42ad5f65edacbe5272de4afc7))
* build and pack in docker ([2a322d1](https://github.com/unraid/api/commit/2a322d12570ba3e797fb84289a42b010f3f88467))
* buildx build caching ([b38be3c](https://github.com/unraid/api/commit/b38be3ceb7c7299fe30c90ed5a75e131af3b33da))
* checkout correct branch on close ([#1123](https://github.com/unraid/api/issues/1123)) ([a20b812](https://github.com/unraid/api/commit/a20b812b020adfade129ebd9fb0e6536004f8bee))
* cleanup config entries ([943e73f](https://github.com/unraid/api/commit/943e73fa696b6ecec3227be914ab4962c4fee79d))
* cleanup disclaimer and command to add users ([6be3af8](https://github.com/unraid/api/commit/6be3af8d7569d9c413dd9349df52e3fa4cb4f631))
* cleanup unused variables ([b50e289](https://github.com/unraid/api/commit/b50e2896f765cecfe631aa839186a6124beb41a3))
* cli Commands ([f8e5367](https://github.com/unraid/api/commit/f8e5367f3eb47daa5bcbd7711ae5835369502a1d))
* CLI options for adding and deleting users ([16bf6d4](https://github.com/unraid/api/commit/16bf6d4c27ae8fa8d6d05ec4b28ce49a12673278))
* code review changes ([fe38acc](https://github.com/unraid/api/commit/fe38acc92e4b1891b0b61fdb4947ec91070cb535))
* codeowners ([ab090b4](https://github.com/unraid/api/commit/ab090b48ec7291597a135a72b8e55c2d1bb389f3))
* coderabbit suggestion ([11ac36c](https://github.com/unraid/api/commit/11ac36c3616a90853d91467526fd39ecba17db88))
* comment URL for plugin on PR ([9840b33](https://github.com/unraid/api/commit/9840b334b4466a4f72e3e57055338a3d5557553d))
* configure PM2 on startup ([2b908f1](https://github.com/unraid/api/commit/2b908f100b9eefaccf2264d5ff9945667568acf0))
* copy ([7e33e5c](https://github.com/unraid/api/commit/7e33e5ca32e95168bb82f090c1acaee43bce1f25))
* copy node modules ([bb0436c](https://github.com/unraid/api/commit/bb0436c7fec54be9ea63681104e720bb5b499f58))
* copy only needed files for nodejs ([acf587a](https://github.com/unraid/api/commit/acf587aa53ca25a3beae86afc608fc9ed68919ef))
* create key cli command logic and add to index command list ([9b2a62d](https://github.com/unraid/api/commit/9b2a62d642b0942e3787e4ddd582a66e40321ab2))
* csv validation ([84aae15](https://github.com/unraid/api/commit/84aae15a73014592c226fa3701e34e57c7b60b46))
* default value for option ([6513fc4](https://github.com/unraid/api/commit/6513fc49de61c836e1aabf32a874d7da7da18adb))
* delete unused imports ([97a3772](https://github.com/unraid/api/commit/97a3772d95aff534d85c410e58391d30494d9237))
* diff ([02c0c5f](https://github.com/unraid/api/commit/02c0c5f8e09476ddcd207c49e1c7d6c764c40d69))
* disable button on submit ([2ceb5da](https://github.com/unraid/api/commit/2ceb5da3c70826cc50df476decb6b117025f46c0))
* disable casbin logging ([2518e7c](https://github.com/unraid/api/commit/2518e7c506f0d3aa9f44031d61dce95d9db0a4cf))
* do not move upgradepkg ([ea16419](https://github.com/unraid/api/commit/ea16419929e0233e2c1ce37e2f4b79e3e64ce619))
* docstrings ([b836ba7](https://github.com/unraid/api/commit/b836ba72516c554ee8973d69aaaa4ed35b465fa7))
* don't remove directory, only files ([c2227cb](https://github.com/unraid/api/commit/c2227cbaadbbfe3dda6a89690a396db5bd6db444))
* dont pass entire server state for privacy ([54e3f17](https://github.com/unraid/api/commit/54e3f17bd9e541f50970c696bbe8b602ec38a748))
* download fixtures from the web ([1258c2b](https://github.com/unraid/api/commit/1258c2bc1813f0fa3cd52b4932302ad12b4edd01))
* download nodejs and install on legacy OS versions ([2a95e4b](https://github.com/unraid/api/commit/2a95e4beb2364510003f187459e28bb610583c41))
* eliminate all alpha beta gamma variable usage ([fbdbce9](https://github.com/unraid/api/commit/fbdbce97ec2171ec7057f0f159e73032e984705a))
* enable PR releases on non-mainline merges ([7ae8d03](https://github.com/unraid/api/commit/7ae8d03166952a602f0b7ebaf1cc65a9a8d27e7b))
* enable sandbox in dev mode ([4536d70](https://github.com/unraid/api/commit/4536d7092d77c68f5a996fd63bf74ce6e64f5efe))
* enable sandbox with developer command ([c354d48](https://github.com/unraid/api/commit/c354d482283295547afeb99c5e110b0181197c44))
* enable token sign in with comma separated subs in myservers.config ([ebed5bd](https://github.com/unraid/api/commit/ebed5bddea1445d9aaaee60d54758dc74b77271e))
* error state outside of button ([18c63e0](https://github.com/unraid/api/commit/18c63e0b0c7451c99eacabb504e18f8070ff7dc2))
* error when nodejs download fails ([6a9b14c](https://github.com/unraid/api/commit/6a9b14c68170d6430328cbb793d750f3177bdb32))
* exit after running status ([12f551c](https://github.com/unraid/api/commit/12f551c9d91692b40b73d96133d45c04f795548e))
* exit cli after running command ([04bf528](https://github.com/unraid/api/commit/04bf528616fcbdf916916734a12d5fd32db9a06d))
* expose mutations for notifications over graphql ([59dc330](https://github.com/unraid/api/commit/59dc33029d03c3d3cda5b4c2a60772e2b7d01811))
* extensive file checking ([ab881c8](https://github.com/unraid/api/commit/ab881c8aed8dd4aa9fd71c32b50d3514d1496fa5))
* extract node to usr/local/ ([4c0b55b](https://github.com/unraid/api/commit/4c0b55b269f47a9d8f746344ae701e353d80509a))
* fallback to local ([a2579c2](https://github.com/unraid/api/commit/a2579c2a7f80f54b4cc61533aec9ecc41a7e7f54))
* faster failure logic ([b439434](https://github.com/unraid/api/commit/b439434f1574e174fcf23f3a5f5b8df8e092eb1e))
* fix header strategy ([4187b77](https://github.com/unraid/api/commit/4187b77a107c0f37e47a1e272c5acb9b798ad3be))
* fix issues with permissions and invalid modules ([e0cfb40](https://github.com/unraid/api/commit/e0cfb40c847a53def1057ae00c97f9306713c3d1))
* fix missing flash line ([6897aad](https://github.com/unraid/api/commit/6897aad67f5c8b38450aa81e612b8aa98a9328c7))
* fix missing import in ESM ([8e99bdd](https://github.com/unraid/api/commit/8e99bdd8f97e772b07374d833debff4eadbf6501))
* fix more imports ([028df06](https://github.com/unraid/api/commit/028df06cd2279d219bd0b3039ad8680de6138b83))
* fix pm2 setup and add link command ([de9500f](https://github.com/unraid/api/commit/de9500ffa6f3aa1842152e0ab26f54c8c5c6e5cb))
* force linting on build ([43e6639](https://github.com/unraid/api/commit/43e663998a55e83c142067cb64ae7a331395fe68))
* generate key one time ([afe53c3](https://github.com/unraid/api/commit/afe53c30ea9987e6d8728faa2cb7291f8a126ecb))
* glob for files ([3fe281f](https://github.com/unraid/api/commit/3fe281f1ae28e3cbc089b5244a6ae2863b20adcb))
* hide sign in from the dropdown text ([3e68aaf](https://github.com/unraid/api/commit/3e68aaf8cdc0fb20c6e1b819a8571f419d94a811))
* hypervisor async imports ([32686ca](https://github.com/unraid/api/commit/32686ca4f0c25c43c6a9f7162bb8179b39e58f7e))
* ID prefixer improvement ([ed55b32](https://github.com/unraid/api/commit/ed55b32645d7414657c7775d5a786fa2653294d5))
* ignore generated code ([68265a2](https://github.com/unraid/api/commit/68265a26efa588b60001310b9a11b398f04ae88f))
* implement mutations for updating many notifications at once ([6c90508](https://github.com/unraid/api/commit/6c90508c64e453849d06818cca2a3f6f7dfbf172))
* improve packing ([9ef02d5](https://github.com/unraid/api/commit/9ef02d53666b70d41fdd186364808deac715e1ff))
* initial patcher implementation using the diff tool ([c87acbb](https://github.com/unraid/api/commit/c87acbb146c2e4e30997c964cd8be325dee68cea))
* initial setup of permissions on keys ([#1068](https://github.com/unraid/api/issues/1068)) ([cf0fa85](https://github.com/unraid/api/commit/cf0fa850954ea2f018e338a132149f872b966df4))
* initial version of modification service ([b80469d](https://github.com/unraid/api/commit/b80469d38e519a7ba0e6eae636cda2a821e2d465))
* inject after form ([a4b276f](https://github.com/unraid/api/commit/a4b276f7874580bbf9827025730777715c9983da))
* install nghttp3 ([7e6cf85](https://github.com/unraid/api/commit/7e6cf858b270e615ec3eeddd394d0c2e6d810e21))
* install node ([4b85338](https://github.com/unraid/api/commit/4b853389d4ee7d0fb8e539d948dac21e748f642a))
* integrate cross-domain authentication to api ([7749783](https://github.com/unraid/api/commit/77497830c13d8e3ce1c348a8c79d8835ad5e3eb2))
* kill timeout extended ([22d4026](https://github.com/unraid/api/commit/22d40264a02672a818053b5280d63a03ff7336b9))
* linting continues on error ([a3499d6](https://github.com/unraid/api/commit/a3499d6feee56319657c37bb77277d5c637ee0b5))
* log size and only tar files ([731f2f8](https://github.com/unraid/api/commit/731f2f8e77a77b544a7f526c78aabfacca71eee4))
* logrotate test ([4504c39](https://github.com/unraid/api/commit/4504c39a2bbcf51385578b69a9fdc7b81a950e98))
* lots of progress on colors ([dc8b2ee](https://github.com/unraid/api/commit/dc8b2ee01b454d307e779d495dbcf11227760480))
* make notification id logic ([d5e0b3a](https://github.com/unraid/api/commit/d5e0b3a81ef3406b40e3376b5bca2fd101aa9c11))
* manually install libvirt in build process to ensure it is included in the final build ([e695481](https://github.com/unraid/api/commit/e695481363f0d5d7add9d0e0d50d1e113b3024f6))
* massive rc.unraid-api updates to facilitate installing and linking ([ded03d8](https://github.com/unraid/api/commit/ded03d86b25b51af98de2b7e7397a641dd0c082a))
* more cleanup ([9f6aeec](https://github.com/unraid/api/commit/9f6aeecfd90d069a0b2a642f99ef9622f4e0526d))
* more pm2 fixes ([8257bdf](https://github.com/unraid/api/commit/8257bdff3624211ee645349abdec303bf271538e))
* more process improvements ([9491be1](https://github.com/unraid/api/commit/9491be1038ee2e0e24be111bd8e8c78ec2890124))
* mount git folder to builder ([91350ea](https://github.com/unraid/api/commit/91350ea8535511c964d5869bd74a37830fc1bc40))
* move fixtures into __test__ folder ([22a901d](https://github.com/unraid/api/commit/22a901de9b0c274d3f75ed4b4618cd6cd90324ba))
* move ssoenabled to a boolean flag rather than ids ([404a02b](https://github.com/unraid/api/commit/404a02b26bae6554d15e317f613ebc727c8f702f))
* move to singular build and test step ([c9c8e86](https://github.com/unraid/api/commit/c9c8e8653321eaa0292a16e970d6dd4e79a3928f))
* move variable declarations to theme.ts ([3c82ee1](https://github.com/unraid/api/commit/3c82ee1e9acc197c9768a624cdef8c2e23c56d00))
* myservers_fb keepalive location ([e07e7f3](https://github.com/unraid/api/commit/e07e7f335c8ea4a73966ada90c26b7c82dbb025e))
* name package with PR number ([a642bf1](https://github.com/unraid/api/commit/a642bf15fd813dca522808765994414e4ed5a56c))
* nghttp3 sha256 missing ([589cc9b](https://github.com/unraid/api/commit/589cc9b4624d9d0e00ec3b86873d8ecb6a861427))
* nodejs issues with version 2 ([9c6e52c](https://github.com/unraid/api/commit/9c6e52c2fa46e7504bc3fa500770373d8c1d1690))
* **NotificationService:** endpoint to manually recalculate notification overview ([18e150f](https://github.com/unraid/api/commit/18e150f908b937cccd13171830fc418c3600cdbe))
* **NotificationsService:** use existing notifier script to create notifications when possible ([2f1711f](https://github.com/unraid/api/commit/2f1711f06a2fa0a679aeae12176cb2dd763494a4))
* nuxt config simplification and formatting ([02ffde2](https://github.com/unraid/api/commit/02ffde24d19594949faa97f9d070383b498fdcbe))
* only run mainline build ([b6ee6f9](https://github.com/unraid/api/commit/b6ee6f9c9f7740e91856754caecb6630bc62f37b))
* only write config when a specific config update action occurs ([ec29778](https://github.com/unraid/api/commit/ec29778e37a50f43eb164991bcf2a6ff9c266033))
* or button on sign in page ([1433e93](https://github.com/unraid/api/commit/1433e938d7ac01af326e2875c582a6aa6d622615))
* pack everything in API ([178a6f6](https://github.com/unraid/api/commit/178a6f6b0d7cf2fc4b2ad4cfbd9a928f880c222c))
* package scripts ([123aa77](https://github.com/unraid/api/commit/123aa77fe6f21e64809083d8b872ef2152be1ad1))
* pass env into builder ([e75ac99](https://github.com/unraid/api/commit/e75ac99d8e19e7ea66671c211bd4cf85ec3b81b0))
* plg builder improvements to be more explicit ([78c2f03](https://github.com/unraid/api/commit/78c2f035da0f0c6aaaedbee11c8c4f2a8cd42d0f))
* **plugin:** rm Date & Time format settings from Notification Settings ([e2148f3](https://github.com/unraid/api/commit/e2148f3c2eaf77ad707eddb7989cc20ec8df70ab))
* pm2 fixes ([5b322b4](https://github.com/unraid/api/commit/5b322b4faed6e8f0bb0742832cb94c8027d0e12b))
* pm2 fully working ([ecb642b](https://github.com/unraid/api/commit/ecb642b6a88bd66c3ccd1f4e1dcd2c92d7ff4b35))
* pm2 initial setup ([3cee381](https://github.com/unraid/api/commit/3cee381c442032d24c9c33dfa6d8a43581061fca))
* PR builds ([0025852](https://github.com/unraid/api/commit/00258524fa3f7ed745abfe471dfdb780b6d2b365))
* process env fixed and copy gql files ([8b90620](https://github.com/unraid/api/commit/8b90620d28378caabc7bbc1745ca43a1ddf8bd87))
* properly read log level from environment ([b5151e9](https://github.com/unraid/api/commit/b5151e9ba76a6814e24e8da34e8a3c1bf1cc2144))
* properly set outputs ([aa6904e](https://github.com/unraid/api/commit/aa6904e0a455a75a8187259d491b04366ad50fb0))
* rem converter ([d2489df](https://github.com/unraid/api/commit/d2489df6eaaa267a2f51b896c6c14ac9c5b00f85))
* remove apiKey from server ([b110a11](https://github.com/unraid/api/commit/b110a118fb153c0af09a74755deb468b3760ba27))
* remove console log disabler ([0cf24d2](https://github.com/unraid/api/commit/0cf24d2a212e1f44e0b379221c93aa436c3b7179))
* remove many unneded simple libraries ([483e6dc](https://github.com/unraid/api/commit/483e6dc28d70b933dc956b4ffd6da4ade8ab7eb9))
* remove more unused calls ([5d2923f](https://github.com/unraid/api/commit/5d2923f8ee5bfd574420fca85e2c4aefbe7b33d6))
* remove nghttp3 and only bundle nodejs ([8d8df15](https://github.com/unraid/api/commit/8d8df1592e5af127a992d5634ee9d344055cdf2c))
* remove sso if disabled on Unraid-API start ([3bc407c](https://github.com/unraid/api/commit/3bc407c54e8e7aeadebd9ac223d71f21ef97fca1))
* remove sso user command ([bbd809b](https://github.com/unraid/api/commit/bbd809b83826e81eef38a06e66f3393e4f83e81e))
* remove sso user options ([e34041f](https://github.com/unraid/api/commit/e34041f86ef7ab6cf5e2fdf7efb86458d190edc1))
* remove unused config sections ([f0b9c4f](https://github.com/unraid/api/commit/f0b9c4f44ab0ee8f75bf96fde2413988ef4f6a8c))
* remove unused fields ([d2d0f7c](https://github.com/unraid/api/commit/d2d0f7cd9acb53ea2372245d7ef669c7ca24ee8a))
* remove unused vars ([0507713](https://github.com/unraid/api/commit/0507713972e344ad47bd077554d5888269669e9c))
* remove wtfnode ([cbdcc47](https://github.com/unraid/api/commit/cbdcc476b617539611478cf6f29bbb57d0be83b3))
* rename api key resource back to api_key ([ee9666b](https://github.com/unraid/api/commit/ee9666b317d7feb5c15d53e2a6b902c7771c8c7a))
* rename modification file ([70a93f2](https://github.com/unraid/api/commit/70a93f2cc63e0e62242be6fe1a717515a6fbec85))
* responsive notifications ([d427054](https://github.com/unraid/api/commit/d427054443176563faa3e44249219c1d938e4b07))
* restart the API when an SSO user is added ([a6b0c90](https://github.com/unraid/api/commit/a6b0c906a423df048401750943f02dfdc9bc2619))
* restoring sso error ([234bf7d](https://github.com/unraid/api/commit/234bf7dfa4b0be88b6cc13996d8f29ec819da26e))
* revert local api key value ([ff40e7a](https://github.com/unraid/api/commit/ff40e7ae392052d3d9e1b084c5f4851e8ebd529e))
* right workin directory ([0d99ab0](https://github.com/unraid/api/commit/0d99ab0d74356e4ea309cb86dfc710ed93ab70e7))
* rollback if patch exists before applying ([c2f4e8d](https://github.com/unraid/api/commit/c2f4e8d4e5c758601bd20ba491fd077b434ba45e))
* secondary changes ([d75331a](https://github.com/unraid/api/commit/d75331a67e3566875ce8642fce80195e79932a4c))
* separate install process ([b90a516](https://github.com/unraid/api/commit/b90a51600c3f70615b117f157d41585e55ef49de))
* server identifier changes ([b9686e9](https://github.com/unraid/api/commit/b9686e9c67f2a48df419848e18c5451123813185))
* service tests for modifier service ([08c1502](https://github.com/unraid/api/commit/08c150259f2b4630d973803f4edff69c8bf0ec3a))
* session issues ([5981693](https://github.com/unraid/api/commit/5981693abd605337f9174ba4c85fd1bfc243edeb))
* set background color on webcomponents ([b66e684](https://github.com/unraid/api/commit/b66e6847c895f216a5dec42410186b81a31af1a9))
* shared call to createPatch ([eb3e263](https://github.com/unraid/api/commit/eb3e263fb32a748bfa06ec6d119ee51d242707cf))
* sidebar notification count ([694f01b](https://github.com/unraid/api/commit/694f01b6c4ab83c4131ae42bc11002d0300497c5))
* simplify getting version ([8fb8cb3](https://github.com/unraid/api/commit/8fb8cb304ee1b0f007b81a679dc3eadf098f6b4b))
* sso button token exchange ([f6f2390](https://github.com/unraid/api/commit/f6f2390b0169ceaf90ab88edfab3f2809bfe86b5))
* sso testing page and form disable on submit ([ffc6d8a](https://github.com/unraid/api/commit/ffc6d8a286d7c6ba751894464000f9870784507c))
* start command path ([a7aece5](https://github.com/unraid/api/commit/a7aece5570d3fdb073bda1dc7a89b7ea6e7eedf6))
* state using crypto ([afce130](https://github.com/unraid/api/commit/afce13099f5018d0c39765bfdd181adc8383a105))
* style improvements ([b0f395e](https://github.com/unraid/api/commit/b0f395ef76f11047eaa13091df277df0459e9d8f))
* substantial docs updates ([928bd03](https://github.com/unraid/api/commit/928bd03a4853a28a6b563ed82f95681a7f712b3a))
* swap to action ([ef7281b](https://github.com/unraid/api/commit/ef7281b2a863422593de5e948fcfad1a6df489f2))
* swap to async exit hook ([4302f31](https://github.com/unraid/api/commit/4302f316820a109c76408092994727b2dc030a15))
* switch to nest-commander ([1ab2ab5](https://github.com/unraid/api/commit/1ab2ab5b58a1f49cd6b05aaa84bfeced49d68e8e))
* track node version in slackware ([42b010e](https://github.com/unraid/api/commit/42b010e4a141f2a338d65f4f727bf1d15521a5c6))
* try catch restart ([89abee6](https://github.com/unraid/api/commit/89abee680bdbdaa9946ddb991f0e6b5ada9ccdf7))
* **ui:** webgui-compatible web component library ([#1075](https://github.com/unraid/api/issues/1075)) ([1c7b2e0](https://github.com/unraid/api/commit/1c7b2e091b0975438860a8e1fc3db5fd8d3fcf93))
* unnecessary comment ([0c52256](https://github.com/unraid/api/commit/0c5225612875b96319b28ef447db69ecab15cfda))
* unraid single sign on with account app ([5183104](https://github.com/unraid/api/commit/5183104b322a328eea3e4b2f6d86fd9d4b1c76e3))
* unraid ui component library ([#976](https://github.com/unraid/api/issues/976)) ([03e2fee](https://github.com/unraid/api/commit/03e2feebc73d620b21e54912e0bbddc1826880e1))
* update based on review feedback ([4383971](https://github.com/unraid/api/commit/43839711e3365e31120e156abac3746c55e8e694))
* Update plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php ([42c0d58](https://github.com/unraid/api/commit/42c0d58da4d0570b7d865a8774964c18120ed585))
* upgrade dependencies ([0a0cac3](https://github.com/unraid/api/commit/0a0cac3da74c2fe20f7100a9ad5d1caafa74b157))
* upload files directly to cloudflare ([1982fc2](https://github.com/unraid/api/commit/1982fc238fefa1c67323bdc11ec1fb9c9f43c387))
* use execa for start and stop ([46ab014](https://github.com/unraid/api/commit/46ab0144d41b425015487c251c1884744223ba29))
* use plugin file for install and uninstall ([c9ac3a5](https://github.com/unraid/api/commit/c9ac3a5a0a3103fbd9c33a5d909fa475614a704a))
* use state passing to validate requests ([4480c14](https://github.com/unraid/api/commit/4480c14c932fd8b42ba44989abdbecb49252e6f3))
* use text-secondary-foreground instead of gray ([463a1f7](https://github.com/unraid/api/commit/463a1f7b611599a19a23d3c75156c0a16da83312))
* use zod to parse config ([19cf1be](https://github.com/unraid/api/commit/19cf1be079f2ccb9e0cfa10f2fb97a18f15c5729))
* validate entries correctly ([b101a69](https://github.com/unraid/api/commit/b101a695e18d71ddd170462b3d49289352166489))
* validate token format in both PHP and CLI ([6ef05a3](https://github.com/unraid/api/commit/6ef05a3d7770f799e7d587c2cef8d29f6058bee1))
* viewport watch refactor ([9aefa38](https://github.com/unraid/api/commit/9aefa382ec64f08b1da8a3748ce16f637d562c8c))
* vite ([c78ba4a](https://github.com/unraid/api/commit/c78ba4a774d053d4a9dca938020e4393c5a1fc75))
* vite dev mode ([7646c6b](https://github.com/unraid/api/commit/7646c6b6c437a2b523245a29d829ead44fb57d28))
* warning on missing fields ([0ef9aec](https://github.com/unraid/api/commit/0ef9aecccdde879e3be44d0b2a0fa4d8befc53b5))
* **web:** activation modal steps, updated copy ([#1079](https://github.com/unraid/api/issues/1079)) ([8af9d8c](https://github.com/unraid/api/commit/8af9d8c58895010e3ddc03cc5fa075ac1e264f50))
* **web:** add an 'all' option to notification filter ([7c2a72e](https://github.com/unraid/api/commit/7c2a72e0c9537827c3c96df7b6378c03e2cc2852))
* **web:** add confirmation before archiving or deleting all notifications ([d16f08c](https://github.com/unraid/api/commit/d16f08c266953ddb84223f90f1275d19c9d3c380))
* **web:** add count labels to notification tabs ([4caea3d](https://github.com/unraid/api/commit/4caea3dfc2c7067062f3ce8d863f9385ad030dbd))
* **web:** add delete all notifications button to archive view in notifications sidebar ([3bda9d6](https://github.com/unraid/api/commit/3bda9d6a4ca01cc5580012b0133e72929d6dab40))
* **web:** add empty state to notifications list ([5675fe1](https://github.com/unraid/api/commit/5675fe14d9d36ab638cb5e1b907f24bcf71cb7f1))
* **web:** add gql archival mutations to notifications sidebar & item ([5f93be9](https://github.com/unraid/api/commit/5f93be9f55f3262502951a726f8fc015d73abc92))
* **web:** add link to settings in notification sidebar ([f1a4d87](https://github.com/unraid/api/commit/f1a4d873481c212ffde1af7e38327a53a7e41d43))
* **web:** add loading and error states to notification sidebar ([2e9183a](https://github.com/unraid/api/commit/2e9183a479e0ec5f7cfc34bb81ccfd05e4bd2b29))
* **web:** clear notifications indicator after opening sidebar ([68958d1](https://github.com/unraid/api/commit/68958d17b78220c77c3cda4f0f4068b3ce623688))
* **web:** delete notifications from archive view ([c8fc15d](https://github.com/unraid/api/commit/c8fc15d20bae527193ed289aef622a953a0d00bc))
* **web:** display error when a notification mutation fails ([838ed86](https://github.com/unraid/api/commit/838ed86ffa47207ca2282a9ddabe245da713ba23))
* **web:** enhance notifications indicator in UPC ([#950](https://github.com/unraid/api/issues/950)) ([6376848](https://github.com/unraid/api/commit/63768486e4ec64ab32666a26adf96f4db4a53e81))
* **web:** implement notification filtering ([fa5156b](https://github.com/unraid/api/commit/fa5156bbc1f6bcc6c2e71b64b1e063120a868410))
* **web:** make empty notification message clearer ([abab00d](https://github.com/unraid/api/commit/abab00ddccb7e485f2603b554704f531be79dd45))
* **web:** make notifications list scrollable inside the sheet & tabs ([4c5d97b](https://github.com/unraid/api/commit/4c5d97b380de5574226e653c372c45ce61ea3ebb))
* **web:** move notification indicator icons to top-right of bell icon ([2fe4303](https://github.com/unraid/api/commit/2fe4303387023d303d7e50fc4d9a41f1eafdcc45))
* **web:** open official release notes via header os version ([54a893f](https://github.com/unraid/api/commit/54a893f396b29251b982ff1f26d376d24b962b93))
* **web:** pull date format from display/date and time settings ([b058067](https://github.com/unraid/api/commit/b058067b628ca7866a9ba0a6c4c5e4d5505d98cb))
* **web:** reconcile pagination with notifications apollo cache ([e38bc2c](https://github.com/unraid/api/commit/e38bc2c1218019cd1632123709620808c7543d11))
* **web:** remove notification indicator pulse ([f320a77](https://github.com/unraid/api/commit/f320a77330c8cc7b92e170b0099d6c7f93b11c0e))
* **web:** rm api-key validation from connect sign in ([#986](https://github.com/unraid/api/issues/986)) ([7b105d1](https://github.com/unraid/api/commit/7b105d18678e88a064f0643d6e857704789e0ee8))
* **web:** rm old notification bell upon plugin installation ([#979](https://github.com/unraid/api/issues/979)) ([e09c07c](https://github.com/unraid/api/commit/e09c07c5070d59ac032baeff1ed253b5c00f4163))
* **web:** support markdown in notification messages ([90cbef7](https://github.com/unraid/api/commit/90cbef774962e9d8ede47df7a4c1ca06f2a6651b))
* **web:** update cache & view when archiving notifications ([08ab4d1](https://github.com/unraid/api/commit/08ab4d1a96c729c47feb868c10f398cad6dee5ba))
* **web:** use Markdown helper class to interact with markdown ([f9c2d35](https://github.com/unraid/api/commit/f9c2d353133b01e74fe1bfbc420df3980d944012))
* **web:** wip query api for notifications ([dec48b2](https://github.com/unraid/api/commit/dec48b2b0081362c5d0435eaabff1fb657d5f431))
* WIP create teleport composable ([20e795e](https://github.com/unraid/api/commit/20e795ed6921337ae7875b483f2ab94860b74797))
* wip Notification UI starter ([2f9e2ee](https://github.com/unraid/api/commit/2f9e2eef2db61221be66683caa4e75368aa475e0))
* WIP notifications w/ shadcn ([5a90b32](https://github.com/unraid/api/commit/5a90b3285ad8524cea58cbaca8293675b3dc257b))
* WIP sidebar filter select ([0c214fa](https://github.com/unraid/api/commit/0c214faaf69a69fc4da10a4a71b9c7cf7bd128c2))
* workflow changes ([c97bfb8](https://github.com/unraid/api/commit/c97bfb8794d779ef253236fbdb7bb9909f4dfbca))
* working ([29d7bd7](https://github.com/unraid/api/commit/29d7bd729bdfed79a3ce9c50014b3f1f32f9ac4e))
* wrap Notifications in a GraphQL Node & implement notification overviews ([bf89178](https://github.com/unraid/api/commit/bf89178cb7e359e73da8c8f27253734be982dcc8))
* zod config no longer any ([c32c5f5](https://github.com/unraid/api/commit/c32c5f57127b9469bde8806d78dc364562e73d9f))
### Bug Fixes
* 12 hour timestamp logic corrected ([03be43b](https://github.com/unraid/api/commit/03be43b4579f1dcf6a666a144f75b3063576748a))
* actually install dependencies ([0895420](https://github.com/unraid/api/commit/089542061294e354b0e63a9f41001b77c0d62fed))
* add another missing symlink ([4e7f3ff](https://github.com/unraid/api/commit/4e7f3ff4d9aa0e4af417a50e2b30537dda3c759c))
* add ecosystem config ([7dd5531](https://github.com/unraid/api/commit/7dd553174e1c3aaaf71380abfe57348f30815bde))
* add error check to nodejs ([c8e0fe8](https://github.com/unraid/api/commit/c8e0fe87a34d7f066b7d0900dda205a40616bfb6))
* add max var ([ed681e1](https://github.com/unraid/api/commit/ed681e1d27fb7fa13bc8cbb5238da06e453a7c3b))
* add return to resolver and update jsdoc for getNotifications ([a5e7d29](https://github.com/unraid/api/commit/a5e7d2956074376ef8e708b2bb7416cd2af3fe12))
* allow concurrent testing with a shared patcher instance ([623846e](https://github.com/unraid/api/commit/623846ef46eb24a32c62516de58e8bc5d0219833))
* always mangle ([e3a1eec](https://github.com/unraid/api/commit/e3a1eec5b62d010d05bb0908ba15fd8cb4f9d717))
* **api:** append time to formatted date when a custom date format is selected ([0ac8ed9](https://github.com/unraid/api/commit/0ac8ed9d9e7e239e471eedf466832aed0270d123))
* **api:** delay pm2 start until server has booted ([bd3188e](https://github.com/unraid/api/commit/bd3188efea4d3656994ffae32bd53f821c96358d))
* **api:** exclude duplicates from legacy script in archive retrieval ([8644e13](https://github.com/unraid/api/commit/8644e130979ed8740c5a8da0b3984266e2b3684c))
* **api:** improve defaults in PM2 service ([#1116](https://github.com/unraid/api/issues/1116)) ([57526de](https://github.com/unraid/api/commit/57526dede69e3a6547d05183e43c5b36dd1cae89))
* **api:** load dynamix config in the same way as the webgui ([2c4fd24](https://github.com/unraid/api/commit/2c4fd2419ce05a10c8543a7f679852b54df3d10f)), closes [/github.com/unraid/webgui/blob/95c6913c62e64314b985e08222feb3543113b2ec/emhttp/plugins/dynamix/include/Wrappers.php#L42](https://github.com/unraid//github.com/unraid/webgui/blob/95c6913c62e64314b985e08222feb3543113b2ec/emhttp/plugins/dynamix/include/Wrappers.php/issues/L42)
* **api:** make cookie recognition during websocket connection more ([353e012](https://github.com/unraid/api/commit/353e012db8ab5280863f32392c520b4a330c13cc))
* **api:** pm2 start script & limit auto restarts ([#1040](https://github.com/unraid/api/issues/1040)) ([ebcd347](https://github.com/unraid/api/commit/ebcd3479e735724626ffc6907c338d5080898bee))
* **api:** retry mothership connection up to 3x before logout ([#1069](https://github.com/unraid/api/issues/1069)) ([c27bb1b](https://github.com/unraid/api/commit/c27bb1be4c7a9ab201585586f3bc5e4afa1c7791))
* **api:** sanitize incoming user session id's ([f5e3424](https://github.com/unraid/api/commit/f5e3424b79702e8f959b5519e83370a9e1d2033b))
* **api:** slow init of unraid-api cli ([#1022](https://github.com/unraid/api/issues/1022)) ([5dbbae7](https://github.com/unraid/api/commit/5dbbae796792a62234497d056eac019aa084b21c))
* **api:** strip server id prefixes from graphql request variables ([326d054](https://github.com/unraid/api/commit/326d0540f0865735f220e0fc7c5822913a7865ea))
* **api:** update deploy-dev script to dist instead of src ([55cce09](https://github.com/unraid/api/commit/55cce09e65521762a6fe388d5b9b88ace1337c26))
* **api:** validate cookie session data ([491f680](https://github.com/unraid/api/commit/491f680607ce7244d9e47a457e44cde711fbe00c))
* apollo client lint issues ([a6d6dcc](https://github.com/unraid/api/commit/a6d6dcc2acc2b529c6f6821ce57865e521b84075))
* app running ([5f71670](https://github.com/unraid/api/commit/5f716701715595f93fd0bc63b92ecf02335daa41))
* apply and rollback error handling ([e22191b](https://github.com/unraid/api/commit/e22191bc77bc09f5c6c4ad57e5073829cf966ba4))
* attempt to restore upgradepkg if install failed ([19c2a79](https://github.com/unraid/api/commit/19c2a79ce6c31c989f3d7f70cf7d8e2c219517b2))
* authorization type error ([#987](https://github.com/unraid/api/issues/987)) ([7a4799e](https://github.com/unraid/api/commit/7a4799e9cd4caef6acfc3661d205a377fcf499ab))
* back to default configs ([b5711c9](https://github.com/unraid/api/commit/b5711c91284072991bcf409ac6126cd4b46afc7c))
* backup restore formatting ([15210f6](https://github.com/unraid/api/commit/15210f64b0938ec884a3ef4379d245c661eab9a3))
* basic test fixed ([2f38035](https://github.com/unraid/api/commit/2f38035520ca0fe796c981d08b9136d89ffc5888))
* better js file handling ([ddf160e](https://github.com/unraid/api/commit/ddf160e878a352842e813154b607945ccc7b4081))
* better loader functionality and error handling ([8a57d2d](https://github.com/unraid/api/commit/8a57d2dccbcb9c2effc5df5d8c69ad02713de24a))
* better logging when error ([6e4e3f8](https://github.com/unraid/api/commit/6e4e3f85abf64f8d799e33c33823810e71ef13e2))
* build issues based on removed code ([59c1d5a](https://github.com/unraid/api/commit/59c1d5a3f991c4e3625a8853ade17d2ca8936474))
* builder cache ([56771f6](https://github.com/unraid/api/commit/56771f6ee210297406d6bddff04de816ba0bb2d5))
* capitalize name ([31166b3](https://github.com/unraid/api/commit/31166b3483dc01847ad555618c43f8248411bdfa))
* changelog parser ([6fecec8](https://github.com/unraid/api/commit/6fecec8d4af3a4fccf2886791188711e1d2db77b))
* check width before changing viewport ([f07381b](https://github.com/unraid/api/commit/f07381b243501ecc6d54063881faad77a99a7655))
* cleaner logs for starting API ([79f26ef](https://github.com/unraid/api/commit/79f26ef251cb42e7f2106d00c6c05e2bf17b8227))
* cleanup commands ([052aea0](https://github.com/unraid/api/commit/052aea06a0d30963532f29f9961fce0ffc7fa3e8))
* clearer error messaging ([e373849](https://github.com/unraid/api/commit/e37384966c5b9079bb507052dcaba56232c1c42a))
* code review feedback ([c66079e](https://github.com/unraid/api/commit/c66079e9a8e0ef47e5054118d0581bec708ac604))
* completion script registration ([05c8c9b](https://github.com/unraid/api/commit/05c8c9bf078ece2061ad8ae32497f52b8c9b94dc))
* connect key role ([2dcfc1c](https://github.com/unraid/api/commit/2dcfc1c19a1d085df84f0b1b50c096e3220205dd))
* connect plugin location ([7867a93](https://github.com/unraid/api/commit/7867a932eb0bda43d3fb3613bdba227717510e4a))
* convert updateId function to iterative instead of recursive ([65c20d2](https://github.com/unraid/api/commit/65c20d210987bc4dbb19f3e200fffa655b5fe2f4))
* **CookieService:** potential race condition in unit tests ([1f2a380](https://github.com/unraid/api/commit/1f2a380b775adf44fdb3c85278fe0151584284f1))
* **cors:** excessive instantiation of CookieService to improve memory overhead ([28c553d](https://github.com/unraid/api/commit/28c553d4c8d7c744e2b5554b1254cdd2bfda5ff5))
* create api key for connect on startup ([58329bc](https://github.com/unraid/api/commit/58329bc29521ebc26b27ee20013ac3926c5088c2))
* create api key permissions ([cefb644](https://github.com/unraid/api/commit/cefb644bd7fa513f553ca0ca4c49f0fb42a74112))
* create connect key ([6b1ab7b](https://github.com/unraid/api/commit/6b1ab7b74ae1d2938fa9105180a5f66e9604fd41))
* cwd on ecosystem.config.json ([dfd0da4](https://github.com/unraid/api/commit/dfd0da4ca23078f6de2e54d5e5bd6cba06334abc))
* dark theme as array ([1021d0d](https://github.com/unraid/api/commit/1021d0da0d7a919dedec70656bb52775575aa9e7))
* default overwrite false test ([cf59107](https://github.com/unraid/api/commit/cf59107e568d91be684176335db5300bee9be865))
* delete .original files ([a9eb21a](https://github.com/unraid/api/commit/a9eb21aac0f373990aaa3f7a99731612540533cf))
* delete boot script and update nvmrc ([ecd6b44](https://github.com/unraid/api/commit/ecd6b443c7dee8c5c5dee959f9ff3ace192204c7))
* delete unused line ([de4882e](https://github.com/unraid/api/commit/de4882ea17f54e788049cc5bb96b99b16822b6b4))
* delete upgradepkg ([74f0177](https://github.com/unraid/api/commit/74f0177ba0fd57722caa3ec14318d35167d3c6f7))
* deprecated version warning ([89d0bd2](https://github.com/unraid/api/commit/89d0bd2e6da35fb1e8d95627d38edb54f82e0c6b))
* **deps:** update apollo graphql packages ([7b1ee99](https://github.com/unraid/api/commit/7b1ee9940cca46e563bb79c7056996315f9decc5))
* **deps:** update dependency @apollo/client to v3.12.6 ([22ce615](https://github.com/unraid/api/commit/22ce61574f862eac4cdf8c00141bfbf1ac948055))
* **deps:** update dependency @apollo/client to v3.12.6 ([bb7800a](https://github.com/unraid/api/commit/bb7800a8c088705fd8310671a9896cbe9b0184e5))
* **deps:** update dependency @apollo/client to v3.12.9 ([6607cf2](https://github.com/unraid/api/commit/6607cf20c10a091d466c6a8031eebc17feb3e3fc))
* **deps:** update dependency @floating-ui/dom to v1.6.13 ([08798d2](https://github.com/unraid/api/commit/08798d2f77683412807d684d7a8e63f1aadb0c34))
* **deps:** update dependency @floating-ui/dom to v1.6.13 ([4d4c218](https://github.com/unraid/api/commit/4d4c218ac78e82a18679ec7b4939523db032b99b))
* **deps:** update dependency @floating-ui/vue to v1.1.6 ([b4b7d89](https://github.com/unraid/api/commit/b4b7d898b62f746180b7f5730b5d9b5033dcecc2))
* **deps:** update dependency @floating-ui/vue to v1.1.6 ([4c07d38](https://github.com/unraid/api/commit/4c07d389523f277950b8d2d359102f889587e5ce))
* **deps:** update dependency @graphql-tools/load-files to v7.0.1 ([4e5c724](https://github.com/unraid/api/commit/4e5c7242e43cc356f1c69adcfcd25b57896af476))
* **deps:** update dependency @nestjs/schedule to v4.1.2 ([faf0de5](https://github.com/unraid/api/commit/faf0de5a19256efb83dc45a484e3cba65596ccd7))
* **deps:** update dependency chokidar to v4.0.3 ([d63a93c](https://github.com/unraid/api/commit/d63a93c55004d17b6d17634c55ffbc5670ebbec7))
* **deps:** update dependency dotenv to v16.4.7 ([c66a650](https://github.com/unraid/api/commit/c66a6502b027853046d126a14ddee870ffabd10c))
* **deps:** update dependency execa to v9.5.2 ([d487c90](https://github.com/unraid/api/commit/d487c90ccc20162c76f0cdf49a736c1fee4271bd))
* **deps:** update dependency express to v4.21.2 ([a070306](https://github.com/unraid/api/commit/a07030684c8777e47eb4a51be0ea680b7f217e74))
* **deps:** update dependency focus-trap to v7.6.4 ([41ff232](https://github.com/unraid/api/commit/41ff232a3232dda66e5cdc2d4808a820a90a5d34))
* **deps:** update dependency focus-trap to v7.6.4 ([f0e3038](https://github.com/unraid/api/commit/f0e3038ee7426aafb6cef01b85b47893c2238302))
* **deps:** update dependency got to v14.4.5 ([975a47c](https://github.com/unraid/api/commit/975a47c7d47841c49443f46264feb54abf53698c))
* **deps:** update dependency graphql-ws to v5.16.2 ([a189a03](https://github.com/unraid/api/commit/a189a0308a734e66750fe5059f7c59d8c9532bd8))
* **deps:** update dependency graphql-ws to v5.16.2 ([25d8f08](https://github.com/unraid/api/commit/25d8f085b67c2e53876d837c739214dc874116b8))
* **deps:** update dependency ini to v4.1.3 ([4c88cbe](https://github.com/unraid/api/commit/4c88cbee4b2d5f6717241dadac23bfe90ce15193))
* **deps:** update dependency node-window-polyfill to v1.0.4 ([8bfa88f](https://github.com/unraid/api/commit/8bfa88f4bc932eb82dd9b33a494811ea15764758))
* **deps:** update dependency openid-client to v6.1.7 ([0f50517](https://github.com/unraid/api/commit/0f50517a8544e1eb9b08ad1b3f05f798491b7f23))
* **deps:** update dependency p-retry to v6.2.1 ([c6f3241](https://github.com/unraid/api/commit/c6f324155019e066701723a57b642c6e3ba8332d))
* **deps:** update dependency pm2 to v5.4.3 ([a754090](https://github.com/unraid/api/commit/a75409026dd4e3d9ed120802012b67b179327448))
* **deps:** update dependency radix-vue to v1.9.12 ([0fd433f](https://github.com/unraid/api/commit/0fd433fe2a6b3f787624cb5a98efeae0f6c31cfd))
* **deps:** update dependency radix-vue to v1.9.13 ([249feff](https://github.com/unraid/api/commit/249feff5cfe0bbb60bfa8f943b76b9c16c6c161b))
* **deps:** update dependency uuid to v11.0.5 ([7e3398b](https://github.com/unraid/api/commit/7e3398b2efabf1a5407d6e20c165eb4923b3bab2))
* **deps:** update graphql-tools monorepo ([cd7e2fe](https://github.com/unraid/api/commit/cd7e2feea199276a1d431cf355e54e12e5960d9a))
* **deps:** update graphqlcodegenerator monorepo ([0446c59](https://github.com/unraid/api/commit/0446c5924a6a9dd15b875628ca0f1197cfe521c4))
* **deps:** update graphqlcodegenerator monorepo ([15c789d](https://github.com/unraid/api/commit/15c789dbb34b85bed55c2731fb8ae8260f5f311f))
* **deps:** update nest monorepo to v10.4.15 ([07b1ea9](https://github.com/unraid/api/commit/07b1ea9a10634a597909ae1d237cc3b1e7f959b7))
* **deps:** update nest-graphql monorepo to v12.2.2 ([91aabd9](https://github.com/unraid/api/commit/91aabd9ffbfb8c2ceb4110217dfc05de8859077d))
* detection script path bin instead of sbin ([7138fd2](https://github.com/unraid/api/commit/7138fd297abc1435f61e7e4c8f4a5a91662c64f0))
* dev mode ([fd64e01](https://github.com/unraid/api/commit/fd64e01e0c87db03fc2d4d0f32a0e8205fbe8b84))
* disable permissions bypass to avoid incorrect role assignment to api keys ([343489e](https://github.com/unraid/api/commit/343489e52c526757c1449a6f5074def50e73a380))
* dnserr on new line ([a3398a2](https://github.com/unraid/api/commit/a3398a29e15269be006e887fba6366c81b1d00f5))
* do not process.exit on restart or stop command ([933575f](https://github.com/unraid/api/commit/933575fc2badbb09b3a9d3c66724e37a9ee246f2))
* docker formatting and build mkdir issues ([f447739](https://github.com/unraid/api/commit/f447739585e8ed8449653802c1f0d4d11d6c65de))
* don't check code for execa ([508a5eb](https://github.com/unraid/api/commit/508a5eb49d9514dca9953317d9fa93314fe63e4c))
* don't LS in the release folder ([ab9d969](https://github.com/unraid/api/commit/ab9d9695394a300d6b0799e9b644dd5d0f969d72))
* dont remove login file without a backup presetn ([0370e4f](https://github.com/unraid/api/commit/0370e4f7ea3e3df0d2264264324d8e53ffc0c086))
* downgrade marked to fix changelog preview issue ([cfb3a45](https://github.com/unraid/api/commit/cfb3a45533d3c1bd31c44094f7ae2912e77a673e))
* edit settings padding issue ([adf349b](https://github.com/unraid/api/commit/adf349b76560b5f1fd4c320da35b3c6f660895fb))
* ensure directory exists before making connect key ([9e27ec9](https://github.com/unraid/api/commit/9e27ec98b68a49bdd6dc4b03de8c0cc3a1470a5e))
* env correct ([9929856](https://github.com/unraid/api/commit/99298566382313f1da9c374dbec3652c1b2812d3))
* env input ([c182c06](https://github.com/unraid/api/commit/c182c06d9443597521eaacd2052d905e0138cac4))
* EOF ([25ac1b5](https://github.com/unraid/api/commit/25ac1b5f0772d3ce63d47eb8a1dc640be125b68b))
* eslint config ([b28a605](https://github.com/unraid/api/commit/b28a605300cee1e9ce1f5db3a165b4d0d2080316))
* excess spacing in api-key.service ([1deb002](https://github.com/unraid/api/commit/1deb0023287a39d40e52e89c515a28e62352f62c))
* execa upgrade snapshots fixed ([d8244f7](https://github.com/unraid/api/commit/d8244f7aac02dd97c756a9784391cd661f3536ba))
* extra log line ([1183063](https://github.com/unraid/api/commit/1183063aa7063afd8222def18f5e1fd6077e8c88))
* extra spacing in config.ts ([f3ee7be](https://github.com/unraid/api/commit/f3ee7be80f2c60266fbb13597a70f0a389fb577f))
* file modification service fixes ([aa5b3f4](https://github.com/unraid/api/commit/aa5b3f4e47ed88df23af00dfcccb7b64786b6231))
* find by key, not ID ([3c3fa1e](https://github.com/unraid/api/commit/3c3fa1e27cfabbe6926c3da8870751397eed1def))
* floating-ui fixes ([1c3b43b](https://github.com/unraid/api/commit/1c3b43b4464662e1a1b21695e601cd7f7e4fd734))
* forced restarting on commands ([925866d](https://github.com/unraid/api/commit/925866d389e337fcb8c249ead929e1f65854465b))
* format authrequest mod as other files ([180a81d](https://github.com/unraid/api/commit/180a81dbae8e749eae237fc8cee2950c790eedf0))
* formatting issue ([42ca969](https://github.com/unraid/api/commit/42ca9691f7547a4340501863c1882efc0aee4c60))
* further resolve sso sub ids issues ([ef3d0ea](https://github.com/unraid/api/commit/ef3d0ead687d4a6071da290c0df29c12163303e1))
* handle special chars better ([d364bb1](https://github.com/unraid/api/commit/d364bb1fc4c042469ce4b0ca6001a807d0b002da))
* improve typing and format lookup ([c6097f8](https://github.com/unraid/api/commit/c6097f86e42fcc57209c1344029abe854198edca))
* initial feedback about report addressed ([5dee09c](https://github.com/unraid/api/commit/5dee09c77ad375de2eca59f650e5fea2070087b5))
* install as-integrations/fastify ([ff4546d](https://github.com/unraid/api/commit/ff4546d6692d2a4799f2dbeef0d5e5c6bac62561))
* install syntax error ([ec83480](https://github.com/unraid/api/commit/ec83480eb6aea09b98b9135516dc1fc8cdd6c692))
* integration of `unraid-ui` tailwind config in `web` ([#1074](https://github.com/unraid/api/issues/1074)) ([f3cd85b](https://github.com/unraid/api/commit/f3cd85bd3f02bdbe4c44136189d1c61935015844))
* invalid type ([e13794f](https://github.com/unraid/api/commit/e13794f8e56081335ebc16b00a2f8631f9639909))
* length ([83579f1](https://github.com/unraid/api/commit/83579f1fbd03ffe929d009c20d214b4de62835c6))
* lint ([0f218b8](https://github.com/unraid/api/commit/0f218b8b72e397734823efab8f2141973a3a80ce))
* lint ([82bca54](https://github.com/unraid/api/commit/82bca54c594265ddf23a298691bd7ef6d4b47f32))
* lint ([ceb443d](https://github.com/unraid/api/commit/ceb443da15d177a950c36af61b93a7126cf4ca85))
* lint ([da04e7c](https://github.com/unraid/api/commit/da04e7ce0873d7802a936952d91e9867f0868a6e))
* lint ([7d87f0e](https://github.com/unraid/api/commit/7d87f0eee23dfa0f391fd342d38ed9084f18d8d4))
* lint issues ([48e482b](https://github.com/unraid/api/commit/48e482b913d4f27f49ae669c7c19dc0714d3c0c7))
* linter error ([6dba28d](https://github.com/unraid/api/commit/6dba28dd1bbe0125f842271eeae9daf54826b063))
* load builder image to cache ([5497bc3](https://github.com/unraid/api/commit/5497bc3235bd3c8b427b3418a7be2e3ece4c4abc))
* load notifications from file system instead of redux state ([53a37cd](https://github.com/unraid/api/commit/53a37cd1d20bb2b738bdeda832a42196d662b8a4))
* load PM2 from node_modules ([5a07e8c](https://github.com/unraid/api/commit/5a07e8cae5ce9ced9980a9df950d053196edf65b))
* local variable assignment ([f7d9ccc](https://github.com/unraid/api/commit/f7d9ccc0820f0fe8efa55b1c2d75f79819c764c4))
* logging location ([572b922](https://github.com/unraid/api/commit/572b922f4d72759a5ed7d06ddfa4af3bfb655c6b))
* logrotate error ([8c64dd2](https://github.com/unraid/api/commit/8c64dd2f2c65aa83ce0e2d501357ee595c976e56))
* lowercase or ([386cbde](https://github.com/unraid/api/commit/386cbdef5c9158290e03c670efb992cf11d5af1b))
* make cli.js executable ([644db0e](https://github.com/unraid/api/commit/644db0ef3315b00361524ea0fe440083f35088a0))
* marked single input ([ceacbbe](https://github.com/unraid/api/commit/ceacbbe5d46466627df0fccc5ca8e7c56fa36a37))
* missing ip-regex module ([fde7202](https://github.com/unraid/api/commit/fde720264bf395c0047356c3084878c8878aadfa))
* missing server type ([f1b721b](https://github.com/unraid/api/commit/f1b721bd72b875d9ff8c0bca2cc1eee506ba7697))
* mock ensureDirSync ([7e012e6](https://github.com/unraid/api/commit/7e012e6a2eb96ccddf5a1f69d7580b4bdfe7a0a9))
* more color work ([c48f826](https://github.com/unraid/api/commit/c48f8268def64ef8828dea556360b375b8cb32c7))
* more filename fixes and PR var passing ([088dbed](https://github.com/unraid/api/commit/088dbed9b2fabfaf55b003fb1fa9c10c558f21d5))
* more generic test ([0f651db](https://github.com/unraid/api/commit/0f651dbf61a1822b492aa80030f0bc231bc6f606))
* more verbose logging for node install to find issues ([445af0c](https://github.com/unraid/api/commit/445af0c147ef641dac05ebeb2d44e63e8a4df799))
* mv paths() to top of NotificationsService to make it more intuitive ([7138568](https://github.com/unraid/api/commit/713856818dfaf7d7f5807eacc3b7d61561888082))
* no more node_dl_server ([77779a6](https://github.com/unraid/api/commit/77779a655f18e9d474ad8a7e61c8ef51090a49d8))
* no nodehost ([6787ec7](https://github.com/unraid/api/commit/6787ec7d65ecef27652ca48193fe64f2ea82ca4e))
* no vite-node in non-dev mode ([023f73f](https://github.com/unraid/api/commit/023f73f3992b42f60aa56d8dd51a5e698c140306))
* node install process improvements ([b8540dd](https://github.com/unraid/api/commit/b8540ddeb888678edd24db31a6747583761d5aa9))
* node_txz naming ([b7c24ca](https://github.com/unraid/api/commit/b7c24ca861e92bf01118a17bc7e2322063e6a800))
* **NotificationItem:** icon & text alignment in header ([98716f7](https://github.com/unraid/api/commit/98716f70a6a2c29610e1ed7cda1aedad5065134d))
* **NotificationService:** file watcher initialization ([b7e3f8e](https://github.com/unraid/api/commit/b7e3f8e42ae4bf488228503b4e5234b1e7a38180))
* **NotificationsService:** edge-case in deleteAllNotifications by adding fs-extra package ([fef763a](https://github.com/unraid/api/commit/fef763a3298b9bf4aae2e18db4722637ce9bd7e4))
* **NotificationsSidebar:** occupy full viewport on small screens ([1f81fb8](https://github.com/unraid/api/commit/1f81fb8b92bef102b0b7d2daf562c9b4e296473e))
* oauth2 api prefix ([ec00add](https://github.com/unraid/api/commit/ec00adde20d4d9eca28f6b18615073305f491a73))
* only instantiate service one time ([933dc81](https://github.com/unraid/api/commit/933dc81b6c50db5a33f586f7094e1ea524b9a9fa))
* only test if API was changed ([5871143](https://github.com/unraid/api/commit/5871143b809d6a6784e949e20212599a54afc71f))
* only test when API is changed ([ddea0e8](https://github.com/unraid/api/commit/ddea0e8e11816c58e5b50cb611e5796fbca3fecf))
* only toast unread notifications, not archived ones ([cc59be6](https://github.com/unraid/api/commit/cc59be6cb3efc71226ee50f9f04e37a2e4b50de6))
* padding and glob function issues ([1d3f2eb](https://github.com/unraid/api/commit/1d3f2eb8213115c3385ac2d29ee8f53560347ba8))
* pass env through to docker ([200be38](https://github.com/unraid/api/commit/200be384f9dec86a1e77f46a0e6a336e86ba7563))
* pass ssoSubIds only ([5adf13e](https://github.com/unraid/api/commit/5adf13ee070bdcd849339460b9888e51d224e765))
* pass token to password field ([499b023](https://github.com/unraid/api/commit/499b023d359ed5181450ee9e04cbbf4531a4a680))
* patch-utils unused ([047808d](https://github.com/unraid/api/commit/047808dce0cd9e9b4b273a9124dbd45ca9446208))
* paths now correct, better download logic ([16db2d9](https://github.com/unraid/api/commit/16db2d908dcb2c65508b367712c51bf9872a95e5))
* pkg_build ([d4bff0e](https://github.com/unraid/api/commit/d4bff0ee96e6e0974978465573e72e68d09fd829))
* plugin download route and add env node to cli script ([78bd982](https://github.com/unraid/api/commit/78bd9820200a0996d9b8c5f718a97c20ed4feab4))
* PR build missing files ([57f9b95](https://github.com/unraid/api/commit/57f9b95134be5c3dd8053f57f82e91a0e0622d3e))
* production env for web build ([b4107f6](https://github.com/unraid/api/commit/b4107f6c4d4db47d7331f4b3d30a4ace724a8f0e))
* proper directory in rc.unraid-api ([a3add5a](https://github.com/unraid/api/commit/a3add5ae165b09dd695a2ddabf6131ac8700825f))
* proper file replacements ([e0042f3](https://github.com/unraid/api/commit/e0042f353b47cfa72a485d6a58ad0b956ea6dbc2))
* properly log error with template string ([3781f1f](https://github.com/unraid/api/commit/3781f1f41c7f0eef604daee0402ed9a2bb27cd46))
* properly restart the API when installed ([765593a](https://github.com/unraid/api/commit/765593a3da1e3b8bee1ae6c8aa1d9d0f2498d41c))
* pull node version directly from nvmrc ([b2e6948](https://github.com/unraid/api/commit/b2e694881218c08765b26ada08ed6ab325177b1e))
* pull token from query not params ([2e827e7](https://github.com/unraid/api/commit/2e827e7cabe4a6a069d4e8779015e5896d8a1d1d))
* race condition when updating notification types ([f048f56](https://github.com/unraid/api/commit/f048f566627e91947cc98550412ca68d728c52c7))
* re-add type-check ([60e9d1d](https://github.com/unraid/api/commit/60e9d1d912c983cf04e3e6cf15e221c39938612a))
* recreate package-lock to fix issues ([ad5a537](https://github.com/unraid/api/commit/ad5a53793d25ac9f63bae6df6c2a30d8d2780c67))
* remove console log ([8e75b82](https://github.com/unraid/api/commit/8e75b8254bbda93ded786750226090b769bed5c4))
* remove console logs with vue plugin ([2b2e923](https://github.com/unraid/api/commit/2b2e9236ce55cfc3ca10f708ed08e09dcfd402d1))
* remove devDependencies from output package json ([294869b](https://github.com/unraid/api/commit/294869bbea7f8a1863f8aafae6b074330e057679))
* remove extra space ([a99ee03](https://github.com/unraid/api/commit/a99ee03fc37059b3a018db289c43fc419a634524))
* remove isNaN in favor of number.isNaN ([03e3a46](https://github.com/unraid/api/commit/03e3a46092db613281176b88cae284f6448027c6))
* remove line from or in button ([1a1bce7](https://github.com/unraid/api/commit/1a1bce7b64b1cf90505f811e11b585ff87476f72))
* remove memory key generation ([b84db13](https://github.com/unraid/api/commit/b84db1322104c7f26f7b6378f25a2757b3010c6d))
* remove uneeded env variable ([f688a35](https://github.com/unraid/api/commit/f688a350d3d0a1c47be5896e6fbf92eeb8433967))
* remove unused constructor ([e0e2a7b](https://github.com/unraid/api/commit/e0e2a7b41c5e599ed4cf3bf49c7faea3b71f0b70))
* remove unused date-fns ([fe94ef5](https://github.com/unraid/api/commit/fe94ef5ba88df56aad87089081dd5fe4518fa414))
* remove unused disableProductionConsoleLogs call ([691661b](https://github.com/unraid/api/commit/691661b952394e61a1b79c41419745fbf6caba20))
* remove unused imports ([65c1891](https://github.com/unraid/api/commit/65c18917563745cab9711e9900086e90ab44e284))
* remove unused job dependency ([84533d8](https://github.com/unraid/api/commit/84533d8fa56dc19635ea33d79bd8e644e539edd2))
* remove unused login entries ([7833b5d](https://github.com/unraid/api/commit/7833b5db386f724318857fc31d825fb3534c84b9))
* remove usage of Role.UPC ([d1e2f6e](https://github.com/unraid/api/commit/d1e2f6e0b391cb4eca75a0997b41cb99a9953d42))
* render function fixed ([8008ab4](https://github.com/unraid/api/commit/8008ab46fb2f231b68201758a258fd43e2e1672e))
* replace express cookie parser with fastify's ([0acebb0](https://github.com/unraid/api/commit/0acebb0dd25e919f3cc132eb7f96927992fc4151))
* report issues + pm2 issues ([28c383e](https://github.com/unraid/api/commit/28c383e1d111d4ac4226d7d966533ba80ca5d9a1))
* reset config to be closer to default ([b7fbb0b](https://github.com/unraid/api/commit/b7fbb0b6af0453f5f6a17087bb7e68c393b9fe3f))
* resource busy when removing all subdirectories ([29936c9](https://github.com/unraid/api/commit/29936c90938fb83bc2f154315ca63a9d7cc98552))
* restart command elegant ([296117b](https://github.com/unraid/api/commit/296117b51aac8a4c15366f2271af858868b6e071))
* restore upgradepkg before install ([fddca27](https://github.com/unraid/api/commit/fddca2738c0ec016e744169d88b35a55dea092fa))
* revert changes to indicator.vue ([84d2a83](https://github.com/unraid/api/commit/84d2a832c0f64e52be05670eb438b21bff2e5163))
* revert myservers.cfg ([d0896f3](https://github.com/unraid/api/commit/d0896f3ef8aebdd9c76d805ed6a35b4a5d5a1b08))
* rm getServerIdentifier wrapping Notifications id ([eaea306](https://github.com/unraid/api/commit/eaea306d54f633f563c7340f8a992e03b631f903))
* rm rf to fix build issues ([a27cbe0](https://github.com/unraid/api/commit/a27cbe00d813ede6f31d4824fd63ff29a1ef6972))
* sandbox defaults in dev mode wrong ([2a24919](https://github.com/unraid/api/commit/2a2491936cf85013be836450ab7ed0cc11207e11))
* sequential test execution for generic-modification ([79ee1f7](https://github.com/unraid/api/commit/79ee1f7552cee47c6f5a8eb5942468292212e2f2))
* shell path to unraid-api ([15d11e4](https://github.com/unraid/api/commit/15d11e477bb2a08d785a7b22bd51900279a55508))
* staging build issues ([e6bcb8d](https://github.com/unraid/api/commit/e6bcb8de7daee463f7ac0dbf977e085e108302ba))
* start command simplification ([e1faf3a](https://github.com/unraid/api/commit/e1faf3aa8db5973eb1bb0ea7a4844f820504618d))
* stop command exits ([2dbfdb6](https://github.com/unraid/api/commit/2dbfdb670a773114c0fdc68c7cf9d29fa4e28a9b))
* strip components from tar line ([911cd5b](https://github.com/unraid/api/commit/911cd5bc0b0983df4ca8c9057bea5166f7d1c7f1))
* subdependenies ([f1ad3b0](https://github.com/unraid/api/commit/f1ad3b0af13345189e10973b422f4e5c6b5d7839))
* swap to flexible IDs in tests ([b95559d](https://github.com/unraid/api/commit/b95559d9a1e743f92bc3b776892286e8d7abfc1e))
* swap to placeholder key ([d1864d0](https://github.com/unraid/api/commit/d1864d0020ed56ab2368d23b48604b55cff21ae4))
* switch to useToggle ([848233f](https://github.com/unraid/api/commit/848233f05465053876ac6f9f6ac4bfad2a48abff))
* test issues ([e4b55b1](https://github.com/unraid/api/commit/e4b55b133bb2dc4bf2ccfd6fd2fc244daadbea53))
* test simplification to ensure no redownloads ([e07dad3](https://github.com/unraid/api/commit/e07dad3a6947aa186c4ac03032b5b3813cd046b6))
* tests ([25c1c1a](https://github.com/unraid/api/commit/25c1c1a55a3fb32b76bf5cb7257a4ba44f717a89))
* tests and validate token clears screen ([7f48ddd](https://github.com/unraid/api/commit/7f48dddcd2e2ea1ae3a55ecc54d5ac274535b714))
* text classes ([1e17cfc](https://github.com/unraid/api/commit/1e17cfc2bca5e8431188804b28f4645eb42cdc9f))
* theme store now uses singular variables object ([5ca6e40](https://github.com/unraid/api/commit/5ca6e40b2d4942385b12a4325d6b8a551cb3f44b))
* thorw on invalid token body ([f1af763](https://github.com/unraid/api/commit/f1af763eaf0dd8215eed470293d3a7f98784f38a))
* trigger loading correctly ([e18f3d3](https://github.com/unraid/api/commit/e18f3d3e566011054163ec7827494fa047b26ec9))
* type & build errors ([800969a](https://github.com/unraid/api/commit/800969a87a45d1d3ca9eca65657fddeccba66f28))
* type error on element render ([a2563eb](https://github.com/unraid/api/commit/a2563eb8e710a9ac7259c4260cad9a3454565dae))
* type for generic test ([e856535](https://github.com/unraid/api/commit/e85653592a9d6eadcd0be89bf90a96c5d313fda3))
* unit test failure ([fed165e](https://github.com/unraid/api/commit/fed165eab0358fe032a99e5afbdb19813b00b741))
* unit test issues ([c58f7a7](https://github.com/unraid/api/commit/c58f7a7f246902c7d354eb51d1c87c8ea3b636a3))
* unit tests updated ([9548505](https://github.com/unraid/api/commit/954850535bec6b09aaf66b01d3ee749c8a22de5d))
* unneeded await on api-key service ([0325be7](https://github.com/unraid/api/commit/0325be757ee4c04b5c23365ff592f521a492595b))
* unraid-api in usr/bin ([580babd](https://github.com/unraid/api/commit/580babdafddd89ee2fb0b07aa7f5dff865be37d2))
* unused import ([83fbea5](https://github.com/unraid/api/commit/83fbea5632b1de71afa4d0ca3224a946bf76fd23))
* unused imports ([a5447aa](https://github.com/unraid/api/commit/a5447aa2f4c99968651fa3750d6bf0e8d68678de))
* unused node dl line ([7ea1c3a](https://github.com/unraid/api/commit/7ea1c3a8f24e47f2e10994ffe629135dc4614159))
* upc header text color ([f989026](https://github.com/unraid/api/commit/f9890260d1c4abe69dac3ac4c05ebab17aab5161))
* update tests ([d0696a9](https://github.com/unraid/api/commit/d0696a93810893ccd6c676df1c639ca279992428))
* upgradepkg ([90cf1a8](https://github.com/unraid/api/commit/90cf1a8eea67d3dbc736ecdfba47e0025b1dc31c))
* use an enum and defaults for sandbox value ([eb56483](https://github.com/unraid/api/commit/eb56483ba2693944d39f4409c91b75ee82a7d30b))
* use batchProcess ([ffbb9d7](https://github.com/unraid/api/commit/ffbb9d7750568bfa849d21e051503d1fcca5355f))
* use correct ini encoder in notification service ([d1f8c61](https://github.com/unraid/api/commit/d1f8c61f1b9ea5745acdfd2d60de4725b4dffe05))
* use cwd when running application ([e016652](https://github.com/unraid/api/commit/e01665264b6f45366cdacf60c0f3553adfbd85d3))
* use foreground text color for UPC ([87b8165](https://github.com/unraid/api/commit/87b816550d413dc9023c5057efe18b9cb26761e7))
* use placeholder in test API key ([c6b7755](https://github.com/unraid/api/commit/c6b7755214de8bedd5c0f2735473c2a559b1e26f))
* use unraid binary path to call unraid commands ([555087d](https://github.com/unraid/api/commit/555087dcdd2bc9e5a6f2ccbdaff30a1bc89ad712))
* used TGZ instead of TXZ for nghttp3 ([09ad394](https://github.com/unraid/api/commit/09ad39483fed7a8155176b6568114b4e6679d587))
* variable naming ([dbffc0d](https://github.com/unraid/api/commit/dbffc0d293cefcb8d923cbcb17ad1f1a1d5e302d))
* variables passed properly ([e0875e7](https://github.com/unraid/api/commit/e0875e7a1b273969939d6902a55f4a9772640078))
* version and EOF key ([cafa47d](https://github.com/unraid/api/commit/cafa47d283d9b637c1e8dfbd7407186e58233358))
* watch all events to load keys ([59ca177](https://github.com/unraid/api/commit/59ca17787e4d36113b0a8c5ef2117acfc491c49c))
* **web:** add default values to optional vue component props ([d3092e4](https://github.com/unraid/api/commit/d3092e487ead2ca4647928008ee54f3cd6b333c2))
* **web:** dedupe incoming notifications during cache merge ([4a40729](https://github.com/unraid/api/commit/4a40729e3721d01ac45614f4b7d1c48aec483cbc))
* **web:** display error message in sidebar when api is offline ([#984](https://github.com/unraid/api/issues/984)) ([125c0a1](https://github.com/unraid/api/commit/125c0a140b4e9b5401bacf1addab1820c412917e))
* **web:** edge case where archived notifications don't appear ([0a8c574](https://github.com/unraid/api/commit/0a8c5746fc2b8f8639643f013c1f19f0d7236d41))
* **web:** env var typo ([22cf90b](https://github.com/unraid/api/commit/22cf90b27fadec3024d9d038c53683e8f8a723bc))
* **web:** escaping html-encoded symbols like apostrophes in translations ([#1002](https://github.com/unraid/api/issues/1002)) ([04a3362](https://github.com/unraid/api/commit/04a33621e1d406d75ed0ff9af9f1f945813a1e8d))
* **web:** flash of disconnected api state on page load ([a8c02f4](https://github.com/unraid/api/commit/a8c02f4c49433b440a6f9c70f269bf69076655dc))
* **web:** infinite scroll loop when there's only 1 page of notifications ([e9f2fc4](https://github.com/unraid/api/commit/e9f2fc424c350d07c756ae7573e90f615bcae25b))
* **web:** infinite trigger at bottom of infinite scroll ([eb691d3](https://github.com/unraid/api/commit/eb691d3514d8dc079987bfa566de4aa86094ef67))
* **web:** inline shadcn variables into tailwind config to simplify build ([07fd7fe](https://github.com/unraid/api/commit/07fd7fe120f42ddf15c19f2a7df135fb9741187b))
* **web:** notification styles & alignment ([#968](https://github.com/unraid/api/issues/968)) ([0d65e12](https://github.com/unraid/api/commit/0d65e12cede3324261fd3b219745b1e7793a33de))
* **web:** refetch notifications for sidebar when new notifications arrive ([591bf4a](https://github.com/unraid/api/commit/591bf4a643ccc13c151c0a8cafad833d3137043e))
* **web:** remove unused infinite-scroll emit from SheetContent ([95db23f](https://github.com/unraid/api/commit/95db23f8e13574a50e0ba3860bbfd54fd663c20e))
* **web:** remove warn and error console log removal ([#1086](https://github.com/unraid/api/issues/1086)) ([9375639](https://github.com/unraid/api/commit/9375639e4a71ecfe8d4b877301c1f9bb22800a72))
* **web:** replace incorrect custom types with codegen from gql & update values to match expected shapes ([fc93ef8](https://github.com/unraid/api/commit/fc93ef8e32607c807f9bd8529088a69937bdaefc))
* **web:** replace manual height hack in notifications infinite scroll ([de1e272](https://github.com/unraid/api/commit/de1e272357264afc0f7f5fdd653c6a865105d710))
* **web:** reset infinite scroll when notification filters change ([da6de2c](https://github.com/unraid/api/commit/da6de2ccdb710772a199c8cba8952adc247412db))
* **web:** sanitize changelog markup after parsing ([c960292](https://github.com/unraid/api/commit/c96029273283f5970a5029eea1d7f451bbd0071b))
* **web:** stop opening notification sidebar to archive tab ([325e75f](https://github.com/unraid/api/commit/325e75f5d444908a2227fbe2e94be9ab5196ad8e))
* **web:** theme header differences ([#1085](https://github.com/unraid/api/issues/1085)) ([1ccdd8d](https://github.com/unraid/api/commit/1ccdd8dc71ee5e1e3aacabd113d1cf213ca7c7ae))
* **web:** track 'notification seen' state across tabs & page loads ([#1121](https://github.com/unraid/api/issues/1121)) ([64cf6ec](https://github.com/unraid/api/commit/64cf6ecc6aec25cd8edee5659efb09f288bb9908))
* **web:** update unread total immediately upon archiving ([#982](https://github.com/unraid/api/issues/982)) ([ff5fd8e](https://github.com/unraid/api/commit/ff5fd8e5eb8eb4803db1265e31b0c1352af20251))
## [3.11.0](https://github.com/unraid/api/compare/v3.10.1...v3.11.0) (2024-09-11)

View File

@@ -19,15 +19,18 @@ WORKDIR /app
# Set app env
ENV NODE_ENV=development
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
COPY tsconfig.json .eslintrc.ts .npmrc .env.production .env.staging ./
# Install pnpm
RUN corepack enable && corepack prepare pnpm@8.15.4 --activate && npm i -g npm@latest
COPY package.json package-lock.json ./
COPY tsconfig.json .eslintrc.ts .prettierrc.cjs .npmrc .env.production .env.staging package.json pnpm-lock.yaml .npmrc ./
# Install deps
RUN npm i
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
EXPOSE 4000
EXPOSE 3001
###########################################################
# Builder Image
@@ -39,4 +42,4 @@ ENV NODE_ENV=production
COPY . .
CMD ["npm", "run", "build-and-pack"]
CMD ["pnpm", "run", "build-and-pack"]

View File

@@ -17,11 +17,11 @@ root@Devon:~# unraid-api
Unraid API
Thanks for using the official Unraid API
Thanks for using the official Unraid API
Usage:
$ unraid-api command <options>
$ unraid-api command <options>
Commands:
@@ -29,34 +29,48 @@ Commands:
Options:
-h, --help Prints this usage guide.
-d, --debug Enabled debug mode.
-p, --port string Set the graphql port.
--environment production/staging/development Set the working environment.
--log-level ALL/TRACE/DEBUG/INFO/WARN/ERROR/FATAL/MARK/OFF Set the log level.
-h, --help Prints this usage guide.
-d, --debug Enabled debug mode.
-p, --port string Set the graphql port.
--environment production/staging/development Set the working environment.
--log-level ALL/TRACE/DEBUG/INFO/WARN/ERROR/FATAL/MARK/OFF Set the log level.
Copyright © 2024 Lime Technology, Inc.
```
## Report
To view the current status of the unraid-api and its connection to mothership, run:
## Key
To create and work with Unraid API keys, used for the local API, run the following command to view all available options. These options may change over time.
```sh
unraid-api key --help
```
## Report
To view the current status of the unraid-api and its connection to mothership, run:
```sh
unraid-api report
```
To view verbose data (anonymized), run:
```
```sh
unraid-api report -v
```
To view non-anonymized verbose data, run:
```
```sh
unraid-api report -vv
```
## Secrets
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/).
## License
Copyright Lime Technology Inc. All rights reserved.

View File

@@ -1,9 +1,8 @@
[api]
version="3.11.0"
version="4.0.1"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
sandbox="yes"
[remote]
wanaccess="yes"
wanport="8443"
@@ -14,9 +13,8 @@ email="test@example.com"
username="zspearmint"
avatar="https://via.placeholder.com/200"
regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
idtoken=""
refreshtoken=""
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
ssoSubIds=""

View File

@@ -1,191 +0,0 @@
{
"admin": {
"extends": "user",
"permissions": [
{
"resource": "apikey",
"action": "read:any",
"attributes": "*"
},
{
"resource": "array",
"action": "read:any",
"attributes": "*"
},
{
"resource": "cpu",
"action": "read:any",
"attributes": "*"
},
{
"resource": "device",
"action": "read:any",
"attributes": "*"
},
{
"resource": "device/unassigned",
"action": "read:any",
"attributes": "*"
},
{
"resource": "disk",
"action": "read:any",
"attributes": "*"
},
{
"resource": "disk/settings",
"action": "read:any",
"attributes": "*"
},
{
"resource": "display",
"action": "read:any",
"attributes": "*"
},
{
"resource": "docker/container",
"action": "read:any",
"attributes": "*"
},
{
"resource": "docker/network",
"action": "read:any",
"attributes": "*"
},
{
"resource": "info",
"action": "read:any",
"attributes": "*"
},
{
"resource": "license-key",
"action": "read:any",
"attributes": "*"
},
{
"resource": "machine-id",
"action": "read:any",
"attributes": "*"
},
{
"resource": "memory",
"action": "read:any",
"attributes": "*"
},
{
"resource": "notifications",
"action": "read:any",
"attributes": "*"
},
{
"resource": "online",
"action": "read:any",
"attributes": "*"
},
{
"resource": "os",
"action": "read:any",
"attributes": "*"
},
{
"resource": "parity-history",
"action": "read:any",
"attributes": "*"
},
{
"resource": "permission",
"action": "read:any",
"attributes": "*"
},
{
"resource": "servers",
"action": "read:any",
"attributes": "*"
},
{
"resource": "service",
"action": "read:any",
"attributes": "*"
},
{
"resource": "service/emhttpd",
"action": "read:any",
"attributes": "*"
},
{
"resource": "service/unraid-api",
"action": "read:any",
"attributes": "*"
},
{
"resource": "services",
"action": "read:any",
"attributes": "*"
},
{
"resource": "share",
"action": "read:any",
"attributes": "*"
},
{
"resource": "software-versions",
"action": "read:any",
"attributes": "*"
},
{
"resource": "unraid-version",
"action": "read:any",
"attributes": "*"
},
{
"resource": "user",
"action": "read:any",
"attributes": "*"
},
{
"resource": "var",
"action": "read:any",
"attributes": "*"
},
{
"resource": "vars",
"action": "read:any",
"attributes": "*"
},
{
"resource": "vm/domain",
"action": "read:any",
"attributes": "*"
},
{
"resource": "vm/network",
"action": "read:any",
"attributes": "*"
}
]
},
"user": {
"extends": "guest",
"permissions": [
{
"resource": "apikey",
"action": "read:own",
"attributes": "*"
},
{
"resource": "permission",
"action": "read:any",
"attributes": "*"
}
]
},
"guest": {
"permissions": [
{
"resource": "welcome",
"action": "read:any",
"attributes": "*"
}
]
}
}

View File

@@ -1,8 +0,0 @@
{
"id": "10f356da-1e9e-43b8-9028-a26a645539a6",
"key": "73717ca0-8c15-40b9-bcca-8d85656d1438",
"name": "Test API Key",
"description": "Testing API key creation",
"roles": ["guest", "connect"],
"createdAt": "2024-10-29T19:59:12.569Z"
}

View File

@@ -1,9 +1,10 @@
{
"createdAt": "2024-12-20T15:05:55.336Z",
"createdAt": "2025-01-27T16:22:56.501Z",
"description": "API key for Connect user",
"id": "d166bf8b-3615-444a-8932-c460b2132ba3",
"id": "b5b4aa3d-8e40-4c92-bc40-d50182071886",
"key": "_______________________LOCAL_API_KEY_HERE_________________________",
"name": "Connect",
"permissions": [],
"roles": [
"connect"
]

View File

@@ -1,5 +0,0 @@
timestamp=1683971161
event=Unraid Parity check
subject=Notice [UNRAID] - Parity check finished (0 errors)
description=Canceled
importance=warning

View File

@@ -1,9 +1,8 @@
[api]
version="3.11.0"
version="4.0.1"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
sandbox="yes"
[remote]
wanaccess="yes"
wanport="8443"
@@ -14,12 +13,12 @@ email="test@example.com"
username="zspearmint"
avatar="https://via.placeholder.com/200"
regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
idtoken=""
accesstoken=""
idtoken=""
refreshtoken=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
ssoSubIds=""
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://10-100-0-1.hash.myunraid.net:4443, https://10-100-0-2.hash.myunraid.net:4443, https://10-123-1-2.hash.myunraid.net:4443, https://221-123-121-112.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
[connectionStatus]
minigraph="PRE_INIT"
upnpStatus=""

View File

@@ -1,27 +1,12 @@
version: '3.8'
x-volumes: &volumes
x-common: &common
volumes:
- ./dev:/app/dev
- ./src:/app/src
- ./package.json:/app/package.json
- ./package-lock.json:/app/package-lock.json
- ./tsconfig.json:/app/tsconfig.json
- ./vite.config.ts:/app/vite.config.ts
- ./dist/:/app/dist/
- ./deploy/:/app/deploy/
- ./README.md:/app/README.md
- ./scripts/:/app/scripts/
- ../.git/:/app/.git/
- ./.env.production:/app/.env.production
- ./.env.staging:/app/.env.staging
- ./.env.test:/app/.env.test
- ./.env.development:/app/.env.development
- ./codegen.ts:/app/codegen.ts
- ./fix-array-type.cjs:/app/fix-array-type.cjs
- /var/run/docker.sock:/var/run/docker.sock
- ./unraid-api.js:/app/unraid-api.js
- ./ecosystem.config.json:/app/ecosystem.config.json
- ./:/app
- pnpm-store:/pnpm/store
- ../libvirt:/libvirt
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:-unknown}
- IS_TAGGED=${IS_TAGGED:-false}
services:
@@ -33,14 +18,10 @@ services:
context: .
target: development
dockerfile: Dockerfile
<<: *volumes
<<: *common
stdin_open: true
tty: true
entrypoint: /bin/bash
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles:
- builder
@@ -52,24 +33,23 @@ services:
context: .
target: development
dockerfile: Dockerfile
<<: *volumes
<<: *common
command: npm run start:dev
environment:
- IS_DOCKER=true
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
profiles:
- builder
builder:
image: unraid-api:builder
environment:
- GIT_SHA=${GIT_SHA:?err}
- IS_TAGGED=${IS_TAGGED}
build:
context: .
target: builder
dockerfile: Dockerfile
<<: *volumes
<<: *common
profiles:
- builder
- builder
volumes:
pnpm-store:
name: "pnpm-store"
pnpm-cache:
name: "pnpm-cache"

View File

@@ -0,0 +1,82 @@
# Repository Organization
This document describes the high-level architecture of the Unraid API repository.
## Overview
The repository consists of:
- API Server (NestJS)
- Redux Store
- Core Modules
- Tests
## API Server Architecture
The API server is built with NestJS and provides the core functionality for interacting with Unraid systems.
### Key Components:
- `src/unraid-api/` - Core NestJS implementation
- `src/core/` - Legacy business logic and utilities
- `src/store/` - Redux store and state management
- `src/common/` - Shared utilities and types
## Redux Store
The store manages application state through several modules:
### Store Modules
- `config` - User settings, authentication, and API configuration
- `emhttp` - Unraid system and array state
- `registration` - License management
- `cache` - Application caching
- `docker` - Container management
- `upnp` - UPnP functionality
- `dynamix` - Plugin state
- `minigraph` - Mothership connectivity
- `notifications` - System notifications
### Store Listeners
Key listeners that handle side effects:
- Array state changes
- Configuration updates
- Remote access changes
- Server state updates
- UPnP changes
- WAN access changes
### Store Synchronization
The store syncs data in two ways:
- Flash Storage - Persistent configuration
- Memory Storage - Runtime state
## Project Structure
The repository is organized into several packages:
- `api/` - NestJS API server
- `plugin/` - Unraid plugin package
- `web/` - Frontend application
- `unraid-ui/` - Shared UI components
## Development Flow
New development should focus on the NestJS implementation in `src/unraid-api/`:
1. Create new features in `src/unraid-api/` using NestJS patterns
2. Use dependency injection and NestJS modules
3. Legacy code in `src/core/` should be gradually migrated
4. State management still uses Redux store when needed
## Best Practices
1. Follow NestJS architectural patterns
2. Use TypeScript decorators and strong typing
3. Implement proper dependency injection
4. Write unit tests for new services

View File

@@ -1,34 +0,0 @@
# How to enable introspection and view possible API endpoints for the Unraid API
1. Install the API on your machine
2. Stop the running api with `unraid-api stop`
3. Enable an allowed origin for Apollo Studio:
- Edit the file at `/boot/config/plugins/dynamix.my.servers/myservers.cfg
- Add the line `extraOrigins="studio.apollographql.com"` inside of the `[api]` block
```ini
[api]
...
extraOrigins="studio.apollographql.com"
[local]
...rest
```
- Also copy out the `[upc] -> apikey` setting, it should look something like `unupc_52e45431703b1e79cef709bfaf7ddc469bafc12e091b7c9bca0f6e96dc`
4. Enable introspection
```sh
INTROSPECTION=true LOG_LEVEL=trace LOG_TYPE=pretty unraid-api start --debug
```
- If you run this command and it says the Unraid API is already started, run `unraid-api stop` before trying it again.
5. Use introspection to your server with Apollo Sandbox:
- Navigate your *Chrome* browser to [Apollo Sandbox](https://studio.apollographql.com/sandbox/explorer/)
- Click the settings icon in the top right corner and set the URL to your servers URL + /graphql. For example a server URL might be: `https://192-168-1-3.277ace5dd0892eacd83f517b39fb3d1dd32078b5.myunraid.net:8443/graphql` or `http://tower.local/graphql`
- Also set the API key you copied earlier in the header section. Set the key as `x-api-key` and the value to the API key you copied in Step 2.
![Image of connection settings window with proper settings](./images/connection_settings.png)
6. Now that your server should be connected, you should see the schema populate. To perform queries, click the plus icon on the field on the left side to add them to a query and then click to run icon on the right.

View File

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

162
api/docs/public/cli.md Normal file
View File

@@ -0,0 +1,162 @@
# CLI Commands
### Start
```bash
unraid-api start [--log-level <level>]
```
Starts the Unraid API service.
Options:
- `--log-level`: Set logging level (trace|debug|info|warn|error)
### Stop
```bash
unraid-api stop [--delete]
```
Stops the Unraid API service.
- `--delete`: Optional. Delete the PM2 home directory
### Restart
```bash
unraid-api restart
```
Restarts the Unraid API service.
### 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
```bash
unraid-api developer
```
Configure developer features for the API (e.g., GraphQL sandbox).
## API Key Management
### API Key Commands
```bash
unraid-api apikey [options]
```
Create and manage API keys.
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
### 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

@@ -0,0 +1,202 @@
# Using the Unraid API
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
1. First, enable developer mode using the CLI:
```bash
unraid-api developer
```
2. Follow the prompts to enable the sandbox. This will allow you to access the Apollo Sandbox interface.
3. Access the GraphQL playground by navigating to:
```txt
http://YOUR_SERVER_IP/graphql
```
## Authentication
Most queries and mutations require authentication. You can authenticate using either:
1. API Keys
2. Cookies (default method when signed into the WebGUI)
### Creating an API Key
Use the CLI to create an API key:
```bash
unraid-api apikey --create
```
Follow the prompts to set:
- Name
- Description
- Roles
- Permissions
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
1. Check System Status:
```graphql
query {
info {
os {
platform
distro
release
uptime
}
cpu {
manufacturer
brand
cores
threads
}
}
}
```
2. Monitor Array Status:
```graphql
query {
array {
state
capacity {
disks {
free
used
total
}
}
disks {
name
size
status
temp
}
}
}
```
3. 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
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
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
- 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 or run `unraid-api --help`.

37
api/docs/public/index.md Normal file
View File

@@ -0,0 +1,37 @@
# Unraid API
The Unraid API provides a GraphQL interface for programmatic interaction with your Unraid server. It enables automation, monitoring, and integration capabilities.
## Current Availability
The API is available through the Unraid Connect Plugin:
1. Install Unraid Connect Plugin from 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#accessing-the-graphql-sandbox)
## Future Availability
The API will be integrated directly into the Unraid operating system in an upcoming OS release. This integration will:
- Make the API a core part of the Unraid system
- Remove the need for separate plugin installation
- Enable deeper system integration capabilities
## Documentation Sections
- [CLI Commands](./cli.md) - Reference for all available command-line interface commands
- [Using the Unraid API](./how-to-use-the-api.md) - Comprehensive guide on using the GraphQL API
- [Upcoming Features](./upcoming-features.md) - Roadmap of planned features and improvements
## Key Features
The API provides:
- GraphQL Interface: Modern, flexible API with strong typing
- Authentication: Secure access via API keys or session cookies
- Comprehensive Coverage: Access to system information, array management, and Docker operations
- Developer Tools: Built-in GraphQL sandbox for testing
- Role-Based Access: Granular permission control
For detailed usage instructions, see [CLI Commands](./cli.md).

View File

@@ -0,0 +1,71 @@
# Upcoming Features
Note: This roadmap outlines planned features and improvements for the Unraid API. Features and timelines may change based on development priorities and community feedback.
## Core Infrastructure
| Feature | Status | Tag |
|---------|--------|-----|
| API Development Environment Improvements | Done | v4.0.0 |
| Include API in Unraid OS | Planned (Q1 2025) | - |
| Make API Open Source | Planned (Q1 2025) | - |
| Separate API from Connect Plugin | Planned (Q2 2025) | - |
| Developer Tools for Plugins | Planned (Q2 2025) | - |
## Security & Authentication
| Feature | Status | Tag |
|---------|--------|-----|
| Permissions System Rewrite | Done | v4.0.0 |
| User Interface Component Library | In Progress | - |
## User Interface Improvements
| Feature | Status | Tag |
|---------|--------|-----|
| New Settings Pages | Planned (Q2 2025) | - |
| Custom Theme Creator | Planned (Q2-Q3 2025) | - |
| New Connect Settings Interface | Planned (Q1 2025) | - |
## Array Management
| Feature | Status | Tag |
|---------|--------|-----|
| Array Status Monitoring | Done | v4.0.0 |
| Storage Pool Creation Interface | Planned (Q2 2025) | - |
| Storage Pool Status Interface | Planned (Q2 2025) | - |
## Docker Integration
| Feature | Status | Tag |
|---------|--------|-----|
| Docker Container Status Monitoring | Done | v4.0.0 |
| New Docker Status Interface Design | Planned (Q3 2025) | - |
| New Docker Status Interface | Planned (Q3 2025) | - |
| Docker Container Setup Interface | Planned (Q3 2025) | - |
| Docker Compose Support | Planned | - |
## Share Management
| Feature | Status | Tag |
|---------|--------|-----|
| Array/Cache Share Status Monitoring | Done | v4.0.0 |
| Storage Share Creation & Settings | Planned | - |
| Storage Share Management Interface | Planned | - |
## Plugin System
| Feature | Status | Tag |
|---------|--------|-----|
| New Plugins Interface | Planned (Q3 2025) | - |
| Plugin Management Interface | Planned | - |
| Plugin Development Tools | Planned | - |
## Notifications
| Feature | Status | Tag |
|---------|--------|-----|
| Notifications System | Done | v4.0.0 |
| Notifications Interface | Done | v4.0.0 |
Features marked as "Done" are available in current releases. The tag column shows the version where a feature was first introduced.

View File

@@ -1,21 +1,19 @@
{
"$schema": "https://json.schemastore.org/pm2-ecosystem",
"apps": [
{
"name": "unraid-api",
"script": "./dist/main.js",
"cwd": "/usr/local/unraid-api",
"log": "/var/log/unraid-api/unraid-api.log",
"exec_mode": "fork",
"wait_ready": true,
"listen_timeout": 30000,
"listen_timeout": 15000,
"max_restarts": 10,
"min_uptime": 10000,
"ignore_watch": [
"node_modules",
"src",
".env.*",
"myservers.cfg"
]
"watch": false,
"ignore_watch": ["node_modules", "src", ".env.*", "myservers.cfg"],
"log_file": "/var/log/graphql-api.log",
"kill_timeout": 10000
}
]
}
}

View File

@@ -5,12 +5,12 @@ default:
@just list-commands
setup:
npm install
npm run container:build
pnpm install
pnpm run container:build
# builds js files that can run on an unraid server
@build:
npm run build
pnpm run build
# deploys to an unraid server
@deploy:
@@ -18,4 +18,3 @@ setup:
# build & deploy
bd: build deploy

27660
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,41 +1,47 @@
{
"name": "@unraid/api",
"version": "3.11.0",
"version": "4.1.1",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
"enabled": true
},
"repository": "git@github.com:unraid/api.git",
"author": "Lime Technology, Inc. <unraid.net>",
"license": "UNLICENSED",
"engines": {
"pnpm": ">=8.0.0"
},
"scripts": {
"// Main application commands": "",
"start": "node dist/main.js",
"build:docker": "./scripts/dc.sh run --rm builder",
"dev": "vite",
"command": "pnpm run build && clear && ./dist/cli.js",
"// Build commands": "",
"build": "vite build --mode=production",
"postbuild": "chmod +x dist/main.js && chmod +x dist/cli.js",
"build-and-pack": "./scripts/build.mjs",
"build:docker": "./scripts/dc.sh run --rm builder",
"build-and-pack": "tsx ./scripts/build.ts",
"// Code generation commands": "",
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.ts -r dotenv/config './.env.staging'",
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen --config codegen.ts --watch -r dotenv/config",
"codegen:local": "NODE_TLS_REJECT_UNAUTHORIZED=0 MOTHERSHIP_GRAPHQL_LINK='https://mothership.localhost/ws' graphql-codegen --config codegen.ts --watch",
"// Development and quality tools": "",
"tsc": "tsc --noEmit",
"lint": "eslint --flag unstable_ts_config --config .eslintrc.ts src/",
"lint:fix": "eslint --flag unstable_ts_config --fix --config .eslintrc.ts src/",
"test:watch": "vitest --pool=forks",
"test": "vitest run --pool=forks",
"coverage": "vitest run --pool=forks --coverage",
"lint": "eslint --config .eslintrc.ts src/",
"lint:fix": "eslint --fix --config .eslintrc.ts src/",
"release": "standard-version",
"dev": "vite",
"// Testing commands": "",
"test": "NODE_ENV=test vitest run",
"test:watch": "NODE_ENV=test vitest --ui",
"coverage": "NODE_ENV=test vitest run --coverage",
"// Container management commands": "",
"container:build": "./scripts/dc.sh build dev",
"container:start": "./scripts/dc.sh run --rm --service-ports dev",
"container:test": "./scripts/dc.sh run --rm builder npm run test",
"container:start": "pnpm run container:stop && ./scripts/dc.sh run --rm --service-ports dev",
"container:stop": "./scripts/dc.sh stop dev",
"container:test": "./scripts/dc.sh run --rm builder pnpm run test",
"container:enter": "./scripts/dc.sh exec dev /bin/bash"
},
"files": [
".env.staging",
".env.production",
"ecosystem.config.json",
"README.md",
"src",
"node_modules/"
],
"bin": {
"unraid-api": "dist/cli.js"
},
@@ -50,6 +56,7 @@
"@graphql-tools/schema": "^10.0.7",
"@graphql-tools/utils": "^10.5.5",
"@nestjs/apollo": "^12.2.1",
"@nestjs/common": "^10.4.7",
"@nestjs/core": "^10.4.7",
"@nestjs/graphql": "^12.2.1",
"@nestjs/passport": "^10.0.3",
@@ -57,10 +64,10 @@
"@nestjs/schedule": "^4.1.1",
"@nestjs/throttler": "^6.2.1",
"@reduxjs/toolkit": "^2.3.0",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"@types/diff": "^7.0.1",
"@unraid/libvirt": "^1.0.5",
"accesscontrol": "^2.2.1",
"btoa": "^1.2.1",
"bycontract": "^2.0.11",
"bytes": "^3.1.2",
"cacheable-lookup": "^7.0.0",
@@ -71,15 +78,18 @@
"cli-table": "^0.3.11",
"command-exists": "^1.2.9",
"convert": "^5.5.1",
"cookie": "^1.0.2",
"cron": "3.2.1",
"cross-fetch": "^4.0.0",
"diff": "^7.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dotenv": "^16.4.5",
"execa": "^9.5.1",
"exit-hook": "^4.0.0",
"express": "^4.21.1",
"filenamify": "^6.0.0",
"fs-extra": "^11.2.0",
"glob": "^11.0.1",
"global-agent": "^3.0.0",
"got": "^14.4.4",
"graphql": "^16.9.0",
@@ -92,22 +102,19 @@
"graphql-ws": "^5.16.0",
"ini": "^4.1.2",
"ip": "^2.0.1",
"ip-regex": "^5.0.0",
"jose": "^5.9.6",
"lodash-es": "^4.17.21",
"multi-ini": "^2.3.2",
"mustache": "^4.2.0",
"nest-access-control": "^3.1.0",
"nest-authz": "^2.11.0",
"nest-commander": "^3.15.0",
"nestjs-pino": "^4.1.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^6.1.3",
"p-retry": "^6.2.0",
"passport-custom": "^1.1.1",
"passport-http-header-strategy": "^1.1.0",
"path-type": "^6.0.0",
"pidusage": "^3.0.2",
"pino": "^9.5.0",
"pino-http": "^10.3.0",
"pino-pretty": "^11.3.0",
@@ -115,13 +122,10 @@
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.6.3",
"stoppable": "^1.1.0",
"strftime": "^0.10.3",
"systeminformation": "^5.23.5",
"ts-command-line-args": "^2.5.1",
"systeminformation": "^5.25.11",
"uuid": "^11.0.2",
"ws": "^8.18.0",
"xhr2": "^0.2.1",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -132,7 +136,7 @@
"@graphql-codegen/typed-document-node": "^5.0.11",
"@graphql-codegen/typescript": "^4.1.1",
"@graphql-codegen/typescript-operations": "^4.3.1",
"@graphql-codegen/typescript-resolvers": "4.4.1",
"@graphql-codegen/typescript-resolvers": "4.4.2",
"@graphql-typed-document-node/core": "^3.2.0",
"@ianvs/prettier-plugin-sort-imports": "^4.4.0",
"@nestjs/testing": "^10.4.7",
@@ -140,13 +144,11 @@
"@rollup/plugin-node-resolve": "^15.3.0",
"@swc/core": "^1.10.1",
"@types/async-exit-hook": "^2.0.2",
"@types/btoa": "^1.2.5",
"@types/bytes": "^3.1.4",
"@types/cli-table": "^0.3.4",
"@types/command-exists": "^1.2.3",
"@types/cors": "^2.8.17",
"@types/dockerode": "^3.3.31",
"@types/express": "^5.0.0",
"@types/graphql-fields": "^1.3.9",
"@types/graphql-type-uuid": "^0.2.6",
"@types/ini": "^4.1.1",
@@ -154,7 +156,6 @@
"@types/lodash": "^4.17.13",
"@types/mustache": "^4.2.5",
"@types/node": "^22.9.0",
"@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.4",
"@types/semver": "^7.5.8",
"@types/sendmail": "^1.4.7",
@@ -163,31 +164,32 @@
"@types/uuid": "^10.0.0",
"@types/ws": "^8.5.13",
"@types/wtfnode": "^0.7.3",
"@vitest/coverage-v8": "^2.1.8",
"@vitest/ui": "^2.1.4",
"@vitest/coverage-v8": "^3.0.5",
"@vitest/ui": "^3.0.5",
"cz-conventional-changelog": "3.3.0",
"eslint": "^9.14.0",
"graphql-codegen-typescript-validation-schema": "^0.16.0",
"eslint-plugin-no-relative-import-paths": "^1.6.1",
"eslint-plugin-prettier": "^5.2.3",
"graphql-codegen-typescript-validation-schema": "^0.17.0",
"jiti": "^2.4.0",
"nodemon": "^3.1.7",
"rollup-plugin-node-externals": "^7.1.3",
"standard-version": "^9.5.0",
"tsx": "^4.19.2",
"typescript": "^5.6.3",
"typescript-eslint": "^8.13.0",
"unplugin-swc": "^1.5.1",
"vite": "^5.4.10",
"vite": "^5.4.14",
"vite-plugin-node": "^4.0.0",
"vite-plugin-static-copy": "^2.0.0",
"vite-tsconfig-paths": "^5.1.0",
"vitest": "^2.1.8",
"zx": "^8.2.0"
},
"optionalDependencies": {
"@vmngr/libvirt": "github:unraid/libvirt"
"vitest": "^3.0.5",
"zx": "^8.3.2"
},
"overrides": {
"eslint": {
"jiti": "2"
}
}
},
"packageManager": "pnpm@8.15.4+sha512.0bd3a9be9eb0e9a692676deec00a303ba218ba279d99241475616b398dbaeedd11146f92c2843458f557b1d127e09d4c171e105bdcd6b61002b39685a8016b9e",
"private": true
}

View File

@@ -1,86 +0,0 @@
#!/usr/bin/env zx
import { exit } from 'process';
import { cd, $ } from 'zx';
import { getDeploymentVersion } from './get-deployment-version.mjs';
try {
// Enable colours in output
process.env.FORCE_COLOR = '1';
// Ensure we have the correct working directory
process.env.WORKDIR ??= process.env.PWD;
cd(process.env.WORKDIR);
// Create deployment directories - ignore if they already exist
await $`mkdir -p ./deploy/release`;
await $`mkdir -p ./deploy/pre-pack`;
await $`rm -rf ./deploy/release/*`;
await $`rm -rf ./deploy/pre-pack/*`;
// Build Generated Types
await $`npm run codegen`;
await $`npm run build`;
// Copy app files to plugin directory
await $`cp -r ./src/ ./deploy/pre-pack/src/`;
await $`cp -r ./dist/ ./deploy/pre-pack/dist/`;
// Copy environment to deployment directory
const files = [
'.env.production',
'.env.staging',
'tsconfig.json',
'codegen.ts',
'ecosystem.config.json',
'vite.config.ts',
]
for (const file of files) {
await $`cp ./${file} ./deploy/pre-pack/${file}`;
}
// Get package details
const { name, version, ...rest } = await import('../package.json', {
assert: { type: 'json' },
}).then((pkg) => pkg.default);
const deploymentVersion = getDeploymentVersion(process.env, version);
// Create deployment package.json
await $`echo ${JSON.stringify({
...rest,
name,
version: deploymentVersion,
})} > ./deploy/pre-pack/package.json`;
// # Create final tgz
await $`cp ./README.md ./deploy/pre-pack/`;
await $`cp -r ./node_modules ./deploy/pre-pack/node_modules`;
// Install production dependencies
cd('./deploy/pre-pack');
await $`npm prune --omit=dev`;
await $`npm install --omit=dev`;
await $`npm install github:unraid/libvirt`;
// Now we'll pack everything in the pre-pack directory
await $`tar -czf ../unraid-api-${deploymentVersion}.tgz .`;
// Move unraid-api.tgz to release directory
await $`mv ../unraid-api-${deploymentVersion}.tgz ../release`;
} catch (error) {
// Error with a command
if (Object.keys(error).includes('stderr')) {
console.log(`Failed building package. Exit code: ${error.exitCode}`);
console.log(`Error: ${error.stderr}`);
} else {
// Normal js error
console.log('Failed building package.');
console.log(`Error: ${error.message}`);
}
exit(error.exitCode);
}

67
api/scripts/build.ts Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env zx
import { mkdir, readFile, rm, writeFile } from 'fs/promises';
import { exit } from 'process';
import { $, cd } from 'zx';
import { getDeploymentVersion } from './get-deployment-version.js';
try {
// Create release and pack directories
// Clean existing deploy folder
await rm('./deploy', { recursive: true }).catch(() => {});
await mkdir('./deploy/release', { recursive: true });
await mkdir('./deploy/pack', { recursive: true });
// Build Generated Types
await $`pnpm run codegen`;
await $`pnpm run build`;
// Copy app files to plugin directory
// Get package details
const packageJson = await readFile('./package.json', 'utf-8');
const parsedPackageJson = JSON.parse(packageJson);
const deploymentVersion = await getDeploymentVersion(process.env, parsedPackageJson.version);
// Update the package.json version to the deployment version
parsedPackageJson.version = deploymentVersion;
// Create a temporary directory for packaging
await mkdir('./deploy/pack/', { recursive: true });
await writeFile('./deploy/pack/package.json', JSON.stringify(parsedPackageJson, null, 4));
// Copy necessary files to the pack directory
await $`cp -r dist README.md .env.* ecosystem.config.json ./deploy/pack/`;
// Change to the pack directory and install dependencies
cd('./deploy/pack');
console.log('Installing production dependencies...');
$.verbose = true;
await $`pnpm install --prod --ignore-workspace --node-linker hoisted`;
// chmod the cli
await $`chmod +x ./dist/cli.js`;
await $`chmod +x ./dist/main.js`;
// Create the tarball
await $`tar -czf ../release/unraid-api.tgz ./`;
// Clean up
cd('..');
} catch (error) {
// Error with a command
if (Object.keys(error).includes('stderr')) {
console.log(`Failed building package. Exit code: ${error.exitCode}`);
console.log(`Error: ${error.stderr}`);
} else {
// Normal js error
console.log('Failed building package.');
console.log(`Error: ${error.message}`);
}
exit(error.exitCode);
}

View File

@@ -34,6 +34,7 @@ if [ ! -d "$source_directory" ]; then
fi
fi
# Change ownership on copy
# Replace the value inside the rsync command with the user's input
rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/unraid-api"
@@ -44,14 +45,11 @@ echo "$rsync_command"
eval "$rsync_command"
exit_code=$?
# Run unraid-api restart on remote host
dev=${DEV:-true}
# Chown the directory
ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api"
if [ "$dev" = true ]; then
ssh root@"${server_name}" "INTROSPECTION=true unraid-api restart"
else
ssh root@"${server_name}" "unraid-api restart"
fi
# Run unraid-api restart on remote host
ssh root@"${server_name}" "INTROSPECTION=true LOG_LEVEL=trace unraid-api restart"
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then

View File

@@ -1,15 +1,16 @@
import { execSync } from 'child_process';
import { execa } from 'execa';
const runCommand = (command) => {
const runCommand = async (command: string, args: string[]) => {
try {
return execSync(command, { stdio: 'pipe' }).toString().trim();
const { stdout } = await execa(command, args);
return stdout.trim();
} catch (error) {
console.log('Failed to get value from tag command: ', command, error.message);
return;
console.log('Failed to execute command:', command, args.join(' '), error.message);
return undefined;
}
};
export const getDeploymentVersion = (env = process.env, packageVersion) => {
export const getDeploymentVersion = async (env = process.env, packageVersion: string) => {
if (env.API_VERSION) {
console.log(`Using env var for version: ${env.API_VERSION}`);
return env.API_VERSION;
@@ -17,9 +18,11 @@ export const getDeploymentVersion = (env = process.env, packageVersion) => {
console.log(`Using env vars for git tags: ${env.GIT_SHA} ${env.IS_TAGGED}`);
return env.IS_TAGGED ? packageVersion : `${packageVersion}+${env.GIT_SHA}`;
} else {
const gitShortSHA = runCommand('git rev-parse --short HEAD');
const isCommitTagged = runCommand('git describe --tags --abbrev=0 --exact-match') !== undefined;
const gitShortSHA = await runCommand('git', ['rev-parse', '--short', 'HEAD']);
const isCommitTagged = await runCommand('git', ['describe', '--tags', '--abbrev=0', '--exact-match']) !== undefined;
console.log('gitShortSHA', gitShortSHA, 'isCommitTagged', isCommitTagged);
if (!gitShortSHA) {
console.error('Failed to get git short SHA');
process.exit(1);

View File

@@ -1,48 +1,44 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import { store } from '@app/store/index';
import { loadConfigFile } from '@app/store/modules/config';
import { loadStateFiles } from '@app/store/modules/emhttp';
import 'reflect-metadata';
import { expect, test } from 'vitest';
// Preloading imports for faster tests
import '@app/common/allowed-origins';
import '@app/store/modules/emhttp';
import '@app/store';
test('Returns allowed origins', async () => {
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { getAllowedOrigins } = await import('@app/common/allowed-origins');
const { loadConfigFile } = await import('@app/store/modules/config');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
// Get allowed origins
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
[
"/var/run/unraid-notifications.sock",
"/var/run/unraid-php.sock",
"/var/run/unraid-cli.sock",
"http://localhost:8080",
"https://localhost:4443",
"https://tower.local:4443",
"https://192.168.1.150:4443",
"https://tower:4443",
"https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443",
"https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443",
"https://10-252-0-1.hash.myunraid.net:4443",
"https://10-252-1-1.hash.myunraid.net:4443",
"https://10-253-3-1.hash.myunraid.net:4443",
"https://10-253-4-1.hash.myunraid.net:4443",
"https://10-253-5-1.hash.myunraid.net:4443",
"https://10-100-0-1.hash.myunraid.net:4443",
"https://10-100-0-2.hash.myunraid.net:4443",
"https://10-123-1-2.hash.myunraid.net:4443",
"https://221-123-121-112.hash.myunraid.net:4443",
"https://google.com",
"https://test.com",
"https://connect.myunraid.net",
"https://connect-staging.myunraid.net",
"https://dev-my.myunraid.net:4000",
]
`);
[
"/var/run/unraid-notifications.sock",
"/var/run/unraid-php.sock",
"/var/run/unraid-cli.sock",
"http://localhost:8080",
"https://localhost:4443",
"https://tower.local:4443",
"https://192.168.1.150:4443",
"https://tower:4443",
"https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443",
"https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443",
"https://10-252-0-1.hash.myunraid.net:4443",
"https://10-252-1-1.hash.myunraid.net:4443",
"https://10-253-3-1.hash.myunraid.net:4443",
"https://10-253-4-1.hash.myunraid.net:4443",
"https://10-253-5-1.hash.myunraid.net:4443",
"https://10-100-0-1.hash.myunraid.net:4443",
"https://10-100-0-2.hash.myunraid.net:4443",
"https://10-123-1-2.hash.myunraid.net:4443",
"https://221-123-121-112.hash.myunraid.net:4443",
"https://google.com",
"https://test.com",
"https://connect.myunraid.net",
"https://connect-staging.myunraid.net",
"https://dev-my.myunraid.net:4000",
]
`);
});

View File

@@ -1,457 +0,0 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Returns default permissions 1`] = `
RolesBuilder {
"_grants": {
"admin": {
"$extend": [
"guest",
],
"apikey": {
"read:any": [
"*",
],
},
"array": {
"read:any": [
"*",
],
},
"cloud": {
"read:own": [
"*",
],
},
"config": {
"read:any": [
"*",
],
"update:own": [
"*",
],
},
"connect": {
"read:own": [
"*",
],
"update:own": [
"*",
],
},
"cpu": {
"read:any": [
"*",
],
},
"crash-reporting-enabled": {
"read:any": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"device": {
"read:any": [
"*",
],
},
"device/unassigned": {
"read:any": [
"*",
],
},
"disk": {
"read:any": [
"*",
],
},
"disk/settings": {
"read:any": [
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"docker/container": {
"read:any": [
"*",
],
},
"docker/network": {
"read:any": [
"*",
],
},
"flash": {
"read:any": [
"*",
],
},
"info": {
"read:any": [
"*",
],
},
"license-key": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"machine-id": {
"read:any": [
"*",
],
},
"memory": {
"read:any": [
"*",
],
},
"notifications": {
"create:any": [
"*",
],
"read:any": [
"*",
],
},
"online": {
"read:any": [
"*",
],
},
"os": {
"read:any": [
"*",
],
},
"owner": {
"read:any": [
"*",
],
},
"parity-history": {
"read:any": [
"*",
],
},
"permission": {
"read:any": [
"*",
],
},
"registration": {
"read:any": [
"*",
],
},
"servers": {
"read:any": [
"*",
],
},
"service": {
"read:any": [
"*",
],
},
"service/emhttpd": {
"read:any": [
"*",
],
},
"service/unraid-api": {
"read:any": [
"*",
],
},
"services": {
"read:any": [
"*",
],
},
"share": {
"read:any": [
"*",
],
},
"software-versions": {
"read:any": [
"*",
],
},
"unraid-version": {
"read:any": [
"*",
],
},
"uptime": {
"read:any": [
"*",
],
},
"user": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
"vms": {
"read:any": [
"*",
],
},
"vms/domain": {
"read:any": [
"*",
],
},
"vms/network": {
"read:any": [
"*",
],
},
},
"guest": {
"me": {
"read:any": [
"*",
],
},
"welcome": {
"read:any": [
"*",
],
},
},
"my_servers": {
"$extend": [
"guest",
],
"array": {
"read:any": [
"*",
],
},
"config": {
"read:any": [
"*",
],
},
"connect": {
"read:any": [
"*",
],
},
"connect/dynamic-remote-access": {
"read:any": [
"*",
],
"update:own": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"dashboard": {
"read:any": [
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"docker": {
"read:any": [
"*",
],
},
"docker/container": {
"read:any": [
"*",
],
},
"info": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"network": {
"read:any": [
"*",
],
},
"notifications": {
"read:any": [
"*",
],
},
"services": {
"read:any": [
"*",
],
},
"unraid-version": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
"vms": {
"read:any": [
"*",
],
},
"vms/domain": {
"read:any": [
"*",
],
},
},
"notifier": {
"$extend": [
"guest",
],
"notifications": {
"create:own": [
"*",
],
},
},
"upc": {
"$extend": [
"guest",
],
"apikey": {
"read:own": [
"*",
],
},
"cloud": {
"read:own": [
"*",
],
},
"config": {
"read:any": [
"*",
],
"update:own": [
"*",
],
},
"connect": {
"read:own": [
"*",
],
"update:own": [
"*",
],
},
"crash-reporting-enabled": {
"read:any": [
"*",
],
},
"customizations": {
"read:any": [
"*",
],
},
"disk": {
"read:any": [
"*",
],
},
"display": {
"read:any": [
"*",
],
},
"flash": {
"read:any": [
"*",
],
},
"info": {
"read:any": [
"*",
],
},
"logs": {
"read:any": [
"*",
],
},
"notifications": {
"read:any": [
"*",
],
"update:any": [
"*",
],
},
"os": {
"read:any": [
"*",
],
},
"owner": {
"read:any": [
"*",
],
},
"permission": {
"read:any": [
"*",
],
},
"registration": {
"read:any": [
"*",
],
},
"servers": {
"read:any": [
"*",
],
},
"vars": {
"read:any": [
"*",
],
},
},
},
"_isLocked": false,
}
`;

View File

@@ -1,212 +1,209 @@
import { test, expect, vi } from 'vitest';
import { expect, test, vi } from 'vitest';
import { getArrayData } from '@app/core/modules/array/get-array-data';
import { store } from '@app/store';
import { loadConfigFile } from '@app/store/modules/config';
import { loadStateFiles } from '@app/store/modules/emhttp';
vi.mock('@app/core/pubsub', () => ({
pubsub: { publish: vi.fn() },
}));
test('Creates an array event', async () => {
const { getArrayData } = await import(
'@app/core/modules/array/get-array-data'
);
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { loadConfigFile } = await import('@app/store/modules/config');
// Load state files into store
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
const arrayEvent = getArrayData(store.getState);
expect(arrayEvent).toMatchObject(
{
"boot": {
"comment": "Unraid OS boot device",
"critical": null,
"device": "sda",
"exportable": true,
"format": "unknown",
"fsFree": 3191407,
"fsSize": 4042732,
"fsType": "vfat",
"fsUsed": 851325,
"id": "Cruzer",
"idx": 32,
"name": "flash",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": true,
"size": 3956700,
"status": "DISK_OK",
"temp": null,
"transport": "usb",
"type": "Flash",
"warning": null,
expect(arrayEvent).toMatchObject({
boot: {
comment: 'Unraid OS boot device',
critical: null,
device: 'sda',
exportable: true,
format: 'unknown',
fsFree: 3191407,
fsSize: 4042732,
fsType: 'vfat',
fsUsed: 851325,
id: 'Cruzer',
idx: 32,
name: 'flash',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: true,
size: 3956700,
status: 'DISK_OK',
temp: null,
transport: 'usb',
type: 'Flash',
warning: null,
},
"caches": [
{
"comment": "",
"critical": null,
"device": "sdi",
"exportable": false,
"format": "MBR: 4KiB-aligned",
"fsFree": 111810683,
"fsSize": 250059317,
"fsType": "btrfs",
"fsUsed": 137273827,
"id": "Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z",
"idx": 30,
"name": "cache",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": false,
"size": 244198552,
"status": "DISK_OK",
"temp": 22,
"transport": "ata",
"type": "Cache",
"warning": null,
},
{
"comment": null,
"critical": null,
"device": "nvme0n1",
"exportable": false,
"format": "MBR: 4KiB-aligned",
"fsFree": null,
"fsSize": null,
"fsType": null,
"fsUsed": null,
"id": "KINGSTON_SA2000M8250G_50026B7282669D9E",
"idx": 31,
"name": "cache2",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": false,
"size": 244198552,
"status": "DISK_OK",
"temp": 27,
"transport": "nvme",
"type": "Cache",
"warning": null,
},
caches: [
{
comment: '',
critical: null,
device: 'sdi',
exportable: false,
format: 'MBR: 4KiB-aligned',
fsFree: 111810683,
fsSize: 250059317,
fsType: 'btrfs',
fsUsed: 137273827,
id: 'Samsung_SSD_850_EVO_250GB_S2R5NX0H643734Z',
idx: 30,
name: 'cache',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: false,
size: 244198552,
status: 'DISK_OK',
temp: 22,
transport: 'ata',
type: 'Cache',
warning: null,
},
{
comment: null,
critical: null,
device: 'nvme0n1',
exportable: false,
format: 'MBR: 4KiB-aligned',
fsFree: null,
fsSize: null,
fsType: null,
fsUsed: null,
id: 'KINGSTON_SA2000M8250G_50026B7282669D9E',
idx: 31,
name: 'cache2',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: false,
size: 244198552,
status: 'DISK_OK',
temp: 27,
transport: 'nvme',
type: 'Cache',
warning: null,
},
],
"capacity": {
"disks": {
"free": "27",
"total": "30",
"used": "3",
},
"kilobytes": {
"free": "19495825571",
"total": "41994745901",
"used": "22498920330",
},
capacity: {
disks: {
free: '27',
total: '30',
used: '3',
},
kilobytes: {
free: '19495825571',
total: '41994745901',
used: '22498920330',
},
},
"disks": [
{
"comment": "Seagate Exos",
"critical": 75,
"device": "sdf",
"exportable": false,
"format": "GPT: 4KiB-aligned",
"fsFree": 13882739732,
"fsSize": 17998742753,
"fsType": "xfs",
"fsUsed": 4116003021,
"id": "ST18000NM000J-2TV103_ZR5B1W9X",
"idx": 1,
"name": "disk1",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": true,
"size": 17578328012,
"status": "DISK_OK",
"temp": 30,
"transport": "ata",
"type": "Data",
"warning": 50,
},
{
"comment": "",
"critical": null,
"device": "sdj",
"exportable": false,
"format": "GPT: 4KiB-aligned",
"fsFree": 93140746,
"fsSize": 11998001574,
"fsType": "xfs",
"fsUsed": 11904860828,
"id": "WDC_WD120EDAZ-11F3RA0_5PJRD45C",
"idx": 2,
"name": "disk2",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": true,
"size": 11718885324,
"status": "DISK_OK",
"temp": 30,
"transport": "ata",
"type": "Data",
"warning": null,
},
{
"comment": "",
"critical": null,
"device": "sde",
"exportable": false,
"format": "GPT: 4KiB-aligned",
"fsFree": 5519945093,
"fsSize": 11998001574,
"fsType": "xfs",
"fsUsed": 6478056481,
"id": "WDC_WD120EMAZ-11BLFA0_5PH8BTYD",
"idx": 3,
"name": "disk3",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": true,
"size": 11718885324,
"status": "DISK_OK",
"temp": 30,
"transport": "ata",
"type": "Data",
"warning": null,
},
disks: [
{
comment: 'Seagate Exos',
critical: 75,
device: 'sdf',
exportable: false,
format: 'GPT: 4KiB-aligned',
fsFree: 13882739732,
fsSize: 17998742753,
fsType: 'xfs',
fsUsed: 4116003021,
id: 'ST18000NM000J-2TV103_ZR5B1W9X',
idx: 1,
name: 'disk1',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: true,
size: 17578328012,
status: 'DISK_OK',
temp: 30,
transport: 'ata',
type: 'Data',
warning: 50,
},
{
comment: '',
critical: null,
device: 'sdj',
exportable: false,
format: 'GPT: 4KiB-aligned',
fsFree: 93140746,
fsSize: 11998001574,
fsType: 'xfs',
fsUsed: 11904860828,
id: 'WDC_WD120EDAZ-11F3RA0_5PJRD45C',
idx: 2,
name: 'disk2',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: true,
size: 11718885324,
status: 'DISK_OK',
temp: 30,
transport: 'ata',
type: 'Data',
warning: null,
},
{
comment: '',
critical: null,
device: 'sde',
exportable: false,
format: 'GPT: 4KiB-aligned',
fsFree: 5519945093,
fsSize: 11998001574,
fsType: 'xfs',
fsUsed: 6478056481,
id: 'WDC_WD120EMAZ-11BLFA0_5PH8BTYD',
idx: 3,
name: 'disk3',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: true,
size: 11718885324,
status: 'DISK_OK',
temp: 30,
transport: 'ata',
type: 'Data',
warning: null,
},
],
"id": expect.any(String),
"parities": [
{
"comment": null,
"critical": null,
"device": "sdh",
"exportable": false,
"format": "GPT: 4KiB-aligned",
"fsFree": null,
"fsSize": null,
"fsType": null,
"fsUsed": null,
"id": "ST18000NM000J-2TV103_ZR585CPY",
"idx": 0,
"name": "parity",
"numErrors": 0,
"numReads": 0,
"numWrites": 0,
"rotational": true,
"size": 17578328012,
"status": "DISK_OK",
"temp": 25,
"transport": "ata",
"type": "Parity",
"warning": null,
},
id: expect.any(String),
parities: [
{
comment: null,
critical: null,
device: 'sdh',
exportable: false,
format: 'GPT: 4KiB-aligned',
fsFree: null,
fsSize: null,
fsType: null,
fsUsed: null,
id: 'ST18000NM000J-2TV103_ZR585CPY',
idx: 0,
name: 'parity',
numErrors: 0,
numReads: 0,
numWrites: 0,
rotational: true,
size: 17578328012,
status: 'DISK_OK',
temp: 25,
transport: 'ata',
type: 'Parity',
warning: null,
},
],
"state": "STOPPED",
}
);
state: 'STOPPED',
});
});

View File

@@ -0,0 +1,22 @@
import { expect, test, vi } from 'vitest';
import { ConsoleNotifier } from '@app/core/notifiers/console';
vi.mock('@app/core/log', () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
graphqlLogger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
test('Creates a console notifier', () => {
const notifier = new ConsoleNotifier();
expect(notifier.level).toBe('info');
expect(notifier.template).toBe('{{{ data }}}');
});

View File

@@ -0,0 +1,24 @@
import { expect, test, vi } from 'vitest';
import { UnraidLocalNotifier } from '@app/core/notifiers/unraid-local';
vi.mock('@app/core/log', () => ({
logger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
graphqlLogger: {
info: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
}));
test('Creates an email notifier', () => {
const notifier = new UnraidLocalNotifier({ level: 'info' });
expect(notifier.level).toBe('normal');
expect(notifier.template).toBe('{{ message }}');
const rendered = notifier.render({ message: 'Remote access started' });
expect(rendered).toEqual('Remote access started');
});

View File

@@ -1,8 +0,0 @@
import 'reflect-metadata';
import { expect, test } from 'vitest';
import { setupPermissions } from '@app/core/permissions';
test('Returns default permissions', () => {
expect(setupPermissions()).toMatchSnapshot();
});

View File

@@ -0,0 +1,23 @@
import { expect, test, vi } from 'vitest';
import type { SliceState } from '@app/store/modules/emhttp';
import { getters } from '@app/store';
test('Returns true if the array is started', async () => {
vi.spyOn(getters, 'emhttp').mockImplementation(
() => ({ var: { mdState: 'STARTED' } }) as unknown as SliceState
);
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running');
expect(arrayIsRunning()).toBe(true);
vi.spyOn(getters, 'emhttp').mockReset();
});
test('Returns false if the array is stopped', async () => {
vi.spyOn(getters, 'emhttp').mockImplementation(
() => ({ var: { mdState: 'Stopped' } }) as unknown as SliceState
);
const { arrayIsRunning } = await import('@app/core/utils/array/array-is-running');
expect(arrayIsRunning()).toBe(false);
vi.spyOn(getters, 'emhttp').mockReset();
});

View File

@@ -10,77 +10,74 @@ test('it creates a FLASH config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "",
"version": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a MEMORY config with NO OPTIONAL values', () => {
const basicConfig = initialState;
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a FLASH config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
// 2fa & t2fa should be ignored
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
@@ -90,40 +87,37 @@ test('it creates a FLASH config with OPTIONAL values', () => {
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'flash');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});
test('it creates a MEMORY config with OPTIONAL values', () => {
const basicConfig = cloneDeep(initialState);
// 2fa & t2fa should be ignored
// 2fa & t2fa should be ignored
basicConfig.remote['2Fa'] = 'yes';
basicConfig.local['2Fa'] = 'yes';
basicConfig.local.showT2Fa = 'yes';
@@ -132,38 +126,35 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
basicConfig.connectionStatus.upnpStatus = 'Turned On';
const config = getWriteableConfig(basicConfig, 'memory');
expect(config).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "Turned On",
},
"local": {},
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
"upc": {
"apikey": "",
},
}
`);
{
"api": {
"extraOrigins": "myextra.origins",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "Turned On",
},
"local": {
"sandbox": "no",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "yes",
"username": "",
"wanaccess": "",
"wanport": "",
},
}
`);
});

View File

@@ -1,7 +1,8 @@
import { test, expect } from 'vitest';
import { parse } from 'ini';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { Serializer } from 'multi-ini';
import { expect, test } from 'vitest';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
test('MultiIni breaks when serializing an object with a boolean inside', async () => {
const objectToSerialize = {

View File

@@ -1,23 +1,23 @@
import { getBannerPathIfPresent, getCasePathIfPresent } from "@app/core/utils/images/image-file-helpers";
import { loadDynamixConfigFile } from "@app/store/actions/load-dynamix-config-file";
import { store } from "@app/store/index";
import { expect, test } from 'vitest';
import { expect, test } from "vitest";
import { getBannerPathIfPresent, getCasePathIfPresent } from '@app/core/utils/images/image-file-helpers';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
import { store } from '@app/store/index';
test('get case path returns expected result', () => {
expect(getCasePathIfPresent()).resolves.toContain('/dev/dynamix/case-model.png')
})
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)', () => {
expect(getBannerPathIfPresent()).resolves.toMatchInlineSnapshot('null')
})
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();
expect(getBannerPathIfPresent()).resolves.toContain('/dev/dynamix/banner.png');
})
test('get banner path returns the banner (state loaded)', async () => {
await store.dispatch(loadDynamixConfigFile()).unwrap();
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();
expect(getBannerPathIfPresent('notabanner.png')).resolves.toMatchInlineSnapshot('null');
});
await expect(getBannerPathIfPresent('notabanner.png')).resolves.toMatchInlineSnapshot('null');
});

View File

@@ -0,0 +1,8 @@
import { expect, test } from 'vitest';
import { cleanStdout } from '@app/core/utils/misc/clean-stdout';
test('Returns trimmed stdout from execa command', () => {
expect(cleanStdout({ stdout: 'test' })).toBe('test');
expect(cleanStdout({ stdout: 'test ' })).toBe('test');
});

View File

@@ -0,0 +1,64 @@
import { expect, test } from 'vitest';
import { store } from '@app/store';
import { FileLoadStatus, StateFileKey } from '@app/store/types';
import '@app/core/utils/misc/get-key-file';
import '@app/store/modules/emhttp';
test('Before loading key returns null', async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
const { status } = store.getState().registration;
expect(status).toBe(FileLoadStatus.UNLOADED);
await expect(getKeyFile()).resolves.toBe(null);
});
test('Requires emhttp to be loaded to find key file', async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
const { loadRegistrationKey } = await import('@app/store/modules/registration');
// Load registration key into store
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.toBe(null);
});
test('Returns empty key if key location is empty', async () => {
const { getKeyFile } = await import('@app/core/utils/misc/get-key-file');
const { updateEmhttpState } = await import('@app/store/modules/emhttp');
// Set key file location as empty
// This should only happen if the user doesn't have a key file
store.dispatch(
updateEmhttpState({
field: StateFileKey.var,
state: {
regFile: '',
},
})
);
// Check if store has state files loaded
const { status } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
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');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
// Load state files into store
await store.dispatch(loadStateFiles());
// 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"'
);
});

View File

@@ -1,9 +1,11 @@
import { test, expect } from 'vitest';
import { parseConfig } from '@app/core/utils/misc/parse-config';
import { Parser as MultiIniParser } from 'multi-ini';
import { readFile, writeFile } from 'fs/promises';
import { parse } from 'ini';
import { Parser as MultiIniParser } from 'multi-ini';
import { expect, test } from 'vitest';
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
import { parseConfig } from '@app/core/utils/misc/parse-config';
const iniTestData = `["root"]
idx="0"
@@ -22,11 +24,11 @@ desc=""
passwd="no"`;
test('it loads a config from a passed in ini file successfully', () => {
const res = parseConfig<any>({
file: iniTestData,
type: 'ini',
});
expect(res).toMatchInlineSnapshot(`
const res = parseConfig<any>({
file: iniTestData,
type: 'ini',
});
expect(res).toMatchInlineSnapshot(`
{
"root": {
"desc": "Console and webGui login account",
@@ -48,26 +50,26 @@ test('it loads a config from a passed in ini file successfully', () => {
},
}
`);
expect(res?.root.desc).toEqual('Console and webGui login account');
expect(res?.root.desc).toEqual('Console and webGui login account');
});
test('it loads a config from disk properly', () => {
const path = './dev/states/var.ini';
const res = parseConfig<any>({ filePath: path, type: 'ini' });
expect(res.DOMAIN_SHORT).toEqual(undefined);
expect(res.domainShort).toEqual('');
expect(res.shareCount).toEqual('0');
const path = './dev/states/var.ini';
const res = parseConfig<any>({ filePath: path, type: 'ini' });
expect(res.DOMAIN_SHORT).toEqual(undefined);
expect(res.domainShort).toEqual('');
expect(res.shareCount).toEqual('0');
});
test('Confirm Multi-Ini Parser Still Broken', () => {
const parser = new MultiIniParser();
const res = parser.parse(iniTestData);
expect(res).toMatchInlineSnapshot('{}');
const parser = new MultiIniParser();
const res = parser.parse(iniTestData);
expect(res).toMatchInlineSnapshot('{}');
});
test('Combine Ini and Multi-Ini to read and then write a file with quotes', async () => {
const parsedFile = parse(iniTestData);
expect(parsedFile).toMatchInlineSnapshot(`
const parsedFile = parse(iniTestData);
expect(parsedFile).toMatchInlineSnapshot(`
{
"root": {
"desc": "Console and webGui login account",
@@ -90,10 +92,10 @@ test('Combine Ini and Multi-Ini to read and then write a file with quotes', asyn
}
`);
const ini = safelySerializeObjectToIni(parsedFile);
await writeFile('/tmp/test.ini', ini);
const file = await readFile('/tmp/test.ini', 'utf-8');
expect(file).toMatchInlineSnapshot(`
const ini = safelySerializeObjectToIni(parsedFile);
await writeFile('/tmp/test.ini', ini);
const file = await readFile('/tmp/test.ini', 'utf-8');
expect(file).toMatchInlineSnapshot(`
"[root]
idx="0"
name="root"

View File

@@ -1,12 +1,13 @@
import { expect, test } from 'vitest';
import { getShares } from '@app/core/utils/shares/get-shares';
import { store } from '@app/store';
import { loadStateFiles } from '@app/store/modules/emhttp';
test('Returns both disk and user shares', async () => {
await store.dispatch(loadStateFiles());
await store.dispatch(loadStateFiles());
expect(getShares()).toMatchInlineSnapshot(`
expect(getShares()).toMatchInlineSnapshot(`
{
"disks": [],
"users": [
@@ -96,8 +97,8 @@ test('Returns both disk and user shares', async () => {
});
test('Returns shares by type', async () => {
await store.dispatch(loadStateFiles());
expect(getShares('user')).toMatchInlineSnapshot(`
await store.dispatch(loadStateFiles());
expect(getShares('user')).toMatchInlineSnapshot(`
{
"allocator": "highwater",
"cachePool": "cache",
@@ -119,7 +120,7 @@ test('Returns shares by type', async () => {
"used": 33619300,
}
`);
expect(getShares('users')).toMatchInlineSnapshot(`
expect(getShares('users')).toMatchInlineSnapshot(`
[
{
"allocator": "highwater",
@@ -203,12 +204,12 @@ test('Returns shares by type', async () => {
},
]
`);
expect(getShares('disk')).toMatchInlineSnapshot('null');
expect(getShares('disks')).toMatchInlineSnapshot('[]');
expect(getShares('disk')).toMatchInlineSnapshot('null');
expect(getShares('disks')).toMatchInlineSnapshot('[]');
});
test('Returns shares by name', async () => {
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
expect(getShares('user', { name: 'domains' })).toMatchInlineSnapshot(`
{
"allocator": "highwater",
"cachePool": "cache",
@@ -230,8 +231,8 @@ test('Returns shares by name', async () => {
"used": 33619300,
}
`);
expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null');
// @TODO: disk shares need to be added to the dev ini files
expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null');
expect(getShares('disk', { name: 'non-existent-disk-share' })).toMatchInlineSnapshot('null');
expect(getShares('user', { name: 'non-existent-user-share' })).toMatchInlineSnapshot('null');
// @TODO: disk shares need to be added to the dev ini files
expect(getShares('disk', { name: 'disk1' })).toMatchInlineSnapshot('null');
expect(getShares('disk', { name: 'non-existent-disk-share' })).toMatchInlineSnapshot('null');
});

View File

@@ -0,0 +1,34 @@
import { afterEach, expect, test, vi } from 'vitest';
import { checkDNS } from '@app/graphql/resolvers/query/cloud/check-dns';
import { store } from '@app/store';
import { clearKey } from '@app/store/modules/cache';
import { CacheKeys } from '@app/store/types';
afterEach(() => {
store.dispatch(clearKey(CacheKeys.checkDns));
});
test('it resolves dns successfully', async () => {
// @TODO
const dns = await checkDNS('example.com');
expect(dns.cloudIp).not.toBeNull();
}, 25_000);
test('testing twice results in a cache hit', async () => {
// Hit mothership
const getters = await import('@app/store/getters');
const dnsSpy = vi.spyOn(getters, 'getDnsCache');
const dns = await checkDNS();
expect(dns.cloudIp).toBeTypeOf('string');
expect(dnsSpy.mock.results[0]).toMatchInlineSnapshot(`
{
"type": "return",
"value": undefined,
}
`);
const dnslookup2 = await checkDNS();
expect(dnslookup2.cloudIp).toEqual(dns.cloudIp);
expect(dnsSpy.mock.results[1].value.cloudIp).toEqual(dns.cloudIp);
expect(store.getState().cache.nodeCache.getTtl(CacheKeys.checkDns)).toBeGreaterThan(500);
});

View File

@@ -1,10 +1,26 @@
import 'reflect-metadata';
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
import { expect, test } from "vitest";
import packageJson from '@app/../package.json'
import { expect, test } from 'vitest';
import packageJson from '@app/../package.json';
import { checkMothershipAuthentication } from '@app/graphql/resolvers/query/cloud/check-mothership-authentication';
test('It fails to authenticate with mothership with no credentials', async () => {
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`);
expect(packageJson.version).not.toBeNull();
await expect(checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
}, 15_000)
try {
await expect(
checkMothershipAuthentication('BAD', 'BAD')
).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`
);
expect(packageJson.version).not.toBeNull();
await expect(
checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')
).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
} catch (error) {
if (error instanceof Error && error.message.includes('Timeout')) {
// Test succeeds on timeout
return;
}
throw error;
}
});

View File

@@ -1,124 +1,197 @@
import { expect, test } from 'vitest';
import { type Nginx } from '../../../../core/types/states/nginx';
import { getUrlForField, getUrlForServer, getServerIps, type NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
import type { NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
import { type Nginx } from '@app/core/types/states/nginx';
import {
getServerIps,
getUrlForField,
getUrlForServer,
} from '@app/graphql/resolvers/subscription/network';
import { store } from '@app/store';
import { loadStateFiles } from '@app/store/modules/emhttp';
import { loadConfigFile } from '@app/store/modules/config';
import { loadStateFiles } from '@app/store/modules/emhttp';
test.each([
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
])('getUrlForField', ({ httpPort, httpsPort, url }) => {
const responseInsecure = getUrlForField({
port: httpPort,
url,
});
const responseInsecure = getUrlForField({
port: httpPort,
url,
});
const responseSecure = getUrlForField({
portSsl: httpsPort,
url,
});
if (httpPort === 80) {
expect(responseInsecure.port).toBe('');
} else {
expect(responseInsecure.port).toBe(httpPort.toString());
}
const responseSecure = getUrlForField({
portSsl: httpsPort,
url,
});
if (httpPort === 80) {
expect(responseInsecure.port).toBe('');
} else {
expect(responseInsecure.port).toBe(httpPort.toString());
}
if (httpsPort === 443) {
expect(responseSecure.port).toBe('');
} else {
expect(responseSecure.port).toBe(httpsPort.toString());
}
if (httpsPort === 443) {
expect(responseSecure.port).toBe('');
} else {
expect(responseSecure.port).toBe(httpsPort.toString());
}
});
test('getUrlForServer - field exists, ssl disabled', () => {
const result = getUrlForServer({ nginx: { lanIp: '192.168.1.1', sslEnabled: false, httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
const result = getUrlForServer({
nginx: {
lanIp: '192.168.1.1',
sslEnabled: false,
httpPort: 123,
httpsPort: 445,
} as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
});
test('getUrlForServer - field exists, ssl yes', () => {
const result = getUrlForServer({
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
const result = getUrlForServer({
nginx: {
lanIp: '192.168.1.1',
sslEnabled: true,
sslMode: 'yes',
httpPort: 123,
httpsPort: 445,
} as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
});
test('getUrlForServer - field exists, ssl yes, port empty', () => {
const result = getUrlForServer(
{ nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
const result = getUrlForServer({
nginx: {
lanIp: '192.168.1.1',
sslEnabled: true,
sslMode: 'yes',
httpPort: 80,
httpsPort: 443,
} as const as Nginx,
field: 'lanIp',
});
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
});
test('getUrlForServer - field exists, ssl auto', async () => {
const getResult = async () => getUrlForServer({
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'auto', httpPort: 123, httpsPort: 445 } as const as Nginx,
field: 'lanIp',
});
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`);
const getResult = async () =>
getUrlForServer({
nginx: {
lanIp: '192.168.1.1',
sslEnabled: true,
sslMode: 'auto',
httpPort: 123,
httpsPort: 445,
} as const as Nginx,
field: 'lanIp',
});
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`
);
});
test('getUrlForServer - field does not exist, ssl disabled', async () => {
const getResult = async () => getUrlForServer(
{
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
ports: {
port: ':123', portSsl: ':445', defaultUrl: new URL('https://my-default-url.unraid.net'),
},
// @ts-expect-error Field doesn't exist
field: 'idontexist',
});
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
const getResult = async () =>
getUrlForServer({
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
ports: {
port: ':123',
portSsl: ':445',
defaultUrl: new URL('https://my-default-url.unraid.net'),
},
// @ts-expect-error Field doesn't exist
field: 'idontexist',
});
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`
);
});
test('getUrlForServer - FQDN - field exists, port non-empty', () => {
const result = getUrlForServer({
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
const result = getUrlForServer({
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
});
test('getUrlForServer - FQDN - field exists, port empty', () => {
const result = getUrlForServer({ nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
const result = getUrlForServer({
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
field: 'lanFqdn',
});
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
});
test.each([
[{ nginx: { lanFqdn: 'my-fqdn.unraid.net', sslEnabled: false, sslMode: 'no', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'lanFqdn' as NginxUrlFields }],
[{ nginx: { wanFqdn: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn' as NginxUrlFields }],
[{ nginx: { wanFqdn6: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'auto', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn6' as NginxUrlFields }],
[
{
nginx: {
lanFqdn: 'my-fqdn.unraid.net',
sslEnabled: false,
sslMode: 'no',
httpPort: 80,
httpsPort: 443,
} as const as Nginx,
field: 'lanFqdn' as NginxUrlFields,
},
],
[
{
nginx: {
wanFqdn: 'my-fqdn.unraid.net',
sslEnabled: true,
sslMode: 'yes',
httpPort: 80,
httpsPort: 443,
} as const as Nginx,
field: 'wanFqdn' as NginxUrlFields,
},
],
[
{
nginx: {
wanFqdn6: 'my-fqdn.unraid.net',
sslEnabled: true,
sslMode: 'auto',
httpPort: 80,
httpsPort: 443,
} as const as Nginx,
field: 'wanFqdn6' as NginxUrlFields,
},
],
])('getUrlForServer - FQDN', ({ nginx, field }) => {
const result = getUrlForServer({ nginx, field });
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
const result = getUrlForServer({ nginx, field });
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
});
test('getUrlForServer - field does not exist, ssl disabled', async () => {
const getResult = async () => getUrlForServer({ nginx:
{ lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
// @ts-expect-error Field doesn't exist
field: 'idontexist' });
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
const getResult = async () =>
getUrlForServer({
nginx: { lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
// @ts-expect-error Field doesn't exist
field: 'idontexist',
});
await expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(
`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`
);
});
test('integration test, loading nginx ini and generating all URLs', async () => {
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
await store.dispatch(loadStateFiles());
await store.dispatch(loadConfigFile());
const urls = getServerIps();
expect(urls.urls).toMatchInlineSnapshot(`
const urls = getServerIps();
expect(urls.urls).toMatchInlineSnapshot(`
[
{
"ipv4": "https://tower.local:4443/",
@@ -198,7 +271,7 @@ test('integration test, loading nginx ini and generating all URLs', async () =>
},
]
`);
expect(urls.errors).toMatchInlineSnapshot(`
expect(urls.errors).toMatchInlineSnapshot(`
[
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
]

View File

@@ -4,45 +4,45 @@ import { beforeEach, expect, test, vi } from 'vitest';
import '@app/mothership/utils/convert-to-fuzzy-time';
vi.mock('fs', () => ({
default: {
readFileSync: vi.fn().mockReturnValue('my-file'),
writeFileSync: vi.fn(),
existsSync: vi.fn(),
},
readFileSync: vi.fn().mockReturnValue('my-file'),
existsSync: vi.fn(),
default: {
readFileSync: vi.fn().mockReturnValue('my-file'),
writeFileSync: vi.fn(),
existsSync: vi.fn(),
},
readFileSync: vi.fn().mockReturnValue('my-file'),
existsSync: vi.fn(),
}));
vi.mock('@graphql-tools/schema', () => ({
makeExecutableSchema: vi.fn(),
makeExecutableSchema: vi.fn(),
}));
vi.mock('@app/core/log', () => ({
default: { relayLogger: { trace: vi.fn() } },
relayLogger: { trace: vi.fn() },
logger: { trace: vi.fn() },
default: { relayLogger: { trace: vi.fn() } },
relayLogger: { trace: vi.fn() },
logger: { trace: vi.fn() },
}));
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
vi.resetModules();
vi.clearAllMocks();
});
const generateTestCases = () => {
const cases: Array<{ min: number; max: number }> = [];
for (let i = 0; i < 15; i += 1) {
const min = Math.round(Math.random() * 100);
const max = min + (Math.round(Math.random() * 20));
cases.push({ min, max });
}
const cases: Array<{ min: number; max: number }> = [];
for (let i = 0; i < 15; i += 1) {
const min = Math.round(Math.random() * 100);
const max = min + Math.round(Math.random() * 20);
cases.push({ min, max });
}
return cases;
return cases;
};
test.each(generateTestCases())('Successfully converts to fuzzy time %o', async ({ min, max }) => {
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
const { convertToFuzzyTime } = await import('@app/mothership/utils/convert-to-fuzzy-time');
const res = convertToFuzzyTime(min, max);
expect(res).toBeGreaterThanOrEqual(min);
expect(res).toBeLessThanOrEqual(max);
const res = convertToFuzzyTime(min, max);
expect(res).toBeGreaterThanOrEqual(min);
expect(res).toBeLessThanOrEqual(max);
});

View File

@@ -1,6 +1,7 @@
import { config } from 'dotenv';
config({
path: './.env.test',
debug: false,
encoding: 'utf-8',
})
});

View File

@@ -0,0 +1,6 @@
import { vi } from 'vitest';
vi.mock('@app/core/utils/misc/send-form-to-keyserver', () => {
const sendFormToKeyServer = vi.fn().mockResolvedValue({ body: JSON.stringify({ valid: true }) });
return { sendFormToKeyServer };
});

View File

@@ -0,0 +1,36 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Before init returns default values for all fields 1`] = `
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {
"sandbox": "no",
},
"nodeEnv": "test",
"remote": {
"accesstoken": "",
"allowedOrigins": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"ssoSubIds": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"status": "UNLOADED",
}
`;

View File

@@ -1,46 +1,11 @@
import { expect, test } from 'vitest';
import { store } from '@app/store';
import { MyServersConfigMemory } from '@app/types/my-servers-config';
test('Before init returns default values for all fields', async () => {
const state = store.getState().config;
expect(state).toMatchInlineSnapshot(`
{
"api": {
"extraOrigins": "",
"version": "",
},
"connectionStatus": {
"minigraph": "PRE_INIT",
"upnpStatus": "",
},
"local": {},
"nodeEnv": "test",
"notifier": {
"apikey": "",
},
"remote": {
"accesstoken": "",
"allowedOrigins": "",
"apikey": "",
"avatar": "",
"dynamicRemoteAccessType": "DISABLED",
"email": "",
"idtoken": "",
"localApiKey": "",
"refreshtoken": "",
"regWizTime": "",
"upnpEnabled": "",
"username": "",
"wanaccess": "",
"wanport": "",
},
"status": "UNLOADED",
"upc": {
"apikey": "",
},
}
`);
expect(state).toMatchSnapshot();
}, 10_000);
test('After init returns values from cfg file for all fields', async () => {
@@ -61,11 +26,10 @@ test('After init returns values from cfg file for all fields', async () => {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {},
nodeEnv: 'test',
notifier: {
apikey: 'unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5',
local: {
sandbox: expect.any(String),
},
nodeEnv: 'test',
remote: {
accesstoken: '',
allowedOrigins: '',
@@ -77,15 +41,13 @@ test('After init returns values from cfg file for all fields', async () => {
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no',
username: 'zspearmint',
wanaccess: 'yes',
wanport: '8443',
},
status: 'LOADED',
upc: {
apikey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
},
})
);
});
@@ -114,11 +76,10 @@ test('updateUserConfig merges in changes to current state', async () => {
minigraph: 'PRE_INIT',
upnpStatus: '',
},
local: {},
nodeEnv: 'test',
notifier: {
apikey: 'unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5',
local: {
sandbox: expect.any(String),
},
nodeEnv: 'test',
remote: {
accesstoken: '',
allowedOrigins: '',
@@ -130,15 +91,13 @@ test('updateUserConfig merges in changes to current state', async () => {
localApiKey: '_______________________LOCAL_API_KEY_HERE_________________________',
refreshtoken: '',
regWizTime: '1611175408732_0951-1653-3509-FBA155FA23C0',
ssoSubIds: '',
upnpEnabled: 'no',
username: 'zspearmint',
wanaccess: 'yes',
wanport: '8443',
},
status: 'LOADED',
upc: {
apikey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
},
})
} as MyServersConfigMemory)
);
});

View File

@@ -1,4 +1,5 @@
import { test, expect } from 'vitest';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import { FileLoadStatus } from '@app/store/types';
@@ -6,9 +7,9 @@ import { FileLoadStatus } from '@app/store/types';
import '@app/store/modules/emhttp';
test('Before init returns default values for all fields', async () => {
const { status, ...state } = store.getState().emhttp;
expect(status).toBe(FileLoadStatus.UNLOADED);
expect(state).toMatchInlineSnapshot(`
const { status, ...state } = store.getState().emhttp;
expect(status).toBe(FileLoadStatus.UNLOADED);
expect(state).toMatchInlineSnapshot(`
{
"devices": [],
"disks": [],
@@ -24,16 +25,27 @@ test('Before init returns default values for all fields', async () => {
});
test('After init returns values from cfg file for all fields', async () => {
const { loadStateFiles } = await import('@app/store/modules/emhttp');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
// Load state files into store
await store.dispatch(loadStateFiles());
// Load state files into store
await store.dispatch(loadStateFiles());
// Check if store has state files loaded
const { devices, networks, nfsShares, nginx, shares, disks, smbShares, status, users, var: varState } = store.getState().emhttp;
expect(status).toBe(FileLoadStatus.LOADED);
expect(devices).toMatchInlineSnapshot('[]');
expect(networks).toMatchInlineSnapshot(`
// Check if store has state files loaded
const {
devices,
networks,
nfsShares,
nginx,
shares,
disks,
smbShares,
status,
users,
var: varState,
} = store.getState().emhttp;
expect(status).toBe(FileLoadStatus.LOADED);
expect(devices).toMatchInlineSnapshot('[]');
expect(networks).toMatchInlineSnapshot(`
[
{
"bonding": true,
@@ -99,7 +111,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(nginx).toMatchInlineSnapshot(`
expect(nginx).toMatchInlineSnapshot(`
{
"certificateName": "*.thisisfourtyrandomcharacters012345678900.myunraid.net",
"certificatePath": "/boot/config/ssl/certs/certificate_bundle.pem",
@@ -184,7 +196,7 @@ test('After init returns values from cfg file for all fields', async () => {
"wanIp": "",
}
`);
expect(disks).toMatchInlineSnapshot(`
expect(disks).toMatchInlineSnapshot(`
[
{
"comment": null,
@@ -356,7 +368,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(shares).toMatchInlineSnapshot(`
expect(shares).toMatchInlineSnapshot(`
[
{
"allocator": "highwater",
@@ -432,7 +444,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(nfsShares).toMatchInlineSnapshot(`
expect(nfsShares).toMatchInlineSnapshot(`
[
{
"enabled": false,
@@ -620,7 +632,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(smbShares).toMatchInlineSnapshot(`
expect(smbShares).toMatchInlineSnapshot(`
[
{
"caseSensitive": "auto",
@@ -911,7 +923,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(users).toMatchInlineSnapshot(`
expect(users).toMatchInlineSnapshot(`
[
{
"description": "Console and webGui login account",
@@ -936,7 +948,7 @@ test('After init returns values from cfg file for all fields', async () => {
},
]
`);
expect(varState).toMatchInlineSnapshot(`
expect(varState).toMatchInlineSnapshot(`
{
"bindMgt": false,
"cacheNumDevices": NaN,

View File

@@ -1,14 +0,0 @@
import { setupNotificationWatch } from '@app/core/modules/notifications/setup-notification-watch';
import { sleep } from '@app/core/utils/misc/sleep';
import { loadDynamixConfigFile } from '@app/store/actions/load-dynamix-config-file';
import { store } from '@app/store/index';
import { expect, test } from 'vitest';
test('loads notifications properly', async () => {
await store.dispatch(loadDynamixConfigFile()).unwrap();
const watch = await setupNotificationWatch();
expect(watch).not.toBeNull();
await sleep(400);
expect(store.getState().notifications.notifications).toMatchSnapshot();
await watch?.close();
});

View File

@@ -1,4 +1,5 @@
import { expect, test } from 'vitest';
import { store } from '@app/store';
test('Returns paths', async () => {

View File

@@ -0,0 +1,66 @@
import { expect, test } from 'vitest';
import { store } from '@app/store';
import { loadRegistrationKey } from '@app/store/modules/registration';
import { FileLoadStatus, StateFileKey } from '@app/store/types';
// Preloading imports for faster tests
test('Before loading key returns null', async () => {
const { status, keyFile } = store.getState().registration;
expect(status).toBe(FileLoadStatus.UNLOADED);
expect(keyFile).toBe(null);
});
test('Requires emhttp to be loaded to find key file', async () => {
// Load registration key into store
await store.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status, keyFile } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
expect(keyFile).toBe(null);
});
test('Returns empty key if key location is empty', async () => {
const { updateEmhttpState } = await import('@app/store/modules/emhttp');
const { loadRegistrationKey } = await import('@app/store/modules/registration');
// Set key file location as empty
// This should only happen if the user doesn't have a key file
store.dispatch(
updateEmhttpState({
field: StateFileKey.var,
state: {
regFile: '',
},
})
);
// Load registration key into store
await store.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status, keyFile } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
expect(keyFile).toBe('');
});
test('Returns decoded key file if key location exists', async () => {
const { loadRegistrationKey } = await import('@app/store/modules/registration');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
// Load state files into store
await store.dispatch(loadStateFiles());
// Load registration key into store
await store.dispatch(loadRegistrationKey());
// Check if store has state files loaded
const { status, keyFile } = store.getState().registration;
expect(status).toBe(FileLoadStatus.LOADED);
expect(keyFile).toMatchInlineSnapshot(
'"hVs1tLjvC9FiiQsIwIQ7G1KszAcexf0IneThhnmf22SB0dGs5WzRkqMiSMmt2DtR5HOXFUD32YyxuzGeUXmky3zKpSu6xhZNKVg5atGM1OfvkzHBMldI3SeBLuUFSgejLbpNUMdTrbk64JJdbzle4O8wiQgkIpAMIGxeYLwLBD4zHBcfyzq40QnxG--HcX6j25eE0xqa2zWj-j0b0rCAXahJV2a3ySCbPzr1MvfPRTVb0rr7KJ-25R592hYrz4H7Sc1B3p0lr6QUxHE6o7bcYrWKDRtIVoZ8SMPpd1_0gzYIcl5GsDFzFumTXUh8NEnl0Q8hwW1YE-tRc6Y_rrvd7w"'
);
});

View File

@@ -0,0 +1,18 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import type { DevicesIni } from '@app/store/state-parsers/devices';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/devices');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'devs.ini');
const stateFile = parseConfig<DevicesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot('[]');
});

View File

@@ -0,0 +1,83 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import type { NetworkIni } from '@app/store/state-parsers/network';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/network');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'network.ini');
const stateFile = parseConfig<NetworkIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"bonding": true,
"bondingMiimon": "100",
"bondingMode": "1",
"bondname": "",
"bondnics": [
"eth0",
"eth1",
"eth2",
"eth3",
],
"brfd": "0",
"bridging": true,
"brname": "",
"brnics": "bond0",
"brstp": "0",
"description": [
"",
],
"dhcp6Keepresolv": false,
"dhcpKeepresolv": false,
"dnsServer1": "1.1.1.1",
"dnsServer2": "8.8.8.8",
"gateway": [
"192.168.1.1",
],
"gateway6": [
"",
],
"ipaddr": [
"192.168.1.150",
],
"ipaddr6": [
"",
],
"metric": [
"",
],
"metric6": [
"",
],
"mtu": "",
"netmask": [
"255.255.255.0",
],
"netmask6": [
"",
],
"privacy6": [
"",
],
"protocol": [
"",
],
"type": "access",
"useDhcp": [
true,
],
"useDhcp6": [
false,
],
},
]
`);
});

View File

@@ -0,0 +1,205 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import type { NfsSharesIni } from '@app/store/state-parsers/nfs';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/nfs');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'sec_nfs.ini');
const stateFile = parseConfig<NfsSharesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"enabled": false,
"hostList": "",
"name": "disk1",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk2",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk3",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk4",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk5",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk6",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk7",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk8",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk9",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk10",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk11",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk12",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk13",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk14",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk15",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk16",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk17",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk18",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk19",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk20",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk21",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "disk22",
"readList": [],
"security": "public",
"writeList": [],
},
{
"enabled": false,
"hostList": "",
"name": "abc",
"readList": [],
"security": "public",
"writeList": [],
},
]
`);
});

View File

@@ -1,16 +1,18 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { NginxIni } from '@app/store/state-parsers/nginx';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/nginx');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'nginx.ini');
const stateFile = parseConfig<NginxIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchSnapshot();
});
const { parse } = await import('@app/store/state-parsers/nginx');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'nginx.ini');
const stateFile = parseConfig<NginxIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchSnapshot();
});

View File

@@ -1,18 +1,20 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { SharesIni } from '@app/store/state-parsers/shares';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/shares');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'shares.ini');
const stateFile = parseConfig<SharesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
const { parse } = await import('@app/store/state-parsers/shares');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'shares.ini');
const stateFile = parseConfig<SharesIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"allocator": "highwater",

View File

@@ -1,18 +1,20 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { SlotsIni } from '@app/store/state-parsers/slots';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/slots');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'disks.ini');
const stateFile = parseConfig<SlotsIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
const { parse } = await import('@app/store/state-parsers/slots');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'disks.ini');
const stateFile = parseConfig<SlotsIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"comment": null,

View File

@@ -0,0 +1,308 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import type { SmbIni } from '@app/store/state-parsers/smb';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/smb');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'sec.ini');
const stateFile = parseConfig<SmbIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk1",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk2",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk3",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk4",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk5",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk6",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk7",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk8",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk9",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk10",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk11",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk12",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk13",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk14",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk15",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk16",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk17",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk18",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk19",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk20",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk21",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "disk22",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"caseSensitive": "auto",
"enabled": true,
"fruit": "no",
"name": "abc",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
{
"enabled": true,
"fruit": "no",
"name": "flash",
"readList": [],
"security": "public",
"timemachine": {
"volsizelimit": NaN,
},
"writeList": [],
},
]
`);
});

View File

@@ -0,0 +1,42 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import type { UsersIni } from '@app/store/state-parsers/users';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/users');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'users.ini');
const stateFile = parseConfig<UsersIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
[
{
"description": "Console and webGui login account",
"id": "0",
"name": "root",
"password": true,
"role": "admin",
},
{
"description": "",
"id": "1",
"name": "xo",
"password": true,
"role": "user",
},
{
"description": "",
"id": "2",
"name": "test_user",
"password": false,
"role": "user",
},
]
`);
});

View File

@@ -1,19 +1,21 @@
import { join } from 'path';
import { expect, test } from 'vitest';
import { store } from '@app/store';
import type { VarIni } from '@app/store/state-parsers/var';
import { store } from '@app/store';
test('Returns parsed state file', async () => {
const { parse } = await import('@app/store/state-parsers/var');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'var.ini');
const stateFile = parseConfig<VarIni>({
filePath,
type: 'ini',
});
const { parse } = await import('@app/store/state-parsers/var');
const { parseConfig } = await import('@app/core/utils/misc/parse-config');
const { paths } = store.getState();
const filePath = join(paths.states, 'var.ini');
const stateFile = parseConfig<VarIni>({
filePath,
type: 'ini',
});
expect(parse(stateFile)).toMatchInlineSnapshot(`
expect(parse(stateFile)).toMatchInlineSnapshot(`
{
"bindMgt": false,
"cacheNumDevices": NaN,

View File

@@ -0,0 +1,30 @@
import { expect, test, vi } from 'vitest';
vi.mock('@app/core/pubsub', () => ({
pubsub: { publish: vi.fn() },
}));
test('Creates a registration event', async () => {
const { createRegistrationEvent } = await import('@app/store/sync/registration-sync');
const { store } = await import('@app/store');
const { loadStateFiles } = await import('@app/store/modules/emhttp');
// Load state files into store
await store.dispatch(loadStateFiles());
const state = store.getState();
const registrationEvent = createRegistrationEvent(state);
expect(registrationEvent).toMatchInlineSnapshot(`
{
"registration": {
"guid": "13FE-4200-C300-58C372A52B19",
"keyFile": {
"contents": null,
"location": "/app/dev/Unraid.net/Pro.key",
},
"state": "PRO",
"type": "PRO",
},
}
`);
});

View File

@@ -0,0 +1,20 @@
import { type Mapping } from '@runonflux/nat-upnp';
import { expect, test, vi } from 'vitest';
import { getWanPortForUpnp } from '@app/upnp/helpers';
test('it successfully gets a wan port given no exclusions', () => {
const port = getWanPortForUpnp(null, 36_000, 38_000);
expect(port).toBeGreaterThan(35_999);
expect(port).toBeLessThan(38_001);
});
test('it fails to get a wan port given exclusions', () => {
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 36_000, 36_000);
expect(port).toBeNull();
});
test('it succeeds in getting a wan port given exclusions', () => {
const port = getWanPortForUpnp([{ public: { port: 36_000 } }] as Mapping[], 30_000, 36_000);
expect(port).not.toBeNull();
});

View File

@@ -1,13 +1,39 @@
#!/usr/bin/env node
import '@app/dotenv';
import { main } from '@app/cli/index';
import { internalLogger } from '@app/core/log';
import { execa } from 'execa';
import { CommandFactory } from 'nest-commander';
import { internalLogger, logger } from '@app/core/log';
import { LOG_LEVEL } from '@app/environment';
import { CliModule } from '@app/unraid-api/cli/cli.module';
import { LogService } from '@app/unraid-api/cli/log.service';
const getUnraidApiLocation = async () => {
try {
const shellToUse = await execa('which unraid-api');
return shellToUse.stdout.trim();
} catch (err) {
logger.debug('Could not find unraid-api in PATH, using default location');
return '/usr/bin/unraid-api';
}
};
try {
await main();
await CommandFactory.run(CliModule, {
cliName: 'unraid-api',
logger: LOG_LEVEL === 'TRACE' ? new LogService() : false, // - enable this to see nest initialization issues
completion: {
fig: false,
cmd: 'completion-script',
nativeShell: { executablePath: await getUnraidApiLocation() },
},
});
process.exit(0);
} catch (error) {
console.log(error);
logger.error('ERROR:', error);
internalLogger.error({
message: 'Failed to start unraid-api',
error,

View File

@@ -1,365 +0,0 @@
import { stdout } from 'process';
import readLine from 'readline';
import { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client/core/index.js';
import ipRegex from 'ip-regex';
import { setEnv } from '@app/cli/set-env';
import { cliLogger } from '@app/core/log';
import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running';
import { API_VERSION } from '@app/environment';
import { MinigraphStatus } from '@app/graphql/generated/api/types';
import { getters, store } from '@app/store';
import { loadConfigFile } from '@app/store/modules/config';
import { loadStateFiles } from '@app/store/modules/emhttp';
import type { getCloudQuery, getServersQuery } from '../../graphql/generated/api/operations';
import { getApiApolloClient } from '../../graphql/client/api/get-api-client';
import { getCloudDocument, getServersDocument } from '../../graphql/generated/api/operations';
type CloudQueryResult = NonNullable<ApolloQueryResult<getCloudQuery>['data']['cloud']>;
type ServersQueryResultServer = NonNullable<ApolloQueryResult<getServersQuery>['data']['servers']>[0];
type Verbosity = '' | '-v' | '-vv';
type ServersPayload = {
online: ServersQueryResultServer[];
offline: ServersQueryResultServer[];
invalid: ServersQueryResultServer[];
};
type ReportObject = {
os: {
serverName: string;
version: string;
};
api: {
version: string;
status: 'running' | 'stopped';
environment: string;
nodeVersion: string;
};
apiKey: 'valid' | 'invalid' | string;
servers?: ServersPayload | null;
myServers: {
status: 'authenticated' | 'signed out';
myServersUsername?: string;
};
minigraph: {
status: MinigraphStatus;
timeout: number | null;
error: string | null;
};
cloud: {
status: string;
error?: string;
ip?: string;
allowedOrigins?: string[] | null;
};
};
// This should return the status of the apiKey and mothership
export const getCloudData = async (
client: ApolloClient<NormalizedCacheObject>
): Promise<CloudQueryResult | null> => {
try {
const cloud = await client.query({ query: getCloudDocument });
return cloud.data.cloud ?? null;
} catch (error: unknown) {
cliLogger.trace(
'Failed fetching cloud from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
return null;
}
};
export const getServersData = async ({
client,
v,
}: {
client: ApolloClient<NormalizedCacheObject>;
v: Verbosity;
}): Promise<ServersPayload | null> => {
if (v === '') {
return null;
}
try {
const servers = await client.query({ query: getServersDocument });
const foundServers = servers.data.servers.reduce<ServersPayload>(
(acc, curr) => {
switch (curr.status) {
case 'online':
acc.online.push(curr);
break;
case 'offline':
acc.offline.push(curr);
break;
default:
acc.invalid.push(curr);
break;
}
return acc;
},
{ online: [], offline: [], invalid: [] }
);
return foundServers;
} catch (error: unknown) {
cliLogger.trace(
'Failed fetching servers from local graphql with "%s"',
error instanceof Error ? error.message : 'Unknown Error'
);
return {
online: [],
offline: [],
invalid: [],
};
}
};
const hashUrlRegex = () => /(.*)([a-z0-9]{40})(.*)/g;
export const anonymiseOrigins = (origins?: string[]): string[] => {
const originsWithoutSocks = origins?.filter((url) => !url.endsWith('.sock')) ?? [];
return originsWithoutSocks
.map((origin) =>
origin
// Replace 40 char hash string with "HASH"
.replace(hashUrlRegex(), '$1HASH$3')
// Replace ipv4 address using . separator with "IPV4ADDRESS"
.replace(ipRegex(), 'IPV4ADDRESS')
// Replace ipv4 address using - separator with "IPV4ADDRESS"
.replace(new RegExp(ipRegex().toString().replace('\\.', '-')), '/IPV4ADDRESS')
// Report WAN port
.replace(`:${getters.config().remote.wanport || 443}`, ':WANPORT')
)
.filter(Boolean);
};
const getAllowedOrigins = (cloud: CloudQueryResult | null, v: Verbosity): string[] | null => {
switch (v) {
case '-vv':
return cloud?.allowedOrigins.filter((url) => !url.endsWith('.sock')) ?? [];
case '-v':
return anonymiseOrigins(cloud?.allowedOrigins ?? []);
default:
return null;
}
};
const getReadableCloudDetails = (reportObject: ReportObject, v: Verbosity): string => {
const error = reportObject.cloud.error ? `\n ERROR [${reportObject.cloud.error}]` : '';
const status = reportObject.cloud.status ? reportObject.cloud.status : 'disconnected';
const ip = reportObject.cloud.ip && v !== '' ? `\n IP: [${reportObject.cloud.ip}]` : '';
return `
STATUS: [${status}] ${ip} ${error}`;
};
const getReadableMinigraphDetails = (reportObject: ReportObject): string => {
const statusLine = `STATUS: [${reportObject.minigraph.status}]`;
const errorLine = reportObject.minigraph.error ? ` ERROR: [${reportObject.minigraph.error}]` : null;
const timeoutLine = reportObject.minigraph.timeout
? ` TIMEOUT: [${(reportObject.minigraph.timeout || 1) / 1_000}s]`
: null; // 1 in case of divide by zero
return `
${statusLine}${errorLine ? `\n${errorLine}` : ''}${timeoutLine ? `\n${timeoutLine}` : ''}`;
};
// Convert server to string output
const serverToString = (v: Verbosity) => (server: ServersQueryResultServer) =>
`${server?.name ?? 'No Server Name'}${
v === '-v' || v === '-vv'
? `[owner="${server.owner?.username ?? 'No Owner Found'}"${
v === '-vv' ? ` guid="${server.guid ?? 'No GUID'}"]` : ']'
}`
: ''
}`;
const getReadableServerDetails = (reportObject: ReportObject, v: Verbosity): string => {
if (!reportObject.servers) {
return '';
}
if (reportObject.api.status === 'stopped') {
return '\nSERVERS: API is offline';
}
const invalid =
(v === '-v' || v === '-vv') && reportObject.servers.invalid.length > 0
? `
INVALID: ${reportObject.servers.invalid.map(serverToString(v)).join(',')}`
: '';
return `
SERVERS:
ONLINE: ${reportObject.servers.online.map(serverToString(v)).join(',')}
OFFLINE: ${reportObject.servers.offline.map(serverToString(v)).join(',')}${invalid}`;
};
const getReadableAllowedOrigins = (reportObject: ReportObject): string => {
const { cloud } = reportObject;
if (cloud?.allowedOrigins) {
return `
ALLOWED_ORIGINS: ${cloud.allowedOrigins.join(', ').trim()}`;
}
return '';
};
const getVerbosity = (argv: string[]): Verbosity => {
if (argv.includes('-v')) {
return '-v';
}
if (argv.includes('-vv')) {
return '-vv';
}
return '';
};
export const report = async (...argv: string[]) => {
// Check if the user has raw output enabled
const rawOutput = argv.includes('--raw');
// Check if we have a tty attached to stdout
// If we don't then this is being piped to a log file, etc.
const hasTty = process.stdout.isTTY;
// Check if we should show interactive logs
// If this has a tty it's interactive
// AND
// If they don't have --raw
const isInteractive = hasTty && !rawOutput;
const stdoutLogger = readLine.createInterface({
input: process.stdin,
output: process.stdout,
});
try {
setEnv('LOG_TYPE', 'raw');
// Show loading message
if (isInteractive) {
stdoutLogger.write('Generating report please wait…');
}
const jsonReport = argv.includes('--json');
const v = getVerbosity(argv);
// Find all processes called "unraid-api" which aren't this process
const unraidApiRunning = await isUnraidApiRunning();
// Load my servers config file into store
await store.dispatch(loadConfigFile());
await store.dispatch(loadStateFiles());
const { config, emhttp } = store.getState();
if (!config.upc.apikey) throw new Error('Missing UPC API key');
const client = getApiApolloClient({ localApiKey: config.remote.localApiKey || '' });
// Fetch the cloud endpoint
const cloud = await getCloudData(client);
// Log cloud response
cliLogger.trace('Cloud response %s', JSON.stringify(cloud, null, 0));
// Query local graphql using upc's API key
// Get the servers array
const servers = await getServersData({ client, v });
// Check if the API key is valid
const isApiKeyValid = cloud?.apiKey.valid ?? false;
const reportObject: ReportObject = {
os: {
serverName: emhttp.var.name,
version: emhttp.var.version,
},
api: {
version: API_VERSION,
status: unraidApiRunning ? 'running' : 'stopped',
environment: process.env.ENVIRONMENT ?? 'THIS_WILL_BE_REPLACED_WHEN_BUILT',
nodeVersion: process.version,
},
apiKey: isApiKeyValid ? 'valid' : (cloud?.apiKey.error ?? 'invalid'),
...(servers ? { servers } : {}),
myServers: {
status: config?.remote?.username ? 'authenticated' : 'signed out',
...(config?.remote?.username
? {
myServersUsername: config?.remote?.username?.includes('@')
? 'REDACTED'
: config?.remote.username,
}
: {}),
},
minigraph: {
status: cloud?.minigraphql.status ?? MinigraphStatus.PRE_INIT,
timeout: cloud?.minigraphql.timeout ?? null,
error:
(cloud?.minigraphql.error ?? !cloud?.minigraphql.status) ? 'API Disconnected' : null,
},
cloud: {
status: cloud?.cloud.status ?? 'error',
...(cloud?.cloud.error ? { error: cloud.cloud.error } : {}),
...(cloud?.cloud.status === 'ok' ? { ip: cloud.cloud.ip ?? 'NO_IP' } : {}),
...(getAllowedOrigins(cloud, v) ? { allowedOrigins: getAllowedOrigins(cloud, v) } : {}),
},
};
// If we have trace logs or the user selected --raw don't clear the screen
if (process.env.LOG_LEVEL !== 'trace' && isInteractive) {
// Clear the original log about the report being generated
readLine.cursorTo(process.stdout, 0, 0);
readLine.clearScreenDown(process.stdout);
}
if (jsonReport) {
stdout.write(JSON.stringify(reportObject) + '\n');
stdoutLogger.close();
return reportObject;
} else {
// Generate the actual report
const report = `
<-----UNRAID-API-REPORT----->
SERVER_NAME: ${reportObject.os.serverName}
ENVIRONMENT: ${reportObject.api.environment}
UNRAID_VERSION: ${reportObject.os.version}
UNRAID_API_VERSION: ${reportObject.api.version}
UNRAID_API_STATUS: ${reportObject.api.status}
API_KEY: ${reportObject.apiKey}
MY_SERVERS: ${reportObject.myServers.status}${
reportObject.myServers.myServersUsername
? `\nMY_SERVERS_USERNAME: ${reportObject.myServers.myServersUsername}`
: ''
}
CLOUD: ${getReadableCloudDetails(reportObject, v)}
MINI-GRAPH: ${getReadableMinigraphDetails(reportObject)}${getReadableServerDetails(
reportObject,
v
)}${getReadableAllowedOrigins(reportObject)}
</----UNRAID-API-REPORT----->
`;
stdout.write(report);
stdoutLogger.close();
return report;
}
} catch (error: unknown) {
console.log({ error });
if (error instanceof Error) {
cliLogger.trace(error);
stdoutLogger.write(`\nFailed generating report with "${error.message}"\n`);
return;
}
stdout.write(`${error as string}`);
stdoutLogger.close();
}
};

View File

@@ -1,10 +0,0 @@
import { start } from '@app/cli/commands/start';
import { stop } from '@app/cli/commands/stop';
/**
* Stop a running API process and then start it again.
*/
export const restart = async () => {
await stop();
await start();
};

View File

@@ -1,16 +0,0 @@
import { PM2_PATH } from '@app/consts';
import { cliLogger } from '@app/core/log';
import { execSync } from 'child_process';
import { join } from 'node:path';
/**
* Start a new API process.
*/
export const start = async () => {
cliLogger.info('Starting unraid-api with command', `${PM2_PATH} start ${join(import.meta.dirname, 'ecosystem.config.json')} --update-env`);
execSync(`${PM2_PATH} start ${join(import.meta.dirname, '../../', 'ecosystem.config.json')} --update-env`, {
env: process.env,
stdio: 'inherit',
cwd: process.cwd()
});
};

View File

@@ -1,7 +0,0 @@
import { PM2_PATH } from '@app/consts';
import { execSync } from 'child_process';
export const status = async () => {
execSync(`${PM2_PATH} status unraid-api`, { stdio: 'inherit' });
process.exit(0);
};

View File

@@ -1,6 +0,0 @@
import { PM2_PATH } from '@app/consts';
import { execSync } from 'child_process';
export const stop = async () => {
execSync(`${PM2_PATH} stop unraid-api`, { stdio: 'inherit' });
};

View File

@@ -1,64 +0,0 @@
import { copyFile, readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { cliLogger } from '@app/core/log';
import { getters } from '@app/store';
import { start } from '@app/cli/commands/start';
import { stop } from '@app/cli/commands/stop';
export const switchEnv = async () => {
const paths = getters.paths();
const basePath = paths['unraid-api-base'];
const envFlashFilePath = paths['myservers-env'];
const envFile = await readFile(envFlashFilePath, 'utf-8').catch(() => '');
await stop();
cliLogger.debug(
'Checking %s for current ENV, found %s',
envFlashFilePath,
envFile
);
// Match the env file env="production" which would be [0] = env="production", [1] = env and [2] = production
const matchArray = /([a-zA-Z]+)=["]*([a-zA-Z]+)["]*/.exec(envFile);
// Get item from index 2 of the regex match or return undefined
const [, , currentEnvInFile] =
matchArray && matchArray.length === 3 ? matchArray : [];
let newEnv = 'production';
// Switch from staging to production
if (currentEnvInFile === 'staging') {
newEnv = 'production';
}
// Switch from production to staging
if (currentEnvInFile === 'production') {
newEnv = 'staging';
}
if (currentEnvInFile) {
cliLogger.debug(
'Switching from "%s" to "%s"...',
currentEnvInFile,
newEnv
);
} else {
cliLogger.debug('No ENV found, setting env to "production"...');
}
// Write new env to flash
const newEnvLine = `env="${newEnv}"`;
await writeFile(envFlashFilePath, newEnvLine);
cliLogger.debug('Writing %s to %s', newEnvLine, envFlashFilePath);
// Copy the new env over to live location before restarting
const source = join(basePath, `.env.${newEnv}`);
const destination = join(basePath, '.env');
cliLogger.debug('Copying %s to %s', source, destination);
await copyFile(source, destination);
cliLogger.info('Now using %s', newEnv);
await start();
};

View File

@@ -1,56 +0,0 @@
import { execSync } from 'child_process';
import type { Flags } from '@app/cli/options';
import { args, mainOptions, options } from '@app/cli/options';
import { setEnv } from '@app/cli/set-env';
import { PM2_PATH } from '@app/consts';
const command = mainOptions.command as unknown as string;
export const main = async (...argv: string[]) => {
if (mainOptions.debug) {
const { cliLogger } = await import('@app/core/log');
const { getters } = await import('@app/store');
const ENVIRONMENT = await import('@app/environment');
cliLogger.debug({ paths: getters.paths(), environment: ENVIRONMENT }, 'Starting CLI');
}
setEnv('PORT', process.env.PORT ?? mainOptions.port ?? '9000');
if (!command) {
// Run help command
const { parse } = await import('ts-command-line-args');
parse<Flags>(args, {
...options,
partial: true,
stopAtFirstUnknown: true,
argv: ['-h'],
});
}
// Only import the command we need when we use it
const commands = {
start: import('@app/cli/commands/start').then((pkg) => pkg.start),
stop: import('@app/cli/commands/stop').then((pkg) => pkg.stop),
restart: import('@app/cli/commands/restart').then((pkg) => pkg.restart),
logs: async () => execSync(`${PM2_PATH} logs unraid-api --lines 200`, { stdio: 'inherit' }),
'switch-env': import('@app/cli/commands/switch-env').then((pkg) => pkg.switchEnv),
version: import('@app/cli/commands/version').then((pkg) => pkg.version),
status: import('@app/cli/commands/status').then((pkg) => pkg.status),
report: import('@app/cli/commands/report').then((pkg) => pkg.report),
'validate-token': import('@app/cli/commands/validate-token').then((pkg) => pkg.validateToken),
};
// Unknown command
if (!Object.keys(commands).includes(command)) {
throw new Error(`Invalid command "${command}"`);
}
// Resolve the command import
const commandMethod = await commands[command];
// Run the command
await commandMethod(...argv);
process.exit(0);
};

View File

@@ -1,12 +1,11 @@
import { getters, type RootState, store } from '@app/store';
import uniq from 'lodash/uniq';
import {
getServerIps,
getUrlForField,
} from '@app/graphql/resolvers/subscription/network';
import { FileLoadStatus } from '@app/store/types';
import { logger } from '../core';
import { uniq } from 'lodash-es';
import type { RootState } from '@app/store';
import { logger } from '@app/core';
import { GRAPHQL_INTROSPECTION } from '@app/environment';
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network';
import { getters, store } from '@app/store';
import { FileLoadStatus } from '@app/store/types';
const getAllowedSocks = (): string[] => [
// Notifier bridge
@@ -19,9 +18,7 @@ const getAllowedSocks = (): string[] => [
'/var/run/unraid-cli.sock',
];
const getLocalAccessUrlsForServer = (
state: RootState = store.getState()
): string[] => {
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
const { emhttp } = state;
if (emhttp.status !== FileLoadStatus.LOADED) {
return [];
@@ -40,22 +37,17 @@ const getLocalAccessUrlsForServer = (
}).toString(),
];
} catch (error: unknown) {
logger.debug(
'Caught error in getLocalAccessUrlsForServer: \n%o',
error
);
logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error);
return [];
}
};
const getRemoteAccessUrlsForAllowedOrigins = (
state: RootState = store.getState()
): string[] => {
const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => {
const { urls } = getServerIps(state);
if (urls) {
return urls.reduce<string[]>((acc, curr) => {
if (curr.ipv4 && curr.ipv6 || curr.ipv4) {
if ((curr.ipv4 && curr.ipv6) || curr.ipv4) {
acc.push(curr.ipv4.toString());
} else if (curr.ipv6) {
acc.push(curr.ipv6.toString());
@@ -74,11 +66,7 @@ export const getExtraOrigins = (): string[] => {
return extraOrigins
.replaceAll(' ', '')
.split(',')
.filter(
(origin) =>
origin.startsWith('http://') ||
origin.startsWith('https://')
);
.filter((origin) => origin.startsWith('http://') || origin.startsWith('https://'));
}
return [];
@@ -99,9 +87,7 @@ const getApolloSandbox = (): string[] => {
return [];
};
export const getAllowedOrigins = (
state: RootState = store.getState()
): string[] =>
export const getAllowedOrigins = (state: RootState = store.getState()): string[] =>
uniq([
...getAllowedSocks(),
...getLocalAccessUrlsForServer(),

View File

@@ -1,4 +1,4 @@
import { uptime } from 'os';
// Get uptime on boot and convert to date
export const bootTimestamp = new Date(new Date().getTime() - (uptime() * 1_000));
export const bootTimestamp = new Date(new Date().getTime() - uptime() * 1_000);

View File

@@ -0,0 +1,15 @@
import { getters } from '@app/store';
import { FileLoadStatus } from '@app/store/types';
/**
* Unraid version string.
* @returns The current version.
*/
export const getUnraidVersion = async (): Promise<string> => {
const { status, var: emhttpVar } = getters.emhttp();
if (status === FileLoadStatus.LOADED) {
return emhttpVar.version;
}
return 'unknown';
};

View File

@@ -0,0 +1,9 @@
import { satisfies } from 'semver';
import { getters } from '@app/store';
/**
* Compare version against the current unraid version.
*/
export const compareUnraidVersion = (range: string) =>
satisfies(getters.emhttp().var.version, range, { includePrerelease: true });

View File

@@ -1,7 +1,9 @@
import { PORT } from '@app/environment';
import { type JSONWebKeySet } from 'jose';
import { join } from 'path';
import type { JSONWebKeySet } from 'jose';
import { PORT } from '@app/environment';
export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
const envPort = PORT;
const protocol = isHttp ? 'http' : 'ws';
@@ -18,7 +20,6 @@ export const getInternalApiAddress = (isHttp = true, nginxPort = 80) => {
// Prod mode (user didn't change webgui port)
return `${protocol}://127.0.0.1/graphql`;
};
// Milliseconds
@@ -68,17 +69,16 @@ export const JWKS_LOCAL_PAYLOAD: JSONWebKeySet = {
},
],
};
export const OAUTH_BASE_URL =
'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
export const OAUTH_BASE_URL = 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_btSkhlsEk';
export const OAUTH_CLIENT_ID = '53ci4o48gac8vq5jepubkjmo36';
export const OAUTH_OPENID_CONFIGURATION_URL =
OAUTH_BASE_URL + '/.well-known/openid-configuration';
export const OAUTH_OPENID_CONFIGURATION_URL = OAUTH_BASE_URL + '/.well-known/openid-configuration';
export const JWKS_REMOTE_LINK = OAUTH_BASE_URL + '/.well-known/jwks.json';
export const RCD_SCRIPT = 'rc.unraid-api';
export const KEYSERVER_VALIDATION_ENDPOINT =
'https://keys.lime-technology.com/validate/apikey';
export const KEYSERVER_VALIDATION_ENDPOINT = 'https://keys.lime-technology.com/validate/apikey';
/** Set the max retries for the GraphQL Client */
export const MAX_RETRIES_FOR_LINEAR_BACKOFF = 100;
export const PM2_PATH = join(import.meta.dirname, '../../', 'node_modules', '.bin', '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');

View File

@@ -1,11 +1,10 @@
import { AppError } from '@app/core/errors/app-error';
/**
* API key error.
*/
* API key error.
*/
export class ApiKeyError extends AppError {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}

View File

@@ -2,36 +2,36 @@
* Generic application error.
*/
export class AppError extends Error {
/** The HTTP status associated with this error. */
public status: number;
/** The HTTP status associated with this error. */
public status: number;
/** Should we kill the application when thrown. */
public fatal = false;
/** Should we kill the application when thrown. */
public fatal = false;
constructor(message: string, status?: number) {
// Calling parent constructor of base Error class.
super(message);
constructor(message: string, status?: number) {
// Calling parent constructor of base Error class.
super(message);
// Saving class name in the property of our custom error as a shortcut.
this.name = this.constructor.name;
// Saving class name in the property of our custom error as a shortcut.
this.name = this.constructor.name;
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor);
// Capturing stack trace, excluding constructor call from it.
Error.captureStackTrace(this, this.constructor);
// We're using HTTP status codes with `500` as the default
this.status = status ?? 500;
}
// We're using HTTP status codes with `500` as the default
this.status = status ?? 500;
}
/**
* Convert error to JSON format.
*/
toJSON() {
return {
error: {
name: this.name,
message: this.message,
stacktrace: this.stack,
},
};
}
/**
* Convert error to JSON format.
*/
toJSON() {
return {
error: {
name: this.name,
message: this.message,
stacktrace: this.stack,
},
};
}
}

View File

@@ -4,7 +4,7 @@ import { AppError } from '@app/core/errors/app-error';
* The attempted operation can only be processed while the array is stopped.
*/
export class ArrayRunningError extends AppError {
constructor() {
super('Array needs to be stopped before any changes can occur.');
}
constructor() {
super('Array needs to be stopped before any changes can occur.');
}
}

View File

@@ -4,7 +4,11 @@ import { FatalAppError } from '@app/core/errors/fatal-error';
* Atomic write error
*/
export class AtomicWriteError extends FatalAppError {
constructor(message: string, private readonly filePath: string, status = 500) {
super(message, status);
}
constructor(
message: string,
private readonly filePath: string,
status = 500
) {
super(message, status);
}
}

View File

@@ -4,8 +4,8 @@ import { FatalAppError } from '@app/core/errors/fatal-error';
* Em cmd client error.
*/
export class EmCmdError extends FatalAppError {
constructor(method: string, option: string, options: string[]) {
const message = `Invalid option "${option}" for ${method}, allowed options ${JSON.stringify(options)}`;
super(message);
}
constructor(method: string, option: string, options: string[]) {
const message = `Invalid option "${option}" for ${method}, allowed options ${JSON.stringify(options)}`;
super(message);
}
}

View File

@@ -4,5 +4,5 @@ import { AppError } from '@app/core/errors/app-error';
* Fatal application error.
*/
export class FatalAppError extends AppError {
fatal = true;
fatal = true;
}

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