Compare commits

..

2 Commits

Author SHA1 Message Date
renovate[bot]
ce16b90c3e Merge 8f07eab623 into dd759d9f0f 2025-07-10 14:24:14 +00:00
renovate[bot]
8f07eab623 chore(deps): pin dependencies 2025-07-10 14:24:09 +00:00
42 changed files with 2464 additions and 3514 deletions

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.17.0'
node-version: '20.19.3'
- uses: pnpm/action-setup@v4
name: Install pnpm
@@ -33,7 +33,7 @@ jobs:
run_install: false
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
version: 1.0

View File

@@ -45,7 +45,7 @@ jobs:
node-version-file: ".nvmrc"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential libvirt-daemon-system
version: 1.0
@@ -190,7 +190,7 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0
@@ -267,7 +267,7 @@ jobs:
${{ runner.os }}-pnpm-store-
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: bash procps python3 libvirt-dev jq zstd git build-essential
version: 1.0

View File

@@ -31,7 +31,7 @@ jobs:
python-version: "3.13.5"
- name: Cache APT Packages
uses: awalsh128/cache-apt-pkgs-action@v1.5.1
uses: awalsh128/cache-apt-pkgs-action@v1.4.3
with:
packages: libvirt-dev
version: 1.0

2
.nvmrc
View File

@@ -1 +1 @@
22.17.0
22.16.0

View File

@@ -1 +1 @@
{".":"4.10.0"}
{".":"4.9.4"}

View File

