mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
12 Commits
4.28.1-bui
...
v4.29.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c39b0b267c | ||
|
|
73135b8328 | ||
|
|
e42d619b6d | ||
|
|
560db880cc | ||
|
|
d6055f102b | ||
|
|
d099e7521d | ||
|
|
bb9b539732 | ||
|
|
0e44e73bf7 | ||
|
|
277ac42046 | ||
|
|
e1e3ea7eb6 | ||
|
|
8b155d1f1c | ||
|
|
d13a1f6174 |
@@ -1 +1 @@
|
||||
{".":"4.28.1"}
|
||||
{".":"4.29.2"}
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "4.28.1",
|
||||
"version": "4.29.2",
|
||||
"main": "src/cli/index.ts",
|
||||
"type": "module",
|
||||
"corepack": {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "unraid-monorepo",
|
||||
"private": true,
|
||||
"version": "4.28.1",
|
||||
"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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/connect-plugin",
|
||||
"version": "4.28.1",
|
||||
"version": "4.29.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"commander": "14.0.0",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)_"
|
||||
|
||||
45
readme.md
45
readme.md
@@ -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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/ui",
|
||||
"version": "4.28.1",
|
||||
"version": "4.29.2",
|
||||
"private": true,
|
||||
"license": "GPL-2.0-or-later",
|
||||
"type": "module",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/web",
|
||||
"version": "4.28.1",
|
||||
"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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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')"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user