Compare commits

...

14 Commits

Author SHA1 Message Date
github-actions[bot]
c39b0b267c chore(main): release 4.29.2 (#1857)
🤖 I have created a release *beep* *boop*
---


## [4.29.2](https://github.com/unraid/api/compare/v4.29.1...v4.29.2)
(2025-12-19)


### Bug Fixes

* unraid-connect plugin not loaded when connect is installed
([#1856](https://github.com/unraid/api/issues/1856))
([73135b8](73135b8328))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 15:13:29 -05:00
Pujit Mehrotra
73135b8328 fix: unraid-connect plugin not loaded when connect is installed (#1856)
Previously, api plugins could only be installed as `peerDependencies` in
the api. This change allows them to be listed as `dependencies` as well.
This makes plugin loading (eg loading Connect) more robust.

Tests:

- [x] Re-logging on 7.3.0-beta.0.5
2025-12-19 15:06:52 -05:00
github-actions[bot]
e42d619b6d chore(main): release 4.29.1 (#1854)
🤖 I have created a release *beep* *boop*
---


## [4.29.1](https://github.com/unraid/api/compare/v4.29.0...v4.29.1)
(2025-12-19)


### Bug Fixes

* revert replace docker overview table with web component (7.3+)
([#1853](https://github.com/unraid/api/issues/1853))
([560db88](560db880cc))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 12:19:44 -05:00
Eli Bosley
560db880cc fix: revert replace docker overview table with web component (7.3+) (#1853)
Reverts unraid/api#1764
2025-12-19 12:12:41 -05:00
github-actions[bot]
d6055f102b chore(main): release 4.29.0 (#1849)
🤖 I have created a release *beep* *boop*
---


## [4.29.0](https://github.com/unraid/api/compare/v4.28.2...v4.29.0)
(2025-12-19)


### Features

* replace docker overview table with web component (7.3+)
([#1764](https://github.com/unraid/api/issues/1764))
([277ac42](277ac42046))


### Bug Fixes

* handle race condition between guid loading and license check
([#1847](https://github.com/unraid/api/issues/1847))
([8b155d1](8b155d1f1c))
* resolve issue with "Continue" button when updating
([#1852](https://github.com/unraid/api/issues/1852))
([d099e75](d099e7521d))
* update myservers config references to connect config references
([#1810](https://github.com/unraid/api/issues/1810))
([e1e3ea7](e1e3ea7eb6))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-19 11:53:48 -05:00
Eli Bosley
d099e7521d fix: resolve issue with "Continue" button when updating (#1852)
- Replaced BrandLoading with BrandButton in UpdateOs component for
better user interaction.
- Updated test cases to reflect changes in rendering logic, ensuring the
account button is displayed when no reboot is pending.
- Added functionality to navigate to account update when the button is
clicked.
- Introduced WEBGUI_REDIRECT URL for handling update installations in
the store logic.
2025-12-19 11:44:19 -05:00
Pujit Mehrotra
bb9b539732 chore: fix local plugin builds & docs (#1851)
Raised by [MitchellThompkins](https://github.com/MitchellThompkins) in
#1848

- Documents how to use Docker to build a local Connect plugin
- Local Plugin flow will now build workspace packages before proceeding
with plugin infra + build
- Removes recommendation to run `pnpm build:watch` from root, as this
race conditions and build cache issues.
- Makes `pnpm dev` from root parallel, preventing servers from blocking
each other.

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

## Summary by CodeRabbit

* **Documentation**
* Updated development workflow documentation to emphasize Docker-based
plugin builds
* Restructured development modes into three workflows: local Docker
builds, direct deployment, and development servers
  * Updated build and deployment instructions

* **Chores**
  * Modified dev script for parallel execution
  * Refactored build scripts with improved dependency handling

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-18 16:33:37 -05:00
Pujit Mehrotra
0e44e73bf7 chore(web): mv predev call to prebuild step (#1850)
Fixes #1848

## Background

The `build:dev` script is used for the `unraid:deploy` workflow, and it
implicitly triggered the `predev` script to build the `unraid-ui`
package as needed.

`web` builds depend on `unraid-ui`. In the past, `unraid-ui` was built
during `pnpm install` via a `prepare` step in its `package.json`.
However, this approach doesn't ensure that `web` builds correctly; stale
`unraid-ui` builds could cause false-positives.

So, instead of doing that, we call `predev` from `prebuild`, ensuring
that both local builds and the `unraid:deploy` workflow lazily get the
correct build of `unraid-ui`.
2025-12-18 11:50:17 -05:00
Pujit Mehrotra
277ac42046 feat: replace docker overview table with web component (7.3+) (#1764)
## Summary

Introduces a new Vue-based Docker container management interface
replacing the legacy webgui table.

### Container Management
- Start, stop, pause, resume, and remove containers via GraphQL
mutations
- Bulk actions for managing multiple containers at once
- Container update detection with one-click updates
- Real-time container statistics (CPU, memory, I/O)

### Organization & Navigation
- Folder-based container organization with drag-and-drop support
- Accessible reordering via keyboard controls
- Customizable column visibility with persistent preferences
- Column resizing and reordering
- Filtering and search across container properties

### Auto-start Configuration
- Dedicated autostart view with delay configuration
- Drag-and-drop reordering of start/stop sequences

### Logs & Console
- Integrated log viewer with filtering and download
- Persistent console sessions with shell selection
- Slideover panel for quick access

### Networking
- Port conflict detection and alerts
- Tailscale integration for container networking status
- LAN IP and port information display

### Additional Features
- Orphaned container detection and cleanup
- Template mapping management
- Critical notifications system
- WebUI visit links with Tailscale support

<sub>PR Summary by Claude Opus 4.5</sub>
2025-12-18 11:11:05 -05:00
Pujit Mehrotra
e1e3ea7eb6 fix: update myservers config references to connect config references (#1810)
`myservers.cfg` no longer gets written to or read (except for migration
purposes), so it'd be better to read from the new values instead of
continuing to use the old ones @elibosley @Squidly271 .

unless i'm missing something! see #1805

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

* **New Features**
* Switches to a centralized remote-access configuration with a legacy
fallback and richer client-side handling.
* Optional GraphQL submission path for applying remote settings when
available.

* **Bug Fixes**
* Normalized boolean and port handling to prevent incorrect values
reaching the UI.
* Improved error handling and UI state restoration during save/apply
flows.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-18 10:34:06 -05:00
Pujit Mehrotra
8b155d1f1c fix: handle race condition between guid loading and license check (#1847)
On errors, a `console.error` message should be emitted from the browser
console, tagged `[ReplaceCheck.check]`.

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

## Summary by CodeRabbit

* **New Features**
* Added retry capability for license eligibility checks with a
contextual "Retry" button that appears in error states.

* **Bug Fixes**
* Fixed license status initialization to correctly default to ready
state.
* Enhanced error messaging with specific messages for different failure
scenarios (missing credentials, access denied, server errors).
* Improved status display handling to prevent potential runtime errors.

* **Localization**
  * Added "Retry" text translation.

* **Tests**
  * Updated and added tests for reset functionality and error handling.

<sub>✏️ Tip: You can customize this high-level summary in your review
settings.</sub>

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-12-18 08:51:01 -05:00
github-actions[bot]
d13a1f6174 chore(main): release 4.28.2 (#1845)
🤖 I have created a release *beep* *boop*
---


## [4.28.2](https://github.com/unraid/api/compare/v4.28.1...v4.28.2)
(2025-12-16)


### Bug Fixes

* **api:** timeout on startup on 7.0 and 6.12
([#1844](https://github.com/unraid/api/issues/1844))
([e243ae8](e243ae836e))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-16 11:47:31 -05:00
Eli Bosley
e243ae836e fix(api): timeout on startup on 7.0 and 6.12 (#1844)
Updated the total startup budget, bootstrap reserved time, and maximum
operation timeout values to enhance API startup reliability. The total
startup budget is now set to 30 seconds, with 20 seconds reserved for
bootstrap and a maximum operation timeout of 5 seconds.
2025-12-16 11:37:42 -05:00
github-actions[bot]
01a63fd86b chore(main): release 4.28.1 (#1843)
🤖 I have created a release *beep* *boop*
---


## [4.28.1](https://github.com/unraid/api/compare/v4.28.0...v4.28.1)
(2025-12-16)


### Bug Fixes

* empty commit to release as 4.28.1
([df78608](df78608457))

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

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-16 11:02:11 -05:00
26 changed files with 471 additions and 155 deletions

View File

@@ -1 +1 @@
{".":"4.28.0"}
{".":"4.29.2"}

View File

@@ -1,5 +1,47 @@
# Changelog
## [4.29.2](https://github.com/unraid/api/compare/v4.29.1...v4.29.2) (2025-12-19)
### Bug Fixes
* unraid-connect plugin not loaded when connect is installed ([#1856](https://github.com/unraid/api/issues/1856)) ([73135b8](https://github.com/unraid/api/commit/73135b832801f5c76d60020161492e4770958c3d))
## [4.29.1](https://github.com/unraid/api/compare/v4.29.0...v4.29.1) (2025-12-19)
### Bug Fixes
* revert replace docker overview table with web component (7.3+) ([#1853](https://github.com/unraid/api/issues/1853)) ([560db88](https://github.com/unraid/api/commit/560db880cc138324f9ff8753f7209b683a84c045))
## [4.29.0](https://github.com/unraid/api/compare/v4.28.2...v4.29.0) (2025-12-19)
### Features
* replace docker overview table with web component (7.3+) ([#1764](https://github.com/unraid/api/issues/1764)) ([277ac42](https://github.com/unraid/api/commit/277ac420464379e7ee6739c4530271caf7717503))
### Bug Fixes
* handle race condition between guid loading and license check ([#1847](https://github.com/unraid/api/issues/1847)) ([8b155d1](https://github.com/unraid/api/commit/8b155d1f1c99bb19efbc9614e000d852e9f0c12d))
* resolve issue with "Continue" button when updating ([#1852](https://github.com/unraid/api/issues/1852)) ([d099e75](https://github.com/unraid/api/commit/d099e7521d2062bb9cf84f340e46b169dd2492c5))
* update myservers config references to connect config references ([#1810](https://github.com/unraid/api/issues/1810)) ([e1e3ea7](https://github.com/unraid/api/commit/e1e3ea7eb68cc6840f67a8aec937fd3740e75b28))
## [4.28.2](https://github.com/unraid/api/compare/v4.28.1...v4.28.2) (2025-12-16)
### Bug Fixes
* **api:** timeout on startup on 7.0 and 6.12 ([#1844](https://github.com/unraid/api/issues/1844)) ([e243ae8](https://github.com/unraid/api/commit/e243ae836ec1a7fde37dceeb106cc693b20ec82b))
## [4.28.1](https://github.com/unraid/api/compare/v4.28.0...v4.28.1) (2025-12-16)
### Bug Fixes
* empty commit to release as 4.28.1 ([df78608](https://github.com/unraid/api/commit/df786084572eefb82e086c15939b50cc08b9db10))
## [4.28.0](https://github.com/unraid/api/compare/v4.27.2...v4.28.0) (2025-12-15)

View File

@@ -62,15 +62,18 @@ To build all packages in the monorepo:
pnpm build
```
### Watch Mode Building
### Plugin Building (Docker Required)
For continuous building during development:
The plugin build requires Docker. This command automatically builds all dependencies (API, web) before starting Docker:
```bash
pnpm build:watch
cd plugin
pnpm run docker:build-and-run
# Then inside the container:
pnpm build
```
This is useful when you want to see your changes reflected without manually rebuilding. This will also allow you to install a local plugin to test your changes.
This serves the plugin at `http://YOUR_IP:5858/` for installation on your Unraid server.
### Package-Specific Building

View File

@@ -7,7 +7,7 @@
"cwd": "/usr/local/unraid-api",
"exec_mode": "fork",
"wait_ready": true,
"listen_timeout": 15000,
"listen_timeout": 30000,
"max_restarts": 10,
"min_uptime": 10000,
"watch": false,

View File

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

View File

@@ -83,6 +83,10 @@ try {
if (parsedPackageJson.dependencies?.[dep]) {
delete parsedPackageJson.dependencies[dep];
}
// Also strip from peerDependencies (npm doesn't understand workspace: protocol)
if (parsedPackageJson.peerDependencies?.[dep]) {
delete parsedPackageJson.peerDependencies[dep];
}
});
}

View File

@@ -32,11 +32,11 @@ let server: NestFastifyApplication<RawServerDefault> | null = null;
// PM2 listen_timeout is 15 seconds (ecosystem.config.json)
// We use 13 seconds as our total budget to ensure our timeout triggers before PM2 kills us
const TOTAL_STARTUP_BUDGET_MS = 13_000;
const TOTAL_STARTUP_BUDGET_MS = 30_000;
// Reserve time for the NestJS bootstrap (the most critical and time-consuming operation)
const BOOTSTRAP_RESERVED_MS = 8_000;
const BOOTSTRAP_RESERVED_MS = 20_000;
// Maximum time for any single pre-bootstrap operation
const MAX_OPERATION_TIMEOUT_MS = 2_000;
const MAX_OPERATION_TIMEOUT_MS = 5_000;
const unlinkUnixPort = () => {
if (isNaN(parseInt(PORT, 10))) {

View File

@@ -91,13 +91,9 @@ export class PluginService {
return name;
})
);
const { peerDependencies } = getPackageJson();
// All api plugins must be installed as peer dependencies of the unraid-api package
if (!peerDependencies) {
PluginService.logger.warn('Unraid-API peer dependencies not found; skipping plugins.');
return [];
}
const pluginTuples = Object.entries(peerDependencies).filter(
const { peerDependencies = {}, dependencies = {} } = getPackageJson();
const allDependencies = { ...peerDependencies, ...dependencies };
const pluginTuples = Object.entries(allDependencies).filter(
(entry): entry is [string, string] => {
const [pkgName, version] = entry;
return pluginNames.has(pkgName) && typeof version === 'string';

View File

@@ -1,13 +1,13 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.28.0",
"version": "4.29.2",
"scripts": {
"build": "pnpm -r build",
"build:watch": "pnpm -r --parallel --filter '!@unraid/ui' build:watch",
"codegen": "pnpm -r codegen",
"i18n:extract": "pnpm --filter @unraid/api i18n:extract && pnpm --filter @unraid/web i18n:extract",
"dev": "pnpm -r dev",
"dev": "pnpm -r --parallel dev",
"unraid:deploy": "pnpm -r unraid:deploy",
"test": "pnpm -r test",
"test:watch": "pnpm -r --parallel test:watch",

View File

@@ -18,7 +18,8 @@
"dist"
],
"scripts": {
"build": "rimraf dist && tsc --project tsconfig.build.json",
"build": "pnpm clean && tsc --project tsconfig.build.json",
"clean": "rimraf dist",
"prepare": "npm run build",
"test": "vitest run",
"test:watch": "vitest",

View File

@@ -4,50 +4,32 @@ Tool for building and testing Unraid plugins locally as well as packaging them f
## Development Workflow
### 1. Watch for Changes
### 1. Build the Plugin
The watch command will automatically sync changes from the API, UI components, and web app into the plugin source:
```bash
# Start watching all components
pnpm run watch:all
# Or run individual watchers:
pnpm run api:watch # Watch API changes
pnpm run ui:watch # Watch Unraid UI component changes
pnpm run wc:watch # Watch web component changes
```
This will copy:
- API files to `./source/dynamix.unraid.net/usr/local/unraid-api`
- UI components to `./source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components`
- Web components to the same UI directory
### 2. Build the Plugin
> **Note:** Building the plugin requires Docker.
Once your changes are ready, build the plugin package:
```bash
# Build using Docker - on non-Linux systems
# Start Docker container (builds dependencies automatically)
pnpm run docker:build-and-run
# Or build with the build script
pnpm run build:validate
# Inside the container, build the plugin
pnpm build
```
This will create the plugin files in `./deploy/release/`
This will:
### 3. Serve and Install
1. Build the API release (`api/deploy/release/`)
2. Build the web standalone components (`web/dist/`)
3. Start Docker container with HTTP server on port 5858
4. Build the plugin package (when you run `pnpm build`)
Start a local HTTP server to serve the plugin files:
The plugin files will be created in `./deploy/` and served automatically.
```bash
# Serve the plugin files
pnpm run http-server
```
### 2. Install on Unraid
Then install the plugin on your Unraid development machine by visiting:
Install the plugin on your Unraid development machine by visiting:
`http://SERVER_NAME.local/Plugins`
@@ -59,8 +41,7 @@ Replace `SERVER_NAME` with your development machine's hostname.
## Development Tips
- Run watchers in a separate terminal while developing
- The http-server includes CORS headers for local development
- The HTTP server includes CORS headers for local development
- Check the Unraid system log for plugin installation issues
## Environment Setup
@@ -81,22 +62,10 @@ Replace `SERVER_NAME` with your development machine's hostname.
### Build Commands
- `build` - Build the plugin package
- `build:validate` - Build with environment validation
- `build` - Build the plugin package (run inside Docker container)
- `docker:build` - Build the Docker container
- `docker:run` - Run the builder in Docker
- `docker:build-and-run` - Build and run in Docker
### Watch Commands
- `watch:all` - Watch all component changes
- `api:watch` - Watch API changes
- `ui:watch` - Watch UI component changes
- `wc:watch` - Watch web component changes
### Server Commands
- `http-server` - Serve the plugin files locally
- `docker:build-and-run` - Build dependencies and start Docker container
### Environment Commands

View File

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

View File

@@ -33,6 +33,23 @@ if [ ! -d "$WEB_DIST_DIR" ]; then
mkdir -p "$WEB_DIST_DIR"
fi
# Build dependencies before starting Docker (always rebuild to prevent staleness)
echo "Building dependencies..."
echo "Building API release..."
if ! (cd .. && pnpm --filter @unraid/api build:release); then
echo "Error: API build failed. Aborting."
exit 1
fi
echo "Building web standalone..."
if ! (cd .. && pnpm --filter @unraid/web build); then
echo "Error: Web build failed. Aborting."
exit 1
fi
echo "Dependencies built successfully."
# Stop any running plugin-builder container first
echo "Stopping any running plugin-builder containers..."
docker ps -q --filter "name=${CONTAINER_NAME}" | xargs -r docker stop

View File

@@ -15,12 +15,29 @@ Tag="globe"
*/
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
require_once "$docroot/plugins/dynamix.my.servers/include/api-config.php";
require_once "$docroot/plugins/dynamix.my.servers/include/connect-config.php";
require_once "$docroot/webGui/include/Wrappers.php";
$serverState = new ServerState();
$keyfile = $serverState->keyfileBase64;
$myServersFlashCfg = $serverState->myServersFlashCfg;
$connectConfig = ConnectConfig::getConfig();
$legacyRemoteCfg = $serverState->myServersFlashCfg['remote'] ?? [];
$remoteDynamicRemoteAccessType = $connectConfig['dynamicRemoteAccessType'] ?? ($legacyRemoteCfg['dynamicRemoteAccessType'] ?? null);
$remoteWanAccessRaw = $connectConfig['wanaccess'] ?? ($legacyRemoteCfg['wanaccess'] ?? null);
$remoteUpnpEnabledRaw = $connectConfig['upnpEnabled'] ?? ($legacyRemoteCfg['upnpEnabled'] ?? null);
$remoteWanPortRaw = $connectConfig['wanport'] ?? ($legacyRemoteCfg['wanport'] ?? null);
$wanaccessEnabled = filter_var($remoteWanAccessRaw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($wanaccessEnabled === null) {
$wanaccessEnabled = false;
}
$upnpEnabled = filter_var($remoteUpnpEnabledRaw, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
if ($upnpEnabled === null) {
$upnpEnabled = false;
}
$remoteWanPort = is_numeric($remoteWanPortRaw) ? (int)$remoteWanPortRaw : 0;
$showT2Fa = (file_exists('/boot/config/plugins/dynamix.my.servers/showT2Fa'));
@@ -37,9 +54,7 @@ $passwd_result = exec('/usr/bin/passwd --status root');
$boolWebUIAuth = $isRegistered && (($passwd_result !== false) && (substr($passwd_result, 0, 6) == 'root P'));
// Helper to determine the current value for the remote access input
$dynamicRemoteAccessType = $myServersFlashCfg['remote']['dynamicRemoteAccessType'] ?? null;
$upnpEnabled = ($myServersFlashCfg['remote']['upnpEnabled'] ?? null) === 'yes';
$wanaccessEnabled = ($myServersFlashCfg['remote']['wanaccess'] ?? null) === 'yes';
$dynamicRemoteAccessType = $remoteDynamicRemoteAccessType ?? null;
$currentRemoteAccessValue = 'OFF';
if ($dynamicRemoteAccessType === 'STATIC') {
@@ -59,6 +74,12 @@ if ($dynamicRemoteAccessType === 'STATIC') {
$enableRemoteT2fa = $showT2Fa && $currentRemoteAccessValue !== 'OFF' && $hasMyUnraidNetCert;
$enableLocalT2fa = $showT2Fa && $var['USE_SSL'] === 'auto' && $hasMyUnraidNetCert;
$shade="shade-".($display['theme']??'unk');
$wanAccessOriginal = $remoteWanAccessRaw;
if (is_bool($wanAccessOriginal)) {
$wanAccessOriginal = $wanAccessOriginal ? 'yes' : 'no';
} elseif (!is_string($wanAccessOriginal)) {
$wanAccessOriginal = '';
}
?>
<style>
div.shade-white{background-color:#ededed;margin-top:10px;padding:8px 0 3px 0}
@@ -68,13 +89,18 @@ div.shade-gray{background-color:#121510;margin-top:10px;padding:8px 0 3px 0}
</style>
<script>
const hasMyUnraidNetCert = <?=($hasMyUnraidNetCert ? 'true' : 'false')?>;
const wanAccessOrg = "<?=$myServersFlashCfg['remote']['wanaccess'] ?? null?>";
const wanAccessOrg = "<?=$wanAccessOriginal?>";
function registerServer(button) {
const $remoteAccessInput = $('#remoteAccess');
const $remoteAccessManualPort = $('#wanport');
const parsePort = (val) => {
const parsed = parseInt(val, 10);
return isNaN(parsed) ? null : parsed;
};
let computedRemoteAccessConfig = null;
switch ($remoteAccessInput.val()) {
case 'ALWAYS_MANUAL':
@@ -119,19 +145,64 @@ function registerServer(button) {
break;
}
const enableLocalT2fa = <?=($enableLocalT2fa ? 'true' : 'false')?>;
const enableRemoteT2fa = $remoteAccessInput.val() !== 'OFF' && hasMyUnraidNetCert;
const enableLocalT2fa = <?=($enableLocalT2fa ? 'true' : 'false')?>;
const enableRemoteT2fa = $remoteAccessInput.val() !== 'OFF' && hasMyUnraidNetCert;
var postobj = {
"#cfg": "/boot/config/plugins/dynamix.my.servers/myservers.cfg",
...(computedRemoteAccessConfig ? computedRemoteAccessConfig : {}),
// only allow 'yes' value when fields are enabled
"local_2Fa": enableLocalT2fa ? $('#local2fa').val() : 'no',
"remote_2Fa": enableRemoteT2fa ? $('#remote2fa').val() : 'no',
};
const postobj = {
"#cfg": "/boot/config/plugins/dynamix.my.servers/myservers.cfg",
...(computedRemoteAccessConfig ? computedRemoteAccessConfig : {}),
// only allow 'yes' value when fields are enabled
"local_2Fa": enableLocalT2fa ? $('#local2fa').val() : 'no',
"remote_2Fa": enableRemoteT2fa ? $('#remote2fa').val() : 'no',
};
$(button).prop("disabled", true).html("_(Applying)_ <i class=\"fa fa-spinner fa-spin\" aria-hidden=\"true\"></i>");
$.post('/webGui/include/Dispatcher.php', postobj, function(data2) {
const buildConnectSettingsInput = () => {
const selection = $remoteAccessInput.val();
switch (selection) {
case 'ALWAYS_MANUAL':
return { accessType: 'ALWAYS', forwardType: 'STATIC', port: parsePort($remoteAccessManualPort.val()) };
case 'ALWAYS_UPNP':
return { accessType: 'ALWAYS', forwardType: 'UPNP', port: null };
case 'DYNAMIC_UPNP':
return { accessType: 'DYNAMIC', forwardType: 'UPNP', port: null };
case 'DYNAMIC_MANUAL':
return { accessType: 'DYNAMIC', forwardType: 'STATIC', port: parsePort($remoteAccessManualPort.val()) };
default:
return { accessType: 'DISABLED', forwardType: 'STATIC', port: null };
}
};
const $button = $(button);
const originalLabel = $button.html();
$button.prop("disabled", true).html("_(Applying)_ <i class=\"fa fa-spinner fa-spin\" aria-hidden=\"true\"></i>");
const saveLegacyConfig = new Promise((resolve, reject) => {
$.post('/webGui/include/Dispatcher.php', postobj).done(resolve).fail(reject);
});
const apolloClient = window.apolloClient;
const gql = window.gql || window.graphqlParse;
const mutations = [saveLegacyConfig];
if (apolloClient && gql) {
const updateConnectSettingsMutation = gql(`
mutation UpdateConnectSettings($input: ConnectSettingsInput!) {
updateApiSettings(input: $input) {
accessType
forwardType
port
}
}
`);
mutations.push(
apolloClient.mutate({
mutation: updateConnectSettingsMutation,
variables: { input: buildConnectSettingsInput() },
})
);
}
Promise.all(mutations).then(function() {
<?if(!$isRegistered):?>
swal({
title: "",
@@ -150,7 +221,22 @@ function registerServer(button) {
button.form.submit();
}, delay);
<?endif?>
});
}).catch(function(error) {
let message = "_(Sorry, an error occurred)_";
if (error && error.responseJSON && error.responseJSON.error) {
message = error.responseJSON.error;
} else if (error && error.message) {
message = error.message;
}
$button.prop("disabled", false).html(originalLabel);
swal({
title: "Oops",
text: message,
type: "error",
html: true,
confirmButtonText: "_(Ok)_"
});
});
}
@@ -196,7 +282,7 @@ function dnsCheckServer(button) {
} else {
swal({
title: "Oops",
text: "<?=sprintf(_("The Unraid server is unreachable from outside your network. Be sure you have configured your router to forward port") . " <strong style='font-weight: bold'>%u/TCP</strong> " . _("to the Unraid server at") . " <strong style='font-weight: bold'>%s</strong> " . _("port") . " <strong style='font-weight: bold'>%u</strong>", $myServersFlashCfg['remote']['wanport']??"", htmlspecialchars($eth0['IPADDR:0']??''), $var['PORTSSL']??443)?>",
text: "<?=sprintf(_("The Unraid server is unreachable from outside your network. Be sure you have configured your router to forward port") . " <strong style='font-weight: bold'>%u/TCP</strong> " . _("to the Unraid server at") . " <strong style='font-weight: bold'>%s</strong> " . _("port") . " <strong style='font-weight: bold'>%u</strong>", $remoteWanPort, htmlspecialchars($eth0['IPADDR:0']??''), $var['PORTSSL']??443)?>",
type: "error",
html: true,
confirmButtonText: "_(Ok)_"

View File

@@ -210,22 +210,34 @@ Once you have your key pair, add your public SSH key to your Unraid server:
### Development Modes
The project supports two development modes:
#### Mode 1: Local Plugin Build (Docker)
#### Mode 1: Build Watcher with Local Plugin
This mode builds the plugin continuously and serves it locally for installation on your Unraid server:
Build and test a full plugin locally using Docker:
```sh
# From the root directory (api/)
pnpm build:watch
cd plugin
pnpm run docker:build-and-run
# Then inside the container:
pnpm build
```
This command will output a local plugin URL that you can install on your Unraid server by navigating to Plugins → Install Plugin. Be aware it will take a *while* to build the first time.
This builds all dependencies (API, web), starts a Docker container, and serves the plugin at `http://YOUR_IP:5858/`. Install it on your Unraid server via Plugins → Install Plugin.
#### Mode 2: Development Servers
#### Mode 2: Direct Deployment
For active development with hot-reload:
Deploy individual packages directly to an Unraid server for faster iteration:
```sh
# Deploy API changes
cd api && pnpm unraid:deploy <SERVER_IP>
# Deploy web changes
cd web && pnpm unraid:deploy <SERVER_IP>
```
#### Mode 3: Development Servers
For active development with hot-reload (no Unraid server needed):
```sh
# From the root directory - runs all dev servers concurrently
@@ -238,22 +250,11 @@ Or run individual development servers:
# API server (GraphQL backend at http://localhost:3001)
cd api && pnpm dev
# Web interface (Nuxt frontend at http://localhost:3000)
# Web interface (Nuxt frontend at http://localhost:3000)
cd web && pnpm dev
```
### Building the Full Plugin
To build the complete plugin package (.plg file):
```sh
# From the root directory (api/)
pnpm build:plugin
# The plugin will be created in plugin/dynamix.unraid.net.plg
```
To deploy the plugin to your Unraid server:
### Deploying to Unraid
```sh
# Replace SERVER_IP with your Unraid server's IP address

View File

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

View File

@@ -13,7 +13,9 @@ import { createTestI18n } from '../utils/i18n';
vi.mock('@unraid/ui', () => ({
PageContainer: { template: '<div><slot /></div>' },
BrandLoading: { template: '<div data-testid="brand-loading-mock">Loading...</div>' },
BrandButton: {
template: '<button v-bind="$attrs" @click="$emit(\'click\')"><slot /></button>',
},
}));
const mockAccountStore = {
@@ -97,7 +99,7 @@ describe('UpdateOs.standalone.vue', () => {
});
describe('Initial Rendering and onBeforeMount Logic', () => {
it('shows loader and calls updateOs when path matches and rebootType is empty', async () => {
it('shows account button and does not auto-redirect when path matches and rebootType is empty', async () => {
window.location.pathname = '/Tools/Update';
mockRebootType.value = '';
@@ -105,7 +107,7 @@ describe('UpdateOs.standalone.vue', () => {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()],
stubs: {
// Rely on @unraid/ui mock for PageContainer & BrandLoading
// Rely on @unraid/ui mock for PageContainer & BrandButton
UpdateOsStatus: UpdateOsStatusStub,
UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub,
},
@@ -114,17 +116,9 @@ describe('UpdateOs.standalone.vue', () => {
await nextTick();
// When path matches and rebootType is empty, updateOs should be called
expect(mockAccountStore.updateOs).toHaveBeenCalledWith(true);
// Since v-show is used, both elements exist in DOM
expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
// The loader should be visible when showLoader is true
const loaderWrapper = wrapper.find('[data-testid="brand-loading-mock"]').element.parentElement;
expect(loaderWrapper?.style.display).not.toBe('none');
// The status should be hidden when showLoader is true
const statusWrapper = wrapper.find('[data-testid="update-os-status"]').element.parentElement;
expect(statusWrapper?.style.display).toBe('none');
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(false);
});
it('shows status and does not call updateOs when path does not match', async () => {
@@ -145,8 +139,7 @@ describe('UpdateOs.standalone.vue', () => {
await nextTick();
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
// Since v-show is used, both elements exist in DOM
expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
});
@@ -168,10 +161,30 @@ describe('UpdateOs.standalone.vue', () => {
await nextTick();
expect(mockAccountStore.updateOs).not.toHaveBeenCalled();
// Since v-show is used, both elements exist in DOM
expect(wrapper.find('[data-testid="brand-loading-mock"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="update-os-account-button"]').exists()).toBe(false);
expect(wrapper.find('[data-testid="update-os-status"]').exists()).toBe(true);
});
it('navigates to account update when the button is clicked', async () => {
window.location.pathname = '/Tools/Update';
mockRebootType.value = '';
const wrapper = mount(UpdateOs, {
global: {
plugins: [createTestingPinia({ createSpy: vi.fn }), createTestI18n()],
stubs: {
UpdateOsStatus: UpdateOsStatusStub,
UpdateOsThirdPartyDrivers: UpdateOsThirdPartyDriversStub,
},
},
});
await nextTick();
await wrapper.find('[data-testid="update-os-account-button"]').trigger('click');
expect(mockAccountStore.updateOs).toHaveBeenCalledWith(true);
});
});
describe('Rendering based on rebootType', () => {

View File

@@ -14,6 +14,10 @@ import { useServerStore } from '~/store/server';
vi.mock('@unraid/shared-callbacks', () => ({}));
vi.mock('@unraid/ui', () => ({
BrandLoading: {},
}));
vi.mock('~/composables/services/keyServer', () => ({
validateGuid: vi.fn(),
}));
@@ -62,7 +66,7 @@ describe('ReplaceRenew Store', () => {
expect(store.replaceStatus).toBe('ready');
});
it('should initialize with error state when guid is missing', () => {
it('should initialize with ready state even when guid is missing', () => {
vi.mocked(useServerStore).mockReturnValueOnce({
guid: undefined,
keyfile: mockKeyfile,
@@ -72,7 +76,8 @@ describe('ReplaceRenew Store', () => {
const newStore = useReplaceRenewStore();
expect(newStore.replaceStatus).toBe('error');
// Store now always initializes as 'ready' - errors are set when check() is called
expect(newStore.replaceStatus).toBe('ready');
});
});
@@ -138,6 +143,18 @@ describe('ReplaceRenew Store', () => {
expect(store.renewStatus).toBe('installing');
});
it('should reset all states with reset action', () => {
store.setReplaceStatus('error');
store.keyLinkedStatus = 'error';
store.error = { name: 'Error', message: 'Test error' };
store.reset();
expect(store.replaceStatus).toBe('ready');
expect(store.keyLinkedStatus).toBe('ready');
expect(store.error).toBeNull();
});
describe('check action', () => {
const mockResponse = {
hasNewerKeyfile: false,
@@ -326,8 +343,59 @@ describe('ReplaceRenew Store', () => {
await store.check();
expect(store.replaceStatus).toBe('error');
expect(store.keyLinkedStatus).toBe('error');
expect(console.error).toHaveBeenCalledWith('[ReplaceCheck.check]', testError);
expect(store.error).toEqual(testError);
expect(store.error).toEqual({ name: 'Error', message: 'Test error' });
});
it('should set error when guid is missing during check', async () => {
vi.mocked(useServerStore).mockReturnValue({
guid: '',
keyfile: mockKeyfile,
} as unknown as ReturnType<typeof useServerStore>);
setActivePinia(createPinia());
const testStore = useReplaceRenewStore();
await testStore.check();
expect(testStore.replaceStatus).toBe('error');
expect(testStore.keyLinkedStatus).toBe('error');
expect(testStore.error?.message).toBe('Flash GUID required to check replacement status');
});
it('should set error when keyfile is missing during check', async () => {
vi.mocked(useServerStore).mockReturnValue({
guid: mockGuid,
keyfile: '',
} as unknown as ReturnType<typeof useServerStore>);
setActivePinia(createPinia());
const testStore = useReplaceRenewStore();
await testStore.check();
expect(testStore.replaceStatus).toBe('error');
expect(testStore.keyLinkedStatus).toBe('error');
expect(testStore.error?.message).toBe('Keyfile required to check replacement status');
});
it('should provide descriptive error for 403 status', async () => {
const error403 = { response: { status: 403 }, message: 'Forbidden' };
vi.mocked(validateGuid).mockRejectedValueOnce(error403);
await store.check();
expect(store.error?.message).toBe('Access denied - license may be linked to another account');
});
it('should provide descriptive error for 500+ status', async () => {
const error500 = { response: { status: 500 }, message: 'Server Error' };
vi.mocked(validateGuid).mockRejectedValueOnce(error500);
await store.check();
expect(store.error?.message).toBe('Key server temporarily unavailable - please try again later');
});
});
});

View File

@@ -4,17 +4,41 @@
import { createPinia, setActivePinia } from 'pinia';
import { WEBGUI_REDIRECT } from '~/helpers/urls';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { useUpdateOsStore } from '~/store/updateOs';
const mockSend = vi.fn();
vi.mock('@unraid/shared-callbacks', () => ({
useCallback: vi.fn(() => ({
send: vi.fn(),
send: mockSend,
watcher: vi.fn(),
})),
}));
vi.mock('~/composables/preventClose', () => ({
addPreventClose: vi.fn(),
removePreventClose: vi.fn(),
}));
vi.mock('~/store/account', () => ({
useAccountStore: () => ({
accountActionStatus: 'ready',
}),
}));
vi.mock('~/store/installKey', () => ({
useInstallKeyStore: () => ({
keyInstallStatus: 'ready',
}),
}));
vi.mock('~/store/updateOsActions', () => ({
useUpdateOsActionsStore: () => ({}),
}));
vi.mock('~/composables/services/webgui', () => {
return {
WebguiCheckForUpdate: vi.fn().mockResolvedValue({
@@ -104,6 +128,40 @@ describe('UpdateOs Store', () => {
expect(store.updateOsModalVisible).toBe(false);
});
it('should send update install through redirect.htm', () => {
const originalLocation = window.location;
Object.defineProperty(window, 'location', {
configurable: true,
value: {
...originalLocation,
origin: 'https://littlebox.tail45affd.ts.net',
href: 'https://littlebox.tail45affd.ts.net/Plugins',
},
});
store.fetchAndConfirmInstall('test-sha256');
const expectedUrl = new URL(WEBGUI_REDIRECT, window.location.origin).toString();
expect(mockSend).toHaveBeenCalledWith(
expectedUrl,
[
{
sha256: 'test-sha256',
type: 'updateOs',
},
],
undefined,
'forUpc'
);
Object.defineProperty(window, 'location', {
configurable: true,
value: originalLocation,
});
});
it('should handle errors when checking for updates', async () => {
const { WebguiCheckForUpdate } = await import('~/composables/services/webgui');

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.28.0",
"version": "4.29.2",
"private": true,
"type": "module",
"license": "GPL-2.0-or-later",
@@ -11,8 +11,8 @@
"preview": "vite preview",
"serve": "NODE_ENV=production PORT=${PORT:-4321} vite preview --port ${PORT:-4321}",
"// Build": "",
"prebuild:dev": "pnpm predev",
"build:dev": "pnpm run build && pnpm run deploy-to-unraid:dev",
"prebuild": "pnpm predev",
"build": "NODE_ENV=production vite build && pnpm run manifest-ts",
"prebuild:watch": "pnpm predev",
"build:watch": "vite build --watch && pnpm run manifest-ts",

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { ArrowTopRightOnSquareIcon, KeyIcon } from '@heroicons/vue/24/solid';
import { ArrowPathIcon, ArrowTopRightOnSquareIcon, KeyIcon } from '@heroicons/vue/24/solid';
import { Badge, BrandButton } from '@unraid/ui';
import { DOCS_REGISTRATION_REPLACE_KEY } from '~/helpers/urls';
@@ -11,20 +12,30 @@ import { useReplaceRenewStore } from '~/store/replaceRenew';
const { t } = useI18n();
const replaceRenewStore = useReplaceRenewStore();
const { replaceStatusOutput } = storeToRefs(replaceRenewStore);
const isError = computed(() => replaceStatusOutput.value?.variant === 'red');
const showButton = computed(() => !replaceStatusOutput.value || isError.value);
const handleCheck = () => {
if (isError.value) {
replaceRenewStore.reset();
}
replaceRenewStore.check(true);
};
</script>
<template>
<div class="flex flex-wrap items-center justify-between gap-2">
<BrandButton
v-if="!replaceStatusOutput"
:icon="KeyIcon"
:text="t('registration.replaceCheck.checkEligibility')"
v-if="showButton"
:icon="isError ? ArrowPathIcon : KeyIcon"
:text="isError ? t('common.retry') : t('registration.replaceCheck.checkEligibility')"
class="grow"
@click="replaceRenewStore.check"
@click="handleCheck"
/>
<Badge v-else :variant="replaceStatusOutput.variant" :icon="replaceStatusOutput.icon" size="md">
{{ t(replaceStatusOutput.text ?? 'Unknown') }}
<Badge v-else :variant="replaceStatusOutput?.variant" :icon="replaceStatusOutput?.icon" size="md">
{{ t(replaceStatusOutput?.text ?? 'Unknown') }}
</Badge>
<span class="inline-flex flex-wrap items-center justify-end gap-2">

View File

@@ -19,7 +19,8 @@ import { computed, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n';
import { storeToRefs } from 'pinia';
import { BrandLoading, PageContainer } from '@unraid/ui';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { BrandButton, PageContainer } from '@unraid/ui';
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import UpdateOsStatus from '~/components/UpdateOs/Status.vue';
@@ -47,25 +48,42 @@ const subtitle = computed(() => {
return '';
});
/** when we're not prompting for reboot /Tools/Update will automatically send the user to account.unraid.net/server/update-os */
const showLoader = computed(
() => window.location.pathname === WEBGUI_TOOLS_UPDATE && rebootType.value === ''
// Show a prompt to continue in the Account app when no reboot is pending.
const showRedirectPrompt = computed(
() =>
typeof window !== 'undefined' &&
window.location.pathname === WEBGUI_TOOLS_UPDATE &&
rebootType.value === ''
);
const openAccountUpdate = () => {
accountStore.updateOs(true);
};
onBeforeMount(() => {
if (showLoader.value) {
accountStore.updateOs(true);
}
serverStore.setRebootVersion(props.rebootVersion);
});
</script>
<template>
<PageContainer>
<div v-show="showLoader">
<BrandLoading class="mx-auto my-12 max-w-[160px]" />
<div
v-if="showRedirectPrompt"
class="mx-auto flex max-w-[720px] flex-col items-center gap-4 py-8 text-center"
>
<h1 class="text-2xl font-semibold">{{ t('updateOs.updateUnraidOs') }}</h1>
<p class="text-base leading-relaxed opacity-75">
{{ t('updateOs.update.receiveTheLatestAndGreatestFor') }}
</p>
<BrandButton
data-testid="update-os-account-button"
:icon-right="ArrowTopRightOnSquareIcon"
@click="openAccountUpdate"
>
{{ t('updateOs.update.viewAvailableUpdates') }}
</BrandButton>
</div>
<div v-show="!showLoader">
<div v-else>
<UpdateOsStatus
:show-update-check="true"
:title="t('updateOs.updateUnraidOs')"

View File

@@ -22,6 +22,7 @@ const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
const WEBGUI_GRAPHQL = '/graphql';
const WEBGUI_SETTINGS_MANAGMENT_ACCESS = '/Settings/ManagementAccess';
const WEBGUI_CONNECT_SETTINGS = `${WEBGUI_SETTINGS_MANAGMENT_ACCESS}#UnraidNetSettings`;
const WEBGUI_REDIRECT = '/redirect.htm';
const WEBGUI_TOOLS_DOWNGRADE = '/Tools/Downgrade';
const WEBGUI_TOOLS_REGISTRATION = '/Tools/Registration';
const WEBGUI_TOOLS_UPDATE = '/Tools/Update';
@@ -66,6 +67,7 @@ export {
DOCS_REGISTRATION_LICENSING,
DOCS_REGISTRATION_REPLACE_KEY,
WEBGUI_CONNECT_SETTINGS,
WEBGUI_REDIRECT,
WEBGUI_GRAPHQL,
WEBGUI_SETTINGS_MANAGMENT_ACCESS,
WEBGUI_TOOLS_DOWNGRADE,

View File

@@ -30,6 +30,7 @@
"common.installed": "Installed",
"common.installing": "Installing",
"common.learnMore": "Learn More",
"common.retry": "Retry",
"common.loading2": "Loading…",
"common.success": "Success!",
"common.unknown": "Unknown",

View File

@@ -95,9 +95,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
renewStatus.value = status;
};
const replaceStatus = ref<'checking' | 'eligible' | 'error' | 'ineligible' | 'ready'>(
guid.value ? 'ready' : 'error'
);
const replaceStatus = ref<'checking' | 'eligible' | 'error' | 'ineligible' | 'ready'>('ready');
const setReplaceStatus = (status: typeof replaceStatus.value) => {
replaceStatus.value = status;
};
@@ -169,11 +167,15 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
const check = async (skipCache: boolean = false) => {
if (!guid.value) {
setReplaceStatus('error');
setKeyLinked('error');
error.value = { name: 'Error', message: 'Flash GUID required to check replacement status' };
return;
}
if (!keyfile.value) {
setReplaceStatus('error');
setKeyLinked('error');
error.value = { name: 'Error', message: 'Keyfile required to check replacement status' };
return;
}
try {
@@ -240,11 +242,32 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
} catch (err) {
const catchError = err as WretchError;
setReplaceStatus('error');
error.value = catchError?.message ? catchError : { name: 'Error', message: 'Unknown error' };
setKeyLinked('error');
let errorMessage = 'Unknown error';
if (catchError?.response?.status === 401) {
errorMessage = 'Authentication failed - please sign in again';
} else if (catchError?.response?.status === 403) {
errorMessage = 'Access denied - license may be linked to another account';
} else if (catchError?.response?.status && catchError.response.status >= 500) {
errorMessage = 'Key server temporarily unavailable - please try again later';
} else if (catchError?.message) {
errorMessage = catchError.message;
} else if (typeof navigator !== 'undefined' && !navigator.onLine) {
errorMessage = 'No internet connection';
}
error.value = { name: 'Error', message: errorMessage };
console.error('[ReplaceCheck.check]', catchError);
}
};
const reset = () => {
replaceStatus.value = 'ready';
keyLinkedStatus.value = 'ready';
error.value = null;
};
return {
// state
keyLinkedStatus,
@@ -255,6 +278,7 @@ export const useReplaceRenewStore = defineStore('replaceRenewCheck', () => {
// actions
check,
purgeValidationResponse,
reset,
setReplaceStatus,
setRenewStatus,
error,

View File

@@ -1,6 +1,7 @@
import { computed, ref } from 'vue';
import { defineStore } from 'pinia';
import { WEBGUI_REDIRECT } from '~/helpers/urls';
import dayjs, { extend } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import relativeTime from 'dayjs/plugin/relativeTime';
@@ -71,8 +72,9 @@ export const useUpdateOsStore = defineStore('updateOs', () => {
// fetchAndConfirmInstall logic
const callbackStore = useCallbackActionsStore();
const fetchAndConfirmInstall = (sha256: string) => {
const redirectUrl = new URL(WEBGUI_REDIRECT, window.location.origin).toString();
callbackStore.send(
window.location.href,
redirectUrl,
[
{
sha256,