@@ -1,30 +1,5 @@
# Changelog
## [4.10.0](https://github.com/unraid/api/compare/v4.9.5...v4.10.0) (2025-07-15)
### Features
* trial extension allowed within 5 days of expiration ([#1490](https://github.com/unraid/api/issues/1490)) ([f34a33b](https://github.com/unraid/api/commit/f34a33bc9f1a7e135d453d9d31888789bfc3f878))
### Bug Fixes
* delay `nginx:reload` file mod effect by 10 seconds ([#1512](https://github.com/unraid/api/issues/1512)) ([af33e99](https://github.com/unraid/api/commit/af33e999a0480a77e3e6b2aa833b17b38b835656))
* **deps:** update all non-major dependencies ([#1489](https://github.com/unraid/api/issues/1489)) ([53b05eb](https://github.com/unraid/api/commit/53b05ebe5e2050cb0916fcd65e8d41370aee0624))
* ensure no crash if emhttp state configs are missing ([#1514](https://github.com/unraid/api/issues/1514)) ([1a7d35d](https://github.com/unraid/api/commit/1a7d35d3f6972fd8aff58c17b2b0fb79725e660e))
* **my.servers:** improve DNS resolution robustness for backup server ([#1518](https://github.com/unraid/api/issues/1518)) ([eecd9b1](https://github.com/unraid/api/commit/eecd9b1017a63651d1dc782feaa224111cdee8b6))
* over-eager cloud query from web components ([#1506](https://github.com/unraid/api/issues/1506)) ([074370c](https://github.com/unraid/api/commit/074370c42cdecc4dbc58193ff518aa25735c56b3))
* replace myservers.cfg reads in UpdateFlashBackup.php ([#1517](https://github.com/unraid/api/issues/1517)) ([441e180](https://github.com/unraid/api/commit/441e1805c108a6c1cd35ee093246b975a03f8474))
* rm short-circuit in `rc.unraid-api` if plugin config dir is absent ([#1515](https://github.com/unraid/api/issues/1515)) ([29dcb7d](https://github.com/unraid/api/commit/29dcb7d0f088937cefc5158055f48680e86e5c36))
## [4.9.5](https://github.com/unraid/api/compare/v4.9.4...v4.9.5) (2025-07-10)
### Bug Fixes
* **connect:** rm eager restart on `ERROR_RETYING` connection status ([#1502](https://github.com/unraid/api/issues/1502)) ([dd759d9](https://github.com/unraid/api/commit/dd759d9f0f841b296f8083bc67c6cd3f7a69aa5b))
## [4.9.4](https://github.com/unraid/api/compare/v4.9.3...v4.9.4) (2025-07-09)

View File

@@ -1,12 +1,10 @@
{
"version": "4.9.5",
"version": "4.8.0",
"extraOrigins": [
"https://google.com",
"https://test.com"
],
"sandbox": true,
"ssoSubIds": [],
"plugins": [
"unraid-api-plugin-connect"
]
"plugins": ["unraid-api-plugin-connect"]
}

View File

@@ -247,6 +247,347 @@ A field whose value conforms to the standard URL format as specified in RFC3986:
"""
scalar URL
type DiskPartition {
"""The name of the partition"""
name: String!
"""The filesystem type of the partition"""
fsType: DiskFsType!
"""The size of the partition in bytes"""
size: Float!
}
"""The type of filesystem on the disk partition"""
enum DiskFsType {
XFS
BTRFS
VFAT
ZFS
EXT4
NTFS
}
type Disk implements Node {
id: PrefixedID!
"""The device path of the disk (e.g. /dev/sdb)"""
device: String!
"""The type of disk (e.g. SSD, HDD)"""
type: String!
"""The model name of the disk"""
name: String!
"""The manufacturer of the disk"""
vendor: String!
"""The total size of the disk in bytes"""
size: Float!
"""The number of bytes per sector"""
bytesPerSector: Float!
"""The total number of cylinders on the disk"""
totalCylinders: Float!
"""The total number of heads on the disk"""
totalHeads: Float!
"""The total number of sectors on the disk"""
totalSectors: Float!
"""The total number of tracks on the disk"""
totalTracks: Float!
"""The number of tracks per cylinder"""
tracksPerCylinder: Float!
"""The number of sectors per track"""
sectorsPerTrack: Float!
"""The firmware revision of the disk"""
firmwareRevision: String!
"""The serial number of the disk"""
serialNum: String!
"""The interface type of the disk"""
interfaceType: DiskInterfaceType!
"""The SMART status of the disk"""
smartStatus: DiskSmartStatus!
"""The current temperature of the disk in Celsius"""
temperature: Float
"""The partitions on the disk"""
partitions: [DiskPartition!]!
}
"""The type of interface the disk uses to connect to the system"""
enum DiskInterfaceType {
SAS
SATA
USB
PCIE
UNKNOWN
}
"""
The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk
"""
enum DiskSmartStatus {
OK
UNKNOWN
}
type KeyFile {
location: String
contents: String
}
type Registration implements Node {
id: PrefixedID!
type: registrationType
keyFile: KeyFile
state: RegistrationState
expiration: String
updateExpiration: String
}
enum registrationType {
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
INVALID
TRIAL
}
enum RegistrationState {
TRIAL
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
EEXPIRED
EGUID
EGUID1
ETRIAL
ENOKEYFILE
ENOKEYFILE1
ENOKEYFILE2
ENOFLASH
ENOFLASH1
ENOFLASH2
ENOFLASH3
ENOFLASH4
ENOFLASH5
ENOFLASH6
ENOFLASH7
EBLACKLISTED
EBLACKLISTED1
EBLACKLISTED2
ENOCONN
}
type Vars implements Node {
id: PrefixedID!
"""Unraid version"""
version: String
maxArraysz: Int
maxCachesz: Int
"""Machine hostname"""
name: String
timeZone: String
comment: String
security: String
workgroup: String
domain: String
domainShort: String
hideDotFiles: Boolean
localMaster: Boolean
enableFruit: String
"""Should a NTP server be used for time sync?"""
useNtp: Boolean
"""NTP Server 1"""
ntpServer1: String
"""NTP Server 2"""
ntpServer2: String
"""NTP Server 3"""
ntpServer3: String
"""NTP Server 4"""
ntpServer4: String
domainLogin: String
sysModel: String
sysArraySlots: Int
sysCacheSlots: Int
sysFlashSlots: Int
useSsl: Boolean
"""Port for the webui via HTTP"""
port: Int
"""Port for the webui via HTTPS"""
portssl: Int
localTld: String
bindMgt: Boolean
"""Should telnet be enabled?"""
useTelnet: Boolean
porttelnet: Int
useSsh: Boolean
portssh: Int
startPage: String
startArray: Boolean
spindownDelay: String
queueDepth: String
spinupGroups: Boolean
defaultFormat: String
defaultFsType: String
shutdownTimeout: Int
luksKeyfile: String
pollAttributes: String
pollAttributesDefault: String
pollAttributesStatus: String
nrRequests: Int
nrRequestsDefault: Int
nrRequestsStatus: String
mdNumStripes: Int
mdNumStripesDefault: Int
mdNumStripesStatus: String
mdSyncWindow: Int
mdSyncWindowDefault: Int
mdSyncWindowStatus: String
mdSyncThresh: Int
mdSyncThreshDefault: Int
mdSyncThreshStatus: String
mdWriteMethod: Int
mdWriteMethodDefault: String
mdWriteMethodStatus: String
shareDisk: String
shareUser: String
shareUserInclude: String
shareUserExclude: String
shareSmbEnabled: Boolean
shareNfsEnabled: Boolean
shareAfpEnabled: Boolean
shareInitialOwner: String
shareInitialGroup: String
shareCacheEnabled: Boolean
shareCacheFloor: String
shareMoverSchedule: String
shareMoverLogging: Boolean
fuseRemember: String
fuseRememberDefault: String
fuseRememberStatus: String
fuseDirectio: String
fuseDirectioDefault: String
fuseDirectioStatus: String
shareAvahiEnabled: Boolean
shareAvahiSmbName: String
shareAvahiSmbModel: String
shareAvahiAfpName: String
shareAvahiAfpModel: String
safeMode: Boolean
startMode: String
configValid: Boolean
configError: ConfigErrorState
joinStatus: String
deviceCount: Int
flashGuid: String
flashProduct: String
flashVendor: String
regCheck: String
regFile: String
regGuid: String
regTy: registrationType
regState: RegistrationState
"""Registration owner"""
regTo: String
regTm: String
regTm2: String
regGen: String
sbName: String
sbVersion: String
sbUpdated: String
sbEvents: Int
sbState: String
sbClean: Boolean
sbSynced: Int
sbSyncErrs: Int
sbSynced2: Int
sbSyncExit: String
sbNumDisks: Int
mdColor: String
mdNumDisks: Int
mdNumDisabled: Int
mdNumInvalid: Int
mdNumMissing: Int
mdNumNew: Int
mdNumErased: Int
mdResync: Int
mdResyncCorr: String
mdResyncPos: String
mdResyncDb: String
mdResyncDt: String
mdResyncAction: String
mdResyncSize: Int
mdState: String
mdVersion: String
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
"""Human friendly string of array events happening"""
fsProgress: String
"""
Percentage from 0 - 100 while upgrading a disk or swapping parity drives
"""
fsCopyPrcnt: Int
fsNumMounted: Int
fsNumUnmountable: Int
fsUnmountableMask: String
"""Total amount of user shares"""
shareCount: Int
"""Total amount shares with SMB enabled"""
shareSmbCount: Int
"""Total amount shares with NFS enabled"""
shareNfsCount: Int
"""Total amount shares with AFP enabled"""
shareAfpCount: Int
shareMoverActive: Boolean
csrfToken: String
}
"""Possible error states for configuration"""
enum ConfigErrorState {
UNKNOWN_ERROR
INELIGIBLE
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type Permission {
resource: Resource!
actions: [String!]!
@@ -620,102 +961,6 @@ enum ThemeName {
white
}
type DiskPartition {
"""The name of the partition"""
name: String!
"""The filesystem type of the partition"""
fsType: DiskFsType!
"""The size of the partition in bytes"""
size: Float!
}
"""The type of filesystem on the disk partition"""
enum DiskFsType {
XFS
BTRFS
VFAT
ZFS
EXT4
NTFS
}
type Disk implements Node {
id: PrefixedID!
"""The device path of the disk (e.g. /dev/sdb)"""
device: String!
"""The type of disk (e.g. SSD, HDD)"""
type: String!
"""The model name of the disk"""
name: String!
"""The manufacturer of the disk"""
vendor: String!
"""The total size of the disk in bytes"""
size: Float!
"""The number of bytes per sector"""
bytesPerSector: Float!
"""The total number of cylinders on the disk"""
totalCylinders: Float!
"""The total number of heads on the disk"""
totalHeads: Float!
"""The total number of sectors on the disk"""
totalSectors: Float!
"""The total number of tracks on the disk"""
totalTracks: Float!
"""The number of tracks per cylinder"""
tracksPerCylinder: Float!
"""The number of sectors per track"""
sectorsPerTrack: Float!
"""The firmware revision of the disk"""
firmwareRevision: String!
"""The serial number of the disk"""
serialNum: String!
"""The interface type of the disk"""
interfaceType: DiskInterfaceType!
"""The SMART status of the disk"""
smartStatus: DiskSmartStatus!
"""The current temperature of the disk in Celsius"""
temperature: Float
"""The partitions on the disk"""
partitions: [DiskPartition!]!
}
"""The type of interface the disk uses to connect to the system"""
enum DiskInterfaceType {
SAS
SATA
USB
PCIE
UNKNOWN
}
"""
The SMART (Self-Monitoring, Analysis and Reporting Technology) status of the disk
"""
enum DiskSmartStatus {
OK
UNKNOWN
}
type InfoApps implements Node {
id: PrefixedID!
@@ -1106,60 +1351,6 @@ type Owner {
avatar: String!
}
type KeyFile {
location: String
contents: String
}
type Registration implements Node {
id: PrefixedID!
type: registrationType
keyFile: KeyFile
state: RegistrationState
expiration: String
updateExpiration: String
}
enum registrationType {
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
INVALID
TRIAL
}
enum RegistrationState {
TRIAL
BASIC
PLUS
PRO
STARTER
UNLEASHED
LIFETIME
EEXPIRED
EGUID
EGUID1
ETRIAL
ENOKEYFILE
ENOKEYFILE1
ENOKEYFILE2
ENOFLASH
ENOFLASH1
ENOFLASH2
ENOFLASH3
ENOFLASH4
ENOFLASH5
ENOFLASH6
ENOFLASH7
EBLACKLISTED
EBLACKLISTED1
EBLACKLISTED2
ENOCONN
}
type ProfileModel implements Node {
id: PrefixedID!
username: String!
@@ -1225,197 +1416,6 @@ type Settings implements Node {
api: ApiConfig!
}
type Vars implements Node {
id: PrefixedID!
"""Unraid version"""
version: String
maxArraysz: Int
maxCachesz: Int
"""Machine hostname"""
name: String
timeZone: String
comment: String
security: String
workgroup: String
domain: String
domainShort: String
hideDotFiles: Boolean
localMaster: Boolean
enableFruit: String
"""Should a NTP server be used for time sync?"""
useNtp: Boolean
"""NTP Server 1"""
ntpServer1: String
"""NTP Server 2"""
ntpServer2: String
"""NTP Server 3"""
ntpServer3: String
"""NTP Server 4"""
ntpServer4: String
domainLogin: String
sysModel: String
sysArraySlots: Int
sysCacheSlots: Int
sysFlashSlots: Int
useSsl: Boolean
"""Port for the webui via HTTP"""
port: Int
"""Port for the webui via HTTPS"""
portssl: Int
localTld: String
bindMgt: Boolean
"""Should telnet be enabled?"""
useTelnet: Boolean
porttelnet: Int
useSsh: Boolean
portssh: Int
startPage: String
startArray: Boolean
spindownDelay: String
queueDepth: String
spinupGroups: Boolean
defaultFormat: String
defaultFsType: String
shutdownTimeout: Int
luksKeyfile: String
pollAttributes: String
pollAttributesDefault: String
pollAttributesStatus: String
nrRequests: Int
nrRequestsDefault: Int
nrRequestsStatus: String
mdNumStripes: Int
mdNumStripesDefault: Int
mdNumStripesStatus: String
mdSyncWindow: Int
mdSyncWindowDefault: Int
mdSyncWindowStatus: String
mdSyncThresh: Int
mdSyncThreshDefault: Int
mdSyncThreshStatus: String
mdWriteMethod: Int
mdWriteMethodDefault: String
mdWriteMethodStatus: String
shareDisk: String
shareUser: String
shareUserInclude: String
shareUserExclude: String
shareSmbEnabled: Boolean
shareNfsEnabled: Boolean
shareAfpEnabled: Boolean
shareInitialOwner: String
shareInitialGroup: String
shareCacheEnabled: Boolean
shareCacheFloor: String
shareMoverSchedule: String
shareMoverLogging: Boolean
fuseRemember: String
fuseRememberDefault: String
fuseRememberStatus: String
fuseDirectio: String
fuseDirectioDefault: String
fuseDirectioStatus: String
shareAvahiEnabled: Boolean
shareAvahiSmbName: String
shareAvahiSmbModel: String
shareAvahiAfpName: String
shareAvahiAfpModel: String
safeMode: Boolean
startMode: String
configValid: Boolean
configError: ConfigErrorState
joinStatus: String
deviceCount: Int
flashGuid: String
flashProduct: String
flashVendor: String
regCheck: String
regFile: String
regGuid: String
regTy: registrationType
regState: RegistrationState
"""Registration owner"""
regTo: String
regTm: String
regTm2: String
regGen: String
sbName: String
sbVersion: String
sbUpdated: String
sbEvents: Int
sbState: String
sbClean: Boolean
sbSynced: Int
sbSyncErrs: Int
sbSynced2: Int
sbSyncExit: String
sbNumDisks: Int
mdColor: String
mdNumDisks: Int
mdNumDisabled: Int
mdNumInvalid: Int
mdNumMissing: Int
mdNumNew: Int
mdNumErased: Int
mdResync: Int
mdResyncCorr: String
mdResyncPos: String
mdResyncDb: String
mdResyncDt: String
mdResyncAction: String
mdResyncSize: Int
mdState: String
mdVersion: String
cacheNumDevices: Int
cacheSbNumDisks: Int
fsState: String
"""Human friendly string of array events happening"""
fsProgress: String
"""
Percentage from 0 - 100 while upgrading a disk or swapping parity drives
"""
fsCopyPrcnt: Int
fsNumMounted: Int
fsNumUnmountable: Int
fsUnmountableMask: String
"""Total amount of user shares"""
shareCount: Int
"""Total amount shares with SMB enabled"""
shareSmbCount: Int
"""Total amount shares with NFS enabled"""
shareNfsCount: Int
"""Total amount shares with AFP enabled"""
shareAfpCount: Int
shareMoverActive: Boolean
csrfToken: String
}
"""Possible error states for configuration"""
enum ConfigErrorState {
UNKNOWN_ERROR
INELIGIBLE
INVALID
NO_KEY_SERVER
WITHDRAWN
}
type VmDomain implements Node {
"""The unique identifier for the vm (uuid)"""
id: PrefixedID!

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "4.10.0",
"version": "4.9.4",
"main": "src/cli/index.ts",
"type": "module",
"corepack": {
@@ -10,7 +10,7 @@
"author": "Lime Technology, Inc. <unraid.net>",
"license": "GPL-2.0-or-later",
"engines": {
"pnpm": "10.13.1"
"pnpm": "10.12.4"
},
"scripts": {
"// Development": "",
@@ -57,7 +57,7 @@
"@as-integrations/fastify": "2.1.1",
"@fastify/cookie": "11.0.2",
"@fastify/helmet": "13.0.1",
"@graphql-codegen/client-preset": "4.8.3",
"@graphql-codegen/client-preset": "4.8.2",
"@graphql-tools/load-files": "7.0.1",
"@graphql-tools/merge": "9.0.24",
"@graphql-tools/schema": "10.0.23",
@@ -82,7 +82,7 @@
"accesscontrol": "2.2.1",
"bycontract": "2.0.11",
"bytes": "3.1.2",
"cache-manager": "7.0.1",
"cache-manager": "7.0.0",
"cacheable-lookup": "7.0.0",
"camelcase-keys": "9.1.3",
"casbin": "5.38.0",
@@ -94,11 +94,11 @@
"command-exists": "1.2.9",
"convert": "5.12.0",
"cookie": "1.0.2",
"cron": "4.3.2",
"cron": "4.3.1",
"cross-fetch": "4.1.0",
"diff": "8.0.2",
"dockerode": "4.0.7",
"dotenv": "17.2.0",
"dotenv": "17.1.0",
"execa": "9.6.0",
"exit-hook": "4.0.0",
"fastify": "5.4.0",
@@ -112,7 +112,7 @@
"graphql-scalars": "1.24.2",
"graphql-subscriptions": "3.0.0",
"graphql-tag": "2.12.6",
"graphql-ws": "6.0.6",
"graphql-ws": "6.0.5",
"ini": "5.0.0",
"ip": "2.0.1",
"jose": "6.0.11",
@@ -138,11 +138,11 @@
"rxjs": "7.8.2",
"semver": "7.7.2",
"strftime": "0.10.3",
"systeminformation": "5.27.7",
"systeminformation": "5.27.6",
"uuid": "11.1.0",
"ws": "8.18.3",
"ws": "8.18.2",
"zen-observable-ts": "1.1.0",
"zod": "3.25.76"
"zod": "3.25.67"
},
"peerDependencies": {
"unraid-api-plugin-connect": "workspace:*"
@@ -153,35 +153,35 @@
}
},
"devDependencies": {
"@eslint/js": "9.31.0",
"@eslint/js": "9.29.0",
"@graphql-codegen/add": "5.0.3",
"@graphql-codegen/cli": "5.0.7",
"@graphql-codegen/fragment-matcher": "5.1.0",
"@graphql-codegen/import-types-preset": "3.0.1",
"@graphql-codegen/typed-document-node": "5.1.2",
"@graphql-codegen/typed-document-node": "5.1.1",
"@graphql-codegen/typescript": "4.1.6",
"@graphql-codegen/typescript-operations": "4.6.1",
"@graphql-codegen/typescript-resolvers": "4.5.1",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.5.1",
"@ianvs/prettier-plugin-sort-imports": "4.4.2",
"@nestjs/testing": "11.1.3",
"@originjs/vite-plugin-commonjs": "1.0.3",
"@rollup/plugin-node-resolve": "16.0.1",
"@swc/core": "1.12.14",
"@swc/core": "1.12.4",
"@types/async-exit-hook": "2.0.2",
"@types/bytes": "3.1.5",
"@types/cli-table": "0.3.4",
"@types/command-exists": "1.2.3",
"@types/cors": "2.8.19",
"@types/dockerode": "3.3.42",
"@types/dockerode": "3.3.41",
"@types/graphql-fields": "1.3.9",
"@types/graphql-type-uuid": "0.2.6",
"@types/ini": "4.1.1",
"@types/ip": "1.1.3",
"@types/lodash": "4.17.20",
"@types/lodash": "4.17.18",
"@types/lodash-es": "4.17.12",
"@types/mustache": "4.2.6",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"@types/pify": "6.1.0",
"@types/semver": "7.7.0",
"@types/sendmail": "1.4.7",
@@ -193,27 +193,27 @@
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"cz-conventional-changelog": "3.3.0",
"eslint": "9.31.0",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-n": "17.21.0",
"eslint": "9.29.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-n": "17.20.0",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-prettier": "5.5.0",
"graphql-codegen-typescript-validation-schema": "0.17.1",
"jiti": "2.4.2",
"nodemon": "3.1.10",
"prettier": "3.6.2",
"prettier": "3.5.3",
"rollup-plugin-node-externals": "8.0.1",
"commit-and-tag-version": "9.6.0",
"commit-and-tag-version": "9.5.0",
"tsx": "4.20.3",
"type-fest": "4.41.0",
"typescript": "5.8.3",
"typescript-eslint": "8.37.0",
"typescript-eslint": "8.34.1",
"unplugin-swc": "1.5.5",
"vite": "7.0.4",
"vite": "7.0.3",
"vite-plugin-node": "7.0.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"zx": "8.7.1"
"zx": "8.5.5"
},
"overrides": {
"eslint": {
@@ -228,5 +228,5 @@
}
},
"private": true,
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.12.4"
}

View File

@@ -17,6 +17,7 @@ exports[`Returns paths 1`] = `
"myservers-base",
"myservers-config",
"myservers-config-states",
"myservers-env",
"myservers-keepalive",
"keyfile-base",
"machine-id",

View File

@@ -24,6 +24,7 @@ test('Returns paths', async () => {
'myservers-base': '/boot/config/plugins/dynamix.my.servers/',
'myservers-config': expect.stringContaining('api/dev/Unraid.net/myservers.cfg'),
'myservers-config-states': expect.stringContaining('api/dev/states/myservers.cfg'),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env',
'myservers-keepalive': './dev/Unraid.net/fb_keepalive',
'keyfile-base': expect.stringContaining('api/dev/Unraid.net'),
'machine-id': expect.stringContaining('api/dev/data/machine-id'),

View File

@@ -67,7 +67,6 @@ export const getPackageJsonDependencies = (): string[] | undefined => {
export const API_VERSION = process.env.npm_package_version ?? getPackageJson().version;
/** Controls how the app is built/run (i.e. in terms of optimization) */
export const NODE_ENV =
(process.env.NODE_ENV as 'development' | 'test' | 'staging' | 'production') ?? 'production';
export const environment = {
@@ -77,7 +76,6 @@ export const CHOKIDAR_USEPOLLING = process.env.CHOKIDAR_USEPOLLING === 'true';
export const IS_DOCKER = process.env.IS_DOCKER === 'true';
export const DEBUG = process.env.DEBUG === 'true';
export const INTROSPECTION = process.env.INTROSPECTION === 'true';
/** Determines the app-level & business logic environment (i.e. what data & infrastructure is used) */
export const ENVIRONMENT = process.env.ENVIRONMENT
? (process.env.ENVIRONMENT as 'production' | 'staging' | 'development')
: 'production';

View File

@@ -49,6 +49,7 @@ const initialState = {
resolvePath(process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)),
'myservers.cfg' as const
),
'myservers-env': '/boot/config/plugins/dynamix.my.servers/env' as const,
'myservers-keepalive':
process.env.PATHS_MY_SERVERS_FB ??
('/boot/config/plugins/dynamix.my.servers/fb_keepalive' as const),

View File

@@ -1,6 +1,6 @@
import { writeFileSync } from 'fs';
import { join } from 'path';
import { ensureWriteSync } from '@unraid/shared/util/file.js';
import { isEqual } from 'lodash-es';
import type { RootState } from '@app/store/index.js';
@@ -27,11 +27,8 @@ export const startStoreSync = async () => {
!isEqual(state, lastState) &&
state.paths['myservers-config-states']
) {
ensureWriteSync(
join(state.paths.states, 'config.log'),
JSON.stringify(state.config, null, 2)
);
ensureWriteSync(
writeFileSync(join(state.paths.states, 'config.log'), JSON.stringify(state.config, null, 2));
writeFileSync(
join(state.paths.states, 'graphql.log'),
JSON.stringify(state.minigraph, null, 2)
);

View File

@@ -1,13 +1,13 @@
import { copyFile } from 'fs/promises';
import { copyFile, readFile, writeFile } from 'fs/promises';
import { join } from 'path';
import { Command, CommandRunner, Option } from 'nest-commander';
import { fileExistsSync } from '@app/core/utils/files/file-exists.js';
import { ENVIRONMENT } from '@app/environment.js';
import { cliLogger } from '@app/core/log.js';
import { getters } from '@app/store/index.js';
import { LogService } from '@app/unraid-api/cli/log.service.js';
import { RestartCommand } from '@app/unraid-api/cli/restart.command.js';
import { StartCommand } from '@app/unraid-api/cli/start.command.js';
import { StopCommand } from '@app/unraid-api/cli/stop.command.js';
interface SwitchEnvOptions {
environment?: 'staging' | 'production';
@@ -31,43 +31,60 @@ export class SwitchEnvCommand extends CommandRunner {
constructor(
private readonly logger: LogService,
private readonly restartCommand: RestartCommand
private readonly stopCommand: StopCommand,
private readonly startCommand: StartCommand
) {
super();
}
private async getEnvironmentFromFile(path: string): Promise<'production' | 'staging'> {
const envFile = await readFile(path, 'utf-8').catch(() => '');
this.logger.debug(`Checking ${path} for current ENV, found ${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 production
const [, , currentEnvInFile] = matchArray && matchArray.length === 3 ? matchArray : [];
return this.parseStringToEnv(currentEnvInFile);
}
private switchToOtherEnv(environment: 'production' | 'staging'): 'production' | 'staging' {
if (environment === 'production') {
return 'staging';
}
return 'production';
}
async run(_, options: SwitchEnvOptions): Promise<void> {
const paths = getters.paths();
const basePath = paths['unraid-api-base'];
const currentEnvPath = join(basePath, '.env');
const envFlashFilePath = paths['myservers-env'];
// Determine target environment
const currentEnv = ENVIRONMENT;
const targetEnv = options.environment ?? 'production';
this.logger.info(`Switching environment from ${currentEnv} to ${targetEnv}`);
// Check if target environment file exists
const sourceEnvPath = join(basePath, `.env.${targetEnv}`);
if (!fileExistsSync(sourceEnvPath)) {
this.logger.error(
`Environment file ${sourceEnvPath} does not exist. Cannot switch to ${targetEnv} environment.`
);
process.exit(1);
}
// Copy the target environment file to .env
this.logger.debug(`Copying ${sourceEnvPath} to ${currentEnvPath}`);
this.logger.warn('Stopping the Unraid API');
try {
await copyFile(sourceEnvPath, currentEnvPath);
this.logger.info(`Successfully switched to ${targetEnv} environment`);
} catch (error) {
this.logger.error(`Failed to copy environment file: ${error}`);
process.exit(1);
await this.stopCommand.run([], { delete: false });
} catch (err) {
this.logger.warn('Failed to stop the Unraid API (maybe already stopped?)');
}
// Restart the API to pick up the new environment
this.logger.info('Restarting Unraid API to apply environment changes...');
await this.restartCommand.run();
const newEnv =
options.environment ??
this.switchToOtherEnv(await this.getEnvironmentFromFile(envFlashFilePath));
this.logger.info(`Setting environment to ${newEnv}`);
// Write new env to flash
const newEnvLine = `env="${newEnv}"`;
this.logger.debug('Writing %s to %s', newEnvLine, envFlashFilePath);
await writeFile(envFlashFilePath, newEnvLine);
// 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 this.startCommand.run([], {});
}
}

View File

@@ -10,7 +10,6 @@ export class NginxService {
async reload() {
try {
await execa('/etc/rc.d/rc.nginx', ['reload']);
this.logger.log('Nginx reloaded');
return true;
} catch (err: unknown) {
this.logger.warn('Failed to reload Nginx with error: ', err);

View File

@@ -1,18 +1,14 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { ONE_SECOND_MS } from '@app/consts.js';
import { NginxService } from '@app/unraid-api/nginx/nginx.service.js';
import { ModificationEffect } from '@app/unraid-api/unraid-file-modifier/file-modification.js';
@Injectable()
export class FileModificationEffectService {
private readonly logger = new Logger(FileModificationEffectService.name);
constructor(private readonly nginxService: NginxService) {}
async runEffect(effect: ModificationEffect): Promise<void> {
switch (effect) {
case 'nginx:reload':
this.logger.log('Reloading Nginx in 10 seconds...');
await new Promise((resolve) => setTimeout(resolve, 10 * ONE_SECOND_MS));
await this.nginxService.reload();
break;
}

View File

@@ -1,7 +1,7 @@
{
"name": "unraid-monorepo",
"private": true,
"version": "4.10.0",
"version": "4.9.4",
"scripts": {
"build": "pnpm -r build",
"build:watch": " pnpm -r --parallel build:watch",
@@ -57,5 +57,5 @@
"pnpm lint:fix"
]
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.12.4"
}

View File

@@ -25,10 +25,10 @@
"description": "Unraid Connect plugin for Unraid API",
"devDependencies": {
"@apollo/client": "3.13.8",
"@faker-js/faker": "9.9.0",
"@faker-js/faker": "9.8.0",
"@graphql-codegen/cli": "5.0.7",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.5.1",
"@ianvs/prettier-plugin-sort-imports": "4.4.2",
"@jsonforms/core": "3.6.0",
"@nestjs/apollo": "13.1.0",
"@nestjs/common": "11.1.3",
@@ -41,29 +41,29 @@
"@types/ini": "4.1.1",
"@types/ip": "1.1.3",
"@types/lodash-es": "4.17.12",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"@types/ws": "8.18.1",
"camelcase-keys": "9.1.3",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",
"execa": "9.6.0",
"fast-check": "4.2.0",
"fast-check": "4.1.1",
"got": "14.4.7",
"graphql": "16.11.0",
"graphql-scalars": "1.24.2",
"graphql-subscriptions": "3.0.0",
"graphql-ws": "6.0.6",
"graphql-ws": "6.0.5",
"ini": "5.0.0",
"jose": "6.0.11",
"lodash-es": "4.17.21",
"nest-authz": "2.17.0",
"prettier": "3.6.2",
"prettier": "3.5.3",
"rimraf": "6.0.1",
"rxjs": "7.8.2",
"type-fest": "4.41.0",
"typescript": "5.8.3",
"vitest": "3.2.4",
"ws": "8.18.3",
"ws": "8.18.2",
"zen-observable-ts": "1.1.0"
},
"dependencies": {
@@ -91,7 +91,7 @@
"graphql": "16.11.0",
"graphql-scalars": "1.24.2",
"graphql-subscriptions": "3.0.0",
"graphql-ws": "6.0.6",
"graphql-ws": "6.0.5",
"ini": "5.0.0",
"jose": "6.0.11",
"lodash-es": "4.17.21",

View File

@@ -130,19 +130,11 @@ export class MothershipConnectionService implements OnModuleInit, OnModuleDestro
}
async onModuleInit() {
// Warn on startup if these config values are not set initially
// Crash on startup if these config values are not set initially
const { unraidVersion, flashGuid, apiVersion } = this.configKeys;
const warnings: string[] = [];
[unraidVersion, flashGuid, apiVersion].forEach((key) => {
try {
this.configService.getOrThrow(key);
} catch (error) {
warnings.push(`${key} is not set`);
}
this.configService.getOrThrow(key);
});
if (warnings.length > 0) {
this.logger.warn('Missing config values: %s', warnings.join(', '));
}
// Setup IDENTITY_CHANGED & METADATA_CHANGED events
this.setupIdentitySubscription();
this.setupMetadataChangedEvent();

View File

@@ -15,7 +15,7 @@
"commander": "14.0.0",
"create-create-app": "7.3.0",
"fs-extra": "11.3.0",
"inquirer": "12.7.0",
"inquirer": "12.6.3",
"validate-npm-package-name": "6.0.1"
},
"devDependencies": {
@@ -25,7 +25,7 @@
"@nestjs/graphql": "13.1.0",
"@types/fs-extra": "11.0.4",
"@types/inquirer": "9.0.8",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"@types/validate-npm-package-name": "4.0.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.2",

View File

@@ -1,9 +0,0 @@
# Justfile for unraid-shared
# Default recipe to run when just is called without arguments
default:
@just --list
# Watch for changes in src files and run clean + build
watch:
watchexec -r -e ts,tsx -w src -- pnpm build

View File

@@ -31,9 +31,9 @@
"@jsonforms/core": "3.6.0",
"@nestjs/common": "11.1.3",
"@nestjs/graphql": "13.1.0",
"@types/bun": "1.2.18",
"@types/bun": "1.2.16",
"@types/lodash-es": "4.17.12",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"class-validator": "0.14.2",
"graphql": "16.11.0",
"graphql-scalars": "1.24.2",

View File

@@ -1,24 +1,11 @@
import { accessSync } from 'fs';
import { access, mkdir, writeFile } from 'fs/promises';
import { mkdirSync, writeFileSync } from 'fs';
import { access } from 'fs/promises';
import { F_OK } from 'node:constants';
import { dirname } from 'path';
/**
* Checks if a file exists asynchronously.
* @param path - The file path to check
* @returns Promise that resolves to true if file exists, false otherwise
*/
export const fileExists = async (path: string) =>
access(path, F_OK)
.then(() => true)
.catch(() => false);
/**
* Checks if a file exists synchronously.
* @param path - The file path to check
* @returns true if file exists, false otherwise
*/
export const fileExistsSync = (path: string) => {
try {
accessSync(path, F_OK);
@@ -27,44 +14,3 @@ export const fileExistsSync = (path: string) => {
return false;
}
};
/**
* Writes data to a file, creating parent directories if they don't exist.
*
* This function ensures the directory structure exists before writing the file,
* equivalent to `mkdir -p` followed by file writing.
*
* @param path - The file path to write to
* @param data - The data to write (string or Buffer)
* @throws {Error} If path is invalid (null, empty, or not a string)
* @throws {Error} For any file system errors (EACCES, EPERM, ENOSPC, EISDIR, etc.)
*/
export const ensureWrite = async (path: string, data: string | Buffer) => {
if (!path || typeof path !== 'string') {
throw new Error(`Invalid path provided: ${path}`);
}
await mkdir(dirname(path), { recursive: true });
return await writeFile(path, data);
};
/**
* Writes data to a file synchronously, creating parent directories if they don't exist.
*
* This function ensures the directory structure exists before writing the file,
* equivalent to `mkdir -p` followed by file writing.
*
* @param path - The file path to write to
* @param data - The data to write (string or Buffer)
* @throws {Error} If path is invalid (null, empty, or not a string)
* @throws {Error} For any file system errors (EACCES, EPERM, ENOSPC, EISDIR, etc.)
*/
export const ensureWriteSync = (path: string, data: string | Buffer) => {
if (!path || typeof path !== 'string') {
throw new Error(`Invalid path provided: ${path}`);
}
mkdirSync(dirname(path), { recursive: true });
return writeFileSync(path, data);
};

View File

@@ -1,17 +1,17 @@
{
"name": "@unraid/connect-plugin",
"version": "4.10.0",
"version": "4.9.4",
"private": true,
"dependencies": {
"commander": "14.0.0",
"conventional-changelog": "6.0.0",
"date-fns": "4.1.0",
"glob": "11.0.3",
"glob": "11.0.1",
"html-sloppy-escaper": "0.1.0",
"semver": "7.7.2",
"tsx": "4.20.3",
"zod": "3.25.76",
"zx": "8.7.1"
"semver": "7.7.1",
"tsx": "4.19.3",
"zod": "3.24.2",
"zx": "8.3.2"
},
"type": "module",
"license": "GPL-2.0-or-later",
@@ -37,7 +37,7 @@
"devDependencies": {
"http-server": "14.1.1",
"nodemon": "3.1.10",
"vitest": "3.2.4"
"vitest": "3.0.7"
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.12.4"
}

View File

@@ -331,7 +331,8 @@ exit 0
<![CDATA[
SCRIPTS_DIR="/usr/local/share/dynamix.unraid.net/install/scripts"
# Log file for debugging
mkdir -p "/var/log/unraid-api"
LOGFILE="/var/log/unraid-api/dynamix-unraid-install.log"
mkdir -p "$(dirname "$LOGFILE")"
echo "Starting Unraid Connect installation..."
@@ -343,26 +344,26 @@ CFG_NEW=/boot/config/plugins/dynamix.my.servers
# Setup the API (but don't start it yet)
if [ -x "$SCRIPTS_DIR/setup_api.sh" ]; then
echo "Setting up Unraid API..."
echo "Running setup_api.sh"
# Run and show output to user
"$SCRIPTS_DIR/setup_api.sh"
echo "Running setup_api.sh" >> "$LOGFILE"
# Capture output and add to log file
setup_output=$("$SCRIPTS_DIR/setup_api.sh")
echo "$setup_output" >> "$LOGFILE"
else
echo "ERROR: setup_api.sh not found or not executable"
echo "ERROR: setup_api.sh not found or not executable"
echo "ERROR: setup_api.sh not found or not executable" >> "$LOGFILE"
fi
# Run post-installation verification
if [ -x "$SCRIPTS_DIR/verify_install.sh" ]; then
echo "Running post-installation verification..."
echo "Running verify_install.sh"
# Run and show output to user
"$SCRIPTS_DIR/verify_install.sh"
echo "Running verify_install.sh" >> "$LOGFILE"
# Capture output and add to log file
verify_output=$("$SCRIPTS_DIR/verify_install.sh")
echo "$verify_output" >> "$LOGFILE"
else
echo "ERROR: verify_install.sh not found or not executable"
echo "ERROR: verify_install.sh not found or not executable"
echo "ERROR: verify_install.sh not found or not executable" >> "$LOGFILE"
fi
echo "Installation completed at $(date)"
echo "Installation completed at $(date)" >> "$LOGFILE"
]]>
</INLINE>
</FILE>
@@ -378,18 +379,6 @@ echo "Installation completed at $(date)"
/etc/rc.d/rc.unraid-api cleanup-dependencies
echo "Starting Unraid API service"
echo "DEBUG: Checking PATH: $PATH"
echo "DEBUG: Checking if unraid-api files exist:"
ls -la /usr/local/unraid-api/dist/
echo "DEBUG: Checking symlink:"
ls -la /usr/local/bin/unraid-api
echo "DEBUG: Checking Node.js version:"
node --version
echo "DEBUG: Checking if cli.js is executable:"
ls -la /usr/local/unraid-api/dist/cli.js
echo "DEBUG: Attempting to run unraid-api directly:"
/usr/local/unraid-api/dist/cli.js version || echo "Direct execution failed"
echo "If no additional messages appear within 30 seconds, it is safe to refresh the page."
/etc/rc.d/rc.unraid-api plugins add unraid-api-plugin-connect -b --no-restart
/etc/rc.d/rc.unraid-api start

View File

@@ -166,23 +166,22 @@ _enabled() {
return 1
}
_connected() {
local connect_config username status_cfg connection_status
connect_config=$API_CONFIG_HOME/connect.json
[[ ! -f "${connect_config}" ]] && return 1
CFG=$API_CONFIG_HOME/connect.json
[[ ! -f "${CFG}" ]] && return 1
# is the user signed in?
username=$(jq -r '.username // empty' "${connect_config}" 2>/dev/null)
username=$(jq -r '.username // empty' "${CFG}" 2>/dev/null)
if [ -z "${username}" ]; then
return 1
fi
# are we connected to mothership?
status_cfg="/var/local/emhttp/connectStatus.json"
[[ ! -f "${status_cfg}" ]] && return 1
connection_status=$(jq -r '.connectionStatus // empty' "${status_cfg}" 2>/dev/null)
if [[ "${connection_status}" != "CONNECTED" ]]; then
return 1
fi
# the minigraph status is no longer synced to the connect config file
# to avoid a false negative, we'll omit this check for now.
#
# shellcheck disable=SC1090
# source <(sed -nr '/\[connectionStatus\]/,/\[/{/minigraph/p}' "${CFG}" 2>/dev/null)
# # ensure connected
# if [[ -z "${minigraph}" || "${minigraph}" != "CONNECTED" ]]; then
# return 1
# fi
return 0
}
_haserror() {

View File

@@ -4,6 +4,9 @@
# shellcheck source=/dev/null
source /etc/profile
flash="/boot/config/plugins/dynamix.my.servers"
[[ ! -d "${flash}" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
[[ ! -f "${flash}/env" ]] && echo 'env=production' >"${flash}/env"
unraid_binary_path="/usr/local/bin/unraid-api"
api_base_dir="/usr/local/unraid-api"
scripts_dir="/usr/local/share/dynamix.unraid.net/scripts"

View File

@@ -18,9 +18,10 @@ $cli = php_sapi_name()=='cli';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/plugins/dynamix.my.servers/include/connect-config.php";
$isRegistered = ConnectConfig::isUserSignedIn();
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
$isRegistered = !empty($myservers['remote']['username']);
// Read connection status from the new API status file
$statusFilePath = '/var/local/emhttp/connectStatus.json';
@@ -594,31 +595,9 @@ set_git_config('user.email', 'gitbot@unraid.net');
set_git_config('user.name', 'gitbot');
// ensure dns can resolve backup.unraid.net
$dnsResolved = false;
// Try multiple DNS resolution methods
if (function_exists('dns_get_record')) {
$dnsRecords = dns_get_record("backup.unraid.net", DNS_A);
$dnsResolved = !empty($dnsRecords);
}
// Fallback to gethostbyname if dns_get_record fails
if (!$dnsResolved) {
$ip = gethostbyname("backup.unraid.net");
$dnsResolved = ($ip !== "backup.unraid.net");
}
// Final fallback to system nslookup
if (!$dnsResolved) {
$output = [];
$return_var = 0;
exec('nslookup backup.unraid.net 2>/dev/null', $output, $return_var);
$dnsResolved = ($return_var === 0 && !empty($output));
}
if (!$dnsResolved) {
if (! checkdnsrr("backup.unraid.net","A") ) {
$arrState['loading'] = '';
$arrState['error'] = 'DNS resolution failed for backup.unraid.net - PHP DNS functions (checkdnsrr, dns_get_record, gethostbyname) and system nslookup all failed to resolve the hostname. This indicates a DNS configuration issue on your Unraid server. Check your DNS settings in Settings > Network Settings.';
$arrState['error'] = 'DNS is unable to resolve backup.unraid.net';
response_complete(406, array('error' => $arrState['error']));
}

View File

@@ -1,26 +0,0 @@
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once "$docroot/plugins/dynamix.my.servers/include/api-config.php";
/**
* Wrapper around the API's connect.json configuration file.
*/
class ConnectConfig
{
public const CONFIG_PATH = ApiConfig::CONFIG_DIR . '/connect.json';
public static function getConfig()
{
try {
return json_decode(file_get_contents(self::CONFIG_PATH), true) ?? [];
} catch (Throwable $e) {
return [];
}
}
public static function isUserSignedIn()
{
$config = self::getConfig();
return ApiConfig::isConnectPluginEnabled() && !empty($config['username'] ?? '');
}
}

View File

@@ -1,7 +1,10 @@
#!/bin/bash
#!/bin/sh
# Unraid API Installation Verification Script
# Checks that critical files are installed correctly
# Exit on errors
set -e
echo "Performing comprehensive installation verification..."
# Define critical files to check (POSIX-compliant, no arrays)
@@ -168,7 +171,7 @@ if [ $TOTAL_ERRORS -eq 0 ]; then
else
printf 'Found %d total errors.\n' "$TOTAL_ERRORS"
echo "Installation verification completed with issues."
echo "Please review the errors above and contact support if needed."
echo "See log file for details: /var/log/unraid-api/dynamix-unraid-install.log"
# We don't exit with error as this is just a verification script
exit 0
fi

4361
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/ui",
"version": "4.10.0",
"version": "4.9.4",
"private": true,
"license": "GPL-2.0-or-later",
"type": "module",
@@ -54,68 +54,68 @@
"@jsonforms/core": "3.6.0",
"@jsonforms/vue": "3.6.0",
"@jsonforms/vue-vanilla": "3.6.0",
"@vueuse/core": "13.5.0",
"@vueuse/core": "13.4.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"dompurify": "3.2.6",
"kebab-case": "2.0.2",
"lucide-vue-next": "0.525.0",
"lucide-vue-next": "0.519.0",
"marked": "16.0.0",
"reka-ui": "2.3.2",
"reka-ui": "2.3.1",
"shadcn-vue": "2.2.0",
"tailwind-merge": "2.6.0",
"vue-sonner": "1.3.2"
"vue-sonner": "1.3.0"
},
"devDependencies": {
"@ianvs/prettier-plugin-sort-imports": "4.5.1",
"@storybook/addon-docs": "9.0.17",
"@storybook/addon-links": "9.0.17",
"@storybook/builder-vite": "9.0.17",
"@storybook/vue3-vite": "9.0.17",
"@ianvs/prettier-plugin-sort-imports": "4.4.2",
"@storybook/addon-docs": "9.0.16",
"@storybook/addon-links": "9.0.16",
"@storybook/builder-vite": "9.0.16",
"@storybook/vue3-vite": "9.0.16",
"@tailwindcss/typography": "0.5.16",
"@testing-library/vue": "8.1.0",
"@types/jsdom": "21.1.7",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"@types/testing-library__vue": "5.3.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/eslint-plugin": "8.34.1",
"@unraid/tailwind-rem-to-rem": "1.1.0",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vitest/coverage-v8": "3.2.4",
"@vitest/ui": "3.2.4",
"@vue/test-utils": "2.4.6",
"@vue/tsconfig": "0.7.0",
"autoprefixer": "10.4.21",
"concurrently": "9.2.0",
"eslint": "9.31.0",
"concurrently": "9.1.2",
"eslint": "9.29.0",
"eslint-config-prettier": "10.1.5",
"eslint-plugin-import": "2.32.0",
"eslint-plugin-import": "2.31.0",
"eslint-plugin-no-relative-import-paths": "1.6.1",
"eslint-plugin-prettier": "5.5.1",
"eslint-plugin-storybook": "9.0.17",
"eslint-plugin-vue": "10.3.0",
"happy-dom": "18.0.1",
"eslint-plugin-prettier": "5.5.0",
"eslint-plugin-storybook": "9.0.16",
"eslint-plugin-vue": "10.2.0",
"happy-dom": "18.0.0",
"jiti": "2.4.2",
"postcss": "8.5.6",
"postcss-import": "16.1.1",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "0.6.13",
"rimraf": "6.0.1",
"storybook": "9.0.17",
"storybook": "9.0.16",
"tailwind-rem-to-rem": "github:unraid/tailwind-rem-to-rem",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "1.0.7",
"typescript": "5.8.3",
"typescript-eslint": "8.37.0",
"vite": "7.0.4",
"typescript-eslint": "8.34.1",
"vite": "7.0.3",
"vite-plugin-dts": "3.9.1",
"vite-plugin-vue-devtools": "7.7.7",
"vitest": "3.2.4",
"vue": "3.5.17",
"vue-tsc": "3.0.1",
"wrangler": "4.24.3"
"wrangler": "3.114.10"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.45.1"
"@rollup/rollup-linux-x64-gnu": "4.44.0"
},
"exports": {
".": {
@@ -139,5 +139,5 @@
"import": "./dist/theme/preset.js"
}
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.12.4"
}

View File

@@ -764,323 +764,4 @@ describe('useServerStore', () => {
expect(store.cloudError).toBeDefined();
expect((store.cloudError as { message: string })?.message).toBe('Test error');
});
describe('trial extension features', () => {
it('should determine trial extension eligibility correctly', () => {
const store = getStore();
// Add trialExtensionEligible property to the store
Object.defineProperty(store, 'trialExtensionEligible', {
get: () => !store.regGen || store.regGen < 2,
});
// Eligible - no regGen
store.setServer({ regGen: 0 });
expect(store.trialExtensionEligible).toBe(true);
// Eligible - regGen = 1
store.setServer({ regGen: 1 });
expect(store.trialExtensionEligible).toBe(true);
// Not eligible - regGen = 2
store.setServer({ regGen: 2 });
expect(store.trialExtensionEligible).toBe(false);
// Not eligible - regGen > 2
store.setServer({ regGen: 3 });
expect(store.trialExtensionEligible).toBe(false);
});
it('should calculate trial within 5 days of expiration correctly', () => {
const store = getStore();
// Add properties to the store
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
get: () => {
if (!store.expireTime || store.state !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(store.expireTime);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
},
});
// Not a trial
store.setServer({ state: 'PRO' as ServerState, expireTime: dayjs().add(3, 'day').unix() * 1000 });
expect(store.trialWithin5DaysOfExpiration).toBe(false);
// Trial but no expireTime
store.setServer({ state: 'TRIAL' as ServerState, expireTime: 0 });
expect(store.trialWithin5DaysOfExpiration).toBe(false);
// Trial expiring in 3 days
store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(3, 'day').unix() * 1000 });
expect(store.trialWithin5DaysOfExpiration).toBe(true);
// Trial expiring in exactly 5 days
store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(5, 'day').unix() * 1000 });
expect(store.trialWithin5DaysOfExpiration).toBe(true);
// Trial expiring in 7 days (to ensure it's clearly outside the 5-day window)
store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().add(7, 'day').unix() * 1000 });
expect(store.trialWithin5DaysOfExpiration).toBe(false);
// Trial already expired
store.setServer({ state: 'TRIAL' as ServerState, expireTime: dayjs().subtract(1, 'day').unix() * 1000 });
expect(store.trialWithin5DaysOfExpiration).toBe(false);
});
it('should calculate trial extension renewal window conditions correctly', () => {
const store = getStore();
// Add all necessary properties
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
Object.defineProperty(store, 'trialExtensionEligible', {
get: () => !store.regGen || store.regGen < 2,
});
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
get: () => {
if (!store.expireTime || store.state !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(store.expireTime);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
},
});
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
});
Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', {
get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration,
});
Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', {
get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
});
// Eligible inside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 1,
expireTime: dayjs().add(3, 'day').unix() * 1000,
});
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(true);
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
// Eligible outside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 1,
expireTime: dayjs().add(10, 'day').unix() * 1000,
});
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(true);
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
// Ineligible inside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 2,
expireTime: dayjs().add(3, 'day').unix() * 1000,
});
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(true);
// Ineligible outside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 2,
expireTime: dayjs().add(10, 'day').unix() * 1000,
});
expect(store.trialExtensionEligibleInsideRenewalWindow).toBe(false);
expect(store.trialExtensionEligibleOutsideRenewalWindow).toBe(false);
expect(store.trialExtensionIneligibleInsideRenewalWindow).toBe(false);
});
it('should display correct trial messages based on extension eligibility and renewal window', () => {
const store = getStore();
// Add all necessary properties
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
Object.defineProperty(store, 'trialExtensionEligible', {
get: () => !store.regGen || store.regGen < 2,
});
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
get: () => {
if (!store.expireTime || store.state !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(store.expireTime);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
},
});
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
});
Object.defineProperty(store, 'trialExtensionEligibleOutsideRenewalWindow', {
get: () => store.trialExtensionEligible && !store.trialWithin5DaysOfExpiration,
});
Object.defineProperty(store, 'trialExtensionIneligibleInsideRenewalWindow', {
get: () => !store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
});
// Mock stateData getter to include trial message logic
Object.defineProperty(store, 'stateData', {
get: () => {
if (store.state !== 'TRIAL') {
return {
humanReadable: '',
heading: '',
message: '',
actions: [],
};
}
let trialMessage = '';
if (store.trialExtensionEligibleInsideRenewalWindow) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon. When it expires, <strong>the array will stop</strong>. You may extend your trial now, purchase a license key, or wait until expiration to take action.</p>';
} else if (store.trialExtensionIneligibleInsideRenewalWindow) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon and you have used all available extensions. When it expires, <strong>the array will stop</strong>. To continue using Unraid OS, you must purchase a license key.</p>';
} else if (store.trialExtensionEligibleOutsideRenewalWindow) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>When your <em>Trial</em> expires, <strong>the array will stop</strong>. At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>';
} else {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>You have used all available trial extensions. When your <em>Trial</em> expires, <strong>the array will stop</strong>. To continue using Unraid OS after expiration, you must purchase a license key.</p>';
}
return {
humanReadable: 'Trial',
heading: 'Thank you for choosing Unraid OS!',
message: trialMessage,
actions: [],
};
},
});
// Test case 1: Eligible inside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 1,
expireTime: dayjs().add(3, 'day').unix() * 1000,
});
expect(store.stateData.message).toContain('Your trial is expiring soon');
expect(store.stateData.message).toContain('You may extend your trial now');
// Test case 2: Ineligible inside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 2,
expireTime: dayjs().add(3, 'day').unix() * 1000,
});
expect(store.stateData.message).toContain('Your trial is expiring soon and you have used all available extensions');
expect(store.stateData.message).toContain('To continue using Unraid OS, you must purchase a license key');
// Test case 3: Eligible outside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 0,
expireTime: dayjs().add(10, 'day').unix() * 1000,
});
expect(store.stateData.message).toContain('At that point you may either purchase a license key or request a <em>Trial</em> extension');
// Test case 4: Ineligible outside renewal window
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 2,
expireTime: dayjs().add(10, 'day').unix() * 1000,
});
expect(store.stateData.message).toContain('You have used all available trial extensions');
expect(store.stateData.message).toContain('To continue using Unraid OS after expiration, you must purchase a license key');
});
it('should include trial extend action only when eligible inside renewal window', () => {
const store = getStore();
// Add necessary properties
Object.defineProperty(store, 'expireTime', { value: 0, writable: true });
Object.defineProperty(store, 'trialExtensionEligible', {
get: () => !store.regGen || store.regGen < 2,
});
Object.defineProperty(store, 'trialWithin5DaysOfExpiration', {
get: () => {
if (!store.expireTime || store.state !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(store.expireTime);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
},
});
Object.defineProperty(store, 'trialExtensionEligibleInsideRenewalWindow', {
get: () => store.trialExtensionEligible && store.trialWithin5DaysOfExpiration,
});
// Mock the trialExtendAction
const trialExtendAction = { name: 'trialExtend', text: 'Extend Trial' };
// Mock stateData getter to include actions logic
Object.defineProperty(store, 'stateData', {
get: () => {
if (store.state !== 'TRIAL') {
return {
humanReadable: '',
heading: '',
message: '',
actions: [],
};
}
const actions = [];
if (store.trialExtensionEligibleInsideRenewalWindow) {
actions.push(trialExtendAction);
}
return {
humanReadable: 'Trial',
heading: 'Thank you for choosing Unraid OS!',
message: '',
actions,
};
},
});
// Test case 1: Eligible inside renewal window - should include trialExtend action
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 1,
expireTime: dayjs().add(3, 'day').unix() * 1000,
registered: true,
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
});
expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(true);
// Test case 2: Not eligible inside renewal window - should NOT include trialExtend action
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 2,
expireTime: dayjs().add(3, 'day').unix() * 1000,
registered: true,
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
});
expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(false);
// Test case 3: Eligible outside renewal window - should NOT include trialExtend action
store.setServer({
state: 'TRIAL' as ServerState,
regGen: 1,
expireTime: dayjs().add(10, 'day').unix() * 1000,
registered: true,
connectPluginInstalled: 'true' as ServerconnectPluginInstalled,
});
expect(store.stateData.actions?.some((action: { name: string }) => action.name === 'trialExtend')).toBe(false);
});
});
});

View File

@@ -23,10 +23,6 @@
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>To support more storage devices as your server grows, click Upgrade Key.</p>",
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>When your <em>Trial</em> expires, <strong>the array will stop</strong>. At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>When your <em>Trial</em> expires, <strong>the array will stop</strong>. At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon. When it expires, <strong>the array will stop</strong>. You may extend your trial now, purchase a license key, or wait until expiration to take action.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon. When it expires, <strong>the array will stop</strong>. You may extend your trial now, purchase a license key, or wait until expiration to take action.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon and you have used all available extensions. When it expires, <strong>the array will stop</strong>. To continue using Unraid OS, you must purchase a license key.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon and you have used all available extensions. When it expires, <strong>the array will stop</strong>. To continue using Unraid OS, you must purchase a license key.</p>",
"<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>You have used all available trial extensions. When your <em>Trial</em> expires, <strong>the array will stop</strong>. To continue using Unraid OS after expiration, you must purchase a license key.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>You have used all available trial extensions. When your <em>Trial</em> expires, <strong>the array will stop</strong>. To continue using Unraid OS after expiration, you must purchase a license key.</p>",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of an Unleashed Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>",

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/web",
"version": "4.10.0",
"version": "4.9.4",
"private": true,
"license": "GPL-2.0-or-later",
"scripts": {
@@ -39,13 +39,13 @@
},
"devDependencies": {
"@graphql-codegen/cli": "5.0.7",
"@graphql-codegen/client-preset": "4.8.3",
"@graphql-codegen/client-preset": "4.8.2",
"@graphql-codegen/introspection": "4.0.3",
"@graphql-typed-document-node/core": "3.2.0",
"@ianvs/prettier-plugin-sort-imports": "4.5.1",
"@nuxt/devtools": "2.6.2",
"@nuxt/eslint": "1.5.2",
"@nuxt/test-utils": "3.19.2",
"@ianvs/prettier-plugin-sort-imports": "4.4.2",
"@nuxt/devtools": "2.5.0",
"@nuxt/eslint": "1.4.1",
"@nuxt/test-utils": "3.19.1",
"@nuxtjs/tailwindcss": "6.14.0",
"@pinia/testing": "1.0.2",
"@rollup/plugin-strip": "3.0.4",
@@ -53,34 +53,34 @@
"@testing-library/vue": "8.1.0",
"@types/crypto-js": "4.2.2",
"@types/eslint-config-prettier": "6.11.3",
"@types/node": "22.16.4",
"@types/node": "22.15.32",
"@types/semver": "7.7.0",
"@typescript-eslint/eslint-plugin": "8.37.0",
"@typescript-eslint/eslint-plugin": "8.34.1",
"@unraid/tailwind-rem-to-rem": "1.1.0",
"@vitejs/plugin-vue": "6.0.0",
"@vitejs/plugin-vue": "5.2.4",
"@vitest/coverage-v8": "3.2.4",
"@vue/apollo-util": "4.2.2",
"@vue/test-utils": "2.4.6",
"@vueuse/core": "13.5.0",
"@vueuse/nuxt": "13.5.0",
"eslint": "9.31.0",
"@vueuse/core": "13.4.0",
"@vueuse/nuxt": "13.4.0",
"eslint": "9.29.0",
"eslint-config-prettier": "10.1.5",
"eslint-import-resolver-typescript": "4.4.4",
"eslint-plugin-import": "2.32.0",
"happy-dom": "18.0.1",
"eslint-plugin-import": "2.31.0",
"happy-dom": "18.0.0",
"lodash-es": "4.17.21",
"nuxt": "3.17.7",
"nuxt": "3.17.5",
"nuxt-custom-elements": "2.0.0-beta.32",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.6.14",
"prettier": "3.5.3",
"prettier-plugin-tailwindcss": "0.6.13",
"shadcn-nuxt": "2.2.0",
"tailwindcss": "3.4.17",
"tailwindcss-animate": "1.0.7",
"terser": "5.43.1",
"typescript": "5.8.3",
"vite": "7.0.4",
"vite": "7.0.3",
"vite-plugin-remove-console": "2.2.0",
"vite-plugin-vue-tracer": "1.0.0",
"vite-plugin-vue-tracer": "0.1.4",
"vitest": "3.2.4",
"vue": "3.5.17",
"vue-tsc": "3.0.1",
@@ -88,9 +88,9 @@
},
"dependencies": {
"@apollo/client": "3.13.8",
"@floating-ui/dom": "1.7.2",
"@floating-ui/utils": "0.2.10",
"@floating-ui/vue": "1.1.7",
"@floating-ui/dom": "1.7.1",
"@floating-ui/utils": "0.2.9",
"@floating-ui/vue": "1.1.6",
"@headlessui/vue": "1.7.23",
"@heroicons/vue": "2.2.0",
"@jsonforms/core": "3.6.0",
@@ -102,8 +102,8 @@
"@unraid/shared-callbacks": "1.1.1",
"@unraid/ui": "link:../unraid-ui",
"@vue/apollo-composable": "4.2.2",
"@vueuse/components": "13.5.0",
"@vueuse/integrations": "13.5.0",
"@vueuse/components": "13.4.0",
"@vueuse/integrations": "13.4.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"crypto-js": "4.2.0",
@@ -111,26 +111,26 @@
"focus-trap": "7.6.5",
"graphql": "16.11.0",
"graphql-tag": "2.12.6",
"graphql-ws": "6.0.6",
"graphql-ws": "6.0.5",
"hex-to-rgba": "2.0.1",
"highlight.js": "11.11.1",
"isomorphic-dompurify": "2.26.0",
"lucide-vue-next": "0.525.0",
"isomorphic-dompurify": "2.25.0",
"lucide-vue-next": "0.519.0",
"marked": "16.0.0",
"marked-base-url": "1.1.7",
"marked-base-url": "1.1.6",
"pinia": "3.0.3",
"semver": "7.7.2",
"tailwind-merge": "2.6.0",
"vue-i18n": "11.1.9",
"vue-i18n": "11.1.6",
"vue-web-component-wrapper": "1.7.7",
"vuetify": "3.9.0",
"vuetify": "3.8.10",
"wretch": "2.11.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.45.1"
"@rollup/rollup-linux-x64-gnu": "4.44.0"
},
"overrides": {
"vue": "latest"
},
"packageManager": "pnpm@10.13.1"
"packageManager": "pnpm@10.12.4"
}

View File

@@ -495,7 +495,6 @@ export const useServerStore = defineStore('server', () => {
});
let messageEGUID = '';
let trialMessage = '';
const stateData = computed((): ServerStateData => {
switch (state.value) {
case 'ENOKEYFILE':
@@ -511,26 +510,16 @@ export const useServerStore = defineStore('server', () => {
'<p>Choose an option below, then use our <a href="https://unraid.net/getting-started" target="_blank" rel="noreffer noopener">Getting Started Guide</a> to configure your array in less than 15 minutes.</p>',
};
case 'TRIAL':
if (trialExtensionEligibleInsideRenewalWindow.value) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon. When it expires, <strong>the array will stop</strong>. You may extend your trial now, purchase a license key, or wait until expiration to take action.</p>';
} else if (trialExtensionIneligibleInsideRenewalWindow.value) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>Your trial is expiring soon and you have used all available extensions. When it expires, <strong>the array will stop</strong>. To continue using Unraid OS, you must purchase a license key.</p>';
} else if (trialExtensionEligibleOutsideRenewalWindow.value) {
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>When your <em>Trial</em> expires, <strong>the array will stop</strong>. At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>';
} else { // would be trialExtensionIneligibleOutsideRenewalWindow if it wasn't an else conditionally
trialMessage = '<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>You have used all available trial extensions. When your <em>Trial</em> expires, <strong>the array will stop</strong>. To continue using Unraid OS after expiration, you must purchase a license key.</p>';
}
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction.value] : []),
...[purchaseAction.value, redeemAction.value],
...(trialExtensionEligibleInsideRenewalWindow.value ? [trialExtendAction.value] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Trial',
heading: 'Thank you for choosing Unraid OS!',
message: trialMessage,
message:
'<p>Your <em>Trial</em> key includes all the functionality and device support of an <em>Unleashed</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>',
};
case 'EEXPIRED':
return {
@@ -784,18 +773,6 @@ export const useServerStore = defineStore('server', () => {
return stateData.value.actions.filter((action) => !authActionsNames.includes(action.name));
});
const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2);
const trialWithin5DaysOfExpiration = computed(() => {
if (!expireTime.value || state.value !== 'TRIAL') {
return false;
}
const today = dayjs();
const expirationDate = dayjs(expireTime.value);
const daysUntilExpiration = expirationDate.diff(today, 'day');
return daysUntilExpiration <= 5 && daysUntilExpiration >= 0;
});
const trialExtensionEligibleInsideRenewalWindow = computed(() => trialExtensionEligible.value && trialWithin5DaysOfExpiration.value);
const trialExtensionEligibleOutsideRenewalWindow = computed(() => trialExtensionEligible.value && !trialWithin5DaysOfExpiration.value);
const trialExtensionIneligibleInsideRenewalWindow = computed(() => !trialExtensionEligible.value && trialWithin5DaysOfExpiration.value);
const serverConfigError = computed((): Error | undefined => {
if (!config.value?.valid && config.value?.error) {
@@ -1198,9 +1175,7 @@ export const useServerStore = defineStore('server', () => {
setTimeout(() => {
load();
if (connectPluginInstalled.value) {
loadCloudState();
}
loadCloudState();
}, 500);
onResult((result) => {