Compare commits

...

108 Commits

Author SHA1 Message Date
Zack Spear
ff1651f523 refactor: update console log removal configuration to exclude 'error' 2025-01-07 17:18:27 -08:00
Zack Spear
95b203285b fix: top-level variable name clash w/ webgui 2025-01-07 16:04:42 -08:00
Zack Spear
e85bb28498 refactor: os version links to external docs 2025-01-07 11:40:36 -08:00
Zack Spear
368cec8641 refactor: remove unused console log disabling function from helper utilities 2025-01-07 11:40:10 -08:00
Zack Spear
0ca57389f3 refactor: remove unused console log disabling in I18nHost component 2025-01-07 11:39:57 -08:00
Zack Spear
1229a0ce69 fix: console log removal 2025-01-07 11:39:42 -08:00
Eli Bosley
c8f469c4fb chore(release): 3.8.1 2024-08-13 14:11:02 -04:00
Zack Spear
bc61b45f9f refactor: registration component remove contact support 2024-08-13 14:04:54 -04:00
Eli Bosley
f530d9ea82 chore(release): 3.8.0 2024-08-13 13:50:07 -04:00
ljm42
2046fa5310 refactor: change flag that skips delete on uninstall (#892) 2024-08-13 13:40:18 -04:00
Zack Spear
9ea2327fa0 refactor: registration transfer check ineligible copy 2024-08-13 10:31:50 -07:00
Zack Spear
ff67b54a1b refactor: doc urls use /go links 2024-08-13 10:31:31 -07:00
ljm42
e6bd7a54be feat: always force push 2024-08-13 10:30:49 -07:00
Eli Bosley
5827b5ffa3 feat: swap to docker compose from docker-compose 2024-08-07 11:04:54 -04:00
ljm42
572a1310e0 Use "go links" when linking to Docs (#891) 2024-08-07 10:41:57 -04:00
ljm42
c1403d3826 feat: don't allow flash backup repos larger than 500MB (#890)
* feat: don't allow flash backup repos larger than 500MB

* fix: don't backup dynamix.file.integrity/logs

* feat: max file size for backup limited to 10mb

* feat: limit max repo size to 100MB

* feat: delete large repo again after 90 days
2024-08-07 10:41:36 -04:00
Eli Bosley
29afe9b9e8 feat: settings through the API (#867)
* feat: api settings fully working
* refactor: nuxt config ConnectSettings

---------

Co-authored-by: Zack Spear <hi@zackspear.com>
2024-07-03 13:38:09 -04:00
Zack Spear
e9ff33d263 feat: downgradeOs callback for non stable osCurrentBranch 2024-05-28 11:57:05 -07:00
Zack Spear
a62f60a436 fix: update status button alignment 2024-05-28 11:57:05 -07:00
Eli Bosley
838964c6ef chore: update package.json with new dependencies (#886)
* chore: update package.json with new dependencies

* feat: run codegen

* fix: got and reflect metadata revert version

* fix: pino version mismatch

* feat: update package-lock.json
2024-05-17 11:21:55 -04:00
Zack Spear
800fc12c15 refactor: server state refresh and response mutations 2024-05-16 14:13:01 -07:00
Zack Spear
80175241e3 fix: lint error for web components 2024-05-16 14:13:01 -07:00
Zack Spear
5d801f22f5 chore: ts-expect-error description for webgui troubleshoot form 2024-05-16 14:13:01 -07:00
Zack Spear
ba772add54 refactor: instantiation of web components 2024-05-16 14:13:01 -07:00
Zack Spear
ff24f12cae refactor: optional chaining for click props 2024-05-16 14:13:01 -07:00
Eli Bosley
487f5c1865 fix: tailwind config types 2024-05-16 14:13:01 -07:00
Eli Bosley
e0c90037fb fix: swap undefined to null 2024-05-16 14:13:01 -07:00
Eli Bosley
aa5f603cba fix: apolloClient types 2024-05-16 14:13:01 -07:00
Eli Bosley
409db43973 fix: ts-expect-error unneeded 2024-05-16 14:13:01 -07:00
Zack Spear
cef1b29355 fix: type check 2024-05-16 14:13:01 -07:00
Zack Spear
045750c87e fix: lint issues 2024-05-16 14:13:01 -07:00
Zack Spear
85802e7af7 fix: formattedRegTm type 2024-05-16 14:13:01 -07:00
Zack Spear
4bfdb66d46 fix: i18n t prop type 2024-05-16 14:13:01 -07:00
Zack Spear
81a6a52d9f fix: type errors round 1 2024-05-16 14:13:01 -07:00
Zack Spear
7759fe1dc3 chore(web): update deps + eslint (#887)
* chore: update deps + eslint

* fix: lint + type errors
2024-05-16 09:26:39 -04:00
renovate[bot]
3b2acb29b5 chore(deps): update dependency @vueuse/nuxt to v10.9.0 (#797)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:23:11 -04:00
renovate[bot]
5f2b949ecf chore(deps): update dependency @nuxtjs/tailwindcss to v6.12.0 (#794)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:22:43 -04:00
renovate[bot]
1b956d563e fix(deps): update dependency @vue/apollo-composable to v4.0.2 (#787)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-15 15:22:30 -04:00
Eli Bosley
c6a97f5082 chore(release): 3.7.1 2024-05-15 14:33:43 -04:00
Zack Spear
7f512e47e9 fix: reboot required and available edge case (#885)
* fix: reboot required and available edge case

* chore: add missing web component translations

* chore: translations sort unique ascending, case insensitive

* fix: translation json
2024-05-15 12:24:06 -04:00
Eli Bosley
5d725b0e76 chore(release): 3.7.0 2024-05-14 15:18:07 -04:00
renovate[bot]
fe63607260 chore(deps): update dependency @types/dockerode to v3.3.29 (#768)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-05-13 16:03:35 -04:00
Zack Spear
0a1d4daf6e refactor: update os status updateAvailable 2024-05-13 10:52:30 -07:00
Zack Spear
9e9e385bef chore: UnraidUpdateCancel reorganize 2024-05-13 10:52:30 -07:00
Zack Spear
6fed39e05b chore: update cancel comment 2024-05-13 10:52:30 -07:00
Zack Spear
3dec53d13d chore: UpdateOS Status unused import 2024-05-13 10:52:30 -07:00
Zack Spear
f0ded9f5be fix: update os cancel refresh on update page 2024-05-13 10:52:30 -07:00
Zack Spear
7d55a1c2cd chore: UpdateOS Status unused import 2024-05-13 10:52:30 -07:00
Zack Spear
f3dc9663b8 feat: UI Update OS Cancel 2024-05-13 10:52:30 -07:00
Zack Spear
05c7c481a9 chore: update cancel script 2024-05-13 10:52:30 -07:00
Zack Spear
adcc1543f0 feat: UnraidUpdateCancel script 2024-05-13 10:52:30 -07:00
Zack Spear
95f873c752 refactor: UnraidCheck use current unRAIDServer.plg 2024-05-13 10:52:30 -07:00
Zack Spear
ec90f8b295 fix: plugin file deployment script 2024-05-13 10:52:30 -07:00
Zack Spear
f84195a98d fix(web): lint unused rebootVersion 2024-05-13 10:52:30 -07:00
Zack Spear
5e98a68e2e feat: ui to allow second update without reboot 2024-05-13 10:52:30 -07:00
Zack Spear
b91dbca144 refactor: btnStyle prop for CallbackButton component 2024-05-13 10:52:30 -07:00
Zack Spear
79a01da18d refactor: ButtonStyle type 2024-05-13 10:52:30 -07:00
Zack Spear
14951d3004 refactor: reboot details added to server payload to account 2024-05-13 10:52:30 -07:00
Zack Spear
64c2061bea chore: dev deployment script improvements 2024-05-13 10:52:30 -07:00
Zack Spear
e3adc9a29a chore: dev deployment script improvements 2024-05-13 10:52:30 -07:00
ljm42
6b689ffcce Chore: sync http_get_contents() with webgui (#883) 2024-05-10 12:57:57 -07:00
ljm42
c995a4c5c8 Fix: rc.flashbackup needs to check both signed in and connected (#882)
because /var/local/emhttp/myservers.cfg does not clear the connected status when the user signs out
2024-05-10 10:44:21 -07:00
Zack Spear
8d1e0f67d1 refactor: simplify version_compare in reboot-details 2024-05-08 12:47:29 -07:00
Zack Spear
7877a5dca2 refactor: reboot type detection for downgrade via callback 2024-05-08 12:47:29 -07:00
Zack Spear
16db278ffd feat: downgradeOs callback 2024-05-08 12:47:29 -07:00
ljm42
521b4381f2 Fix bug in flash backup rate limiter (#880)
Don't try to read from an empty file
2024-05-07 17:11:36 -07:00
ljm42
9ae9d40f94 fix: keep minor enhancements from #872 (#878) 2024-05-07 08:39:46 -07:00
Zack Spear
1d562d404c fix(web): registration component remove unused ref 2024-05-06 10:44:16 -07:00
Zack Spear
7ac1b268d9 refactor(web): registration linked learn more callback to my keys 2024-05-06 10:44:16 -07:00
Zack Spear
4833e9dccf chore: translations 2024-05-06 10:44:16 -07:00
Zack Spear
f28b7510fa feat(web): Registration key linked to account status 2024-05-06 10:44:16 -07:00
Zack Spear
37b717b142 refactor(web): button component no style option 2024-05-06 10:44:16 -07:00
Zack Spear
fd8b40d9aa feat(web): callback types myKeys & linkKey 2024-05-06 10:44:16 -07:00
Eli Bosley
1d944781cf feat: add a timestamp to flash backup (#877)
* feat: add a timestamp to flash backup

* feat: update gitignore

* feat: random interval is now 30 minutes
2024-05-06 13:40:42 -04:00
Zack Spear
1f4c64d022 feat(plg): install prevent downgrade of shared page & php files (#873)
* feat(plg): install prevent downgrade of shared page & php files

* chore(plg): remove debug echo

* fix(plg): remove extra char
2024-05-02 14:08:38 -07:00
Zack Spear
f69b5130a3 refactor(web): copy Ineligible for feature updates (#875)
* refactor(web): copy Ineligible for feature updates

* refactor(web): Eligible for free feature updates
2024-05-02 14:04:16 -07:00
Zack Spear
f8b143904b fix: prevent corrupt case model in state.php (#874)
fix: prevent corruprt case model in state.php
2024-05-02 14:00:29 -07:00
Zack Spear
31a5413643 feat(web): registration page array status messaging 2024-05-01 12:21:24 -07:00
Zack Spear
a95fc5ed07 chore: lint fix 2024-05-01 12:21:24 -07:00
Zack Spear
fcd7bb790e refactor(web): ineligible release messaging 2024-05-01 12:21:24 -07:00
Zack Spear
008e10948e fix: prevent local dev from throwing ssl error 2024-05-01 12:21:24 -07:00
Zack Spear
c97a4f1268 feat: registration page server error heading + subheading 2024-05-01 12:21:24 -07:00
Zack Spear
3eba95b8cc feat: array state on registration page 2024-05-01 12:21:24 -07:00
Zack Spear
2bf8f0b937 fix(api): readme discord url 2024-04-30 17:34:46 -07:00
Zack Spear
9ae45d1258 fix(web): discord url 2024-04-30 17:34:46 -07:00
Zack Spear
1835a4cf3f chore(plg): comment explain web component downgrade prevention 2024-04-30 17:12:33 -07:00
Zack Spear
2ab44b894d feat(plg): plg install prevent web component downgrade 2024-04-30 17:12:33 -07:00
Zack Spear
1108f49b07 feat: postbuild script to add timestamp to web component manifest 2024-04-30 17:12:33 -07:00
ljm42
cc69213beb Feat: Flash Backup requires connection to mothership (#868)
* fix: branding

* feat: flash backup requires connection to mothership

* feat: flash backup requires connection to mothership
2024-04-26 12:01:42 -04:00
ljm42
460e557dd8 Flash Backup: exclude large files from repo (#866) 2024-04-23 21:21:01 -04:00
Zack Spear
05e29468d2 refactor: trial messaging replace pro with unleashed (#865)
* refactor: trial messaging replace pro with unleashed

* fix: trial messaging grammar

* refactor: web component translations trial messaging
2024-04-03 13:46:25 -04:00
ljm42
4d3a311fb4 Feat: add support for outgoing proxies (#863) 2024-03-27 15:14:18 -07:00
Zack Spear
bc62d210ec refactor: config error messages (#862) 2024-03-26 12:30:34 -04:00
Eli Bosley
43d3ea6553 chore(release): 3.6.0 2024-03-26 10:19:31 -04:00
Zack Spear
882e3e1ef4 feat: server config enum message w/ ineligible support (#861)
* test: serverState local components data tweaks

* feat: server config enum message w/ ineligible support

* refactor: config error messages

* chore: lint
2024-03-26 09:57:04 -04:00
Eli Bosley
b33c86c99c chore(release): 3.5.3 2024-03-25 09:22:14 -04:00
Zack Spear
cd0248e4c9 refactor: upgrade action button for unleashed to lifetime (#859) 2024-03-20 10:27:33 -04:00
Zack Spear
ecb3ed5003 fix: regDevs usage to allow more flexibility for STARTER (#860)
* fix: regDevs usage to allow more flexibility for STARTER

* fix: lint and type-check
2024-03-20 08:50:02 -04:00
Zack Spear
0569339a41 refactor(upc): remove UpdateDNS requests 2024-03-12 16:36:04 -07:00
ljm42
3e9faead43 Replace UpdateDNS.php with a stub (#857)
* This new stub file makes no network calls and always returns success
* It is meant to be backwards compatible with older releases of Unraid that expect the script to exist
2024-03-12 15:57:17 -04:00
Eli Bosley
6e700b2385 chore(release): 3.5.2 2024-03-06 10:20:09 -05:00
renovate[bot]
464fc4993c fix(deps): update dependency vue-i18n to v9.10.1 (#813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:08:22 -05:00
renovate[bot]
4316c72809 chore(deps): update dependency terser to v5.28.1 (#802)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:08:05 -05:00
renovate[bot]
ce0cebe09c chore(deps): update dependency node to v18.19.1 (#801)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:07:51 -05:00
renovate[bot]
23b90a0d56 fix(deps): update dependency wretch to v2.8.0 (#814)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:07:38 -05:00
Zack Spear
3f8b3536b5 refactor: ENOKEYFILE messaging + button order (#856) 2024-03-05 15:07:20 -05:00
Zack Spear
0dcf785b45 fix: update os check modal button conditionals 2024-02-29 14:49:20 -08:00
Zack Spear
8cf4aff622 fix: update os check modal ineligible date format 2024-02-29 14:16:09 -08:00
125 changed files with 12030 additions and 9655 deletions

View File

@@ -81,10 +81,10 @@ jobs:
- name: Build Docker Compose
run: |
docker network create mothership_default
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build builder
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose build builder
- name: Run Docker Compose
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run builder npm run coverage
run: GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose run builder npm run coverage
lint-web:
defaults:

4
.gitignore vendored
View File

@@ -83,4 +83,6 @@ deploy/*
.cache
.output
.env*
!.env.example
!.env.example
fb_keepalive

View File

@@ -29,5 +29,6 @@
"i18n-ally.localesPaths": [
"locales"
],
"i18n-ally.keystyle": "flat"
"i18n-ally.keystyle": "flat",
"eslint.experimental.useFlatConfig": true,
}

11
api/.env.test Normal file
View File

@@ -0,0 +1,11 @@
VERSION="THIS_WILL_BE_REPLACED_WHEN_BUILT"
PATHS_UNRAID_DATA=./dev/data # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=./dev/states # Where .ini files live (e.g. vars.ini)
PATHS_DYNAMIX_BASE=./dev/dynamix # Dynamix's data directory
PATHS_DYNAMIX_CONFIG=./dev/dynamix/dynamix.cfg # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=./dev/Unraid.net/myservers.cfg # My servers config file
PATHS_MY_SERVERS_FB=./dev/Unraid.net/fb_keepalive # My servers flashbackup timekeeper file
PATHS_KEYFILE_BASE=./dev/Unraid.net # Keyfile location
PORT=5000
NODE_ENV=test

View File

@@ -1 +1 @@
18.17.1
18.19.1

View File

@@ -2,6 +2,102 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [3.8.1](https://github.com/unraid/api/compare/v3.8.0...v3.8.1) (2024-08-13)
## [3.8.0](https://github.com/unraid/api/compare/v3.7.1...v3.8.0) (2024-08-13)
### Features
* always force push ([662f3ce](https://github.com/unraid/api/commit/662f3ce440593e609c64364726f7da16dda0972b))
* don't allow flash backup repos larger than 500MB ([#890](https://github.com/unraid/api/issues/890)) ([30a32f5](https://github.com/unraid/api/commit/30a32f5fe684bb32c084c4125aade5e63ffd788b))
* downgradeOs callback for non stable osCurrentBranch ([17c4489](https://github.com/unraid/api/commit/17c4489e97bda504ca45e360591655ded166c355))
* settings through the API ([#867](https://github.com/unraid/api/issues/867)) ([e73624b](https://github.com/unraid/api/commit/e73624be6be8bc2c70d898b8601a88cc8d20a3e4))
* swap to docker compose from docker-compose ([ec16a6a](https://github.com/unraid/api/commit/ec16a6aab1a2d5c836387da438fbeade07d23425))
### Bug Fixes
* apolloClient types ([f14c767](https://github.com/unraid/api/commit/f14c7673735b92aa167e9e8dcb14a045bcfea994))
* **deps:** update dependency @vue/apollo-composable to v4.0.2 ([#787](https://github.com/unraid/api/issues/787)) ([edfc846](https://github.com/unraid/api/commit/edfc8464b0e0c2f38003ae8420e81532fd18351f))
* formattedRegTm type ([748906e](https://github.com/unraid/api/commit/748906e15d30c6162e2f08f28724c9104c81d123))
* i18n t prop type ([96d519f](https://github.com/unraid/api/commit/96d519f3e6b96ea7c4dc60616522216de20ee140))
* lint error for web components ([bc27b20](https://github.com/unraid/api/commit/bc27b20524934cf896efb84a131cd270431c508c))
* lint issues ([853dc19](https://github.com/unraid/api/commit/853dc195b13fff29160afb44f9ff11d4dd6a3232))
* swap undefined to null ([ebba976](https://github.com/unraid/api/commit/ebba9769873a6536e3fce65978e6475d93280560))
* tailwind config types ([0f77e55](https://github.com/unraid/api/commit/0f77e5596db3356b5dc05129b3ce215a8809e1dc))
* ts-expect-error unneeded ([ee4d4e9](https://github.com/unraid/api/commit/ee4d4e9f12b4488ff39445bc72c1b83a9d93e993))
* type check ([606aad7](https://github.com/unraid/api/commit/606aad703d91b72a14e15da3100dfa355052ed58))
* type errors round 1 ([977d5da](https://github.com/unraid/api/commit/977d5daf04012f16e7b6602167338f0bc363735a))
* update status button alignment ([4f2deaf](https://github.com/unraid/api/commit/4f2deaf70e5caa9f29fc5b2974b278f80b7b3a8a))
### [3.7.1](https://github.com/unraid/api/compare/v3.7.0...v3.7.1) (2024-05-15)
### Bug Fixes
* reboot required and available edge case ([#885](https://github.com/unraid/api/issues/885)) ([76e9cdf](https://github.com/unraid/api/commit/76e9cdf81f06a19c2e4c9a40a4d8e062bad2a607))
## [3.7.0](https://github.com/unraid/api/compare/v3.6.0...v3.7.0) (2024-05-14)
### Features
* add a timestamp to flash backup ([#877](https://github.com/unraid/api/issues/877)) ([b868fd4](https://github.com/unraid/api/commit/b868fd46c3886b2182245a61f20be6df65e46abe))
* add support for outgoing proxies ([#863](https://github.com/unraid/api/issues/863)) ([223693e](https://github.com/unraid/api/commit/223693e0981d5f2884a1f8b8baf03d4dc58e8cb2))
* array state on registration page ([d36fef0](https://github.com/unraid/api/commit/d36fef0545ddb820e67e8bc6cb42ea3644021d66))
* downgradeOs callback ([154a976](https://github.com/unraid/api/commit/154a976109f0a32653a2851988420707631327ca))
* Flash Backup requires connection to mothership ([#868](https://github.com/unraid/api/issues/868)) ([d127208](https://github.com/unraid/api/commit/d127208b5e0f7f9991f515f95b0e266d38cf3287))
* **plg:** install prevent downgrade of shared page & php files ([#873](https://github.com/unraid/api/issues/873)) ([4ac72b1](https://github.com/unraid/api/commit/4ac72b16692c4246c9d2c0b53b23d8b2d95f5de6))
* **plg:** plg install prevent web component downgrade ([8703bd4](https://github.com/unraid/api/commit/8703bd498108f5c05562584a708bd2306e53f7a6))
* postbuild script to add timestamp to web component manifest ([47f08ea](https://github.com/unraid/api/commit/47f08ea3594a91098f67718c0123110c7b5f86f7))
* registration page server error heading + subheading ([6038ebd](https://github.com/unraid/api/commit/6038ebdf39bf47f2cb5c0b1de84764795374f018))
* remove cron to download JS daily ([#864](https://github.com/unraid/api/issues/864)) ([33f6d6b](https://github.com/unraid/api/commit/33f6d6b343de07dbe70de863926906736d42f371)), closes [#529](https://github.com/unraid/api/issues/529)
* ui to allow second update without reboot ([b0f2d10](https://github.com/unraid/api/commit/b0f2d102917f54ab33f0ad10863522b8ff8e3ce5))
* UI Update OS Cancel ([7c02308](https://github.com/unraid/api/commit/7c02308964d5e21990427a2c626c9db2d9e1fed0))
* UnraidUpdateCancel script ([b73bdc0](https://github.com/unraid/api/commit/b73bdc021764762ed12dca494e1345412a45c677))
* **web:** callback types myKeys & linkKey ([c88ee01](https://github.com/unraid/api/commit/c88ee01827396c3fa8a30bb88c4be712c80b1f4f))
* **web:** Registration key linked to account status ([8f6182d](https://github.com/unraid/api/commit/8f6182d426453b73aa19c5f0f59469fa07571694))
* **web:** registration page array status messaging ([23ef5a9](https://github.com/unraid/api/commit/23ef5a975e0d5ff0c246c2df5e6c2cb38979d12a))
### Bug Fixes
* **api:** readme discord url ([ffd5c6a](https://github.com/unraid/api/commit/ffd5c6afb64956e76df22c77104a21bc22798008))
* keep minor enhancements from [#872](https://github.com/unraid/api/issues/872) ([#878](https://github.com/unraid/api/issues/878)) ([94a5aa8](https://github.com/unraid/api/commit/94a5aa87b9979fe0f02f884ac61298473bb3271a))
* plugin file deployment script ([780d87d](https://github.com/unraid/api/commit/780d87d6589a5469f47ac3fdfd50610ecfc394c8))
* prevent corrupt case model in state.php ([#874](https://github.com/unraid/api/issues/874)) ([4ad31df](https://github.com/unraid/api/commit/4ad31dfea9192146dbd2c90bc64a913c696ab0b7))
* prevent local dev from throwing ssl error ([051f647](https://github.com/unraid/api/commit/051f6474becf3b25b242cdc6ceee67247b79f8ba))
* rc.flashbackup needs to check both signed in and connected ([#882](https://github.com/unraid/api/issues/882)) ([ac8068c](https://github.com/unraid/api/commit/ac8068c9b084622d46fe2c9cb320b793c9ea8c52))
* update os cancel refresh on update page ([213c16b](https://github.com/unraid/api/commit/213c16ba3d5a84ebf4965f9d2f4024c66605a613))
* **web:** discord url ([1a6f4c6](https://github.com/unraid/api/commit/1a6f4c6db4ef0e5eefac467ec6583b14cb3546c4))
* **web:** lint unused rebootVersion ([e198ec9](https://github.com/unraid/api/commit/e198ec9d458e262c412c2dcb5a9d279238de1730))
* **web:** registration component remove unused ref ([76f556b](https://github.com/unraid/api/commit/76f556bd64b95ba96af795c9edfa045ebdff4444))
## [3.6.0](https://github.com/unraid/api/compare/v3.5.3...v3.6.0) (2024-03-26)
### Features
* server config enum message w/ ineligible support ([#861](https://github.com/unraid/api/issues/861)) ([4d3a351](https://github.com/unraid/api/commit/4d3a3510777090788573f4cee83694a0dc6f8df5))
### [3.5.3](https://github.com/unraid/api/compare/v3.5.2...v3.5.3) (2024-03-25)
### Bug Fixes
* regDevs usage to allow more flexibility for STARTER ([#860](https://github.com/unraid/api/issues/860)) ([92a9600](https://github.com/unraid/api/commit/92a9600f3a242c5f263f1672eab81054b9cf4fae))
### [3.5.2](https://github.com/unraid/api/compare/v3.5.1...v3.5.2) (2024-03-06)
### Bug Fixes
* **deps:** update dependency vue-i18n to v9.10.1 ([#813](https://github.com/unraid/api/issues/813)) ([69b599c](https://github.com/unraid/api/commit/69b599c5ed8d44864201a32b4d952427d454dc74))
* **deps:** update dependency wretch to v2.8.0 ([#814](https://github.com/unraid/api/issues/814)) ([66900b4](https://github.com/unraid/api/commit/66900b495b82b923264897d38b1529a22b10aa1c))
* update os check modal button conditionals ([282a836](https://github.com/unraid/api/commit/282a83625f417ccefe090b65cc6b73a084727a87))
* update os check modal ineligible date format ([83083de](https://github.com/unraid/api/commit/83083de1e698f73a35635ae6047dcf49fd4b8114))
### [3.5.1](https://github.com/unraid/api/compare/v3.5.0...v3.5.1) (2024-02-29)

View File

@@ -1,7 +1,7 @@
###########################################################
# Development/Build Image
###########################################################
FROM node:18.17.1-bookworm-slim As development
FROM node:18.19.1-bookworm-slim As development
# Install build tools and dependencies
RUN apt-get update -y && apt-get install -y \

58
api/README.md Normal file
View File

@@ -0,0 +1,58 @@
# @unraid/api
## Installation
Install the production plugin via the apps tab (search for "my servers") on Unraid 6.9.2 or later.
## CLI
If you're on a unraid v6.9.2 or later machine this should be available by running `unraid-api` in any directory.
```bash
root@Devon:~# unraid-api
Unraid API
Thanks for using the official Unraid API
Usage:
$ unraid-api command <options>
Commands:
start/stop/restart/version/status/report/switch-env
Options:
-h, --help Prints this usage guide.
-d, --debug Enabled debug mode.
-p, --port string Set the graphql port.
--environment production/staging/development Set the working environment.
--log-level ALL/TRACE/DEBUG/INFO/WARN/ERROR/FATAL/MARK/OFF Set the log level.
Copyright © 2022 Lime Technology, Inc.
```
## Report
To view the current status of the unraid-api and its connection to mothership, run:
```
unraid-api report
```
To view verbose data (anonymized), run:
```
unraid-api report -v
```
To view non-anonymized verbose data, run:
```
unraid-api report -vv
```
## Secrets
If you found this file you're likely a developer. If you'd like to know more about the API and when it's available please join [our discord](https://discord.unraid.net/).
## License
Copyright 2019-2022 Lime Technology Inc. All rights reserved.

View File

@@ -1,5 +1,5 @@
[api]
version="3.4.0"
version="3.5.2+20f10951"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]

View File

@@ -1,5 +1,5 @@
[api]
version="3.4.0"
version="3.5.2+20f10951"
extraOrigins="https://google.com,https://test.com"
[local]
[notifier]
@@ -21,4 +21,5 @@ dynamicRemoteAccessType="DISABLED"
[upc]
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
[connectionStatus]
minigraph="PRE_INIT"
minigraph="ERROR_RETRYING"
upnpStatus="Success: UPNP Lease Renewed [4/24/2024 5:04:54 PM] Public Port [59138] Local Port [443]"

229
api/docs/development.md Normal file
View File

@@ -0,0 +1,229 @@
# Development
## Installation
Install the [production](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.plg) or [staging](https://unraid-dl.sfo2.digitaloceanspaces.com/unraid-api/dynamix.unraid.net.staging.plg) plugin on Unraid 6.9.0-rc1 or later (6.9.2 or higher recommended).
## Connecting to the API
### HTTP
This can be accessed by default via `http://tower.local/graphql`.
See <https://graphql.org/learn/serving-over-http/#http-methods-headers-and-body>
### WS
If you're using the ApolloClient please see <https://github.com/apollographql/subscriptions-transport-ws#full-websocket-transport> otherwise see <https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md>
<br>
<hr>
<br>
## Building in Docker
To get a development environment for testing start by running this docker command:
``docker compose run build-interactive``
which will give you an interactive shell inside of the newly build linux container.
To automatically build the plugin run the command below:
``docker compose run builder``
The builder command will build the plugin into deploy/release, and the interactive plugin lets you build the plugin or install node modules how you like.
## Logs
Logging can be configured via environment variables.
Log levels can be set when the api starts via `LOG_LEVEL=all/trace/debug/info/warn/error/fatal/mark/off`.
Additional detail for the log entry can be added with `LOG_CONTEXT=true` (warning, generates a lot of data).
By default, logs will be sent to syslog. Or you can set `LOG_TRANSPORT=file` to have logs saved in `/var/log/unraid-api/stdout.log`. Or enable debug mode to view logs inline.
Examples:
* `unraid-api start`
* `LOG_LEVEL=debug unraid-api start --debug`
* `LOG_LEVEL=trace LOG_CONTEXT=true LOG_TRANSPORT=file unraid-api start`
Log levels can be increased without restarting the api by issuing this command:
```
kill -s SIGUSR2 `pidof unraid-api`
```
and decreased via:
```
kill -s SIGUSR1 `pidof unraid-api`
```
<br>
<hr>
<br>
## Viewing data sent to mothership
If the environment variable `LOG_MOTHERSHIP_MESSAGES=true` exists, any data the unraid-api sends to mothership will be saved in clear text here: `/var/log/unraid-api/relay-messages.log`
Examples:
* `LOG_MOTHERSHIP_MESSAGES=true unraid-api start`
* `LOG_MOTHERSHIP_MESSAGES=true LOG_LEVEL=debug unraid-api start --debug`
<br>
<hr>
<br>
## Debug mode
Debug mode can be enabled with the `-d` or `--debug` flag.
This will enable the graphql playground and prevent the application starting as a daemon. Logs will be shown inline rather than saved to a file.
Examples:
* `unraid-api start --debug`
* `LOG_LEVEL=debug unraid-api start --debug`
<br>
<hr>
<br>
## Crash API On Demand
The `PLEASE_SEGFAULT_FOR_ME` env var can be to used to make the api crash after 30 seconds:
Examples:
* `PLEASE_SEGFAULT_FOR_ME=true LOG_LEVEL=debug unraid-api start --debug`
* `PLEASE_SEGFAULT_FOR_ME=true unraid-api start`
The crash log will be stored here:
* `/var/log/unraid-api/crash.log`
* `/var/log/unraid-api/crash.json`
`crash.json` just includes the most recent crash, while the reports get appended to `crash.log`.
<br>
<hr>
<br>
## Switching between staging and production environments
1. Stop the api: `unraid-api stop`
2. Switch environments: `unraid-api switch-env`
3. Start the api: `unraid-api start`
4. Confirm the environment: `unraid-api report`
<br>
<hr>
<br>
## Playground
The playground can be access via `http://tower.local/graphql` while in debug mode.
To get your API key open a terminal on your server and run `cat /boot/config/plugins/dynamix.my.servers/myservers.cfg | grep apikey=\"unraid | cut -d '"' -f2`. Add that API key in the "HTTP headers" panel of the playground.
```json
{
"x-api-key":"__REPLACE_ME_WITH_API_KEY__"
}
```
Next add the query you want to run and hit the play icon.
```gql
query welcome {
welcome {
message
}
}
```
You should get something like this back.
```json
{
"data": {
"welcome": {
"message": "Welcome root to this Unraid 6.10.0 server"
}
}
}
```
Click the "Schema" and "Docs" button on the right side of the playground to learn more.
For exploring the schema visually I'd suggest using [Voyager](https://apis.guru/graphql-voyager/) (click Change Schema -> Introspection, then copy/paste the introspection query into the local Graph Playground, and copy/paste the results back into Voyager).
<br>
<hr>
<br>
## Running this locally
```bash
MOTHERSHIP_RELAY_WS_LINK=ws://localhost:8000 \ # Switch to local copy of mothership
PATHS_UNRAID_DATA=$(pwd)/dev/data \ # Where we store plugin data (e.g. permissions.json)
PATHS_STATES=$(pwd)/dev/states \ # Where .ini files live (e.g. vars.ini)
PATHS_DYNAMIX_BASE=$(pwd)/dev/dynamix \ # Dynamix's data directory
PATHS_DYNAMIX_CONFIG=$(pwd)/dev/dynamix/dynamix.cfg \ # Dynamix's config file
PATHS_MY_SERVERS_CONFIG=$(pwd)/dev/Unraid.net/myservers.cfg \ # My servers config file
PORT=8500 \ # What port unraid-api should start on (e.g. /var/run/unraid-api.sock or 8000)
node dist/cli.js --debug # Enable debug logging
```
<br>
<hr>
<br>
## Create a new release
To create a new version run `npm run release` and then run **ONLY** the `git push` section of the commands it returns.
To create a new prerelease run `npm run release -- --prerelease alpha`.
Pushing to this repo will cause an automatic "rolling" release to be built which can be accessed via the page for the associated Github action run.
<br>
<hr>
<br>
## Using a custom version (e.g. testing a new release)
1. Install the staging or production plugin (links in the Installation section at the top of this file)
2. Download or build the api tgz file you want
* Download from [the releases page](https://github.com/unraid/api/releases)
* Build it on your local machine (``docker compose run builder``) and copy from the `deploy/release` folder
3. Copy the file to `/boot/config/plugins/dynamix.my.servers/unraid-api.tgz`.
4. Install the new api: `/etc/rc.d/rc.unraid-api (install / _install)`
* `_install` will no start the plugin for you after running, so you can make sure you launch in dev mode
* `install` will start the plugin after install
5. Start the api: `unraid-api start`
6. Confirm the version: `unraid-api report`
## Cloning Secrets from AWS
1. Go here to create security credentials for your user [S3 Creds](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1&skipRegion=true#/security_credentials)
2. Export your AWS secrets OR run `aws configure` to setup your environment
```sh
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-east-1
```
3. Set variables for staging and production to the ARN of the secret you would like to clone:
* `STAGING_SECRET_ID`
* `PRODUCTION_SECRET_ID`
4. Run `scripts/copy-env-from-aws.sh` to pull down the secrets into their respective files

4623
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@unraid/api",
"version": "3.5.1",
"version": "3.8.1",
"main": "dist/index.js",
"bin": "dist/unraid-api.cjs",
"type": "module",
@@ -59,21 +59,21 @@
"unraid-api"
],
"dependencies": {
"@apollo/client": "^3.8.9",
"@apollo/server": "^4.10.0",
"@apollo/client": "^3.10.4",
"@apollo/server": "^4.10.4",
"@as-integrations/fastify": "^2.1.1",
"@graphql-codegen/client-preset": "^4.1.0",
"@graphql-codegen/client-preset": "^4.2.5",
"@graphql-tools/load-files": "^7.0.0",
"@graphql-tools/merge": "^9.0.1",
"@graphql-tools/schema": "^10.0.2",
"@graphql-tools/utils": "^10.0.12",
"@nestjs/apollo": "^12.0.11",
"@nestjs/core": "^10.3.0",
"@nestjs/graphql": "^12.0.11",
"@graphql-tools/merge": "^9.0.4",
"@graphql-tools/schema": "^10.0.3",
"@graphql-tools/utils": "^10.2.0",
"@nestjs/apollo": "^12.1.0",
"@nestjs/core": "^10.3.8",
"@nestjs/graphql": "^12.1.1",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-fastify": "^10.3.0",
"@nestjs/schedule": "^4.0.0",
"@reduxjs/toolkit": "^2.0.1",
"@nestjs/platform-fastify": "^10.3.8",
"@nestjs/schedule": "^4.0.2",
"@reduxjs/toolkit": "^2.2.4",
"@reflet/cron": "^1.3.1",
"@runonflux/nat-upnp": "^1.0.2",
"accesscontrol": "^2.2.1",
@@ -84,7 +84,7 @@
"bytes": "^3.1.2",
"cacheable-lookup": "^6.1.0",
"catch-exit": "^1.2.2",
"chokidar": "^3.5.3",
"chokidar": "^3.6.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cli-table": "^0.3.11",
@@ -94,21 +94,21 @@
"cross-fetch": "^4.0.0",
"docker-event-emitter": "^0.3.0",
"dockerode": "^3.3.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"find-process": "^1.4.7",
"graphql": "^16.8.1",
"graphql-fields": "^2.0.3",
"graphql-scalars": "^1.22.4",
"graphql-scalars": "^1.23.0",
"graphql-subscriptions": "^2.0.0",
"graphql-tag": "^2.12.6",
"graphql-type-json": "^0.3.2",
"graphql-type-uuid": "^0.2.0",
"graphql-ws": "^5.14.3",
"graphql-ws": "^5.16.0",
"htpasswd-js": "^1.0.2",
"ini": "^4.1.1",
"ip": "^1.1.8",
"jose": "^4.14.2",
"ini": "^4.1.2",
"ip": "^2.0.1",
"jose": "^5.3.0",
"lodash": "^4.17.21",
"multi-ini": "^2.2.0",
"mustache": "^4.2.0",
@@ -117,79 +117,79 @@
"nestjs-pino": "^4.0.0",
"node-cache": "^5.1.2",
"node-window-polyfill": "^1.0.2",
"openid-client": "^5.6.4",
"openid-client": "^5.6.5",
"p-iteration": "^1.1.8",
"p-retry": "^4.6.2",
"passport-http-header-strategy": "^1.1.0",
"pidusage": "^3.0.2",
"pino": "^8.17.2",
"pino": "^9.1.0",
"pino-http": "^9.0.0",
"pino-pretty": "^10.3.1",
"pino-pretty": "^11.0.0",
"reflect-metadata": "^0.1.14",
"request": "^2.88.2",
"semver": "^7.5.4",
"semver": "^7.6.2",
"stoppable": "^1.1.0",
"systeminformation": "^5.21.22",
"systeminformation": "^5.22.9",
"ts-command-line-args": "^2.5.1",
"uuid": "^9.0.1",
"ws": "^8.13.0",
"wtfnode": "^0.9.1",
"ws": "^8.17.0",
"wtfnode": "^0.9.2",
"xhr2": "^0.2.1",
"zod": "^3.22.4"
"zod": "^3.23.8"
},
"devDependencies": {
"@babel/runtime": "^7.23.8",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^5.0.0",
"@graphql-codegen/fragment-matcher": "^5.0.0",
"@babel/runtime": "^7.24.5",
"@graphql-codegen/add": "^5.0.2",
"@graphql-codegen/cli": "^5.0.2",
"@graphql-codegen/fragment-matcher": "^5.0.2",
"@graphql-codegen/import-types-preset": "^3.0.0",
"@graphql-codegen/typed-document-node": "^5.0.1",
"@graphql-codegen/typescript": "^4.0.1",
"@graphql-codegen/typescript-operations": "^4.0.1",
"@graphql-codegen/typescript-resolvers": "4.0.1",
"@graphql-codegen/typed-document-node": "^5.0.6",
"@graphql-codegen/typescript": "^4.0.6",
"@graphql-codegen/typescript-operations": "^4.2.0",
"@graphql-codegen/typescript-resolvers": "4.0.6",
"@graphql-typed-document-node/core": "^3.2.0",
"@nestjs/testing": "^10.3.0",
"@swc/core": "^1.3.102",
"@nestjs/testing": "^10.3.8",
"@swc/core": "^1.5.7",
"@types/async-exit-hook": "^2.0.2",
"@types/btoa": "^1.2.5",
"@types/bytes": "^3.1.4",
"@types/cli-table": "^0.3.4",
"@types/command-exists": "^1.2.3",
"@types/dockerode": "^3.3.16",
"@types/dockerode": "^3.3.29",
"@types/express": "^4.17.21",
"@types/graphql-fields": "^1.3.9",
"@types/graphql-type-uuid": "^0.2.6",
"@types/ini": "^4.1.0",
"@types/lodash": "^4.14.202",
"@types/lodash": "^4.17.1",
"@types/mustache": "^4.2.5",
"@types/node": "^20.11.0",
"@types/node": "^20.12.12",
"@types/pidusage": "^2.0.5",
"@types/pify": "^5.0.4",
"@types/semver": "^7.5.6",
"@types/semver": "^7.5.8",
"@types/sendmail": "^1.4.7",
"@types/stoppable": "^1.1.3",
"@types/uuid": "^9.0.7",
"@types/ws": "^8.5.4",
"@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10",
"@types/wtfnode": "^0.7.3",
"@typescript-eslint/eslint-plugin": "^6.18.1",
"@typescript-eslint/parser": "^6.18.1",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@unraid/eslint-config": "github:unraid/eslint-config",
"@vitest/coverage-v8": "^1.2.0",
"@vitest/ui": "^1.2.0",
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"camelcase-keys": "^8.0.2",
"cz-conventional-changelog": "3.3.0",
"eslint": "^8.56.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^50.0.1",
"eslint-plugin-unused-imports": "^3.0.0",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unused-imports": "^3.2.0",
"execa": "^7.1.1",
"filter-obj": "^5.1.0",
"got": "^13.0.0",
"graphql-codegen-typescript-validation-schema": "^0.12.1",
"got": "^13",
"graphql-codegen-typescript-validation-schema": "^0.14.1",
"ip-regex": "^5.0.0",
"json-difference": "^1.16.0",
"json-difference": "^1.16.1",
"map-obj": "^5.0.2",
"p-props": "^5.0.0",
"path-exists": "^5.0.0",
@@ -198,11 +198,11 @@
"pretty-bytes": "^6.1.1",
"pretty-ms": "^8.0.0",
"standard-version": "^9.5.0",
"tsup": "^8.0.1",
"typescript": "^5.3.3",
"tsup": "^8.0.2",
"typescript": "^5.4.5",
"typesync": "^0.12.1",
"vite-tsconfig-paths": "^4.2.3",
"vitest": "^1.2.0",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.6.0",
"zx": "^7.2.3"
},
"optionalDependencies": {

View File

@@ -1,4 +1,4 @@
#!/bin/sh
# Pass all entered params after the docker-compose call
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose -f docker-compose.yml "$@"
# Pass all entered params after the docker compose call
GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker compose -f docker-compose.yml "$@"

View File

@@ -20,6 +20,7 @@ test('Returns paths', async () => {
"myservers-config",
"myservers-config-states",
"myservers-env",
"myservers-keepalive",
"keyfile-base",
"machine-id",
"log-base",

View File

@@ -68,7 +68,7 @@ const getRemoteAccessUrlsForAllowedOrigins = (
return [];
};
const getExtraOrigins = (): string[] => {
export const getExtraOrigins = (): string[] => {
const { extraOrigins } = getters.config().api;
if (extraOrigins) {
return extraOrigins

View File

@@ -34,6 +34,7 @@ export const FIVE_MINUTES_MS = 5 * ONE_MINUTE;
export const TEN_MINUTES_MS = 10 * ONE_MINUTE;
export const THIRTY_MINUTES_MS = 30 * ONE_MINUTE;
export const ONE_HOUR_MS = 60 * ONE_MINUTE;
export const ONE_DAY_MS = ONE_HOUR_MS * 24;
// Seconds
export const ONE_HOUR_SECS = 60 * 60;

View File

@@ -2,7 +2,7 @@
import * as Types from '@app/graphql/generated/api/types';
import { z } from 'zod'
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { AllowedOriginInput, ApiKey, ApiKeyResponse, ArrayType, ArrayCapacity, ArrayDisk, ArrayDiskFsColor, ArrayDiskStatus, ArrayDiskType, ArrayPendingState, ArrayState, Baseboard, Capacity, Case, Cloud, CloudResponse, Config, ConfigErrorState, ConnectSignInInput, ConnectUserInfoInput, ContainerHostConfig, ContainerMount, ContainerPort, ContainerPortType, ContainerState, Devices, Disk, DiskFsType, DiskInterfaceType, DiskPartition, DiskSmartStatus, Display, DockerContainer, DockerNetwork, Flash, Gpu, Importance, Info, InfoApps, InfoCpu, InfoMemory, KeyFile, Me, MemoryFormFactor, MemoryLayout, MemoryType, MinigraphStatus, MinigraphqlResponse, Mount, Network, Notification, NotificationFilter, NotificationInput, NotificationType, Os, Owner, ParityCheck, Partition, Pci, ProfileModel, Registration, RegistrationState, RelayResponse, RemoteAccess, Server, ServerStatus, Service, SetupRemoteAccessInput, Share, System, Temperature, Theme, UnassignedDevice, Uptime, Usb, User, UserAccount, Vars, Versions, VmDomain, VmState, Vms, WAN_ACCESS_TYPE, WAN_FORWARD_TYPE, Welcome, addApiKeyInput, addUserInput, arrayDiskInput, authenticateInput, deleteUserInput, mdState, registrationType, updateApikeyInput, usersInput } from '@app/graphql/generated/api/types'
import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
type Properties<T> = Required<{
@@ -702,6 +702,15 @@ export function RelayResponseSchema(): z.ZodObject<Properties<RelayResponse>> {
})
}
export function RemoteAccessSchema(): z.ZodObject<Properties<RemoteAccess>> {
return z.object({
__typename: z.literal('RemoteAccess').optional(),
accessType: WAN_ACCESS_TYPESchema,
forwardType: WAN_FORWARD_TYPESchema.nullish(),
port: z.number().nullish()
})
}
export function ServerSchema(): z.ZodObject<Properties<Server>> {
return z.object({
__typename: z.literal('Server').optional(),
@@ -852,6 +861,15 @@ export function UserSchema(): z.ZodObject<Properties<User>> {
})
}
export function UserAccountSchema(): z.ZodObject<Properties<UserAccount>> {
return z.object({
description: z.string(),
id: z.string(),
name: z.string(),
roles: z.string()
})
}
export function VarsSchema(): z.ZodObject<Properties<Vars>> {
return z.object({
__typename: z.literal('Vars').optional(),

View File

@@ -874,6 +874,7 @@ export type Query = {
dockerNetwork: DockerNetwork;
/** All Docker networks */
dockerNetworks: Array<Maybe<DockerNetwork>>;
extraAllowedOrigins: Array<Scalars['String']['output']>;
flash?: Maybe<Flash>;
info?: Maybe<Info>;
/** Current user account */
@@ -883,6 +884,7 @@ export type Query = {
owner?: Maybe<Owner>;
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
registration?: Maybe<Registration>;
remoteAccess: RemoteAccess;
server?: Maybe<Server>;
servers: Array<Server>;
/** Network Shares */
@@ -990,6 +992,13 @@ export type RelayResponse = {
timeout?: Maybe<Scalars['String']['output']>;
};
export type RemoteAccess = {
__typename?: 'RemoteAccess';
accessType: WAN_ACCESS_TYPE;
forwardType?: Maybe<WAN_FORWARD_TYPE>;
port?: Maybe<Scalars['Port']['output']>;
};
export type Server = {
__typename?: 'Server';
apikey: Scalars['String']['output'];
@@ -1646,6 +1655,7 @@ export type ResolversTypes = ResolversObject<{
Registration: ResolverTypeWrapper<Registration>;
RegistrationState: RegistrationState;
RelayResponse: ResolverTypeWrapper<RelayResponse>;
RemoteAccess: ResolverTypeWrapper<RemoteAccess>;
Server: ResolverTypeWrapper<Server>;
ServerStatus: ServerStatus;
Service: ResolverTypeWrapper<Service>;
@@ -1739,6 +1749,7 @@ export type ResolversParentTypes = ResolversObject<{
Query: {};
Registration: Registration;
RelayResponse: RelayResponse;
RemoteAccess: RemoteAccess;
Server: Server;
Service: Service;
SetupRemoteAccessInput: SetupRemoteAccessInput;
@@ -2312,6 +2323,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
dockerContainers?: Resolver<Array<ResolversTypes['DockerContainer']>, ParentType, ContextType, Partial<QuerydockerContainersArgs>>;
dockerNetwork?: Resolver<ResolversTypes['DockerNetwork'], ParentType, ContextType, RequireFields<QuerydockerNetworkArgs, 'id'>>;
dockerNetworks?: Resolver<Array<Maybe<ResolversTypes['DockerNetwork']>>, ParentType, ContextType, Partial<QuerydockerNetworksArgs>>;
extraAllowedOrigins?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
flash?: Resolver<Maybe<ResolversTypes['Flash']>, ParentType, ContextType>;
info?: Resolver<Maybe<ResolversTypes['Info']>, ParentType, ContextType>;
me?: Resolver<Maybe<ResolversTypes['Me']>, ParentType, ContextType>;
@@ -2320,6 +2332,7 @@ export type QueryResolvers<ContextType = Context, ParentType extends ResolversPa
owner?: Resolver<Maybe<ResolversTypes['Owner']>, ParentType, ContextType>;
parityHistory?: Resolver<Maybe<Array<Maybe<ResolversTypes['ParityCheck']>>>, ParentType, ContextType>;
registration?: Resolver<Maybe<ResolversTypes['Registration']>, ParentType, ContextType>;
remoteAccess?: Resolver<ResolversTypes['RemoteAccess'], ParentType, ContextType>;
server?: Resolver<Maybe<ResolversTypes['Server']>, ParentType, ContextType>;
servers?: Resolver<Array<ResolversTypes['Server']>, ParentType, ContextType>;
shares?: Resolver<Maybe<Array<Maybe<ResolversTypes['Share']>>>, ParentType, ContextType>;
@@ -2347,6 +2360,13 @@ export type RelayResponseResolvers<ContextType = Context, ParentType extends Res
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type RemoteAccessResolvers<ContextType = Context, ParentType extends ResolversParentTypes['RemoteAccess'] = ResolversParentTypes['RemoteAccess']> = ResolversObject<{
accessType?: Resolver<ResolversTypes['WAN_ACCESS_TYPE'], ParentType, ContextType>;
forwardType?: Resolver<Maybe<ResolversTypes['WAN_FORWARD_TYPE']>, ParentType, ContextType>;
port?: Resolver<Maybe<ResolversTypes['Port']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;
export type ServerResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Server'] = ResolversParentTypes['Server']> = ResolversObject<{
apikey?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
guid?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
@@ -2755,6 +2775,7 @@ export type Resolvers<ContextType = Context> = ResolversObject<{
Query?: QueryResolvers<ContextType>;
Registration?: RegistrationResolvers<ContextType>;
RelayResponse?: RelayResponseResolvers<ContextType>;
RemoteAccess?: RemoteAccessResolvers<ContextType>;
Server?: ServerResolvers<ContextType>;
Service?: ServiceResolvers<ContextType>;
Share?: ShareResolvers<ContextType>;

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql.js';

View File

@@ -27,12 +27,23 @@ enum WAN_FORWARD_TYPE {
STATIC
}
type RemoteAccess {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
input SetupRemoteAccessInput {
accessType: WAN_ACCESS_TYPE!
forwardType: WAN_FORWARD_TYPE
port: Port
}
type Query {
remoteAccess: RemoteAccess!
extraAllowedOrigins: [String!]!
}
type Mutation {
connectSignIn(input: ConnectSignInInput!): Boolean!
connectSignOut: Boolean!

View File

@@ -2,29 +2,54 @@ import { createSlice } from '@reduxjs/toolkit';
import { join, resolve as resolvePath } from 'path';
const initialState = {
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(process.env.PATHS_UNRAID_DATA ?? '/boot/config/plugins/dynamix.my.servers/data/' as const),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(process.env.PATHS_STATES ?? '/usr/local/emhttp/state/' as const),
'dynamix-base': resolvePath(process.env.PATHS_DYNAMIX_BASE ?? '/boot/config/plugins/dynamix/' as const),
'dynamix-config': resolvePath(process.env.PATHS_DYNAMIX_CONFIG ?? '/boot/config/plugins/dynamix/dynamix.cfg' as const),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(process.env.PATHS_MY_SERVERS_CONFIG ?? '/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const),
'myservers-config-states': join(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,
'keyfile-base': resolvePath(process.env.PATHS_KEYFILE_BASE ?? '/boot/config' as const),
'machine-id': resolvePath(process.env.PATHS_MACHINE_ID ?? '/var/lib/dbus/machine-id' as const),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
core: __dirname,
'unraid-api-base': '/usr/local/bin/unraid-api/' as const,
'unraid-data': resolvePath(
process.env.PATHS_UNRAID_DATA ??
('/boot/config/plugins/dynamix.my.servers/data/' as const)
),
'docker-autostart': '/var/lib/docker/unraid-autostart' as const,
'docker-socket': '/var/run/docker.sock' as const,
'parity-checks': '/boot/config/parity-checks.log' as const,
htpasswd: '/etc/nginx/htpasswd' as const,
'emhttpd-socket': '/var/run/emhttpd.socket' as const,
states: resolvePath(
process.env.PATHS_STATES ?? ('/usr/local/emhttp/state/' as const)
),
'dynamix-base': resolvePath(
process.env.PATHS_DYNAMIX_BASE ??
('/boot/config/plugins/dynamix/' as const)
),
'dynamix-config': resolvePath(
process.env.PATHS_DYNAMIX_CONFIG ??
('/boot/config/plugins/dynamix/dynamix.cfg' as const)
),
'myservers-base': '/boot/config/plugins/dynamix.my.servers/' as const,
'myservers-config': resolvePath(
process.env.PATHS_MY_SERVERS_CONFIG ??
('/boot/config/plugins/dynamix.my.servers/myservers.cfg' as const)
),
'myservers-config-states': join(
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),
'keyfile-base': resolvePath(
process.env.PATHS_KEYFILE_BASE ?? ('/boot/config' as const)
),
'machine-id': resolvePath(
process.env.PATHS_MACHINE_ID ?? ('/var/lib/dbus/machine-id' as const)
),
'log-base': resolvePath('/var/log/unraid-api/' as const),
'var-run': '/var/run' as const,
};
export const paths = createSlice({
name: 'paths',
initialState,
reducers: {},
name: 'paths',
initialState,
reducers: {},
});

View File

@@ -1,9 +1,10 @@
import { LogCleanupService } from '@app/unraid-api/cron/log-cleanup.service';
import { WriteFlashFileService } from '@app/unraid-api/cron/write-flash-file.service';
import { Module } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
@Module({
imports: [ScheduleModule.forRoot()],
providers: [LogCleanupService],
providers: [LogCleanupService, WriteFlashFileService],
})
export class CronModule {}

View File

@@ -0,0 +1,55 @@
import { ONE_DAY_MS, THIRTY_MINUTES_MS } from '@app/consts';
import { sleep } from '@app/core/utils/misc/sleep';
import { convertToFuzzyTime } from '@app/mothership/utils/convert-to-fuzzy-time';
import { getters } from '@app/store/index';
import { Injectable, Logger } from '@nestjs/common';
import { Cron } from '@nestjs/schedule';
import { readFile, writeFile } from 'fs/promises';
@Injectable()
export class WriteFlashFileService {
constructor() {}
private readonly logger = new Logger(WriteFlashFileService.name);
private fileLocation = getters.paths()['myservers-keepalive'];
public randomizeWriteTime = true;
public writeNewTimestamp = async (): Promise<number> => {
const wait = this.randomizeWriteTime
? convertToFuzzyTime(0, THIRTY_MINUTES_MS)
: 0;
await sleep(wait);
const newDate = new Date();
try {
await writeFile(this.fileLocation, newDate.toISOString());
} catch (error) {
this.logger.error(error);
}
return newDate.getTime();
};
public getOrCreateTimestamp = async (): Promise<number> => {
try {
const file = (
await readFile(this.fileLocation, 'utf-8')
).toString();
return Date.parse(file);
} catch (error) {
return await this.writeNewTimestamp();
}
};
@Cron('0 * * * *')
async handleCron() {
try {
const currentDate = new Date().getTime();
const prevDate = await this.getOrCreateTimestamp();
if (currentDate - prevDate > ONE_DAY_MS * 7) {
// Write new timestamp
await this.writeNewTimestamp();
}
} catch (error) {
// File does not exist, write it
await this.writeNewTimestamp();
this.logger.error(error);
}
}
}

View File

@@ -0,0 +1,43 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { WriteFlashFileService } from './write-flash-file.service';
import { readFileSync, writeFileSync } from 'fs';
import { getters } from '@app/store/index';
describe('WriteFlashFileService', () => {
let service: WriteFlashFileService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WriteFlashFileService],
}).compile();
service = module.get<WriteFlashFileService>(WriteFlashFileService);
service.randomizeWriteTime = false;
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should write and update the file when called', async () => {
const timestamp = await service.writeNewTimestamp();
expect(timestamp).toBeGreaterThan(0);
const file = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(file).toBe(new Date(timestamp).toISOString(), 'file contents match the returned timestamp');
// Now make the file very old
writeFileSync(getters.paths()['myservers-keepalive'], '2021-01-01T00:00:00.000Z');
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).toBe('2021-01-01T00:00:00.000Z', 'file was updated');
await service.handleCron();
expect(readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString()).not.toBe('2021-01-01T00:00:00.000Z', 'file was updated');
// Now make the file kind of old (one day )
writeFileSync(getters.paths()['myservers-keepalive'], new Date(Date.now() - (1_000 * 60 * 60 * 24)).toISOString());
const now = Date.now();
await service.handleCron();
const contents = readFileSync(getters.paths()['myservers-keepalive'], 'utf8').toString();
expect(new Date(contents).getTime() + (1_000 * 60 * 60 * 12)).toBeLessThan(new Date(now).getTime(), 'file was updated but is still older than today');
});
});

View File

@@ -1,15 +1,22 @@
import { getAllowedOrigins } from '@app/common/allowed-origins';
import {
getAllowedOrigins,
getExtraOrigins,
} from '@app/common/allowed-origins';
import {
WAN_ACCESS_TYPE,
WAN_FORWARD_TYPE,
type ConnectSignInInput,
type SetupRemoteAccessInput,
} from '@app/graphql/generated/api/types';
import type { Cloud } from '@app/graphql/generated/api/types';
import type { Cloud, RemoteAccess } from '@app/graphql/generated/api/types';
import { connectSignIn } from '@app/graphql/resolvers/mutation/connect/connect-sign-in';
import { checkApi } from '@app/graphql/resolvers/query/cloud/check-api';
import { checkCloud } from '@app/graphql/resolvers/query/cloud/check-cloud';
import { checkMinigraphql } from '@app/graphql/resolvers/query/cloud/check-minigraphql';
import { DynamicRemoteAccessType } from '@app/remoteAccess/types';
import { setupRemoteAccessThunk } from '@app/store/actions/setup-remote-access';
import { store } from '@app/store/index';
import { getters, store } from '@app/store/index';
import { logoutUser } from '@app/store/modules/config';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { UseRoles } from 'nest-access-control';
@@ -45,6 +52,44 @@ export class CloudResolver {
};
}
@Query()
@UseRoles({
resource: 'connect',
action: 'read',
possession: 'own',
})
public async remoteAccess(): Promise<RemoteAccess> {
const hasWanAccess = getters.config().remote.wanaccess === 'yes';
const dynamicRemoteAccessSettings: RemoteAccess = {
accessType: hasWanAccess
? getters.config().remote.dynamicRemoteAccessType !==
DynamicRemoteAccessType.DISABLED
? WAN_ACCESS_TYPE.DYNAMIC
: WAN_ACCESS_TYPE.ALWAYS
: WAN_ACCESS_TYPE.DISABLED,
forwardType: getters.config().remote.upnpEnabled
? WAN_FORWARD_TYPE.UPNP
: WAN_FORWARD_TYPE.STATIC,
port: getters.config().remote.wanport
? Number(getters.config().remote.wanport)
: null,
};
return dynamicRemoteAccessSettings;
}
@Query()
@UseRoles({
resource: 'connect',
action: 'read',
possession: 'own',
})
public async extraAllowedOrigins(): Promise<Array<string>> {
const extraOrigins = getExtraOrigins();
return extraOrigins;
}
@Mutation()
@UseRoles({
resource: 'connect',

View File

@@ -161,11 +161,8 @@ exit 0
<?
$msini = @parse_ini_file('/boot/config/plugins/dynamix.my.servers/myservers.cfg', true);
# for convenience, scan myservers.cfg for deleteOnUninstall="no" and if that exists,
# then skip the rest of the cleanup.
$deleteOnUninstall = ($msini === false || empty($msini['plugin']['deleteOnUninstall']) || $msini['plugin']['deleteOnUninstall'] == 'yes');
if (!$deleteOnUninstall) {
# if no_delete_on_uninstall exists on flash drive then skip the rest of the cleanup (useful when switching between staging and production)
if (file_exists("/boot/config/plugins/dynamix.my.servers/no_delete_on_uninstall")) {
exit(0);
}
@@ -320,16 +317,24 @@ if [ -e /etc/rc.d/rc.unraid-api ]; then
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/sbin/upgradepkg && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR-" ]] && mv -f "$DIR-" "$DIR"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
# certain instances where the directory is not present and others where it is, ensure we delete it before we restore it
if [[ -d "$DIR" ]]; then
rm -rf "$DIR"
fi
if [[ -d "$DIR-" ]]; then
mv -f "$DIR-" "$DIR"
fi
# delete plugin files from flash drive and OS
rm -f /boot/config/plugins/dynamix.my.servers/.gitignore
rm -f /etc/rc.d/rc.unraid-api
@@ -407,23 +412,53 @@ echo
# NOTE: any 'exit 1' after this point will result in a broken install
# Preserve in case plugin is removed
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/sbin/upgradepkg && [[ -f "$FILE" ]] && cp -f "$FILE" "$FILE-"
DIR=/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components && [[ -d "$DIR" ]] && mv -f "$DIR" "$DIR-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
FILE=/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php && [[ -f "$FILE" ]] && mv -f "$FILE" "$FILE-"
# Loop through the array of preserveFilesDirs and perform actions
# string param format
# "{move|copy|move_dir}:{path}:{preventDowngrade|skip}"
# move: move the file to a backup file
# copy: copy the file to a backup file
# move_dir: move the directory to a backup directory
# preventDowngrade: during plg install, if the file exists, do not overwrite it if the plg manifest version is less than the installed webgui version
# skip: do not perform any action if there is a manifest version difference
preserveFilesDirs=(
"move:/usr/local/emhttp/plugins/dynamix/Registration.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Downgrade.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/Update.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/unraidcheck:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.plugin.manager/include/UnraidCheck.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/MyServers.page:skip"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page:skip"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/Registration.page:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers1.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/myservers2.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/state.php:preventDowngrade"
"copy:/sbin/upgradepkg:skip"
"move_dir:/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components:move_dir:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/data/server-state.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/reboot-details.php:preventDowngrade"
"move:/usr/local/emhttp/plugins/dynamix.my.servers/include/translations.php:preventDowngrade"
)
preserveAction() {
local action="$1"
local path="$2"
if [[ "$action" == "move" ]]; then
[[ -f "$path" ]] && mv -f "$path" "$path-"
elif [[ "$action" == "copy" ]]; then
[[ -f "$path" ]] && cp -f "$path" "$path-"
elif [[ "$action" == "move_dir" ]]; then
[[ -d "$path" ]] && mv -f "$path" "$path-"
fi
}
# Loop through the array of preserveFilesDirs and perform actions
for obj in "${preserveFilesDirs[@]}"
do
IFS=':' read -r action path preventType <<< "$obj"
preserveAction "$action" "$path" "$preventType"
done
# patch DefaultPageLayout.php
# search text: <?=_('Version')?>: <?=_var($var,'version','?')?><?=$notes?>
@@ -502,24 +537,101 @@ if test -f "${FILE}" && grep -q "top.Shadowbox" "${FILE}" &>/dev/null; then
sed -i 's/top.Shadowbox/parent.Shadowbox/gm' "${FILE}"
fi
# ensure _var() is defined
# brings older versions of Unraid in sync with 6.12.0
# ensure _var() is defined, brings older versions of Unraid in sync with 6.12.0
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php
if test -f "${FILE}" && ! grep -q "_var" "${FILE}" &>/dev/null; then
TEXT=$(
if test -f "${FILE}" && ! grep -q "function _var" "${FILE}" &>/dev/null; then
ADDTEXT1=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
function _var(&$name, $key=null, $default='') {
return is_null($key) ? ($name ?? $default) : ($name[$key] ?? $default);
}
?>
END_HEREDOC
)
fi
# ensure my_logger() is defined, brings older versions of Unraid in sync with 6.13.0
if test -f "${FILE}" && ! grep -q "function my_logger" "${FILE}" &>/dev/null; then
ADDTEXT2=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
// ensure params passed to logger are properly escaped
function my_logger($message, $logger='webgui') {
exec('logger -t '.escapeshellarg($logger).' -- '.escapeshellarg($message));
}
END_HEREDOC
)
fi
# ensure http_get_contents() is defined, brings older versions of Unraid in sync with 6.13.0
if test -f "${FILE}" && ! grep -q "function http_get_contents" "${FILE}" &>/dev/null; then
ADDTEXT3=$(
cat <<'END_HEREDOC'
// backported by Unraid Connect
// Original PHP code by Chirp Internet: www.chirpinternet.eu
// Please acknowledge use of this code by including this header.
// https://www.the-art-of-web.com/php/http-get-contents/
// Modified for Unraid
/**
* Fetches URL and returns content
* @param string $url The URL to fetch
* @param array $opts Array of options to pass to curl_setopt()
* @param array $getinfo Empty array passed by reference, will contain results of curl_getinfo and curl_error
* @return string|false $out The fetched content
*/
function http_get_contents(string $url, array $opts = [], array &$getinfo = NULL) {
$ch = curl_init();
if(isset($getinfo)) {
curl_setopt($ch, CURLINFO_HEADER_OUT, TRUE);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 15);
curl_setopt($ch, CURLOPT_TIMEOUT, 45);
curl_setopt($ch, CURLOPT_ENCODING, "");
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_REFERER, "");
curl_setopt($ch, CURLOPT_FAILONERROR, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'Unraid');
if(is_array($opts) && $opts) {
foreach($opts as $key => $val) {
curl_setopt($ch, $key, $val);
}
}
$out = curl_exec($ch);
if (curl_errno($ch) == 23) {
// error 23 detected, try CURLOPT_ENCODING = "deflate"
curl_setopt($ch, CURLOPT_ENCODING, "deflate");
$out = curl_exec($ch);
}
if (isset($getinfo)) {
$getinfo = curl_getinfo($ch);
}
if ($errno = curl_errno($ch)) {
$msg = "Curl error $errno: " . (curl_error($ch) ?: curl_strerror($errno)) . ". Requested url: '$url'";
if(isset($getinfo)) {
$getinfo['error'] = $msg;
}
my_logger($msg, "http_get_contents");
}
curl_close($ch);
return $out;
}
END_HEREDOC
)
fi
if [[ -n "${ADDTEXT1}" || -n "${ADDTEXT2}" || -n "${ADDTEXT3}" ]]; then
TMP="$FILE.$RANDOM"
cp -f "$FILE" "$TMP"
cp -f "$FILE" "$FILE-"
# delete last line of the file if it contains `?>`
if test $( tail -n 1 "${FILE}" ) = '?>' ; then
sed -i '$ d' "${FILE}"
if test $( tail -n 1 "${TMP}" ) = '?>' ; then
sed -i '$ d' "${TMP}"
fi
echo "${TEXT}" >>"${FILE}"
[[ -n "${ADDTEXT1}" ]] && echo "${ADDTEXT1}" >>"${TMP}"
[[ -n "${ADDTEXT2}" ]] && echo "${ADDTEXT2}" >>"${TMP}"
[[ -n "${ADDTEXT3}" ]] && echo "${ADDTEXT3}" >>"${TMP}"
echo "?>" >>"${TMP}"
mv "${TMP}" "${FILE}"
fi
# install the main txz
@@ -613,6 +725,54 @@ if [[ "${CHANGED}" == "yes" ]]; then
fi
fi
# Prevent web component file downgrade if the webgui version is newer than the plugin version
# Function to extract "ts" value from JSON file
extract_ts() {
local filepath="$1"
local ts_value=null
ts_value=$(jq -r '.ts' "$filepath" 2>/dev/null)
echo "$ts_value"
}
preventDowngradeAction() {
local action="$1"
local path="$2"
local preventType="$3" # preventDowngrade or skip
# if skip, do nothing
if [[ "$preventType" == "skip" ]]; then
return
fi
# restore the "backup" but keep the original backup for the uninstall plg script
# otherwise, the uninstall script will NOT be able to restore the original file
if [[ "$action" == "move" || "$action" == "copy" ]]; then
[[ -f "$path-" ]] && cp -f "$path-" "$path"
elif [[ "$action" == "move_dir" ]]; then
# if directory exists rm the original and copy the backup
# glob expansion via "$path-/"* …yes the * is necessary as we want to copy the contents of the directory
[[ -d "$path-" ]] && rm -rf "$path" && mkdir "$path" && cp -rf "$path-/"* "$path"
fi
}
# Extract "ts" values from both files
plgWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components"
backupWebComponentPath="/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components-"
plgManifestTs=$(extract_ts "$plgWebComponentPath/manifest.json")
webguiManifestTs=$(extract_ts "$backupWebComponentPath/manifest.json")
# Compare the "ts" values and return the file path of the higher value
if [[ "$webguiManifestTs" -gt "$plgManifestTs" ]]; then
# Loop through the array of preserveFilesDirs and perform actions
for obj in "${preserveFilesDirs[@]}"
do
IFS=':' read -r action path preventType <<< "$obj"
preventDowngradeAction "$action" "$path" "$preventType"
done
echo "♻️ Reverted to stock web component files"
fi
# start background process to install/start the api and flash backup
echo
if [ -f /var/local/emhttp/var.ini ]; then

View File

@@ -1,5 +1,9 @@
#!/bin/bash
# Arguments
# $1: SSH server name
# $2: {--wc-deploy|--wc-build|--wc-skip} / deploy or build web components w/o prompt
# Path to store the last used server name
state_file="$HOME/.deploy_state"
@@ -38,6 +42,30 @@ echo "$rsync_command"
eval "$rsync_command"
exit_code=$?
# if $2 is --wc-deploy, deploy the web components without prompting
if [ "$2" = "--wc-deploy" ]; then
deploy="yes"
elif [ "$2" = "--wc-build" ]; then
deploy="build"
elif [ "$2" = "--wc-skip" ]; then
deploy="no"
fi
# if not deploy yes then ask
if [ -z "$deploy" ]; then
echo
echo
read -rp "Do you want to also deploy the built web components? (yes/no/build): " deploy
fi
if [ "$deploy" = "yes" ]; then
cd web || exit
npm run deploy-wc:dev
elif [ "$deploy" = "build" ]; then
cd web || exit
npm run build:dev
fi
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS

View File

@@ -0,0 +1,217 @@
#!/bin/bash
# This file is /etc/rc.d/rc.flash_backup
# use at queue "f" for flash backup
QUEUE=" -q f "
TASKNAME="/etc/rc.d/rc.flash_backup watch"
TASKACTION="/usr/local/emhttp/plugins/dynamix.my.servers/scripts/UpdateFlashBackup update"
last=$(date +%s)
# set GIT_OPTIONAL_LOCKS=0 globally to reduce/eliminate writes to /boot
export GIT_OPTIONAL_LOCKS=0
FAST=1 # 1 second delay when waiting for git
SLOW=10 # 10 second delay when waiting for git
# wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgit() {
while [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; do
sleep "$1"
done
}
# log to syslog, then wait for existing git commands to complete
# $1 is the time in seconds to sleep when waiting. SLOW or FAST
_waitforgitlog() {
if [[ $(pgrep -f '^git -C /boot' -c) -ne 0 ]]; then
logger "waiting for current backup to complete" --tag flash_backup
_waitforgit "$1"
fi
}
status() {
_connected && CONNECTED="system is connected to Unraid Connect Cloud." || CONNECTED="system is not connected to Unraid Connect Cloud."
if _watching; then
echo "flash backup monitor is running. ${CONNECTED}"
_hasqueue && echo "changes detected, backup queued."
exit 0
else
if _enabled; then
echo "flash backup is enabled but the monitor is not running. ${CONNECTED}"
else
echo "flash backup is disabled so the monitor is disabled. ${CONNECTED}"
fi
exit 1
fi
}
start() {
_start
exit 0
}
stop() {
_stop
exit 0
}
reload() {
_start
sleep 1
status
}
_start() {
# Note: can start if not signed in, but watcher loop will not process until signed in
# only run if flash_backup is enabled
if ! _enabled; then
logger "flash backup disabled, exiting" --tag flash_backup
exit 1
fi
_stop
# start watcher loop as background process
exec ${TASKNAME} &>/dev/null &
}
_stop() {
if _watching; then
logger "stop watching for file changes" --tag flash_backup
# terminate watcher loop/process
pkill --full "${TASKNAME}" &>/dev/null
fi
# do not flush. better to have unsaved changes than to corrupt the backup during shutdown
# note that an existing git process could still be running
}
flush() {
# remove any queued jobs
_removequeue
# wait for existing git commands to finish before flushing
_waitforgitlog "${FAST}"
logger "flush: ${TASKACTION}" --tag flash_backup
# if _connected, push any changes ad-hoc
if _connected; then
# shellcheck disable=SC2086
echo "${TASKACTION}_nolimit &>/dev/null" | at ${QUEUE} -M now &>/dev/null
fi
}
_watching() {
local flash_backup_pid
flash_backup_pid=$(pgrep --full "${TASKNAME}")
if [[ ${flash_backup_pid} ]]; then
return 0
fi
return 1
}
_watch() {
# safely clean up git *.lock files
_clearlocks
# flush: this will ensure we start with a clean repo
flush
# wait for flush to complete
sleep 3
_waitforgitlog "${FAST}"
logger "start watching for file changes" --tag flash_backup
# start watcher loop
while true; do
# if system is connected to Unraid Connect Cloud, see if there are updates to process
_connected && _f1
sleep 60
done
}
_f1() {
# wait for existing git commands to finish before checking for updates
_waitforgit "${SLOW}"
if [ "$(git -C /boot status -s)" ]; then
_hasqueue || _f2
elif _haserror && _beenawhile; then
# we are in an error state and it has been 3 hours since we last tried submitting. run the task now.
_runtaskaction
fi
}
_f2() {
if ! _haserror || [[ $(($(date +"%M") % 10)) -eq 0 ]]; then
logger "adding task: ${TASKACTION}" --tag flash_backup
fi
sed -i "s@uptodate=yes@uptodate=no@" /var/local/emhttp/flashbackup.ini &>/dev/null
_runtaskaction
}
_hasqueue() {
# returns false if the queue is empty, true otherwise
# shellcheck disable=SC2086
if [ -z "$(atq ${QUEUE})" ]; then
return 1
fi
return 0
}
_removequeue() {
# delete any at jobs in queue f
# @TODO shellcheck SC2162
# shellcheck disable=SC2086
atq ${QUEUE} | while read line; do
id=$(echo ${line} | cut -d " " -f 1)
atrm ${id}
done
}
_runtaskaction() {
# shellcheck disable=SC2086
echo "${TASKACTION} &>/dev/null" | at ${QUEUE} -M now +1 minute &>/dev/null
last=$(date +%s)
}
_enabled() {
local output
output=$(git -C /boot config --get remote.origin.url 2>&1)
if [[ "${output}" == *"backup.unraid.net"* ]]; then
return 0
fi
return 1
}
_connected() {
CFG=/var/local/emhttp/myservers.cfg
[[ ! -f "${CFG}" ]] && return 1
# shellcheck disable=SC1090
source <(sed -nr '/\[remote\]/,/\[/{/username/p}' "${CFG}" 2>/dev/null)
# ensure signed in
if [ -z "${username}" ]; then
return 1
fi
# 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() {
errorstring=$(awk -F "=" '/error/ {print $2}' /var/local/emhttp/flashbackup.ini 2>&1 || echo '')
if [ ${#errorstring} -le 2 ]; then
return 1
fi
return 0
}
_beenawhile() {
now=$(date +%s)
age=$((now - last))
maxage=$((3 * 60 * 60)) # three hours
[[ $age -gt $maxage ]] && return 0
return 1
}
# wait for git commands to end, then delete any stale lock files
_clearlocks() {
_waitforgitlog "${FAST}"
find /boot/.git -type f -name '*.lock' -delete
}
case "$1" in
'status')
status
;;
'start')
start
;;
'stop')
stop
;;
'reload')
reload
;;
'flush')
flush
;;
'watch')
_watch
;;
*)
echo "usage $0 status|start|stop|reload|flush"
;;
esac

View File

@@ -0,0 +1,182 @@
#!/bin/bash
# unraid-api-handler
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"
# define env to avoid shellcheck SC2154. Will be overridden by the source command below
env=production
# shellcheck disable=SC1091
source "${flash}/env"
api_base_directory="/usr/local/bin"
# Only allow specific envs
if [ "${env}" != "staging" ] && [ "${env}" != "production" ]; then
echo "\"${env}\" is an unsupported env. Please use \"staging\" or \"production\"."
exit 1
fi
switchenv() {
stop
# Get current environment from file
local envFile="${flash}/env"
local currentEnv
currentEnv=$(
# shellcheck disable=SC1090
source "${envFile}"
echo "${env}"
)
if [[ "${currentEnv}" = "production" ]]; then
echo "Switching from production to staging"
echo 'env="staging"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.staging" "${api_base_directory}/unraid-api/.env"
elif [[ "${currentEnv}" = "staging" ]]; then
echo "Switching from staging to production"
echo 'env="production"' >"${envFile}"
cp "${api_base_directory}/unraid-api/.env.production" "${api_base_directory}/unraid-api/.env"
fi
echo "Run \"unraid-api start\" to start the API."
}
raiseloglevel() {
kill -s SIGUSR2 "$(pidof unraid-api)"
}
lowerloglevel() {
kill -s SIGUSR1 "$(pidof unraid-api)"
}
status() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" status
}
start() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" start 2>&1 | logger &
}
report() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" report "$1" "$2"
}
startdebug() {
LOG_CONTEXT=true LOG_STACKTRACE=true LOG_TRACING=true LOG_LEVEL=debug "${api_base_directory}/unraid-api/unraid-api" start --debug
}
stop() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" stop 2>/dev/null
}
reload() {
LOG_TYPE=raw "${api_base_directory}/unraid-api/unraid-api" restart
}
_install() {
# process file from commandline
if [[ -n "$1" ]]; then
file=$(realpath "${flash}/$1")
if [[ "${file}" == "${flash}"* ]] && [[ "${file}" == *".tgz" || "${file}" == *".zip" ]] && [[ -f "${file}" ]]; then
[[ "${file}" == *".tgz" ]] && ext=tgz || ext=zip
echo "installing $1"
cp "${file}" "${flash}/unraid-api.${ext}"
else
echo "invalid installation file: $1"
exit 1
fi
fi
# If this was downloaded from a Github action it'll be a zip with a tgz inside
# Let's extract the tgz and rename it for the next step
if [[ -f "${flash}/unraid-api.zip" ]]; then
for f in ${flash}/unraid-api.zip; do unzip -p "${f}" >"${flash}/${f%.zip}.tgz"; done
rm -f "${flash}/unraid-api.zip"
fi
# Ensure installation tgz exists
[[ ! -f "${flash}/unraid-api.tgz" ]] && echo "Please reinstall the Unraid Connect plugin" && exit 1
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Install unraid-api
rm -rf "${api_base_directory}/unraid-api"
mkdir -p "${api_base_directory}/unraid-api"
tar -C "${api_base_directory}/unraid-api" -xzf "${flash}/unraid-api.tgz" --strip 1
# Reset permissions
rm -f "${flash}/data/permissions.json"
# Copy env file
cp "${api_base_directory}/unraid-api/.env.${env}" "${api_base_directory}/unraid-api/.env"
# Copy wc files from flash
if [ -f "${flash}/webComps/unraid.min.js" ]; then
rm -rf /usr/local/emhttp/webGui/webComps
mkdir -p /usr/local/emhttp/webGui/webComps
cp ${flash}/webComps/* /usr/local/emhttp/webGui/webComps
else
# not fatal, previous version of unraid.min.js should still exist in /usr/local/emhttp/webGui/webComps
echo "Note: ${flash}/webComps/unraid.min.js is missing"
fi
# bail if expected file does not exist
[[ ! -f "${api_base_directory}/unraid-api/unraid-api" ]] && echo "unraid-api install failed" && exit 1
}
install() {
# Install the files
_install "$1"
# if nginx is running, start the api. if not, it will be started by rc.nginx
if /etc/rc.d/rc.nginx status &>/dev/null; then
# Start new process
start
# Note: do not run another unraid-api command until you see "UNRAID API started successfully!" in syslog
sleep 3
echo "unraid-api installed and started"
else
echo "unraid-api installed"
fi
exit 0
}
uninstall() {
# Stop old process
[[ -f "${api_base_directory}/unraid-api/unraid-api" ]] && stop
# Remove all unraid-api files
rm -rf "${api_base_directory}/unraid-api"
rm -f /var/run/unraid-api.sock
}
case "$1" in
'status')
status
;;
'start')
start
;;
'report')
report "$2" "$3"
;;
'switch-env')
switchenv
;;
'start-debug')
startdebug
;;
'raise-log-level')
raiseloglevel
;;
'lower-log-level')
lowerloglevel
;;
'stop')
stop
;;
'reload')
reload
;;
'restart')
reload
;;
'install')
install "$2"
;;
'_install')
_install "$2"
;;
'uninstall')
uninstall
;;
*)
echo "usage $0 status|start|report|switch-env|start-debug|raise-log-level|lower-log-level|stop|reload|install|uninstall"
;;
esac

View File

@@ -14,6 +14,7 @@ Tag="globe"
* all copies or substantial portions of the Software.
*/
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
require_once "$docroot/webGui/include/Wrappers.php";
$serverState = new ServerState();
$keyfile = $serverState->keyfileBase64;
@@ -140,7 +141,7 @@ function registerServer(button) {
button.form.submit();
});
<?else:?>
// give the unraid-api time to call rc.nginx and UpdateDNS before refreshing the page
// give the unraid-api time to call rc.nginx before refreshing the page
const delay = 4000;
setTimeout(function() {
button.form.submit();
@@ -256,10 +257,10 @@ function changeRemoteAccess(dropdown) {
$useConnectMsgTxt = '';
break;
case 'DYNAMIC_MANUAL':
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>.";
break;
case 'DYNAMIC_UPNP':
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/connect/remote-access' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
$remoteAccessMsgTxt = "<a href='https://docs.unraid.net/go/connect-remote-access/' target='_blank'>Enable Remote Access</a> on the <a href='https://connect.myunraid.net/' target='_blank'>Connect Dashboard</a>, a random WAN port will be assigned by UPnP.";
break;
case 'ALWAYS_MANUAL':
$remoteAccessMsgTxt = "Remote Access is always on.";
@@ -521,7 +522,7 @@ _(Allow Remote Access)_:
<?if(!$isRegistered): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span></dd></dl>
<?elseif(!$isMiniGraphConnected && $myServersFlashCfg['remote']['wanaccess']!="yes"): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span></dd></dl>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span></dd></dl>
<?elseif(!$hasMyUnraidNetCert): // NOTE: manually added close tags so the next section would not be indented ?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you Provision a myunraid.net SSL Cert)_</span><input type="hidden" id="wanport" value="0"></dd></dl>
<?elseif(!$boolWebUIAuth): // NOTE: manually added close tags so the next section would not be indented ?>
@@ -546,7 +547,7 @@ _(Allow Remote Access)_:
<?endif?>
&nbsp;
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
<div markdown="1" id="wanpanel" style="display:'none'">
@@ -626,8 +627,8 @@ _(Enable Transparent 2FA for Local Access)_<!-- do not index -->:
_(Flash backup)_:
<?if(!$isRegistered):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until you have signed in)_</span>
<?elseif(!$isMiniGraphConnected && empty($flashbackup_status['activated'])):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud)_</span>
<?elseif(!$isMiniGraphConnected):?>
: <span><i class="fa fa-warning icon warning"></i> _(Disabled until connected to Unraid Connect Cloud - try reloading the page)_</span>
<?else: // begin show flash backup form ?>
: <span id='flashbackuptext'><span class='blue p0'>_(Loading)_ <i class="fa fa-spinner fa-spin" aria-hidden="true"></i></span></span>
@@ -646,7 +647,7 @@ _(Flash backup)_:
<div markdown="1" id="inactivespanel" style="display:none">
&nbsp;
<?if(disk_free_space('/boot') > 1024*1000*1000):?>
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
: <button type="button" onclick="enableFlashBackup(this)">_(Activate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
<?else:?>
: <button type="button" disabled>_(Activate)_</button> <span><i class="fa fa-warning icon warning"></i> _(In order to activate Flash Backup there must be at least 1GB of free space on your flash drive.)_</span>
<?endif?>
@@ -673,7 +674,7 @@ _(Flash backup)_:
</div>
<div markdown="1" id="activepanel" style="display:none">
&nbsp;
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/connect/help#automated-flash-backup" target="_blank">_(More information.)_</a></span>
: <button type="button" onclick="enableFlashBackup(this)">_(Deactivate)_</button> <span>_(Please note that the flash backup is not encrypted at this time.)_ <a href="https://docs.unraid.net/go/connect-flash-backup/" target="_blank">_(More information.)_</a></span>
<?if (in_array($_COOKIE['UPC_ENV']??'', ['development','staging']) && file_exists("/var/log/gitflash") && filesize("/var/log/gitflash")):?>
&nbsp;

View File

@@ -70,8 +70,8 @@ function save_flash_backup_state($loading='') {
rename($flashbackup_tmp, $flashbackup_ini);
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
function default_flash_backup_state() {
global $arrState;
$arrState = [
'activated' => 'no',
@@ -80,6 +80,12 @@ function load_flash_backup_state() {
'error' => '',
'remoteerror' => ''
];
}
function load_flash_backup_state() {
global $arrState,$flashbackup_ini,$isRegistered;
default_flash_backup_state();
$arrNewState = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
if ($arrNewState) {
@@ -120,7 +126,7 @@ function set_git_config($name, $value) {
function readFromFile($file): string {
$text = "";
if (file_exists($file)) {
if (file_exists($file) && filesize($file) > 0) {
$fp = fopen($file,"r");
if (flock($fp, LOCK_EX)) {
$text = fread($fp, filesize($file));
@@ -193,6 +199,7 @@ function deleteLocalRepo() {
if (is_dir($mainGitDir)) {
rename($mainGitDir, $tmpGitDir);
exec('echo "rm -rf '.$tmpGitDir.' &>/dev/null" | at -q f -M now &>/dev/null');
write_log("local repo deleted");
}
// reset state
@@ -201,6 +208,7 @@ function deleteLocalRepo() {
$arrState['loading'] = '';
$arrState['error'] = '';
$arrState['remoteerror'] = '';
save_flash_backup_state();
}
$validCommands = [
@@ -277,7 +285,14 @@ if ($pgrep_output[0] != "0") {
// check if signed-in
if (!$isRegistered) {
response_complete(406, array('error' => 'Must be signed in to My Servers to use Flash Backup'));
default_flash_backup_state();
response_complete(406, array('error' => 'Must be signed in to Unraid Connect to use Flash Backup'));
}
// check if connected to Unraid Connect Cloud
if (!$isConnected) {
default_flash_backup_state();
response_complete(406, array('error' => 'Must be connected to Unraid Connect Cloud to use Flash Backup'));
}
// keyfile
@@ -314,12 +329,46 @@ if (!empty($loadingMessage)) {
}
if ($command == 'deactivate') {
exec_log('git -C /boot remote remove origin');
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
deleteLocalRepo();
response_complete(200, '{}');
}
// determine size of local repo
$maxRepoSize = 100 * 1000; // 100 MB, for comparison without output of 'du -s'
$repoDelFlag = '/boot/config/plugins/dynamix.my.servers/repodeleted';
$output = [];
if (file_exists('/boot/.git')) exec('du -s /boot/.git/ | cut -f 1', $output);
$repoSize = ($output && $output[0]) ? intval($output[0]) : 0;
if ($repoSize > $maxRepoSize) {
// the local repo is too large
$okToDelRepo = true;
if (file_exists($repoDelFlag)) {
// the local repo is too large, but we have already auto-deleted it in the past. determine how long ago this happened
$repoDelTime = intval(@trim(@file_get_contents($repoDelFlag))); // epoch
$repoAge = round((time()-$repoDelTime)/(60*60*24)); // days
$repoMaxAge = 90; // days
if ($repoAge < $repoMaxAge) {
// the local repo was deleted and recreated less than repoMaxAge days ago, do not delete
write_log("local repo is too large ($repoSize > $maxRepoSize) but was auto-deleted recently ($repoAge < $repoMaxAge)");
$okToDelRepo = false;
}
}
if ($okToDelRepo) {
// the local repo is too large, delete and reactivate it
write_log("local repo is too large ($repoSize > $maxRepoSize), about to delete and reactivate");
file_put_contents($repoDelFlag, time());
exec('/etc/rc.d/rc.flash_backup stop &>/dev/null');
deleteLocalRepo();
// change command to 'activate' and continue script
$command = 'activate';
$loadingMessage = 'Activating';
save_flash_backup_state($loadingMessage);
}
} else {
write_log("local repo size is acceptable ($repoSize < $maxRepoSize)");
}
// build a list of sha256 hashes of the bzfiles
$bzfilehashes = [];
$allbzfiles = ['bzimage','bzfirmware','bzmodules','bzroot','bzroot-gui'];
@@ -422,29 +471,29 @@ if (!file_exists('/boot/.git/info/exclude')) {
}
// setup a nice git description
$gitdesc_text='Unraid flash drive for '.$var['NAME']."\n";
$gitdesc_file='/boot/.git/description';
if (!file_exists($gitdesc_file) || strpos(file_get_contents($gitdesc_file),$var['NAME']) === false) {
file_put_contents($gitdesc_file, 'Unraid flash drive for '.$var['NAME']."\n");
if (!file_exists($gitdesc_file) || (file_get_contents($gitdesc_file) != $gitdesc_text)) {
file_put_contents($gitdesc_file, $gitdesc_text);
}
// configure git to use the noprivatekeys filter
set_git_config('filter.noprivatekeys.clean', '/usr/local/emhttp/plugins/dynamix.my.servers/scripts/git-noprivatekeys-clean');
// configure git to apply the noprivatekeys filter to wireguard config files
$gitattributes_file='/boot/.gitattributes';
if (!file_exists($gitattributes_file) || strpos(file_get_contents($gitattributes_file),'noprivatekeys') === false) {
file_put_contents($gitattributes_file, '# file managed by Unraid, do not modify
$gitattributes_text='# file managed by Unraid, do not modify
config/wireguard/*.cfg filter=noprivatekeys
config/wireguard/*.conf filter=noprivatekeys
config/wireguard/peers/*.conf filter=noprivatekeys
');
';
$gitattributes_file='/boot/.gitattributes';
if (!file_exists($gitattributes_file) || (file_get_contents($gitattributes_file) != $gitattributes_text)) {
file_put_contents($gitattributes_file, $gitattributes_text);
}
// setup git ignore for files we dont need in the flash backup
$gitexclude_file='/boot/.git/info/exclude';
if (!file_exists($gitexclude_file) || strpos(file_get_contents($gitexclude_file),'# version 1.0') === false) {
file_put_contents($gitexclude_file, '# file managed by Unraid, do not modify
# version 1.0
// setup master git exclude file to specify what to include/exclude from repo
$gitexclude_text = '# file managed by Unraid, do not modify
# version 1.2
# Blacklist everything
/*
@@ -479,8 +528,20 @@ config/plugins/**/*.tar.bz2
config/plugins-error
config/plugins-old-versions
config/plugins/dockerMan/images
config/plugins/dynamix.file.integrity/logs
config/wireguard/peers/*.png
');
';
// find large files to exclude from flash backup
$oversize_files = $return_var = null;
exec('find /boot/config -type f -size +10M 2>/dev/null | sed "s|^/boot/||g" 2>/dev/null', $oversize_files, $return_var);
if ($oversize_files && is_array($oversize_files)) {
$gitexclude_text .= "\n# Blacklist large files on this system\n".implode("\n", $oversize_files)."\n";
}
$gitexclude_file='/boot/.git/info/exclude';
if (!file_exists($gitexclude_file) || (file_get_contents($gitexclude_file) != $gitexclude_text)) {
file_put_contents($gitexclude_file, $gitexclude_text);
}
// ensure git user is configured
@@ -529,7 +590,7 @@ if (empty($SSH_PORT)) {
} else {
$arrState['loading'] = '';
if (stripos(implode($ssh_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} else {
$arrState['error'] = 'Unable to connect to backup.unraid.net:22';
}
@@ -557,7 +618,7 @@ if ($command == 'activate') {
exec_log('git -C /boot checkout -B master origin/master');
// establish status
exec_log('git -C /boot status --porcelain 2>&1', $status_output, $return_var);
exec_log('git -C /boot status --porcelain', $status_output, $return_var);
if ($return_var != 0) {
// detect git submodule
@@ -600,7 +661,7 @@ if ($command == 'activate') {
}
// detect corruption #1
exec_log('git -C /boot show --summary 2>&1', $show_output, $return_var);
exec_log('git -C /boot show --summary', $show_output, $return_var);
if ($return_var != 0) {
if (stripos(implode($show_output),'fatal: your current branch appears to be broken') !== false) {
$arrState['error'] = 'Error: Backup corrupted';
@@ -616,24 +677,31 @@ if ($command == 'activate') {
} // end check for ($command == 'activate')
if ($command == 'update' || $command == 'activate') {
// note: this section only runs if there are changes detected
if ($arrState['uptodate'] == 'no') {
// increment git commit counter
appendToFile($commitCountFile, $time."\n");
// find files that are in repo but should not be, according to /boot/.git/info/exclude and various .gitignore files
$invalid_files = $return_var = null;
exec_log('git -C /boot ls-files --cached --ignored --exclude-standard', $invalid_files, $return_var);
foreach ((array) $invalid_files as $invalid_file) {
// remove each of these files from the repo
// this prevents future changes from being tracked but does not remove the file from history.
exec_log("git -C /boot rm --cached --ignore-unmatch '$invalid_file'");
}
// add and commit all file changes
exec_log('git -C /boot add -A');
exec_log('git -C /boot commit -m ' . escapeshellarg($commitmsg));
// push changes upstream
exec_log('git -C /boot push --set-upstream origin master', $push_output, $return_var);
if ($return_var != 0) {
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
}
// push changes upstream
exec_log('git -C /boot push --force --set-upstream origin master', $push_output, $return_var);
if ($return_var != 0) {
// check for permission denied
if (stripos(implode($push_output),'permission denied') !== false) {
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to My Servers Cloud';
$arrState['error'] = ($isConnected) ? 'Permission Denied' : 'Permission Denied, ensure you are connected to Unraid Connect Cloud';
} elseif (stripos(implode($push_output),'fatal: loose object') !== false && stripos(implode($push_output),'is corrupt') !== false) {
// detect corruption #2
$arrState['error'] = 'Error: Backup corrupted';

View File

@@ -14,15 +14,25 @@
* Usage:
* ```
* $rebootDetails = new RebootDetails();
* $rebootType = $rebootDetails->getRebootType();
* $rebootType = $rebootDetails->rebootType;
* ```
*/
class RebootDetails
{
/**
* @var string $rebootType Stores the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
*/
private $rebootType = '';
const CURRENT_CHANGES_TXT_PATH = '/boot/changes.txt';
const CURRENT_README_RELATIVE_PATH = 'plugins/unRAIDServer/README.md';
const CURRENT_VERSION_PATH = '/etc/unraid-version';
const PREVIOUS_BZ_ROOT_PATH = '/boot/previous/bzroot';
const PREVIOUS_CHANGES_TXT_PATH = '/boot/previous/changes.txt';
private $currentVersion = '';
public $rebootType = ''; // 'update', 'downgrade', 'thirdPartyDriversDownloading'
public $rebootReleaseDate = '';
public $rebootVersion = '';
public $previousReleaseDate = '';
public $previousVersion = '';
/**
* Constructs a new RebootDetails object and automatically detects the reboot type during initialization.
@@ -40,66 +50,119 @@ class RebootDetails
{
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
$rebootReadme = @file_get_contents("$docroot/plugins/unRAIDServer/README.md", false, null, 0, 20) ?: '';
/**
* Read the reboot readme, and see if it says "REBOOT REQUIRED" or "DOWNGRADE"
* only relying on the README.md file to save reads from the flash drive.
* because we started allowing downgrades from the account.unraid.net Update OS page, we can't
* fully rely on the README.md value of being accurate.
* For instance if on 6.13.0-beta.2.1 then chose to "Downgrade" to 6.13.0-beta.1.10 from the account app
* the README.md file would still say "REBOOT REQUIRED".
*/
$rebootReadme = @file_get_contents("$docroot/" . self::CURRENT_README_RELATIVE_PATH, false, null, 0, 20) ?: '';
$rebootDetected = preg_match("/^\*\*(REBOOT REQUIRED|DOWNGRADE)/", $rebootReadme);
if (!$rebootDetected) {
return;
}
/**
* if a reboot is required, then:
* get current Unraid version from /etc/unraid-version
* then get the version of the last update from self::CURRENT_CHANGES_TXT_PATH
* if they're different, then a reboot is required
* if the version in self::CURRENT_CHANGES_TXT_PATH is less than the current version, then a downgrade is required
* if the version in self::CURRENT_CHANGES_TXT_PATH is greater than the current version, then an update is required
*/
$this->setCurrentVersion();
$this->setRebootDetails();
if ($this->currentVersion == '' || $this->rebootVersion == '') {
return; // return to prevent potential incorrect outcome
}
$rebootForDowngrade = $rebootDetected && strpos($rebootReadme, 'DOWNGRADE') !== false;
$rebootForUpdate = $rebootDetected && strpos($rebootReadme, 'REBOOT REQUIRED') !== false;
$this->rebootType = $rebootForDowngrade ? 'downgrade' : ($rebootForUpdate ? 'update' : '');
$compareVersions = version_compare($this->rebootVersion, $this->currentVersion);
switch ($compareVersions) {
case -1:
$this->setRebootType('downgrade');
break;
case 0:
// we should never get here, but if we do, then no reboot is required and just return
return;
case 1:
$this->setRebootType('update');
break;
}
// Detect if third-party drivers were part of the update process
$processWaitingThirdPartyDrivers = "inotifywait -q /boot/changes.txt -e move_self,delete_self";
$processWaitingThirdPartyDrivers = "inotifywait -q " . self::CURRENT_CHANGES_TXT_PATH . " -e move_self,delete_self";
// Run the ps command to list processes and check if the process is running
$ps_command = "ps aux | grep -E \"$processWaitingThirdPartyDrivers\" | grep -v \"grep -E\"";
$output = shell_exec($ps_command) ?? '';
if ($this->rebootType != '' && strpos($output, $processWaitingThirdPartyDrivers) !== false) {
$this->rebootType = 'thirdPartyDriversDownloading';
$this->setRebootType('thirdPartyDriversDownloading');
}
}
/**
* Gets the type of reboot required, which can be 'update', 'downgrade', or 'thirdPartyDriversDownloading'.
*
* @return string The type of reboot required.
*/
public function getRebootType()
{
return $this->rebootType;
}
/**
* Detects and retrieves the version information related to the system reboot based on the contents of the '/boot/changes.txt' file.
*
* @return string The system version information or 'Not found' if not found, or 'File not found' if the file is not present.
*/
public function getRebootVersion()
private function readChangesTxt(string $file_path = self::CURRENT_CHANGES_TXT_PATH)
{
$file_path = '/boot/changes.txt';
// Check if the file exists
if (file_exists($file_path)) {
// Open the file for reading
$file = fopen($file_path, 'r');
// Read the file line by line until we find a line that starts with '# Version'
while (($line = fgets($file)) !== false) {
if (strpos($line, '# Version') === 0) {
// Use a regular expression to extract the full version string
if (preg_match('/# Version\s+(\S+)/', $line, $matches)) {
$fullVersion = $matches[1];
return $fullVersion;
} else {
return 'Not found';
}
exec("head -n4 $file_path", $rows);
foreach ($rows as $row) {
$i = stripos($row,'version');
if ($i !== false) {
[$version, $releaseDate] = explode(' ', trim(substr($row, $i+7)));
break;
}
}
// Close the file
fclose($file);
return [
'releaseDate' => $releaseDate ?? 'Not found',
'version' => $version ?? 'Not found',
];
} else {
return 'File not found';
}
}
/**
* Sets the current version of the Unraid server for comparison with the reboot version.
*/
private function setCurrentVersion() {
// output ex: version="6.13.0-beta.2.1"
$raw = @file_get_contents(self::CURRENT_VERSION_PATH) ?: '';
// Regular expression to match the version between the quotes
$pattern = '/version="([^"]+)"/';
if (preg_match($pattern, $raw, $matches)) {
$this->currentVersion = $matches[1];
}
}
private function setRebootDetails()
{
$rebootDetails = $this->readChangesTxt();
$this->rebootReleaseDate = $rebootDetails['releaseDate'];
$this->rebootVersion = $rebootDetails['version'];
}
private function setRebootType($rebootType)
{
$this->rebootType = $rebootType;
}
/**
* If self::PREVIOUS_BZ_ROOT_PATH exists, then the user has the option to downgrade to the previous version.
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
* Then we move some files around and reboot.
*/
public function setPrevious()
{
if (@file_exists(self::PREVIOUS_BZ_ROOT_PATH) && @file_exists(self::PREVIOUS_CHANGES_TXT_PATH)) {
$parseOutput = $this->readChangesTxt(self::PREVIOUS_CHANGES_TXT_PATH);
$this->previousVersion = $parseOutput['version'];
$this->previousReleaseDate = $parseOutput['releaseDate'];
}
}
}

View File

@@ -47,6 +47,7 @@ class ServerState
private $connectPluginVersion;
private $configErrorEnum = [
"error" => 'UNKNOWN_ERROR',
"ineligible" => 'INELIGIBLE',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
@@ -92,7 +93,7 @@ class ServerState
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
$this->caseModel = file_exists($caseModelFile) ? file_get_contents($caseModelFile) : '';
$this->caseModel = file_exists($caseModelFile) ? htmlspecialchars(@file_get_contents($caseModelFile), ENT_HTML5, 'UTF-8') : '';
$this->rebootDetails = new RebootDetails();
@@ -235,13 +236,17 @@ class ServerState
public function getServerState()
{
$serverState = [
"array" => [
"state" => @$this->getWebguiGlobal('var', 'fsState'),
"progress" => @$this->getWebguiGlobal('var', 'fsProgress'),
],
"apiKey" => $this->apiKey,
"apiVersion" => $this->apiVersion,
"avatar" => $this->avatar,
"caseModel" => $this->caseModel,
"config" => [
'valid' => ($this->var['configValid'] === 'yes'),
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : 'UNKNOWN_ERROR',
'error' => isset($this->configErrorEnum[$this->var['configValid']]) ? $this->configErrorEnum[$this->var['configValid']] : null,
],
"connectPluginInstalled" => $this->connectPluginInstalled,
"connectPluginVersion" => $this->connectPluginVersion,
@@ -269,8 +274,9 @@ class ServerState
"osVersion" => $this->osVersion,
"osVersionBranch" => $this->osVersionBranch,
"protocol" => _var($_SERVER, 'REQUEST_SCHEME'),
"rebootType" => $this->rebootDetails->getRebootType(),
"regDev" => @(int)$this->var['regDev'] ?? 0,
"rebootType" => $this->rebootDetails->rebootType,
"rebootVersion" => $this->rebootDetails->rebootVersion,
"regDevs" => @(int)$this->var['regDevs'] ?? 0,
"regGen" => @(int)$this->var['regGen'],
"regGuid" => @$this->var['regGUID'] ?? '',
"regTo" => @htmlspecialchars($this->var['regTo'], ENT_HTML5, 'UTF-8') ?? '',

View File

@@ -72,13 +72,13 @@ class WebComponentTranslations
'<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>' => '<p>' . _('To continue using Unraid OS you may purchase a license key.') . ' ' . _('Alternately, you may request a Trial extension.') . '</p>',
'<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 a <em>Pro</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 **Trial** key includes all the functionality and device support of a **Pro** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* 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 *Trial* 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 **Trial** key includes all the functionality and device support of an **Unleashed** key') . '</p><p>' . _('After your **Trial** has reached expiration, your server *still functions normally* 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 *Trial* extension.') . '</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 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 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 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 a Pro 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 a Pro 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>' . _('*Important:*') . '</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>' => '<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>' . _('*Important:*') . '</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 Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>' => '<p>' . _('Your Trial key requires an internet connection') . '</p><p><a href="/Settings/NetworkSettings" class="underline">' . _('Please check Settings > Network') . '</a></p>',
'<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>' => '<p>' . _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.') . '</p>',
'A Trial key provides all the functionality of a Pro Registration key' => _('A Trial key provides all the functionality of a Pro Registration key'),
'A Trial key provides all the functionality of an Unleashed Registration key' => _('A Trial key provides all the functionality of an Unleashed Registration key'),
'Acklowledge that you have made a Flash Backup to enable this action' => _('Acklowledge that you have made a Flash Backup to enable this action'),
'ago' => _('ago'),
'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin.') . ' ' . _('Get started by installing the plugin.'),
@@ -89,8 +89,10 @@ class WebComponentTranslations
'Beta' => _('Beta'),
'Blacklisted USB Flash GUID' => _('Blacklisted USB Flash GUID'),
'BLACKLISTED' => _('BLACKLISTED'),
'Calculating OS Update Eligibility…' => _('Calculating OS Update Eligibility…'),
'Calculating trial expiration…' => _('Calculating trial expiration…'),
'Callback redirect type not present or incorrect' => _('Callback redirect type not present or incorrect'),
'Cancel {0}' => sprintf(_('Cancel %s'), '{0}'),
'Cancel' => _('Cancel'),
'Cannot access your USB Flash boot device' => _('Cannot access your USB Flash boot device'),
'Cannot validate Unraid Trial key' => _('Cannot validate Unraid Trial key'),
@@ -107,8 +109,10 @@ class WebComponentTranslations
'Close' => _('Close'),
'Configure Connect Features' => _('Configure Connect Features'),
'Confirm and start update' => _('Confirm and start update'),
'Confirm to Install Unraid OS {0}' => sprintf(_('Confirm to Install Unraid OS %s'), '{0}'),
'Connected' => _('Connected'),
'Contact Support' => _('Contact Support'),
'Continue' => _('Continue'),
'Copied' => _('Copied'),
'Copy Key URL' => _('Copy Key URL'),
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{0}'),
@@ -122,24 +126,30 @@ class WebComponentTranslations
'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'),
'Downgrade Unraid OS' => _('Downgrade Unraid OS'),
'Downgrades are only recommended if you\'re unable to solve a critical issue.' => _('Downgrades are only recommended if you\'re unable to solve a critical issue.'),
'Download Diagnostics' => _('Download Diagnostics'),
'Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.' => _('Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.'),
'Download unraid-api Logs' => _('Download unraid-api Logs'),
'Dynamic Remote Access' => _('Dynamic Remote Access'),
'Enable update notifications' => _('Enable update notifications'),
'Enhance your experience with Unraid Connect' => _('Enhance your experience with Unraid Connect'),
'Enhance your Unraid experience with Connect' => _('Enhance your Unraid experience with Connect'),
'Enhance your Unraid experience' => _('Enhance your Unraid experience'),
'Error creatiing a trial key. Please try again later.' => _('Error creatiing a trial key. Please try again later.'),
'Error creating a trial key. Please try again later.' => _('Error creating a trial key. Please try again later.'),
'Error Parsing Changelog • {0}' => sprintf(_('Error Parsing Changelog • %s'), '{0}'),
'Error' => _('Error'),
'Expired {0}' => sprintf(_('Expired %s'), '{0}'),
'Expired' => _('Expired'),
'Expires at {0}' => sprintf(_('Expires at %s'), '{0}'),
'Expires in {0}' => sprintf(_('Expires in %s'), '{0}'),
'Extend License to Update' => _('Extend License to Update'),
'Extend License' => _('Extend License'),
'Extend Trial' => _('Extend Trial'),
'Extending your free trial by 15 days' => _('Extending your free trial by 15 days'),
'Extension Installed' => _('Extension Installed'),
'Failed to {0} {1} Key' => sprintf(_('Failed to %1s %2s Key'), '{0}', '{1}'),
'Failed to install key' => _('Failed to install key'),
'Failed to update Connect account configuration' => _('Failed to update Connect account configuration'),
'Fetching & parsing changelog…' => _('Fetching & parsing changelog…'),
'Fix Error' => _('Fix Error'),
'Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.' => sprintf(_('Flash Backup is not available. Navigate to %s/Main/Settings/Flash to try again then come back to this page.'), '{0}'),
'Flash GUID Error' => _('Flash GUID Error'),
@@ -152,18 +162,24 @@ class WebComponentTranslations
'Go to Connect plugin settings' => _('Go to Connect plugin settings'),
'Go to Connect' => _('Go to Connect'),
'Go to Management Access Now' => _('Go to Management Access Now'),
'Go to Settings > Notifications to enable automatic OS update notifications for future releases.' => _('Go to Settings > Notifications to enable automatic OS update notifications for future releases.'),
'Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.' => _('Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.'),
'Go to Tools > Management Access to ensure your backup is up-to-date.' => _('Go to Tools > Management Access to ensure your backup is up-to-date.'),
'Go to Tools > Registration to fix' => _('Go to Tools > Registration to fix'),
'Go to Tools > Registration to Learn More' => _('Go to Tools > Registration to Learn More'),
'Go to Tools > Update OS for more options.' => _('Go to Tools > Update OS for more options.'),
'Go to Tools > Update' => _('Go to Tools > Update'),
'hour' => sprintf(_('%s hour'), '{n}') . ' | ' . sprintf(_('%s hours'), '{n}'),
'I have made a Flash Backup' => _('I have made a Flash Backup'),
'If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.' => _('If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.'),
'Ignore this message if you are currently connected via Remote Access or VPN.' => _('Ignore this message if you are currently connected via Remote Access or VPN.'),
'Ignore this release until next reboot' => _('Ignore this release until next reboot'),
'Ignored Releases' => _('Ignored Releases'),
'In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.' => _('In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.'),
'Install Connect' => _('Install Connect'),
'Install Recovered' => _('Install Recovered'),
'Install Replaced' => _('Install Replaced'),
'Install Unraid OS {0}' => sprintf(_('Install Unraid OS %s'), '{0}'),
'Install' => _('Install'),
'Installed' => _('Installed'),
'Installing Extended Trial' => _('Installing Extended Trial'),
@@ -175,6 +191,9 @@ class WebComponentTranslations
'Invalid API Key Format' => _('Invalid API Key Format'),
'Invalid API Key' => _('Invalid API Key'),
'Invalid installation' => _('Invalid installation'),
'It\s highly recommended to review the changelog before continuing your update.' => _('It\'s highly recommended to review the changelog before continuing your update.'),
'Key ineligible for {0}' => sprintf(_('Key ineligible for %s'), '{0}'),
'Key ineligible for future releases' => _('Key ineligible for future releases'),
'Keyfile required to check replacement status' => _('Keyfile required to check replacement status'),
'LAN IP {0}' => sprintf(_('LAN IP %s'), '{0}'),
'LAN IP Copied' => _('LAN IP Copied'),
@@ -182,12 +201,15 @@ class WebComponentTranslations
'Last checked: {0}' => sprintf(_('Last checked: %s'), '{0}'),
'Learn more about the error' => _('Learn more about the error'),
'Learn more and fix' => _('Learn more and fix'),
'Learn more and link your key to your account' => _('Learn more and link your key to your account'),
'Learn More' => _('Learn More'),
'Learn more' => _('Learn more'),
'Let\'s Unleash your Hardware!' => _('Let\'s Unleash your Hardware!'),
'License key actions' => _('License key actions'),
'License key type' => _('License key type'),
'License Management' => _('License Management'),
'Link Key' => _('Link Key'),
'Linked to Unraid.net account' => _('Linked to Unraidnet account'),
'Loading' => _('Loading'),
'Manage Unraid.net Account in new tab' => _('Manage Unraid.net Account in new tab'),
'Manage Unraid.net Account' => _('Manage Unraid.net Account'),
@@ -196,6 +218,7 @@ class WebComponentTranslations
'minute' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
'Missing key file' => _('Missing key file'),
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
'More options' => _('More options'),
'Multiple License Keys Present' => _('Multiple License Keys Present'),
'Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.' => _('Never ever be left without a backup of your config.') . ' ' . _('If you need to change flash drives, generate a backup from Connect and be up and running in minutes.'),
'New Version: {0}' => sprintf(_('New Version: %s'), '{0}'),
@@ -204,14 +227,18 @@ class WebComponentTranslations
'No Keyfile' => _('No Keyfile'),
'No thanks' => _('No thanks'),
'No USB flash configuration data' => _('No USB flash configuration data'),
'Not Linked' => _('Not Linked'),
'On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.' => _('On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.'),
'Online Flash Backup' => _('Online Flash Backup'),
'Open a bug report' => _('Open a bug report'),
'Open Dropdown' => _('Open Dropdown'),
'Opens Connect in new tab' => _('Opens Connect in new tab'),
'Original release date {0}' => sprintf(_('Original release date %s'), '{0}'),
'OS Update Eligibility Expired' => _('OS Update Eligibility Expired'),
'Performing actions' => _('Performing actions'),
'Please confirm the update details below' => _('Please confirm the update details below'),
'Please finish the initiated downgrade to enable updates.' => _('Please finish the initiated downgrade to enable updates.'),
'Please finish the initiated update to enable a downgrade.' => _('Please finish the initiated update to enable a downgrade.'),
'Please fix any errors and try again.' => _('Please fix any errors and try again.'),
'Please keep this window open while we perform some actions' => _('Please keep this window open while we perform some actions'),
'Please keep this window open' => _('Please keep this window open'),
@@ -238,14 +265,20 @@ class WebComponentTranslations
'Recover Key' => _('Recover Key'),
'Recovered' => _('Recovered'),
'Redeem Activation Code' => _('Redeem Activation Code'),
'Refresh' => _('Refresh'),
'Registered on' => _('Registered on'),
'Registered to' => _('Registered to'),
'Registration key / USB Flash GUID mismatch' => _('Registration key / USB Flash GUID mismatch'),
'Release date {0}' => sprintf(_('Release date %s'), '{0}'),
'Release requires verification to update' => _('Release requires verification to update'),
'Reload' => _('Reload'),
'Remark: Unraid\'s WAN IPv4 {0} does not match your client\'s WAN IPv4 {1}.' => sprintf(_('Remark: Unraid\'s WAN IPv4 %1s does not match your client\'s WAN IPv4 %2s.'), '{0}', '{1}'),
'Remark: your WAN IPv4 is {0}' => sprintf(_('Remark: your WAN IPv4 is %s'), '{0}'),
'Remove from ignore list' => _('Remove from ignore list'),
'Remove' => _('Remove'),
'Replace Key' => _('Replace Key'),
'Replaced' => _('Replaced'),
'Requires the local unraid-api to be running successfully' => _('Requires the local unraid-api to be running successfully'),
'Restarting unraid-api…' => _('Restarting unraid-api…'),
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
@@ -257,6 +290,7 @@ class WebComponentTranslations
'Sign In to utilize Unraid Connect' => _('Sign In to utilize Unraid Connect'),
'Sign In to your Unraid.net account to get started' => _('Sign In to your Unraid.net account to get started'),
'Sign In with Unraid.net Account' => _('Sign In with Unraid.net Account'),
'Sign In' => _('Sign In'),
'Sign Out Failed' => _('Sign Out Failed'),
'Sign Out of Unraid.net' => _('Sign Out of Unraid.net'),
'Sign Out requires the local unraid-api to be running' => _('Sign Out requires the local unraid-api to be running'),
@@ -298,6 +332,7 @@ class WebComponentTranslations
'Unable to fetch client WAN IPv4' => _('Unable to fetch client WAN IPv4'),
'Unable to open release notes' => _('Unable to open release notes'),
'Unknown error' => _('Unknown error'),
'Unknown' => _('Unknown'),
'unlimited' => _('unlimited'),
'Unraid {0} Available' => sprintf(_('Unraid %s Available'), '{0}'),
'Unraid {0} Update Available' => sprintf(_('Unraid %s Update Available'), '{0}'),
@@ -310,10 +345,13 @@ class WebComponentTranslations
'Unraid logo animating with a wave like effect' => _('Unraid logo animating with a wave like effect'),
'Unraid OS {0} Released' => sprintf(_('Unraid OS %s Released'), '{0}'),
'Unraid OS {0} Update Available' => sprintf(_('Unraid OS %s Update Available'), '{0}'),
'Unraid OS is up-to-date' => _('Unraid OS is up-to-date'),
'Unraid OS Update Available' => _('Unraid OS Update Available'),
'unraid-api is offline' => _('unraid-api is offline'),
'Up-to-date with eligible releases' => _('Up-to-date with eligible releases'),
'Up-to-date' => _('Up-to-date'),
'Update Available' => _('Update Available'),
'Update Released' => _('Update Released'),
'Update Unraid OS confirmation required' => _('Update Unraid OS confirmation required'),
'Update Unraid OS' => _('Update Unraid OS'),
'Updating 3rd party drivers' => _('Updating 3rd party drivers'),
@@ -322,15 +360,20 @@ class WebComponentTranslations
'Uptime {0}' => sprintf(_('Uptime %s'), '{0}'),
'USB Flash device error' => _('USB Flash device error'),
'USB Flash has no serial number' => _('USB Flash has no serial number'),
'Verify to Update' => _('Verify to Update'),
'Version available for restore {0}' => sprintf(_('Version available for restore %s'), '{0}'),
'Version: {0}' => sprintf(_('Version: %s'), '{0}'),
'View Available Updates' => _('View Available Updates'),
'View Changelog & Update' => _('View Changelog & Update'),
'View Changelog for {0}' => sprintf(_('View Changelog for %s'), '{0}'),
'View Changelog on Docs' => _('View Changelog on Docs'),
'View Changelog to Start Update' => _('View Changelog to Start Update'),
'View Changelog' => _('View Changelog'),
'View on Docs' => _('View on Docs'),
'View release notes' => _('View release notes'),
'We recommend backing up your USB Flash Boot Device before starting the update.' => _('We recommend backing up your USB Flash Boot Device before starting the update.'),
'year' => sprintf(_('%s year'), '{n}') . ' | ' . sprintf(_('%s years'), '{n}'),
'You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
'You can also manually create a new backup by clicking the Create Flash Backup button.' => _('You can also manually create a new backup by clicking the Create Flash Backup button.'),
'You can manually create a backup by clicking the Create Flash Backup button.' => _('You can manually create a backup by clicking the Create Flash Backup button.'),
'You have already activated the Flash Backup feature via the Unraid Connect plugin.' => _('You have already activated the Flash Backup feature via the Unraid Connect plugin.'),
@@ -339,7 +382,10 @@ class WebComponentTranslations
'You may still update to releases dated prior to your update expiration date.' => _('You may still update to releases dated prior to your update expiration date.'),
'You\'re one step closer to enhancing your Unraid experience' => _('You\'re one step closer to enhancing your Unraid experience'),
'Your {0} Key has been replaced!' => sprintf(_('Your %s Key has been replaced!'), '{0}'),
'Your free Trial key provides all the functionality of a Pro Registration key' => _('Your free Trial key provides all the functionality of a Pro Registration key'),
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.') . ' ' . sprintf(_('You are still eligible to access OS updates that were published on or before %s.'), '{1}'),
'Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.' => sprintf(_('Your %s license included one year of free updates at the time of purchase.'), '{0}') . ' ' . _('You are now eligible to extend your license and access the latest OS updates.'),
'Your free Trial key provides all the functionality of an Unleashed Registration key' => _('Your free Trial key provides all the functionality of an Unleashed Registration key'),
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
'Your Trial has expired' => _('Your Trial has expired'),
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
];

View File

@@ -11,6 +11,7 @@
$cli = php_sapi_name() == 'cli';
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
/**
* @name response_complete
@@ -85,7 +86,7 @@ switch ($command) {
response_complete(200, array('result' => $output), $output);
break;
case 'wanip':
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
$wanip = trim(http_get_contents("https://wanip4.unraid.net/"));
response_complete(200, array('result' => $wanip), $wanip);
break;
case 'none':

View File

@@ -13,37 +13,17 @@ Tag="upload"
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
/**
* @note icon-update is rotated via CSS in myservers1.php
*/
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
// Create an instance of the RebootDetails class
$rebootDetails = new RebootDetails();
/**
* @note icon-update is rotated via CSS in myservers1.php
*
* If /boot/previous/bzroot exists, then the user has the option to downgrade to the previous version.
* Parse the text file /boot/previous/changes.txt to get the version number of the previous version.
* Then we move some files around and reboot.
*/
$restoreVersion = $restoreBranch = $restoreVersionReleaseDate = 'unknown';
$restoreExists = file_exists('/boot/previous/bzroot');
$restoreChangelogPath = '/boot/previous/changes.txt';
// Get the current reboot details if there are any
$rebootDetails->setPrevious();
$serverNameEscaped = htmlspecialchars(str_replace(' ', '_', strtolower($var['NAME'])));
if (file_exists($restoreChangelogPath)) {
exec("head -n4 $restoreChangelogPath", $rows);
foreach ($rows as $row) {
$i = stripos($row,'version');
if ($i !== false) {
[$restoreVersion, $restoreVersionReleaseDate] = explode(' ', trim(substr($row, $i+7)));
break;
}
}
$restoreBranch = strpos($restoreVersion, 'rc') !== false
? _('Next')
: (strpos($restoreVersion, 'beta') !== false
? _('Beta')
: _('Stable'));
}
?>
<script>
@@ -139,7 +119,7 @@ function startDowngrade() {
$.get(
'/plugins/dynamix.plugin.manager/include/Downgrade.php',
{
version: '<?=$restoreVersion?>',
version: '<?= $rebootDetails->previousVersion ?>',
},
function() {
refresh();
@@ -150,7 +130,7 @@ function startDowngrade() {
function confirmDowngrade() {
swal({
title: "_(Confirm Downgrade)_",
text: "<?= $restoreVersion ?><br>_(A reboot will be required)_",
text: "<?= $rebootDetails->previousVersion ?><br>_(A reboot will be required)_",
html: true,
type: 'warning',
showCancelButton: true,
@@ -167,7 +147,7 @@ function confirmDowngrade() {
<unraid-i18n-host>
<unraid-downgrade-os
reboot-version="<?= $rebootDetails->getRebootVersion() ?>"
restore-version="<?= $restoreExists && $restoreVersion != 'unknown' ? $restoreVersion : '' ?>"
restore-release-date="<?= $restoreExists && $restoreVersionReleaseDate != 'unknown' ? $restoreVersionReleaseDate : '' ?>"></unraid-downgrade-os>
reboot-version="<?= $rebootDetails->rebootVersion ?>"
restore-version="<?= $rebootDetails->previousVersion ?>"
restore-release-date="<?= $rebootDetails->previousReleaseDate ?>"></unraid-downgrade-os>
</unraid-i18n-host>

View File

@@ -46,5 +46,5 @@ function flashBackup() {
</script>
<unraid-i18n-host>
<unraid-update-os reboot-version="<?= $rebootDetails->getRebootVersion() ?>"></unraid-update-os>
<unraid-update-os reboot-version="<?= $rebootDetails->rebootVersion ?>"></unraid-update-os>
</unraid-i18n-host>

View File

@@ -1,6 +1,6 @@
<?php
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -37,7 +37,7 @@ class UnraidOsCheck
private const JSON_FILE_IGNORED = '/tmp/unraidcheck/ignored.json';
private const JSON_FILE_IGNORED_KEY = 'updateOsIgnoredReleases';
private const JSON_FILE_RESULT = '/tmp/unraidcheck/result.json';
private const PLG_PATH = '/var/log/plugins/unRAIDServer.plg';
private const PLG_PATH = '/usr/local/emhttp/plugins/unRAIDServer/unRAIDServer.plg';
public function __construct()
{
@@ -108,6 +108,7 @@ class UnraidOsCheck
function _($text) {return $text;}
}
// this command will set the $notify array
extract(parse_plugin_cfg('dynamix', true));
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
@@ -124,20 +125,11 @@ class UnraidOsCheck
$urlbase = $parsedAltUrl ?? $defaultUrl;
$url = $urlbase.'?'.http_build_query($params);
$response = "";
// use error handler to convert warnings from file_get_contents to errors so they can be captured
function warning_as_error($severity, $message, $filename, $lineno) {
throw new ErrorException($message, 0, $severity, $filename, $lineno);
$curlinfo = [];
$response = http_get_contents($url,[],$curlinfo);
if (array_key_exists('error', $curlinfo)) {
$response = json_encode(array('error' => $curlinfo['error']), JSON_PRETTY_PRINT);
}
set_error_handler("warning_as_error");
try {
$response = file_get_contents($url);
} catch (Exception $e) {
$response = json_encode(array('error' => $e->getMessage()), JSON_PRETTY_PRINT);
}
restore_error_handler();
$responseMutated = json_decode($response, true);
if (!$responseMutated) {
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
@@ -159,14 +151,17 @@ class UnraidOsCheck
// send notification if a newer version is available and not ignored
$isNewerVersion = array_key_exists('isNewer',$responseMutated) ? $responseMutated['isNewer'] : false;
$isReleaseIgnored = in_array($responseMutated['version'], $this->getIgnoredReleases());
$isReleaseIgnored = array_key_exists('version',$responseMutated) ? in_array($responseMutated['version'], $this->getIgnoredReleases()) : false;
if ($responseMutated && $isNewerVersion && !$isReleaseIgnored) {
$output = _var($notify,'plugin');
$server = strtoupper(_var($var,'NAME','server'));
$newver = (array_key_exists('version',$responseMutated) && $responseMutated['version']) ? $responseMutated['version'] : 'unknown';
$script = '/usr/local/emhttp/webGui/scripts/notify';
exec("$script -e ".escapeshellarg("System - Unraid [$newver]")." -s ".escapeshellarg("Notice [$server] - Version update $newver")." -d ".escapeshellarg("A new version of Unraid is available")." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
$event = "System - Unraid [$newver]";
$subject = "Notice [$server] - Version update $newver";
$description = "A new version of Unraid is available";
exec("$script -e ".escapeshellarg($event)." -s ".escapeshellarg($subject)." -d ".escapeshellarg($description)." -i ".escapeshellarg("normal $output")." -l '/Tools/Update' -x");
}
exit(0);

View File

@@ -0,0 +1,60 @@
<?php
class UnraidUpdateCancel
{
private $PLG_FILENAME;
private $PLG_BOOT;
private $PLG_VAR;
private $USR_LOCAL_PLUGIN_UNRAID_PATH;
public function __construct() {
$this->PLG_FILENAME = "unRAIDServer.plg";
$this->PLG_BOOT = "/boot/config/plugins/{$this->PLG_FILENAME}";
$this->PLG_VAR = "/var/log/plugins/{$this->PLG_FILENAME}";
$this->USR_LOCAL_PLUGIN_UNRAID_PATH = "/usr/local/emhttp/plugins/unRAIDServer";
// Handle the cancellation
$revertResult = $this->revertFiles();
// Return JSON response for front-end client
$statusCode = $revertResult['success'] ? 200 : 500;
http_response_code($statusCode);
header('Content-Type: application/json');
echo json_encode($revertResult);
}
public function revertFiles() {
try {
$command = '/sbin/mount | grep -q "/boot/previous/bz"';
exec($command, $output, $returnCode);
if ($returnCode !== 0) {
return ['success' => true]; // Nothing to revert
}
// Clear the results of previous unraidcheck run
@unlink("/tmp/unraidcheck/result.json");
// Revert changes made by unRAIDServer.plg
shell_exec("mv -f /boot/previous/* /boot");
unlink($this->PLG_BOOT);
unlink($this->PLG_VAR);
symlink("{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/{$this->PLG_FILENAME}", $this->PLG_VAR);
// Restore README.md by echoing the content into the file
$readmeFile = "{$this->USR_LOCAL_PLUGIN_UNRAID_PATH}/README.md";
$readmeContent = "**Unraid OS**\n\n";
$readmeContent .= "Unraid OS by [Lime Technology, Inc.](https://lime-technology.com).\n";
file_put_contents($readmeFile, $readmeContent);
return ['success' => true]; // Upgrade handled successfully
} catch (\Throwable $th) {
return [
'success' => false,
'message' => $th->getMessage(),
];
}
}
}
// Self instantiate the class and handle the cancellation
new UnraidUpdateCancel();

View File

@@ -1,6 +1,6 @@
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2024, Lime Technology
* Copyright 2012-2024, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
@@ -11,429 +11,12 @@
*/
?>
<?
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
// add translations
$_SERVER['REQUEST_URI'] = 'settings';
require_once "$docroot/webGui/include/Translations.php";
require_once "$docroot/webGui/include/Helpers.php";
function host_lookup_ip($host) {
$result = @dns_get_record($host, DNS_A);
$ip = ($result) ? $result[0]['ip']??'' : '';
return($ip);
}
function rebindDisabled() {
global $isLegacyCert;
$rebindtesturl = $isLegacyCert ? "rebindtest.unraid.net" : "rebindtest.myunraid.net";
// DNS Rebind Protection - this checks the server but clients could still have issues
$validResponse = array("192.168.42.42", "fd42");
$response = host_lookup_ip($rebindtesturl);
return in_array(explode('::',$response)[0], $validResponse);
}
function format_port($port) {
return ($port != 80 && $port != 443) ? ':'.$port : '';
}
function anonymize_host($host) {
global $anon;
if ($anon) {
$host = preg_replace('/.*\.myunraid\.net/', '*.hash.myunraid.net', $host);
$host = preg_replace('/.*\.unraid\.net/', 'hash.unraid.net', $host);
}
return $host;
}
function anonymize_ip($ip) {
global $anon;
if ($anon && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
$ip = "[redacted]";
}
return $ip;
}
function generate_internal_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
}
return $host;
}
function generate_external_host($host, $ip) {
if (strpos($host,'.myunraid.net') !== false) {
$host = str_replace('*', str_replace('.', '-', $ip), $host);
} elseif (strpos($host,'.unraid.net') !== false) {
$host = "www.".$host;
}
return $host;
}
function verbose_output($httpcode, $result) {
global $cli, $verbose, $anon, $plgversion, $post, $var, $isRegistered, $myservers, $reloadNginx, $nginx, $isLegacyCert;
global $remoteaccess;
global $icon_warn, $icon_ok;
if (!$cli || !$verbose) return;
if ($anon) echo "(Output is anonymized, use '-vv' to see full details)".PHP_EOL;
echo "Unraid OS {$var['version']}".((strpos($plgversion, "base-") === false) ? " with My Servers plugin version {$plgversion}" : '').PHP_EOL;
echo ($isRegistered) ? "{$icon_ok}Signed in to Unraid.net as {$myservers['remote']['username']}".PHP_EOL : "{$icon_warn}Not signed in to Unraid.net".PHP_EOL ;
echo "Use SSL is {$nginx['NGINX_USESSL']}".PHP_EOL;
echo (rebindDisabled()) ? "{$icon_ok}Rebind protection is disabled" : "{$icon_warn}Rebind protection is enabled";
echo " for ".($isLegacyCert ? "unraid.net" : "myunraid.net").PHP_EOL;
if ($post) {
$wanip = trim(@file_get_contents("https://wanip4.unraid.net/"));
// check the data
$certhostname = $nginx['NGINX_CERTNAME'];
if ($certhostname) {
// $certhostname is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$certhostip = host_lookup_ip(generate_internal_host($certhostname, $post['internalip']));
$certhosterr = ($certhostip != $post['internalip']);
}
if ($post['internalhostname'] != $certhostname) {
// $post['internalhostname'] is $nginx['NGINX_LANMDNS'] (no cert, or Server_unraid_bundle.pem) || $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$internalhostip = host_lookup_ip(generate_internal_host($post['internalhostname'], $post['internalip']));
$internalhosterr = ($internalhostip != $post['internalip']);
}
if (!empty($post['externalhostname'])) {
// $post['externalhostname'] is $nginx['NGINX_CERTNAME'] (certificate_bundle.pem)
$externalhostip = host_lookup_ip(generate_external_host($post['externalhostname'], $wanip));
$externalhosterr = ($externalhostip != $wanip);
}
// anonymize data. no caclulations can be done with this data beyond this point.
if ($anon) {
if (!empty($certhostip)) $certhostip = anonymize_ip($certhostip);
if (!empty($certhostname)) $certhostname = anonymize_host($certhostname);
if (!empty($internalhostip)) $internalhostip = anonymize_ip($internalhostip);
if (!empty($externalhostip)) $externalhostip = anonymize_ip($externalhostip);
if (!empty($wanip)) $wanip = anonymize_ip($wanip);
if (!empty($post['internalip'])) $post['internalip'] = anonymize_ip($post['internalip']);
if (!empty($post['internalhostname'])) $post['internalhostname'] = anonymize_host($post['internalhostname']);
if (!empty($post['externalhostname'])) $post['externalhostname'] = anonymize_host($post['externalhostname']);
if (!empty($post['externalport'])) $post['externalport'] = "[redacted]";
}
// always anonymize the keyfile
if (!empty($post['keyfile'])) $post['keyfile'] = "[redacted]";
// output notes
if (!empty($post['internalprotocol']) && !empty($post['internalhostname']) && !empty($post['internalport'])) {
$localurl = $post['internalprotocol']."://".generate_internal_host($post['internalhostname'], $post['internalip']).format_port($post['internalport']);
echo 'Local Access url: '.$localurl.PHP_EOL;
if ($internalhostip) {
// $internalhostip will not be defined for .local domains, ok to skip
echo ($internalhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($post['internalhostname'], $post['internalip'])." resolves to {$internalhostip}";
echo ($internalhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($certhostname) {
echo ($certhosterr) ? $icon_warn : $icon_ok;
echo generate_internal_host($certhostname, $post['internalip']).' ';
echo ($certhostip) ? "resolves to {$certhostip}" : "does not resolve to an IP address";
echo ($certhosterr) ? ", it should resolve to {$post['internalip']}" : "";
echo PHP_EOL;
}
if ($remoteaccess == 'yes' && !empty($post['externalprotocol']) && !empty($post['externalhostname']) && !empty($post['externalport'])) {
$remoteurl = $post['externalprotocol']."://".generate_external_host($post['externalhostname'], $wanip).format_port($post['externalport']);
echo 'Remote Access url: '.$remoteurl.PHP_EOL;
echo ($externalhosterr) ? $icon_warn : $icon_ok;
echo generate_external_host($post['externalhostname'], $wanip).' ';
echo ($externalhosterr) ? "does not resolve to an IP address" : "resolves to {$externalhostip}";
echo PHP_EOL;
}
if ($reloadNginx) {
echo "IP address changes were detected, nginx was reloaded".PHP_EOL;
}
}
// output post data
echo PHP_EOL.'Request:'.PHP_EOL;
echo @json_encode($post, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
if ($result) {
echo "Response (HTTP $httpcode):".PHP_EOL;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
echo @json_encode(@json_decode($mutatedResult, true), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT) . PHP_EOL;
}
}
/**
* @name response_complete
* @param {HTTP Response Status Code} $httpcode https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
* @param {String|Array} $result - strings are assumed to be encoded JSON. Arrays will be encoded to JSON.
* @param {String} $cli_success_msg
*/
function response_complete($httpcode, $result, $cli_success_msg='') {
global $cli, $verbose;
$mutatedResult = is_array($result) ? json_encode($result) : $result;
if ($cli) {
if ($verbose) verbose_output($httpcode, $result);
$json = @json_decode($mutatedResult,true);
if (!empty($json['error'])) {
echo 'Error: '.$json['error'].PHP_EOL;
exit(1);
}
exit($cli_success_msg.PHP_EOL);
}
header('Content-Type: application/json');
http_response_code($httpcode);
exit((string)$mutatedResult);
}
// This is a stub, does nothing but return success
$cli = php_sapi_name()=='cli';
$verbose = $anon = false;
if ($cli && ($argc > 1) && $argv[1] == "-v") {
$verbose = true;
$anon = true;
}
if ($cli && ($argc > 1) && $argv[1] == "-vv") {
$verbose = true;
}
$var = parse_ini_file('/var/local/emhttp/var.ini');
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
$is69 = version_compare($var['version'],"6.9.9","<");
$reloadNginx = false;
$dnserr = false;
$icon_warn = "⚠️ ";
$icon_ok = "✅ ";
$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) : [];
// ensure some vars are defined here so we don't have to test them later
if (empty($myservers['remote']['apikey'])) {
$myservers['remote']['apikey'] = "";
}
if (empty($myservers['remote']['wanaccess'])) {
$myservers['remote']['wanaccess'] = "no";
}
if (empty($myservers['remote']['wanport'])) {
$myservers['remote']['wanport'] = 443;
}
// remoteaccess, externalport
if ($cli) {
$remoteaccess = (empty($nginx['NGINX_WANFQDN'])) ? 'no' : 'yes';
$externalport = $myservers['remote']['wanport'];
} else {
$remoteaccess = $_POST['remoteaccess']??'no';
$externalport = intval($_POST['externalport']??443);
if ($remoteaccess != 'yes') {
$remoteaccess = 'no';
}
if ($externalport < 1 || $externalport > 65535) {
$externalport = 443;
}
if ($myservers['remote']['wanaccess'] != $remoteaccess) {
// update the wanaccess ini value
$orig = file_exists($myservers_flash_cfg_path) ? parse_ini_file($myservers_flash_cfg_path,true) : [];
if (!$orig) {
$orig = ['remote' => $myservers['remote']];
}
$orig['remote']['wanaccess'] = $remoteaccess;
$text = '';
foreach ($orig as $section => $block) {
$pairs = "";
foreach ($block as $key => $value) if (strlen($value)) $pairs .= "$key=\"$value\"\n";
if ($pairs) $text .= "[$section]\n".$pairs;
}
if ($text) file_put_contents($myservers_flash_cfg_path, $text);
// need nginx reload
$reloadNginx = true;
}
exit("success".PHP_EOL);
}
$isRegistered = !empty($myservers['remote']['username']);
// protocols, hostnames, ports
$internalprotocol = 'http';
$internalport = $nginx['NGINX_PORT'];
$internalhostname = $nginx['NGINX_LANMDNS'];
$externalprotocol = 'https';
// keyserver will expand *.hash.myunraid.net or add www to hash.unraid.net as needed
$externalhostname = $nginx['NGINX_CERTNAME'];
$isLegacyCert = preg_match('/.*\.unraid\.net$/', $nginx['NGINX_CERTNAME']);
$isWildcardCert = preg_match('/.*\.myunraid\.net$/', $nginx['NGINX_CERTNAME']);
$internalip = $nginx['NGINX_LANIP'];
if ($nginx['NGINX_USESSL']=='yes') {
// When NGINX_USESSL is 'yes' in 6.9, it could be using either Server_unraid_bundle.pem or certificate_bundle.pem
// When NGINX_USESSL is 'yes' in 6.10, it is is using Server_unraid_bundle.pem
$internalprotocol = 'https';
$internalport = $nginx['NGINX_PORTSSL'];
if ($is69 && $nginx['NGINX_CERTNAME']) {
// this is from certificate_bundle.pem
$internalhostname = $nginx['NGINX_CERTNAME'];
}
}
if ($nginx['NGINX_USESSL']=='auto') {
// NGINX_USESSL cannot be 'auto' in 6.9, it is either 'yes' or 'no'
// When NGINX_USESSL is 'auto' in 6.10, it is using certificate_bundle.pem
$internalprotocol = 'https';
$internalport = $nginx['NGINX_PORTSSL'];
// keyserver will expand *.hash.myunraid.net as needed
$internalhostname = $nginx['NGINX_CERTNAME'];
}
// My Servers version
$plgversion = file_exists("/var/log/plugins/dynamix.unraid.net.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
: ( file_exists("/var/log/plugins/dynamix.unraid.net.staging.plg") ? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
: 'base-'.$var['version'] );
// only proceed when when signed in or when legacy unraid.net SSL certificate exists
if (!$isRegistered && !$isLegacyCert) {
response_complete(406, array('error' => _('Nothing to do')));
}
// keyfile
$keyfile = empty($var['regFILE']) ? false : @file_get_contents($var['regFILE']);
if ($keyfile === false) {
response_complete(406, array('error' => _('Registration key required')));
}
$keyfile = @base64_encode($keyfile);
// build post array
$post = [
'keyfile' => $keyfile,
'plgversion' => $plgversion
];
if ($isLegacyCert) {
// sign in not required to maintain local ddns for unraid.net cert
// enable local ddns regardless of use_ssl value
$post['internalip'] = $internalip;
// if host.unraid.net does not resolve to the internalip and DNS Rebind Protection is disabled, disable caching
if (host_lookup_ip(generate_internal_host($nginx['NGINX_CERTNAME'], $post['internalip'])) != $post['internalip'] && rebindDisabled()) $dnserr = true;
}
if ($isRegistered) {
// if signed in, send data needed to maintain My Servers Dashboard
$post['internalhostname'] = $internalhostname;
$post['internalport'] = $internalport;
$post['internalprotocol'] = $internalprotocol;
$post['remoteaccess'] = $remoteaccess;
$post['servercomment'] = $var['COMMENT'];
$post['servername'] = $var['NAME'];
if ($isWildcardCert) {
// keyserver needs the internalip to generate the local access url
$post['internalip'] = $internalip;
}
if ($remoteaccess == 'yes') {
// include wanip in the cache file so we can track if it changes
$post['_wanip'] = trim(@file_get_contents("https://wanip4.unraid.net/"));
$post['externalhostname'] = $externalhostname;
$post['externalport'] = $externalport;
$post['externalprotocol'] = $externalprotocol;
// if wanip.hash.myunraid.net or www.hash.unraid.net does not resolve to the wanip, disable caching
if (host_lookup_ip(generate_external_host($post['externalhostname'], $post['_wanip'])) != $post['_wanip']) $dnserr = true;
}
}
// Include unraid-api report
$unraidreport = [];
if (file_exists('/usr/local/sbin/unraid-api')) {
$jsonString = trim(@exec("/usr/local/sbin/unraid-api report --json 2>/dev/null"));
$unraidreport = @json_decode($jsonString, true);
if ($unraidreport === false) {
$post['unraidreport'] = $jsonString;
} else {
// remove fields we don't need to submit
unset($unraidreport['servers']);
}
} elseif (strpos($plgversion, "base-") === false) {
// The plugin is installed but the api doesn't exist. This is a failed install. Generate basic troubleshooting data.
if (file_exists('/boot/config/plugins/dynamix.my.servers/env')) {
@extract(parse_ini_file('/boot/config/plugins/dynamix.my.servers/env',true));
}
if (empty($env)) {
$env = "production";
}
$unraidreport['os']['version'] = $var['version'];
$unraidreport['api']['version'] = "failed install";
$unraidreport['api']['status'] = "missing";
$unraidreport['api']['environment'] = $env;
$unraidreport['relay']['status'] = "disconnected";
$unraidreport['minigraph']['status'] = "disconnected";
if ($isRegistered) {
$unraidreport['myServers']['status'] = "authenticated";
$unraidreport['myServers']['myServersUsername'] = $myservers['remote']['username'];
} else {
$unraidreport['myServers']['status'] = "signed out";
}
$unraidreport['apiKey'] = (empty($myservers['remote']['apikey'])) ? "invalid" : "exists";
}
if (!empty($unraidreport)) {
// include unraid-api crash logs
$crashLog = '/var/log/unraid-api/crash.json';
$crashAge = 0;
if (file_exists($crashLog)) {
$crashTime = filemtime($crashLog);
$crashAge = time() - $crashTime; // age of crashLog in seconds
$crashDetails = @json_decode(@file_get_contents($crashLog), true);
if (empty($crashDetails['apiVersion']) && $crashAge < 30*60) {
// found a recent crash log without an apiVersion, assume was created by current version of api
$crashDetails['apiVersion'] = $unraidreport['api']['version'];
// overwrite the crash log so it will always have the apiVersion
file_put_contents($crashLog, json_encode($crashDetails, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
// reset to original timestamp so crashAge remains accurate
touch($crashLog, $crashTime);
}
$unraidreport['crashAge'] = $crashAge;
$unraidreport['crashLogs'] = $crashDetails;
}
// add flash backup status
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
if (empty($flashbackup_status['activated'])) {
$flashbackup_status['activated'] = "";
}
if (empty($flashbackup_status['error'])) {
$flashbackup_status['error'] = "";
}
$unraidreport['flashbackup']['activated'] = ($flashbackup_status['activated']) ? "yes" : "no";
$unraidreport['flashbackup']['error'] = ($flashbackup_status['error']) ? $flashbackup_status['error'] : "no";
// add unraidreport to payload
$post['unraidreport'] = json_encode($unraidreport);
// if the api is stopped and there are no crashLogs, or any crashLogs are more than maxCrashAge, start the api
$maxCrashAge = 1*60*60; // 1 hour
if ($unraidreport['api']['status'] == 'stopped' && (empty($unraidreport['crashLogs']) || $crashAge > $maxCrashAge)) {
exec("echo \"/usr/local/sbin/unraid-api start\" | at -M now >/dev/null 2>&1");
}
}
// if remoteaccess is enabled in 6.10.0-rc3+ and WANIP has changed since nginx started, reload nginx
if (isset($post['_wanip']) && ($post['_wanip'] != $nginx['NGINX_WANIP']) && version_compare($var['version'],"6.10.0-rc2",">")) $reloadNginx = true;
// if remoteaccess is currently disabled (perhaps because a wanip was not available when nginx was started)
// BUT the system is configured to have it enabled AND a wanip is now available
// then reload nginx
if ($remoteaccess == 'no' && $nginx['NGINX_WANACCESS'] == 'yes' && !empty(trim(@file_get_contents("https://wanip4.unraid.net/")))) $reloadNginx = true;
if ($reloadNginx) {
exec("/etc/rc.d/rc.nginx reload &>/dev/null");
}
// maxage is 36 hours
$maxage = 36*60*60;
if ($dnserr || $verbose) $maxage = 0;
$datafile = "/tmp/UpdateDNS.txt";
$datafiletmp = "/tmp/UpdateDNS.txt.new";
$dataprev = @file_get_contents($datafile) ?: '';
$datanew = implode("\n",$post)."\n";
if ($datanew == $dataprev && (time()-filemtime($datafile) < $maxage)) {
response_complete(204, null, _('No change to report'));
}
file_put_contents($datafiletmp,$datanew);
rename($datafiletmp, $datafile);
// do not submit the wanip, it will be captured from the submission if needed for remote access
unset($post['_wanip']);
// report necessary server details to limetech for DNS updates
$ch = curl_init('https://keys.lime-technology.com/account/server/register');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ( ($result === false) || ($httpcode != "200") ) {
// delete cache file to retry submission on next run
@unlink($datafile);
response_complete($httpcode ?? "500", array('error' => $error));
}
response_complete($httpcode, $result, _('success'));
header('Content-Type: application/json');
http_response_code(204);
exit(0);
?>

View File

@@ -2,4 +2,5 @@ VITE_ACCOUNT=https://localhost:8008
VITE_CONNECT=https://connect.myunraid.net
VITE_UNRAID_NET=https://unraid.ddev.site
VITE_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams
VITE_ALLOW_CONSOLE_LOGS=false
VITE_ALLOW_CONSOLE_LOGS=false
VITE_WEBGUI=http://localhost

View File

@@ -1,26 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { readFileSync } = require('fs');
const { parse } = require('dotenv');
const envConfig = parse(readFileSync('.env'));
for (const k in envConfig) {
process.env[k] = envConfig[k];
}
module.exports = {
extends: ['@nuxtjs/eslint-config-typescript'],
ignorePatterns: ['composables/gql/'],
rules: {
'comma-dangle': ['warn', 'only-multiline'],
semi: ['error', 'always'],
quotes: ['warn', 'single'],
'no-console': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
'no-debugger': (process.env.NODE_ENV === 'production' ? 'error' : 'off'),
'@typescript-eslint/no-unused-vars': process.env.NODE_ENV === 'production' ? 'error' : 'warn',
'max-len': 'off',
'vue/multi-word-component-names': 'off',
'vue/v-on-event-hyphenation': 'off',
'vue/no-v-html': 'off',
'no-fallthrough': 'off',
}
};

View File

@@ -1 +1 @@
18.17.1
18.19.1

View File

@@ -10,7 +10,7 @@ import type {
Server,
ServerState,
// ServerUpdateOsResponse,
} from '~/types/server';
} from "~/types/server";
// dayjs plugins
// extend(customParseFormat);
@@ -24,8 +24,32 @@ import type {
// return result;
// }
// ENOKEYFILE
// TRIAL
// BASIC
// PLUS
// PRO
// STARTER
// UNLEASHED
// LIFETIME
// EEXPIRED
// EGUID
// EGUID1
// ETRIAL
// ENOKEYFILE2
// ENOKEYFILE1
// ENOFLASH
// EBLACKLISTED
// EBLACKLISTED1
// EBLACKLISTED2
// ENOCONN
// '1111-1111-5GDB-123412341234' Starter.key = TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg
const staticGuid = '1111-1111-5GDB-123412341234';
const state: ServerState = "BASIC" as ServerState;
const currentFlashGuid = "1111-1111-CFXF-TEST1234ZACK"; // this is the flash drive that's been booted from
const regGuid = "1111-1111-CFXF-TEST1234ZACK"; // this guid is registered in key server
const keyfileBase64 = "asdf"; // @todo raycast download key to base64
// const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
@@ -42,70 +66,51 @@ const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
let expireTime = 0;
let regExp: number | undefined;
// ENOKEYFILE
// TRIAL
// BASIC
// PLUS
// PRO
// EEXPIRED
// EGUID
// EGUID1
// ETRIAL
// ENOKEYFILE2
// ENOKEYFILE1
// ENOFLASH
// EBLACKLISTED
// EBLACKLISTED1
// EBLACKLISTED2
// ENOCONN
const state: ServerState = 'STARTER';
let regDev = 0;
let regTy = '';
let regDevs = 0;
let regTy = "";
switch (state) {
// @ts-ignore
case 'EEXPIRED':
case "EEXPIRED":
expireTime = uptime; // 1 hour ago
// @ts-ignore
case 'ENOCONN':
// @ts-ignore
case 'TRIAL':
break;
case "ENOCONN":
break;
case "TRIAL":
expireTime = oneHourFromNow; // in 1 hour
regTy = 'Trial';
// @ts-ignore
case 'BASIC':
regDev = 6;
// @ts-ignore
case 'PLUS':
regDev = 12;
// @ts-ignore
case 'PRO':
// @ts-ignore
case 'STARTER':
regDev = 4;
// regExp = oneHourFromNow;
// regExp = oneDayFromNow;
regTy = "Trial";
break;
case "BASIC":
regDevs = 6;
regTy = "Basic";
break;
case "PLUS":
regDevs = 12;
regTy = "Plus";
break;
case "PRO":
regDevs = -1;
regTy = "Pro";
break;
case "STARTER":
regDevs = 6;
regExp = ninetyDaysAgo;
// regExp = uptime;
// regExp = 1696363920000; // nori.local's expiration
// @ts-ignore
case 'UNLEASHED':
// regExp = oneHourFromNow;
// regExp = oneDayFromNow;
// regExp = oneDayAgo;
// regExp = uptime;
// regExp = 1696363920000; // nori.local's expiration
// @ts-ignore
case 'LIFETIME':
if (regDev === 0) { regDev = 99999; }
if (regTy === '') { regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); } // title case
regTy = "Starter";
break;
case "UNLEASHED":
regDevs = -1;
regExp = ninetyDaysAgo;
regTy = "Unleashed";
break;
case "LIFETIME":
regDevs = -1;
regTy = "Lifetime";
break;
}
const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
// const connectPluginInstalled = '';
// const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
const connectPluginInstalled = "";
const osVersion = '6.12.5';
const osVersionBranch = 'stable';
const osVersion = "6.12.8";
const osVersionBranch = "stable";
// const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined;
// const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => {
@@ -130,59 +135,60 @@ const osVersionBranch = 'stable';
// };
export const serverState: Server = {
apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
avatar: 'https://source.unsplash.com/300x300/?portrait',
apiKey: "unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810",
avatar: "https://source.unsplash.com/300x300/?portrait",
config: {
// error: 'INVALID',
valid: true,
error: null,
valid: false,
},
connectPluginInstalled,
description: 'DevServer9000',
description: "DevServer9000",
deviceCount: 3,
expireTime,
flashBackupActivated: !!connectPluginInstalled,
flashProduct: 'SanDisk_3.2Gen1',
flashVendor: 'USB',
guid: staticGuid,
flashProduct: "SanDisk_3.2Gen1",
flashVendor: "USB",
guid: currentFlashGuid,
// "guid": "0781-5583-8355-81071A2B0211",
inIframe: false,
// keyfile: 'DUMMY_KEYFILE',
keyfile: 'TkJCrVyXMLWWGKZF6TCEvf0C86UYI9KfUDSOm7JoFP19tOMTMgLKcJ6QIOt9_9Psg_t0yF-ANmzSgZzCo94ljXoPm4BESFByR0K7nyY9KVvU8szLEUcBUT3xC2adxLrAXFNxiPeK-mZqt34n16uETKYvLKL_Sr5_JziG5L5lJFBqYZCPmfLMiguFo1vp0xL8pnBH7q8bYoBnePrAcAVb9mAGxFVPEInSPkMBfC67JLHz7XY1Y_K5bYIq3go9XPtLltJ53_U4BQiMHooXUBJCKXodpqoGxq0eV0IhNEYdauAhnTsG90qmGZig0hZalQ0soouc4JZEMiYEcZbn9mBxPg',
lanIp: '192.168.254.36',
license: '',
locale: 'en_US', // en_US, ja
name: 'dev-static',
keyfile: keyfileBase64,
lanIp: "192.168.254.36",
license: "",
locale: "en_US", // en_US, ja
name: "dev-static",
osVersion,
osVersionBranch,
// registered: connectPluginInstalled ? true : false,
registered: false,
regGen: 0,
regTm: twoDaysAgo,
regTo: 'Zack Spear',
regTo: "Zack Spear",
regTy,
regDevs,
regExp,
// "regGuid": "0781-5583-8355-81071A2B0211",
site: 'http://localhost:4321',
regGuid,
site: "http://localhost:4321",
state,
theme: {
banner: false,
bannerGradient: false,
bgColor: '',
bgColor: "",
descriptionShow: true,
metaColor: '',
name: 'white',
textColor: ''
},
updateOsResponse: {
version: '6.12.6',
name: 'Unraid 6.12.6',
date: '2023-12-13',
isNewer: true,
isEligible: false,
changelog: 'https://docs.unraid.net/unraid-os/release-notes/6.12.6/',
sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf',
metaColor: "",
name: "white",
textColor: "",
},
// updateOsResponse: {
// version: '6.12.6',
// name: 'Unraid 6.12.6',
// date: '2023-12-13',
// isNewer: true,
// isEligible: false,
// changelog: 'https://docs.unraid.net/unraid-os/release-notes/6.12.6/',
// sha256: '2f5debaf80549029cf6dfab0db59180e7e3391c059e6521aace7971419c9c4bf',
// },
uptime,
username: 'zspearmint',
wanFQDN: ''
username: "zspearmint",
wanFQDN: "",
};

View File

@@ -7,6 +7,8 @@ Tag="globe"
/**
* @todo create web component env switcher liker upcEnv(). If we utilize manifest.json then we'll be switching its path.
*/
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Wrappers.php";
$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) : [];
// print_r($mystatus);
@@ -142,7 +144,7 @@ if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
<unraid-key-actions></connect-key-actions>
</div>
<div class="ComponentWrapper">
<unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
<unraid-wan-ip-check php-wan-ip="<?=http_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
</div>
<script>

View File

@@ -1,8 +1,7 @@
<script lang="ts" setup>
const nuxtApp = useNuxtApp();
const { registerEntry } = useCustomElements();
onBeforeMount(() => {
// @ts-ignore
nuxtApp.$customElements.registerEntry('UnraidComponents');
registerEntry('UnraidComponents');
});
</script>

View File

@@ -1,4 +1,5 @@
<script lang="ts" setup>
// eslint-disable vue/no-v-html
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
@@ -25,7 +26,7 @@ const { authAction, stateData } = storeToRefs(serverStore);
size="12px"
:text="t(authAction.text)"
:title="authAction?.title ? t(authAction?.title) : undefined"
@click="authAction.click()"
@click="authAction.click?.()"
/>
</span>
</div>

View File

@@ -6,12 +6,14 @@ import type { ButtonProps } from '~/types/ui/button';
const props = withDefaults(defineProps<ButtonProps>(), {
btnStyle: 'fill',
btnType: 'button',
class: undefined,
click: undefined,
href: undefined,
icon: undefined,
iconRight: undefined,
iconRightHoverDisplay: false,
// iconRightHoverAnimate: true,
noPadding: false,
size: '16px',
text: '',
title: '',
@@ -58,33 +60,35 @@ const classes = computed(() => {
switch (props.size) {
case '12px':
buttonSize = 'text-12px p-8px gap-4px';
buttonSize = `text-12px ${props.noPadding ? 'p-0' : 'p-8px'} gap-4px`;
iconSize = 'w-12px';
break;
case '14px':
buttonSize = 'text-14px p-8px gap-8px';
buttonSize = `text-14px ${props.noPadding ? 'p-0' : 'p-8px'} gap-8px`;
iconSize = 'w-14px';
break;
case '16px':
buttonSize = 'text-16px p-12px gap-8px';
buttonSize = `text-16px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
iconSize = 'w-16px';
break;
case '18px':
buttonSize = 'text-18px p-12px gap-8px';
buttonSize = `text-18px ${props.noPadding ? 'p-0' : 'p-12px'} gap-8px`;
iconSize = 'w-18px';
break;
case '20px':
buttonSize = 'text-20px p-16px gap-8px';
buttonSize = `text-20px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
iconSize = 'w-20px';
break;
case '24px':
buttonSize = 'text-24px p-16px gap-8px';
buttonSize = `text-24px ${props.noPadding ? 'p-0' : 'p-16px'} gap-8px`;
iconSize = 'w-24px';
break;
}
return {
button: `${buttonSize} ${buttonColors} ${buttonDefaults}`,
button: props.btnStyle === 'none'
? `${buttonSize} ${props.class}`
: `${buttonSize} ${buttonColors} ${buttonDefaults} ${props.class}`,
icon: `${iconSize} fill-current flex-shrink-0`,
};
});

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
const apiSettingsStore = useUnraidApiSettingsStore();
const originsText = ref<string>('');
const errors = ref<string[]>([]);
onMounted(async () => {
const allowedOriginsSettings = await apiSettingsStore.getAllowedOrigins();
originsText.value = allowedOriginsSettings.join(', ');
});
const origins = computed<string[]>(() => {
console.log('originsText.value: ' + originsText.value);
const newOrigins: string[] = [];
if (originsText.value) {
originsText.value.split(',').forEach((origin) => {
try {
const newUrl = new URL(origin.trim());
newOrigins.push(newUrl.toString());
} catch (e) {
errors.value.push(`Invalid origin: ${origin}`);
}
});
}
return newOrigins;
});
const setAllowedOrigins = () => {
apiSettingsStore.setAllowedOrigins(origins.value);
};
</script>
<template>
<div class="flex flex-col">
<h2>Setup Allowed Origins</h2>
<input v-model="originsText" type="text" placeholder="Input Comma Separated List of URLs">
<button type="button" @click="setAllowedOrigins()">
Set Allowed Origins
</button>
<div v-for="(error, index) of errors" :key="index">
<p>{{ error }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,17 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
// import { useI18n } from 'vue-i18n';
// const { t } = useI18n();
</script>
<template>
<AuthCe />
<!-- @todo: flashback up -->
<WanIpCheckCe />
<ConnectSettingsRemoteAccess />
<ConnectSettingsAllowedOrigins />
<DownloadApiLogsCe />
</template>

View File

@@ -0,0 +1,60 @@
<script lang="ts" setup>
import { WAN_ACCESS_TYPE, WAN_FORWARD_TYPE } from '~/composables/gql/graphql';
import { useUnraidApiSettingsStore } from '~/store/unraidApiSettings';
const apiSettingsStore = useUnraidApiSettingsStore();
const accessType = ref<WAN_ACCESS_TYPE>(WAN_ACCESS_TYPE.Disabled);
const forwardType = ref<WAN_FORWARD_TYPE | null>(null);
const port = ref<number | null>(null);
onMounted(async () => {
const remoteAccessSettings = await apiSettingsStore.getRemoteAccess();
accessType.value =
remoteAccessSettings?.accessType ?? WAN_ACCESS_TYPE.Disabled;
forwardType.value = remoteAccessSettings?.forwardType ?? null;
port.value = remoteAccessSettings?.port ?? null;
});
const setRemoteAccess = () => {
apiSettingsStore.setupRemoteAccess({
accessType: accessType.value,
...(forwardType.value ? { forwardType: forwardType.value } : {}),
...(port.value ? { port: port.value } : {}),
});
};
watch(accessType, (newVal) => {
if (newVal !== WAN_ACCESS_TYPE.Disabled) {
forwardType.value = WAN_FORWARD_TYPE.Static;
}
});
</script>
<template>
<div class="flex flex-col">
<h2>Setup Remote Access</h2>
<label for="forwardType">Forward Type</label>
<select id="forwardType" v-model="accessType">
<option v-for="(val, index) in Object.values(WAN_ACCESS_TYPE)" :key="index" :value="val">
{{ val }}
</option>
</select>
<template v-if="accessType !== WAN_ACCESS_TYPE.Disabled">
<label for="forwardType">Forward Type</label>
<select id="forwardType" v-model="forwardType">
<option v-for="(val, index) in Object.values(WAN_FORWARD_TYPE)" :key="index" :value="val">
{{ val }}
</option>
</select>
</template>
<template v-if="forwardType === WAN_FORWARD_TYPE.Static && accessType !== WAN_ACCESS_TYPE.Disabled">
<label for="port">Port</label>
<input id="port" v-model="port" type="number">
</template>
<button @click="setRemoteAccess">
Save
</button>
</div>
</template>

View File

@@ -39,7 +39,7 @@ const props = withDefaults(defineProps<Props>(), {
const serverStore = useServerStore();
const { rebootType } = storeToRefs(serverStore);
const { rebootType, osVersionBranch } = storeToRefs(serverStore);
const subtitle = computed(() => {
if (rebootType.value === 'update') {
@@ -48,6 +48,8 @@ const subtitle = computed(() => {
return '';
});
const showExternalDowngrade = computed(() => osVersionBranch.value !== 'stable');
onBeforeMount(() => {
serverStore.setRebootVersion(props.rebootVersion);
});
@@ -59,6 +61,7 @@ onBeforeMount(() => {
:title="t('Downgrade Unraid OS')"
:subtitle="subtitle"
:downgrade-not-available="restoreVersion === '' && rebootType === ''"
:show-external-downgrade="showExternalDowngrade"
:t="t"
/>
<UpdateOsDowngrade

View File

@@ -10,10 +10,12 @@ import { useI18n } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { getReleaseNotesUrl, WEBGUI_TOOLS_DOWNGRADE, WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
import type { UiBadgeProps, UiBadgePropsColor } from '~/types/ui/badge';
const { t } = useI18n();
@@ -25,6 +27,9 @@ const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
export interface UpdateOsStatus extends UserProfileLink {
badge: UiBadgeProps;
}
const updateOsStatus = computed(() => {
if (stateDataError.value) { // only allowed to update when server is does not have a state error
return null;
@@ -32,8 +37,10 @@ const updateOsStatus = computed(() => {
if (rebootTypeText.value) {
return {
badgeColor: 'yellow',
badgeIcon: ExclamationTriangleIcon,
badge: {
color: 'yellow' as UiBadgePropsColor,
icon: ExclamationTriangleIcon,
},
href: rebootType.value === 'downgrade'
? WEBGUI_TOOLS_DOWNGRADE.toString()
: WEBGUI_TOOLS_UPDATE.toString(),
@@ -43,6 +50,10 @@ const updateOsStatus = computed(() => {
if (availableWithRenewal.value || available.value) {
return {
badge: {
color: 'orange' as UiBadgePropsColor,
icon: BellAlertIcon,
},
click: () => { updateOsStore.setModalOpen(true); },
text: availableWithRenewal.value
? t('Update Released')
@@ -59,37 +70,42 @@ const updateOsStatus = computed(() => {
<template>
<div class="flex flex-row justify-start gap-x-4px">
<button
<a
class="group leading-none"
:title="t('View release notes')"
@click="updateOsActionsStore.viewReleaseNotes(t('{0} Release Notes', [osVersion]))"
:href="getReleaseNotesUrl(osVersion).toString()"
target="_blank"
rel="noopener"
>
<UiBadge
color="custom"
:icon="InformationCircleIcon"
icon-styles="text-gamma"
icon-styles="text-header-text-secondary"
size="14px"
class="text-gamma group-hover:text-orange-dark group-focus:text-orange-dark group-hover:underline group-focus:underline"
>
{{ osVersion }}
</UiBadge>
</button>
</a>
<component
:is="updateOsStatus.href ? 'a' : 'button'"
v-if="updateOsStatus"
:href="updateOsStatus.href ?? undefined"
:title="updateOsStatus.title ?? undefined"
class="group"
@click="updateOsStatus.click ? updateOsStatus.click() : undefined"
@click="updateOsStatus.click?.()"
>
<UiBadge
:color="updateOsStatus.badgeColor ?? 'orange'"
:icon="updateOsStatus.badgeIcon ?? BellAlertIcon"
v-if="updateOsStatus.badge"
:color="updateOsStatus.badge.color"
:icon="updateOsStatus.badge.icon"
size="12px"
>
{{ updateOsStatus.text }}
</UiBadge>
<template v-else>
{{ updateOsStatus.text }}
</template>
</component>
</div>
</template>

View File

@@ -2,10 +2,7 @@
import { provide } from 'vue';
import { createI18n, I18nInjectionKey } from 'vue-i18n';
import { disableProductionConsoleLogs } from '~/helpers/functions';
import en_US from '~/locales/en_US.json'; // eslint-disable-line camelcase
disableProductionConsoleLogs();
import en_US from '~/locales/en_US.json';
// import ja from '~/locales/ja.json';
const defaultLocale = 'en_US'; // ja, en_US
@@ -17,6 +14,7 @@ let nonDefaultLocale = false;
* Unfortunately, this was the only way I could get the data from PHP to vue-i18n :(
* I tried using i18n.setLocaleMessage() but it didn't work no matter what I tried.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const windowLocaleData = (window as any).LOCALE_DATA || null;
if (windowLocaleData) {
try {
@@ -33,7 +31,7 @@ const i18n = createI18n<false>({
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
fallbackLocale: defaultLocale,
messages: {
en_US, // eslint-disable-line camelcase
en_US,
// ja,
...(nonDefaultLocale ? parsedMessages : {}),
}

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import type { ServerStateDataAction } from '~/types/server';
@@ -10,7 +11,7 @@ const props = withDefaults(defineProps<{
filterBy?: string[] | undefined;
filterOut?: string[] | undefined;
maxWidth?: boolean;
t: any;
t: ComposerTranslation;
}>(), {
actions: undefined,
filterBy: undefined,
@@ -49,7 +50,7 @@ const filteredKeyActions = computed((): ServerStateDataAction[] | undefined => {
:icon-right-hover-display="true"
:text="t(action.text)"
:title="action.title ? t(action.title) : undefined"
@click="action.click()"
@click="action.click?.()"
/>
</li>
</ul>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';
import useFocusTrap from '~/composables/useFocusTrap';
import type { ComposerTranslation } from 'vue-i18n';
export interface Props {
centerContent?: boolean;
@@ -11,7 +11,7 @@ export interface Props {
open?: boolean;
showCloseX?: boolean;
success?: boolean;
t: any;
t: ComposerTranslation;
tallContent?: boolean;
title?: string;
}
@@ -28,9 +28,11 @@ const props = withDefaults(defineProps<Props>(), {
});
watchEffect(() => {
// toggle body scrollability
return props.open
? document.body.style.setProperty('overflow', 'hidden')
: document.body.style.removeProperty('overflow');
if (props.open) {
document.body.style.setProperty('overflow', 'hidden')
} else {
document.body.style.removeProperty('overflow');
}
});
const emit = defineEmits(['close']);
@@ -38,8 +40,6 @@ const closeModal = () => {
emit('close');
};
const { trapRef } = useFocusTrap();
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
/**
@@ -50,7 +50,6 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
<template>
<TransitionRoot appear :show="open" as="template">
<div
ref="trapRef"
class="fixed inset-0 z-10 overflow-y-auto"
role="dialog"
aria-dialog="true"

View File

@@ -25,12 +25,14 @@ import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
import type { RegistrationItemProps } from '~/types/registration';
import KeyActions from '~/components/KeyActions.vue';
import RegistrationReplaceCheck from '~/components/Registration/ReplaceCheck.vue';
import RegistrationKeyLinkedStatus from '~/components/Registration/KeyLinkedStatus.vue';
import RegistrationUpdateExpirationAction from '~/components/Registration/UpdateExpirationAction.vue';
import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
@@ -38,7 +40,10 @@ const { t } = useI18n();
const replaceRenewCheckStore = useReplaceRenewStore();
const serverStore = useServerStore();
const {
computedArray,
arrayWarning,
authAction,
dateTimeFormat,
deviceCount,
@@ -47,18 +52,21 @@ const {
flashProduct,
keyActions,
keyfile,
computedRegDevs,
regGuid,
regTm,
regTo,
regTy,
regExp,
regUpdatesExpired,
serverErrors,
state,
stateData,
stateDataError,
tooManyDevices,
} = storeToRefs(serverStore);
const formattedRegTm = ref<any>();
const formattedRegTm = ref<string>();
/**
* regTm may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
@@ -74,35 +82,42 @@ watch(regTm, (_newV) => {
});
onBeforeMount(() => {
setFormattedRegTm();
/** automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys */
if (guid.value && keyfile.value) {
replaceRenewCheckStore.check();
}
});
const devicesAvailable = computed((): number => {
switch (regTy.value) {
case 'Starter':
return 4;
case 'Basic':
return 6;
case 'Plus':
return 12;
case 'Unleashed':
case 'Lifetime':
case 'Pro':
case 'Trial':
return 9999;
default:
return 0;
const headingIcon = computed(() => serverErrors.value.length ? ShieldExclamationIcon : ShieldCheckIcon);
const heading = computed(() => {
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.heading;
}
return stateData.value.heading;
});
const subheading = computed(() => {
if (serverErrors.value.length) { // It's rare to have multiple errors but for the time being only show the first error
return serverErrors.value[0]?.message;
}
return stateData.value.message;
});
const showTrialExpiration = computed((): boolean => state.value === 'TRIAL' || state.value === 'EEXPIRED');
const showUpdateEligibility = computed((): boolean => !!(regExp.value));
const keyInstalled = computed((): boolean => !!(!stateDataError.value && state.value !== 'ENOKEYFILE'));
const showTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
const showLinkedAndTransferStatus = computed((): boolean => !!(keyInstalled.value && guid.value && !showTrialExpiration.value));
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
const showFilteredKeyActions = computed((): boolean => !!(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0));
const items = computed((): RegistrationItemProps[] => {
return [
...(computedArray.value
? [{
label: t('Array status'),
text: computedArray.value,
warning: arrayWarning.value,
}]
: []),
...(regTy.value
? [{
label: t('License key type'),
@@ -169,20 +184,28 @@ const items = computed((): RegistrationItemProps[] => {
: []),
...(keyInstalled.value
? [{
error: deviceCount.value > devicesAvailable.value,
error: tooManyDevices.value,
label: t('Attached Storage Devices'),
text: deviceCount.value > devicesAvailable.value
? t('{0} out of {1} allowed devices upgrade your key to support more devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value])
: t('{0} out of {1} devices', [deviceCount.value, devicesAvailable.value > 12 ? t('unlimited') : devicesAvailable.value]),
text: tooManyDevices.value
? t('{0} out of {1} allowed devices upgrade your key to support more devices', [deviceCount.value, computedRegDevs.value])
: t('{0} out of {1} devices', [deviceCount.value, computedRegDevs.value === -1 ? t('unlimited') : computedRegDevs.value]),
}]
: []),
...(showTransferStatus.value
...(showLinkedAndTransferStatus.value
? [{
label: t('Transfer License to New Flash'),
component: RegistrationReplaceCheck,
componentProps: { t },
}]
: []),
...(regTo.value && showLinkedAndTransferStatus.value
? [{
label: t('Linked to Unraid.net account'),
component: RegistrationKeyLinkedStatus,
componentProps: { t },
}]
: []),
...(showFilteredKeyActions.value
? [{
component: KeyActions,
@@ -194,13 +217,6 @@ const items = computed((): RegistrationItemProps[] => {
: []),
];
});
onBeforeMount(() => {
/** automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys */
if (guid.value && keyfile.value) {
replaceRenewCheckStore.check();
}
});
</script>
<template>
@@ -210,17 +226,17 @@ onBeforeMount(() => {
<header class="flex flex-col gap-y-16px">
<h3
class="text-20px md:text-24px font-semibold leading-normal flex flex-row items-center gap-8px"
:class="stateDataError ? 'text-unraid-red' : 'text-green-500'"
:class="serverErrors.length ? 'text-unraid-red' : 'text-green-500'"
>
<component :is="stateDataError ? ShieldExclamationIcon : ShieldCheckIcon" class="w-24px h-24px" />
<component :is="headingIcon" class="w-24px h-24px" />
<span>
{{ stateData.heading }}
{{ heading }}
</span>
</h3>
<div
v-if="stateData.message"
v-if="subheading"
class="prose text-16px leading-relaxed whitespace-normal opacity-75"
v-html="stateData.message"
v-html="subheading"
/>
<span v-if="authAction" class="grow-0">
<BrandButton
@@ -228,7 +244,7 @@ onBeforeMount(() => {
:icon="authAction.icon"
:text="t(authAction.text)"
:title="authAction.title ? t(authAction.title) : undefined"
@click="authAction.click()"
@click="authAction.click?.()"
/>
</span>
</header>

View File

@@ -25,9 +25,9 @@ const evenBgColor = computed(() => {
error && 'text-white bg-unraid-red',
warning && 'text-black bg-yellow-100',
]"
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-5 sm:gap-16px items-start rounded"
class="text-16px p-12px grid grid-cols-1 gap-4px sm:px-20px sm:grid-cols-5 sm:gap-16px items-baseline rounded"
>
<dt v-if="label" class="font-semibold sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-8px">
<dt v-if="label" class="font-semibold leading-normal sm:col-span-2 flex flex-row sm:justify-end sm:text-right items-center gap-x-8px">
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
<span v-html="label" />
</dt>
@@ -35,7 +35,13 @@ const evenBgColor = computed(() => {
class="leading-normal sm:col-span-3"
:class="!label && 'sm:col-start-2'"
>
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
<span
v-if="text"
class="select-all"
:class="{
'opacity-75': !error,
}"
>
{{ text }}
</span>
<template v-if="$slots['right']">

View File

@@ -0,0 +1,74 @@
<script setup lang="ts">
import {
ArrowTopRightOnSquareIcon,
ArrowPathIcon,
LinkIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useAccountStore } from '~/store/account';
import { useReplaceRenewStore } from '~/store/replaceRenew';
const accountStore = useAccountStore();
const replaceRenewStore = useReplaceRenewStore();
const { keyLinkedStatus, keyLinkedOutput } = storeToRefs(replaceRenewStore);
defineProps<{
t: ComposerTranslation;
}>();
</script>
<template>
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="keyLinkedStatus !== 'linked' && keyLinkedStatus !== 'checking'"
btn-style="none"
:no-padding="true"
:title="t('Refresh')"
class="group"
@click="replaceRenewStore.check(true)"
>
<UiBadge
v-if="keyLinkedOutput"
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
:icon-right="ArrowPathIcon"
size="16px"
>
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
</UiBadge>
</BrandButton>
<UiBadge
v-else
:color="keyLinkedOutput.color"
:icon="keyLinkedOutput.icon"
size="16px"
>
{{ t(keyLinkedOutput.text ?? 'Unknown') }}
</UiBadge>
<span class="inline-flex flex-wrap-items-start gap-8px">
<BrandButton
v-if="keyLinkedStatus === 'notLinked'"
btn-style="underline"
:external="true"
:icon="LinkIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Link Key')"
:title="t('Learn more and link your key to your account')"
class="text-14px"
@click="accountStore.linkKey"
/>
<BrandButton
v-else
btn-style="underline"
:external="true"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
@click="accountStore.myKeys"
/>
</span>
</div>
</template>

View File

@@ -4,6 +4,7 @@ import {
KeyIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { DOCS_REGISTRATION_REPLACE_KEY } from '~/helpers/urls';
import { useReplaceRenewStore } from '~/store/replaceRenew';
@@ -12,12 +13,12 @@ const replaceRenewStore = useReplaceRenewStore();
const { replaceStatusOutput } = storeToRefs(replaceRenewStore);
defineProps<{
t: any;
t: ComposerTranslation;
}>();
</script>
<template>
<div class="flex flex-wrap items-start justify-between gap-8px">
<div class="flex flex-wrap items-center justify-between gap-8px">
<BrandButton
v-if="!replaceStatusOutput"
:icon="KeyIcon"
@@ -32,16 +33,18 @@ defineProps<{
:icon="replaceStatusOutput.icon"
size="16px"
>
{{ t(replaceStatusOutput.text) }}
{{ t(replaceStatusOutput.text ?? 'Unknown') }}
</UiBadge>
<BrandButton
btn-style="underline"
:external="true"
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
/>
<span class="inline-flex flex-wrap items-center justify-end gap-8px">
<BrandButton
btn-style="underline"
:external="true"
:href="DOCS_REGISTRATION_REPLACE_KEY.toString()"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Learn More')"
class="text-14px"
/>
</span>
</div>
</template>

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useServerStore } from '~/store/server';
export interface Props {
componentIs?: string;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -27,11 +28,11 @@ const output = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [outputDateTimeFormatted.value])
: props.t('Eligible for updates until {0}', [outputDateTimeFormatted.value]),
? props.t('Ineligible for feature updates released after {0}', [outputDateTimeFormatted.value])
: props.t('Eligible for free feature updates until {0}', [outputDateTimeFormatted.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [outputDateTimeReadableDiff.value])
: props.t('Eligible for updates for {0}', [outputDateTimeReadableDiff.value]),
: props.t('Eligible for free feature updates for {0}', [outputDateTimeReadableDiff.value]),
};
});
</script>

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { DOCS_REGISTRATION_LICENSING } from '~/helpers/urls';
@@ -8,7 +9,7 @@ import { useReplaceRenewStore } from '~/store/replaceRenew';
import { useServerStore } from '~/store/server';
export interface Props {
t: any;
t: ComposerTranslation;
}
const props = defineProps<Props>();
@@ -39,11 +40,11 @@ const output = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for updates until {0}', [formattedRegExp.value]),
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [readableDiffRegExp.value])
: props.t('Eligible for updates for {0}', [readableDiffRegExp.value]),
: props.t('Eligible for free feature updates for {0}', [readableDiffRegExp.value]),
};
});
</script>
@@ -75,7 +76,7 @@ const output = computed(() => {
:text="t('Extend License')"
:title="t('Pay your annual fee to continue receiving OS updates.')"
class="flex-grow"
@click="renewAction.click()"
@click="renewAction.click?.()"
/>
<BrandButton

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import { useAccountStore } from '~/store/account';
import type { ButtonStyle } from '~/types/ui/button';
defineProps<{
t: any;
btnStyle?: ButtonStyle;
t: ComposerTranslation;
}>();
const accountStore = useAccountStore();
@@ -13,6 +16,7 @@ const accountStore = useAccountStore();
<template>
<div class="flex flex-col sm:flex-shrink-0 sm:flex-grow-0 items-center">
<BrandButton
:btn-style="btnStyle"
:icon="ArrowPathIcon"
:icon-right="ArrowTopRightOnSquareIcon"
:text="t('Check for OS Updates')"

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import { computed } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import { usePurchaseStore } from '~/store/purchase';
import { useUpdateOsStore } from '~/store/updateOs';
@@ -17,7 +18,7 @@ import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
export interface Props {
open?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -9,6 +9,7 @@ import {
} from '@heroicons/vue/24/solid';
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useAccountStore } from '~/store/account';
@@ -20,7 +21,7 @@ import type { ButtonProps } from '~/types/ui/button';
export interface Props {
open?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -49,9 +50,20 @@ const {
checkForUpdatesLoading,
} = storeToRefs(updateOsStore);
const {
outputDateTimeFormatted: formattedRegExp,
} = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
/**
* regExp may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
*/
const formattedRegExp = ref<string>();
const setFormattedRegExp = () => { // ran in watch on regExp and onBeforeMount
if (!regExp.value) { return; }
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
formattedRegExp.value = outputDateTimeFormatted.value;
};
watch(regExp, (_newV) => {
setFormattedRegExp();
});
const ignoreThisRelease = ref(false);
// if we had a release ignored and now we don't set ignoreThisRelease to false
@@ -87,8 +99,8 @@ const modalCopy = computed((): ModalCopy | null => {
if (availableWithRenewal.value) {
const description = regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for updates until {0}', [formattedRegExp.value]);
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]);
return {
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
@@ -136,7 +148,8 @@ const actionButtons = computed((): ButtonProps[] | null => {
const buttons: ButtonProps[] = [];
// update available but not stable branch - should link out to account update callback
if (availableRequiresAuth.value) {
// if availableWithRenewal.value is true, then we need to renew the license before we can update so don't show the verify button
if (availableRequiresAuth.value && !availableWithRenewal.value) {
buttons.push({
click: async () => await accountStore.updateOs(),
icon: IdentificationIcon,
@@ -147,7 +160,7 @@ const actionButtons = computed((): ButtonProps[] | null => {
}
// update available - open changelog to commence update
if (available.value) {
if (available.value && updateOsResponse.value?.changelog) {
buttons.push({
btnStyle: availableWithRenewal.value
? 'outline'
@@ -187,10 +200,10 @@ const close = () => {
};
const renderMainSlot = computed(() => {
return checkForUpdatesLoading.value || available.value || availableWithRenewal.value || extraLinks.value?.length > 0 || updateOsIgnoredReleases.value.length > 0;
return !!(checkForUpdatesLoading.value || available.value || availableWithRenewal.value || extraLinks.value?.length > 0 || updateOsIgnoredReleases.value.length > 0);
});
const userFormattedReleaseDate = ref<any>();
const userFormattedReleaseDate = ref<string>();
/**
* availableReleaseDate may not have a value until we get a release in the update os check response.
* So we need to watch for this value to be able to format it based on the user's date time preferences.
@@ -208,6 +221,7 @@ onBeforeMount(() => {
if (availableReleaseDate.value) {
setUserFormattedReleaseDate();
}
setFormattedRegExp();
});
const modalWidth = computed(() => {
@@ -240,9 +254,9 @@ const modalWidth = computed(() => {
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="t(item.text)"
:text="t(item.text ?? '')"
:title="item.title ? t(item.title) : undefined"
@click="item.click ? item.click() : undefined"
@click="item.click?.()"
/>
</div>
@@ -310,9 +324,9 @@ const modalWidth = computed(() => {
:icon="item.icon"
:icon-right="item.iconRight"
:icon-right-hover-display="item.iconRightHoverDisplay"
:text="t(item.text)"
:text="t(item.text ?? '')"
:title="item.title ? t(item.title) : undefined"
@click="item.click ? item.click() : undefined"
@click="item.click?.()"
/>
</div>
</div>

View File

@@ -9,6 +9,7 @@ import {
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { ref } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
@@ -20,7 +21,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
t: any;
t: ComposerTranslation;
releaseDate: string;
version: string;
}>();
@@ -35,7 +36,7 @@ const {
const diagnosticsButton = ref<UserProfileLink | undefined>({
click: () => {
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
downloadDiagnostics();
},
icon: FolderArrowDownIcon,
@@ -45,7 +46,7 @@ const diagnosticsButton = ref<UserProfileLink | undefined>({
const downgradeButton = ref<UserProfileLink>({
click: () => {
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
confirmDowngrade();
},
name: 'downgrade',

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { XMarkIcon } from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import { useThemeStore } from '~/store/theme';
export interface Props {
label: string;
t: any;
t: ComposerTranslation;
}
withDefaults(defineProps<Props>(), {

View File

@@ -1,13 +0,0 @@
<script setup lang="ts">
import { useServerStore } from '~/store/server';
const serverStore = useServerStore();
const { updateOsIgnoredReleases } = storeToRefs(serverStore);
</script>
<template>
<UpdateOsIgnoredRelease
v-for="ignoredRelease in updateOsIgnoredReleases"
:key="ignoredRelease"
:label="ignoredRelease"
/>
</template>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import {
ArrowPathIcon,
ArrowTopRightOnSquareIcon,
BellAlertIcon,
CheckCircleIcon,
ExclamationTriangleIcon,
@@ -11,28 +12,32 @@ import { storeToRefs } from 'pinia';
import { WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
import useDateTimeHelper from '~/composables/dateTime';
import { useAccountStore } from '~/store/account';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { ButtonProps } from '~/types/ui/button';
import type { ComposerTranslation } from 'vue-i18n';
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
export interface Props {
downgradeNotAvailable?: boolean;
restoreVersion?: string | undefined;
showUpdateCheck?: boolean;
t: any;
showExternalDowngrade?: boolean;
t: ComposerTranslation;
title?: string;
subtitle?: string;
}
const props = withDefaults(defineProps<Props>(), {
downgradeNotAvailable: false,
restoreVersion: undefined,
showUpdateCheck: false,
showExternalDowngrade: false,
title: undefined,
subtitle: undefined,
});
const accountStore = useAccountStore();
const serverStore = useServerStore();
const updateOsStore = useUpdateOsStore();
const updateOsActionsStore = useUpdateOsActionsStore();
@@ -41,6 +46,8 @@ const { dateTimeFormat, osVersion, rebootType, rebootVersion, regExp, regUpdates
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
const { ineligibleText, rebootTypeText, status } = storeToRefs(updateOsActionsStore);
const updateAvailable = computed(() => available.value || availableWithRenewal.value);
const {
outputDateTimeReadableDiff: readableDiffRegExp,
outputDateTimeFormatted: formattedRegExp,
@@ -52,11 +59,50 @@ const regExpOutput = computed(() => {
}
return {
text: regUpdatesExpired.value
? props.t('Ineligible for updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for updates until {0}', [formattedRegExp.value]),
? props.t('Ineligible for feature updates released after {0}', [formattedRegExp.value])
: props.t('Eligible for free feature updates until {0}', [formattedRegExp.value]),
title: regUpdatesExpired.value
? props.t('Ineligible as of {0}', [readableDiffRegExp.value])
: props.t('Eligible for updates for {0}', [readableDiffRegExp.value]),
: props.t('Eligible for free feature updates for {0}', [readableDiffRegExp.value]),
};
});
const showRebootButton = computed(() => rebootType.value === 'downgrade' || rebootType.value === 'update');
const checkButton = computed((): ButtonProps => {
if (showRebootButton.value || props.showExternalDowngrade) {
return {
btnStyle: 'outline',
click: () => {
props.showExternalDowngrade
? accountStore.downgradeOs()
: accountStore.updateOs();
},
icon: ArrowTopRightOnSquareIcon,
text: props.t('More options'),
};
}
if (!updateAvailable.value) {
return {
btnStyle: 'outline',
click: () => {
updateOsStore.localCheckForUpdate();
},
icon: ArrowPathIcon,
text: props.t('Check for Update'),
};
}
return {
btnStyle: 'fill',
click: () => {
updateOsStore.setModalOpen(true);
},
icon: BellAlertIcon,
text: availableWithRenewal.value
? props.t('Unraid OS {0} Released', [availableWithRenewal.value])
: props.t('Unraid OS {0} Update Available', [available.value]),
};
});
</script>
@@ -117,8 +163,8 @@ const regExpOutput = computed(() => {
<template v-else>
<UiBadge
v-if="rebootType === ''"
:color="available || availableWithRenewal ? 'orange' : 'green'"
:icon="available || availableWithRenewal ? BellAlertIcon : CheckCircleIcon"
:color="updateAvailable ? 'orange' : 'green'"
:icon="updateAvailable ? BellAlertIcon : CheckCircleIcon"
>
{{ (available
? t('Unraid {0} Available', [available])
@@ -145,17 +191,33 @@ const regExpOutput = computed(() => {
</UiBadge>
</div>
<div class="shrink-0">
<UpdateOsCallbackButton
v-if="showUpdateCheck && rebootType === ''"
:t="t"
/>
<BrandButton
v-else-if="rebootType === 'downgrade' || rebootType === 'update'"
:icon="ArrowPathIcon"
:text="rebootType === 'downgrade' ? t('Reboot Now to Downgrade to {0}', [rebootVersion]) : t('Reboot Now to Update to {0}', [rebootVersion])"
@click="updateOsActionsStore.rebootServer()"
/>
<div class="inline-flex flex-col flex-shrink-0 gap-16px flex-grow items-center md:items-end">
<span v-if="showRebootButton">
<BrandButton
btn-style="fill"
:icon="ArrowPathIcon"
:text="rebootType === 'downgrade' ? t('Reboot Now to Downgrade to {0}', [rebootVersion]) : t('Reboot Now to Update to {0}', [rebootVersion])"
@click="updateOsActionsStore.rebootServer()"
/>
</span>
<span>
<BrandButton
:btn-style="checkButton.btnStyle"
:icon="checkButton.icon"
:text="checkButton.text"
@click="checkButton.click"
/>
</span>
<span v-if="rebootType !== ''">
<BrandButton
btn-style="outline"
:icon="XCircleIcon"
:text="t('Cancel {0}', [rebootType === 'downgrade' ? t('Downgrade') : t('Update')])"
@click="updateOsStore.cancelUpdate()"
/>
</span>
</div>
</div>
</div>

View File

@@ -1,11 +1,12 @@
<script lang="ts" setup>
import { ExclamationTriangleIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
defineProps<{
t: any;
t: ComposerTranslation;
}>();
const { rebootTypeText } = storeToRefs(useUpdateOsActionsStore());

View File

@@ -15,6 +15,7 @@ import {
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { ref, watchEffect } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
@@ -26,7 +27,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
t: any;
t: ComposerTranslation;
}>();
const serverStore = useServerStore();
@@ -82,10 +83,10 @@ const flashBackupBasicStatus = ref<'complete' | 'ready' | 'started'>('ready');
const flashBackupText = computed(() => props.t('Create Flash Backup'));
const startFlashBackup = () => {
console.debug('[startFlashBackup]', Date.now());
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
if (typeof flashBackup === 'function') {
flashBackupBasicStatus.value = 'started';
// @ts-ignore global function provided by the webgui on the update page
// @ts-expect-error global function provided by the webgui on the update page
flashBackup();
checkFlashBackupStatus();
} else {

View File

@@ -7,6 +7,7 @@ import {
import dayjs from 'dayjs';
import { storeToRefs } from 'pinia';
import { ref, watchEffect } from 'vue';
import type { ComposerTranslation } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
@@ -18,7 +19,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{
t: any;
t: ComposerTranslation;
}>();
const serverStore = useServerStore();
@@ -94,7 +95,7 @@ watchEffect(() => {
:text="t('Extend License')"
:title="t('Pay your annual fee to continue receiving OS updates.')"
class="flex-grow"
@click="renewAction.click()"
@click="renewAction.click?.()"
/>
<!-- <BrandButton
btn-style="black"

View File

@@ -11,6 +11,7 @@ import {
XMarkIcon,
} from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { WEBGUI_CONNECT_SETTINGS, WEBGUI_TOOLS_REGISTRATION } from '~/helpers/urls';
@@ -23,7 +24,7 @@ import { useUpdateOsActionsStore } from '~/store/updateOsActions';
export interface Props {
open?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -62,6 +63,7 @@ const {
} = storeToRefs(serverStore);
const {
status: updateOsStatus,
callbackTypeDowngrade,
callbackUpdateRelease,
} = storeToRefs(updateOsActionStore);
/**
@@ -78,7 +80,7 @@ const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/Ma
const heading = computed(() => {
if (updateOsStatus.value === 'confirming') {
return props.t('Update Unraid OS confirmation required');
return callbackTypeDowngrade.value ? props.t('Downgrade Unraid OS confirmation required') : props.t('Update Unraid OS confirmation required');
}
switch (callbackStatus.value) {
case 'error':
@@ -88,10 +90,11 @@ const heading = computed(() => {
case 'success':
return props.t('Success!');
}
return '';
});
const subheading = computed(() => {
if (updateOsStatus.value === 'confirming') {
return props.t('Please confirm the update details below');
return callbackTypeDowngrade.value ? props.t('Please confirm the downgrade details below') : props.t('Please confirm the update details below');
}
if (callbackStatus.value === 'error') {
return props.t('Something went wrong'); /** @todo show actual error messages */
@@ -102,7 +105,7 @@ const subheading = computed(() => {
if (keyActionType.value === 'purchase') { return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]); }
if (keyActionType.value === 'replace') { return props.t('Your {0} Key has been replaced!', [keyType.value]); }
if (keyActionType.value === 'trialExtend') { return props.t('Your Trial key has been extended!'); }
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of a Pro Registration key'); }
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of an Unleashed Registration key'); }
if (keyActionType.value === 'upgrade') { return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]); }
return '';
}
@@ -139,10 +142,6 @@ const keyInstallStatusCopy = computed((): { text: string; } => {
let txt2 = props.t('Installed');
let txt3 = props.t('Install');
switch (keyInstallStatus.value) {
case 'ready':
return {
text: props.t('Ready to Install Key'),
};
case 'installing':
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
@@ -165,15 +164,16 @@ const keyInstallStatusCopy = computed((): { text: string; } => {
return {
text: props.t('Failed to {0} {1} Key', [txt3, keyType.value]),
};
case 'ready':
default:
return {
text: props.t('Ready to Install Key'),
};
}
});
const accountActionStatusCopy = computed((): { text: string; } => {
switch (accountActionStatus.value) {
case 'ready':
return {
text: props.t('Ready to update Connect account configuration'),
};
case 'waiting':
return {
text: accountAction.value?.type === 'signIn'
@@ -198,6 +198,11 @@ const accountActionStatusCopy = computed((): { text: string; } => {
? props.t('Sign In Failed')
: props.t('Sign Out Failed'),
};
case 'ready':
default:
return {
text: props.t('Ready to update Connect account configuration'),
};
}
});
@@ -317,7 +322,7 @@ const showUpdateEligibility = computed(() => {
</p>
<p class="text-14px italic opacity-75">
{{ t('This update will require a reboot') }}
{{ callbackTypeDowngrade ? t('This downgrade will require a reboot') : t('This update will require a reboot') }}
</p>
</div>
</div>
@@ -367,7 +372,7 @@ const showUpdateEligibility = computed(() => {
/>
<BrandButton
:icon="CheckIcon"
:text="t('Confirm and start update')"
:text="callbackTypeDowngrade ? t('Confirm and start downgrade') : t('Confirm and start update')"
@click="confirmUpdateOs"
/>
</template>

View File

@@ -1,11 +1,12 @@
<script setup lang="ts">
import { TransitionRoot } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
defineProps<{ t: any; }>();
defineProps<{ t: ComposerTranslation; }>();
const dropdownStore = useDropdownStore();

View File

@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ExclamationTriangleIcon, CheckCircleIcon, UserCircleIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import BrandLoading from '~/components/Brand/Loading.vue';
import { useUnraidApiStore } from '~/store/unraidApi';
import { useServerStore } from '~/store/server';
const props = defineProps<{ t: any; }>();
const props = defineProps<{ t: ComposerTranslation; }>();
const { username } = storeToRefs(useServerStore());

View File

@@ -5,14 +5,18 @@ import {
ArrowTopRightOnSquareIcon,
BellAlertIcon,
CogIcon,
ExclamationTriangleIcon,
KeyIcon,
UserIcon,
} from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import {
CONNECT_DASHBOARD,
WEBGUI_CONNECT_SETTINGS,
WEBGUI_TOOLS_REGISTRATION,
WEBGUI_TOOLS_DOWNGRADE,
WEBGUI_TOOLS_UPDATE,
} from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useErrorsStore } from '~/store/errors';
@@ -20,7 +24,7 @@ import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{ t: any; }>();
const props = defineProps<{ t: ComposerTranslation; }>();
const accountStore = useAccountStore();
const errorsStore = useErrorsStore();
@@ -31,7 +35,6 @@ const {
keyActions,
connectPluginInstalled,
rebootType,
rebootVersion,
registered,
regUpdatesExpired,
stateData,
@@ -50,7 +53,7 @@ const signOutAction = computed(() => stateData.value.actions?.filter((act: { nam
*/
const filteredKeyActions = computed(() => keyActions.value?.filter(action => !['renew'].includes(action.name)));
const manageUnraidNetAccount = computed(() => {
const manageUnraidNetAccount = computed((): UserProfileLink => {
return {
external: true,
click: () => { accountStore.manage(); },
@@ -60,7 +63,7 @@ const manageUnraidNetAccount = computed(() => {
};
});
const updateOsCheckForUpdatesButton = computed(() => {
const updateOsCheckForUpdatesButton = computed((): UserProfileLink => {
return {
click: () => {
updateOsStore.localCheckForUpdate();
@@ -69,7 +72,7 @@ const updateOsCheckForUpdatesButton = computed(() => {
text: props.t('Check for Update'),
};
});
const updateOsResponseModalOpenButton = computed(() => {
const updateOsResponseModalOpenButton = computed((): UserProfileLink => {
return {
click: () => {
updateOsStore.setModalOpen(true);
@@ -81,25 +84,31 @@ const updateOsResponseModalOpenButton = computed(() => {
: props.t('Unraid OS {0} Update Available', [osUpdateAvailable.value]),
};
});
const updateOsToolsUpdatePageButton = computed(() => {
const rebootDetectedButton = computed((): UserProfileLink => {
return {
external: true,
href: WEBGUI_TOOLS_REGISTRATION.toString(),
icon: KeyIcon,
href: rebootType.value === 'downgrade'
? WEBGUI_TOOLS_DOWNGRADE.toString()
: WEBGUI_TOOLS_UPDATE.toString(),
icon: ExclamationTriangleIcon,
text: rebootType.value === 'downgrade'
? props.t('Reboot Now to Downgrade to {0}', [rebootVersion.value])
: props.t('Reboot Now to Update to {0}', [rebootVersion.value]),
? props.t('Reboot Required for Downgrade')
: props.t('Reboot Required for Update'),
};
});
const updateOsButton = computed(() => {
const updateOsButton = computed((): UserProfileLink[] => {
const btns = [];
if (rebootType.value === 'downgrade' || rebootType.value === 'update') {
return updateOsToolsUpdatePageButton.value;
btns.push(rebootDetectedButton.value);
return btns;
}
if (osUpdateAvailable.value) {
return updateOsResponseModalOpenButton.value;
btns.push(updateOsResponseModalOpenButton.value);
} else {
btns.push(updateOsCheckForUpdatesButton.value);
}
return updateOsCheckForUpdatesButton.value;
return btns;
});
const links = computed(():UserProfileLink[] => {
@@ -114,7 +123,7 @@ const links = computed(():UserProfileLink[] => {
: []),
// ensure we only show the update button when we don't have an error
...(!stateDataError.value ? [updateOsButton.value] : []),
...(!stateDataError.value ? [...updateOsButton.value] : []),
// connect plugin links
...(registered.value && connectPluginInstalled.value

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
// eslint-disable vue/no-v-html
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useErrorsStore } from '~/store/errors';
import type { UserProfileLink } from '~/types/userProfile';
defineProps<{ t: any; }>();
defineProps<{ t: ComposerTranslation; }>();
const errorsStore = useErrorsStore();
const { errors } = storeToRefs(errorsStore);
@@ -19,7 +21,7 @@ const { errors } = storeToRefs(errorsStore);
<div class="text-14px px-12px flex flex-col gap-y-8px" :class="{ 'pb-8px': !error.actions }" v-html="t(error.message)" />
<nav v-if="error.actions">
<li v-for="(link, idx) in error.actions" :key="`link_${idx}`">
<UpcDropdownItem :item="link" :rounded="false" :t="t" />
<UpcDropdownItem :item="link as UserProfileLink" :rounded="false" :t="t" />
</li>
</nav>
</li>

View File

@@ -1,12 +1,14 @@
<script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import type { ServerStateDataAction } from '~/types/server';
import type { UserProfileLink } from '~/types/userProfile';
export interface Props {
item: ServerStateDataAction | UserProfileLink;
rounded?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -32,11 +34,11 @@ const showExternalIconOnHover = computed(() => props.item?.external && props.ite
'rounded-md': rounded,
'disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed': item?.disabled,
}"
@click.stop="item?.click ? item?.click(item?.clickParams) : null"
@click.stop="item?.click ? item?.click(item?.clickParams ?? []) : null"
>
<span class="leading-snug inline-flex flex-row items-center gap-x-8px">
<component :is="item?.icon" class="flex-shrink-0 text-current w-16px h-16px" aria-hidden="true" />
{{ t(item?.text, item?.textParams) }}
{{ t(item?.text, item?.textParams ?? []) }}
</span>
<ArrowTopRightOnSquareIcon
v-if="showExternalIconOnHover"

View File

@@ -1,5 +1,7 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import { useUnraidApiStore } from '~/store/unraidApi';
@@ -8,7 +10,7 @@ import '~/assets/main.css';
import BrandLoadingWhite from '~/components/Brand/LoadingWhite.vue';
defineProps<{ t: any; }>();
defineProps<{ t: ComposerTranslation; }>();
const { expireTime, connectPluginInstalled, state, stateData } = storeToRefs(useServerStore());
const { unraidApiStatus, unraidApiRestartAction } = storeToRefs(useUnraidApiStore());
@@ -20,7 +22,10 @@ const showExpireTime = computed(() => (state.value === 'TRIAL' || state.value ==
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-xl p-16px">
<header>
<h2 class="text-24px text-center font-semibold" v-html="t(stateData.heading)" />
<div class="flex flex-col gap-y-8px" v-html="t(stateData.message)" />
<div
class="text-center prose text-16px leading-relaxed whitespace-normal opacity-75 gap-y-8px"
v-html="t(stateData.message)"
/>
<UpcUptimeExpire
v-if="showExpireTime"
class="text-center opacity-75 mt-12px"
@@ -36,7 +41,7 @@ const showExpireTime = computed(() => (state.value === 'TRIAL' || state.value ==
:icon="unraidApiStatus === 'restarting' ? BrandLoadingWhite : unraidApiRestartAction?.icon"
:text="unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')"
:title="unraidApiStatus === 'restarting' ? t('Restarting unraid-api…') : t('Restart unraid-api')"
@click="unraidApiRestartAction?.click()"
@click="unraidApiRestartAction?.click?.()"
/>
</li>
</ul>

View File

@@ -8,13 +8,14 @@ import {
InformationCircleIcon,
ShieldExclamationIcon,
} from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import { useDropdownStore } from '~/store/dropdown';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
import { useUpdateOsStore } from '~/store/updateOs';
const props = defineProps<{ t: any; }>();
const props = defineProps<{ t: ComposerTranslation; }>();
const dropdownStore = useDropdownStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
@@ -24,9 +25,10 @@ const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore());
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
const text = computed((): string | undefined => {
const text = computed((): string => {
if ((stateData.value.error) && state.value !== 'EEXPIRED') { return props.t('Fix Error'); }
if (!registered.value && connectPluginInstalled.value) { return props.t('Sign In'); }
return '';
});
const title = computed((): string => {

View File

@@ -4,6 +4,7 @@
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import type { ComposerTranslation } from 'vue-i18n';
import useInstallPlugin from '~/composables/installPlugin';
import { CONNECT_DOCS } from '~/helpers/urls';
@@ -14,7 +15,7 @@ import '~/assets/main.css';
export interface Props {
open?: boolean;
t: any;
t: ComposerTranslation;
}
withDefaults(defineProps<Props>(), {

View File

@@ -1,10 +1,11 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import type { ServerStateDataAction } from '~/types/server';
defineProps<{ t: any; }>();
defineProps<{ t: ComposerTranslation; }>();
const { state, stateData } = storeToRefs(useServerStore());
@@ -22,7 +23,7 @@ const upgradeAction = computed((): ServerStateDataAction | undefined => {
<UpcServerStateBuy
class="text-gamma"
:title="t('Upgrade Key')"
@click="upgradeAction.click()"
@click="upgradeAction.click?.()"
>
<h5>Unraid OS <em><strong>{{ t(stateData.humanReadable) }}</strong></em></h5>
</UpcServerStateBuy>
@@ -35,7 +36,7 @@ const upgradeAction = computed((): ServerStateDataAction | undefined => {
<UpcServerStateBuy
class="text-orange-dark relative top-[1px] hidden sm:block"
:title="t('Purchase Key')"
@click="purchaseAction.click()"
@click="purchaseAction.click?.()"
>{{ t('Purchase') }}</UpcServerStateBuy>
</template>
</span>

View File

@@ -1,10 +1,12 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import { useTrialStore } from '~/store/trial';
export interface Props {
open?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {
@@ -41,6 +43,7 @@ const trialStatusCopy = computed((): TrialStatusCopy | null => {
subheading: props.t('Please wait while the page reloads to install your trial key'),
};
case 'ready':
default:
return null;
}
});

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import type { ComposerTranslation } from 'vue-i18n';
import useDateTimeHelper from '~/composables/dateTime';
import { useServerStore } from '~/store/server';
@@ -7,7 +8,7 @@ import { useServerStore } from '~/store/server';
export interface Props {
forExpire?: boolean;
shortText?: boolean;
t: any;
t: ComposerTranslation;
}
const props = withDefaults(defineProps<Props>(), {

View File

@@ -18,12 +18,13 @@ const { t } = useI18n();
const { isRemoteAccess } = storeToRefs(useServerStore());
const wanIp = ref<string | null>();
const fetchError = ref<any>();
const fetchError = ref<string>('');
const loading = ref(false);
const computedError = computed(() => {
const computedError = computed((): string => {
if (!props.phpWanIp) { return t('DNS issue, unable to resolve wanip4.unraid.net'); }
if (fetchError.value) { return fetchError.value; }
return '';
});
onBeforeMount(() => {

View File

@@ -1,5 +1,7 @@
import dayjs, { extend } from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import type { ComposerTranslation } from 'vue-i18n';
import type { DateFormatOption, ServerDateTimeFormat, TimeFormatOption } from '~/types/server';
/** @see https://day.js.org/docs/en/display/format#localized-formats */
@@ -33,6 +35,22 @@ const timeFormatOptions: TimeFormatOption[] = [
];
/**
* the provided ref may not have a value until we get a response from the refreshServerState action
* So we need to watch for this value to be able to format it based on the user's date time preferences.
* @example below is how to use this composable
* const formattedRegExp = ref<any>();
* const setFormattedRegExp = () => { // ran in watch on regExp and onBeforeMount
* if (!regExp.value) { return; }
* const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, regExp.value);
* formattedRegExp.value = outputDateTimeFormatted.value;
* };
* watch(regExp, (_newV) => {
* setFormattedRegExp();
* });
* onBeforeMount(() => {
* setFormattedRegExp();
* });
*
* @param format provided by Unraid server's state.php and set in the server store
* @param t translations
* @param hideMinutesSeconds true will hide minutes and seconds from the output
@@ -41,7 +59,7 @@ const timeFormatOptions: TimeFormatOption[] = [
*/
const useDateTimeHelper = (
format: ServerDateTimeFormat | undefined,
t: any,
t: ComposerTranslation,
hideMinutesSeconds?: boolean,
providedDateTime?: number | undefined,
diffCountUp?: boolean,

View File

@@ -1,3 +1,4 @@
/* eslint-disable */
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql';

View File

@@ -17,6 +17,10 @@ const documents = {
"\n mutation SignOut {\n connectSignOut\n }\n": types.SignOutDocument,
"\n fragment PartialCloud on Cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n": types.PartialCloudFragmentDoc,
"\n query serverState {\n cloud {\n ...PartialCloud\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": types.serverStateDocument,
"\n query getExtraAllowedOrigins {\n extraAllowedOrigins\n }\n": types.getExtraAllowedOriginsDocument,
"\n query getRemoteAccess {\n remoteAccess {\n accessType\n forwardType\n port\n }\n }\n": types.getRemoteAccessDocument,
"\n mutation setAdditionalAllowedOrigins($input: AllowedOriginInput!) {\n setAdditionalAllowedOrigins(input: $input)\n }\n": types.setAdditionalAllowedOriginsDocument,
"\n mutation setupRemoteAccess($input: SetupRemoteAccessInput!) {\n setupRemoteAccess(input: $input)\n }\n": types.setupRemoteAccessDocument,
};
/**
@@ -49,6 +53,22 @@ export function graphql(source: "\n fragment PartialCloud on Cloud {\n error
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query serverState {\n cloud {\n ...PartialCloud\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"): (typeof documents)["\n query serverState {\n cloud {\n ...PartialCloud\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n updateExpiration\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query getExtraAllowedOrigins {\n extraAllowedOrigins\n }\n"): (typeof documents)["\n query getExtraAllowedOrigins {\n extraAllowedOrigins\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query getRemoteAccess {\n remoteAccess {\n accessType\n forwardType\n port\n }\n }\n"): (typeof documents)["\n query getRemoteAccess {\n remoteAccess {\n accessType\n forwardType\n port\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation setAdditionalAllowedOrigins($input: AllowedOriginInput!) {\n setAdditionalAllowedOrigins(input: $input)\n }\n"): (typeof documents)["\n mutation setAdditionalAllowedOrigins($input: AllowedOriginInput!) {\n setAdditionalAllowedOrigins(input: $input)\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n mutation setupRemoteAccess($input: SetupRemoteAccessInput!) {\n setupRemoteAccess(input: $input)\n }\n"): (typeof documents)["\n mutation setupRemoteAccess($input: SetupRemoteAccessInput!) {\n setupRemoteAccess(input: $input)\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};

View File

@@ -1,4 +1,4 @@
/* eslint-disable */
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
@@ -242,6 +242,7 @@ export type Config = {
};
export enum ConfigErrorState {
Ineligible = 'INELIGIBLE',
Invalid = 'INVALID',
NoKeyServer = 'NO_KEY_SERVER',
UnknownError = 'UNKNOWN_ERROR',
@@ -877,6 +878,7 @@ export type Query = {
dockerNetwork: DockerNetwork;
/** All Docker networks */
dockerNetworks: Array<Maybe<DockerNetwork>>;
extraAllowedOrigins: Array<Scalars['String']['output']>;
flash?: Maybe<Flash>;
info?: Maybe<Info>;
/** Current user account */
@@ -886,6 +888,7 @@ export type Query = {
owner?: Maybe<Owner>;
parityHistory?: Maybe<Array<Maybe<ParityCheck>>>;
registration?: Maybe<Registration>;
remoteAccess: RemoteAccess;
server?: Maybe<Server>;
servers: Array<Server>;
/** Network Shares */
@@ -993,6 +996,13 @@ export type RelayResponse = {
timeout?: Maybe<Scalars['String']['output']>;
};
export type RemoteAccess = {
__typename?: 'RemoteAccess';
accessType: WAN_ACCESS_TYPE;
forwardType?: Maybe<WAN_FORWARD_TYPE>;
port?: Maybe<Scalars['Port']['output']>;
};
export type Server = {
__typename?: 'Server';
apikey: Scalars['String']['output'];
@@ -1522,7 +1532,35 @@ export type serverStateQuery = { __typename?: 'Query', cloud?: (
& { ' $fragmentRefs'?: { 'PartialCloudFragment': PartialCloudFragment } }
) | null, config: { __typename?: 'Config', error?: ConfigErrorState | null, valid?: boolean | null }, info?: { __typename?: 'Info', os?: { __typename?: 'Os', hostname?: string | null } | null } | null, owner?: { __typename?: 'Owner', avatar?: string | null, username?: string | null } | null, registration?: { __typename?: 'Registration', state?: RegistrationState | null, expiration?: string | null, updateExpiration?: string | null, keyFile?: { __typename?: 'KeyFile', contents?: string | null } | null } | null, vars?: { __typename?: 'Vars', regGen?: string | null, regState?: RegistrationState | null, configError?: ConfigErrorState | null, configValid?: boolean | null } | null };
export type getExtraAllowedOriginsQueryVariables = Exact<{ [key: string]: never; }>;
export type getExtraAllowedOriginsQuery = { __typename?: 'Query', extraAllowedOrigins: Array<string> };
export type getRemoteAccessQueryVariables = Exact<{ [key: string]: never; }>;
export type getRemoteAccessQuery = { __typename?: 'Query', remoteAccess: { __typename?: 'RemoteAccess', accessType: WAN_ACCESS_TYPE, forwardType?: WAN_FORWARD_TYPE | null, port?: number | null } };
export type setAdditionalAllowedOriginsMutationVariables = Exact<{
input: AllowedOriginInput;
}>;
export type setAdditionalAllowedOriginsMutation = { __typename?: 'Mutation', setAdditionalAllowedOrigins: Array<string> };
export type setupRemoteAccessMutationVariables = Exact<{
input: SetupRemoteAccessInput;
}>;
export type setupRemoteAccessMutation = { __typename?: 'Mutation', setupRemoteAccess: boolean };
export const PartialCloudFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<PartialCloudFragment, unknown>;
export const ConnectSignInDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ConnectSignIn"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ConnectSignInInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignIn"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<ConnectSignInMutation, ConnectSignInMutationVariables>;
export const SignOutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SignOut"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"connectSignOut"}}]}}]} as unknown as DocumentNode<SignOutMutation, SignOutMutationVariables>;
export const serverStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<serverStateQuery, serverStateQueryVariables>;
export const serverStateDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"serverState"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PartialCloud"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"valid"}}]}},{"kind":"Field","name":{"kind":"Name","value":"info"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"os"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hostname"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"owner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatar"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}},{"kind":"Field","name":{"kind":"Name","value":"registration"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"expiration"}},{"kind":"Field","name":{"kind":"Name","value":"keyFile"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contents"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updateExpiration"}}]}},{"kind":"Field","name":{"kind":"Name","value":"vars"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"regGen"}},{"kind":"Field","name":{"kind":"Name","value":"regState"}},{"kind":"Field","name":{"kind":"Name","value":"configError"}},{"kind":"Field","name":{"kind":"Name","value":"configValid"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"PartialCloud"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Cloud"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}},{"kind":"Field","name":{"kind":"Name","value":"apiKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"valid"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"cloud"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"minigraphql"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"Field","name":{"kind":"Name","value":"relay"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode<serverStateQuery, serverStateQueryVariables>;
export const getExtraAllowedOriginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getExtraAllowedOrigins"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"extraAllowedOrigins"}}]}}]} as unknown as DocumentNode<getExtraAllowedOriginsQuery, getExtraAllowedOriginsQueryVariables>;
export const getRemoteAccessDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"getRemoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"remoteAccess"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessType"}},{"kind":"Field","name":{"kind":"Name","value":"forwardType"}},{"kind":"Field","name":{"kind":"Name","value":"port"}}]}}]}}]} as unknown as DocumentNode<getRemoteAccessQuery, getRemoteAccessQueryVariables>;
export const setAdditionalAllowedOriginsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"setAdditionalAllowedOrigins"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AllowedOriginInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdditionalAllowedOrigins"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<setAdditionalAllowedOriginsMutation, setAdditionalAllowedOriginsMutationVariables>;
export const setupRemoteAccessDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"setupRemoteAccess"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SetupRemoteAccessInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setupRemoteAccess"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<setupRemoteAccessMutation, setupRemoteAccessMutationVariables>;

View File

@@ -8,12 +8,12 @@ const useInstallPlugin = () => {
const install = (payload: InstallPluginPayload) => {
console.debug('[installPlugin]', payload);
try {
// @ts-ignore `openPlugin` will be included in 6.10.4+ DefaultPageLayout
// @ts-expect-error global function defined in the webgui's DefaultPageLayout.php
if (typeof openPlugin === 'function') {
const plgUrl = new URL(payload.pluginUrl);
const installString = `${plgUrl.pathname.replace('.plg', '').substring(1)}:install`; // mimic what is done on the install plg page JS but without the regex that's hard to read
console.debug('[installPlugin]', { installString, plgUrl });
// @ts-ignore
// @ts-expect-error global function defined in the webgui's DefaultPageLayout.php
openPlugin(
`plugin ${payload.update ? 'update' : 'install'} ${payload.pluginUrl}${payload.update ? '' : ' forced'}`, // command `forced` is used to bypass the strcmp check in the plugin manager script being wrong for OS versions
payload.modalTitle, // title
@@ -23,8 +23,8 @@ const useInstallPlugin = () => {
1, // hide close button
);
} else {
// `openBox()` is defined in the webgui's DefaultPageLayout.php and used when openPlugin is not available
// @ts-ignore
//
// @ts-expect-error openBox() is defined in the webgui's DefaultPageLayout.php and used when openPlugin is not available
openBox(
`/plugins/dynamix.plugin.manager/scripts/plugin&arg1=install&arg2=${payload.pluginUrl}`,
payload.modalTitle,

View File

@@ -18,6 +18,7 @@ export const startTrial = async (payload: StartTrialPayload): Promise<StartTrial
export interface ValidateGuidResponse {
hasNewerKeyfile : boolean;
linked: boolean;
purchaseable: true;
registered: false;
replaceable: false;

View File

@@ -21,16 +21,6 @@ export const WebguiInstallKey = request.url('/webGui/include/InstallKey.php');
* @param {string} username
*/
export const WebguiUpdate = request.url('/update.php');
/**
* @name WebguiUpdateDns
* @dataForm formUrl
* @description Used after Sign In to ensure URLs will work correctly
* @note this request is delayed by 500ms to allow server to process key install fully
* @todo potentially remove delay
* @param csrf_token
* @type POST
*/
export const WebguiUpdateDns = request.url('/webGui/include/UpdateDNS.php');
/**
* @name WebguiState
* @description used to get current state of server via PHP rather than unraid-api
@@ -102,6 +92,10 @@ interface WebguiUnraidCheckPayload {
version?: string;
}
interface WebguiUnraidCheckIgnoreResponse {
updateOsIgnoredReleases: string[];
}
export const WebguiCheckForUpdate = async (): Promise<ServerUpdateOsResponse | unknown> => {
console.debug('[WebguiCheckForUpdate]');
try {
@@ -131,7 +125,7 @@ export const WebguiCheckForUpdate = async (): Promise<ServerUpdateOsResponse | u
}
};
export const WebguiUpdateIgnore = async (payload: WebguiUnraidCheckPayload): Promise<any | void> => {
export const WebguiUpdateIgnore = async (payload: WebguiUnraidCheckPayload): Promise<WebguiUnraidCheckIgnoreResponse> => {
console.debug('[WebguiUpdateIgnore] payload', payload);
try {
const response = await request
@@ -152,3 +146,25 @@ export const WebguiUpdateIgnore = async (payload: WebguiUnraidCheckPayload): Pro
throw new Error('Error ignoring update');
}
};
export interface WebguiUpdateCancelResponse {
message?: string;
success?: boolean;
}
export const WebguiUpdateCancel = async (): Promise<WebguiUpdateCancelResponse> => {
console.debug('[WebguiUpdateCancel]');
try {
const response = await request
.url('/plugins/dynamix.plugin.manager/include/UnraidUpdateCancel.php')
.get()
.json(json => json as WebguiUpdateCancelResponse)
.catch((error) => {
console.error('[WebguiUpdateCancel] catch failed to execute UpdateUpdateCancel', error);
throw new Error('Error attempting to revert OS files to cancel update');
});
return response as WebguiUpdateCancelResponse;
} catch (error) {
console.error('[WebguiUpdateCancel] catch failed to execute UpdateUpdateCancel', error);
throw new Error('Error attempting to revert OS files to cancel update');
}
};

View File

@@ -2,7 +2,6 @@
* @see https://www.telerik.com/blogs/how-to-trap-focus-modal-vue-3
*/
import { customRef } from 'vue';
// eslint-disable-next-line import/named
import { createFocusTrap } from 'focus-trap';
const useFocusTrap = (focusTrapArgs) => {

11
web/eslint.config.mjs Normal file
View File

@@ -0,0 +1,11 @@
// @ts-check
import withNuxt from './.nuxt/eslint.config.mjs'
export default withNuxt(
{
rules: {
'vue/multi-word-component-names': 'off', // turn off to allow web component parents to work and not trigger errors
'vue/no-v-html': 'off',
},
},
)

View File

@@ -1,10 +1,2 @@
/** Output key + value as string for each item in the object. Adds new line after each item. */
export const OBJ_TO_STR = (obj: object): string => Object.entries(obj).reduce((str, [p, val]) => `${str}${p}: ${val}\n`, '');
/** Removes our dev logs from prod builds */
export const disableProductionConsoleLogs = () => {
if (import.meta.env.PROD && !import.meta.env.VITE_ALLOW_CONSOLE_LOGS) {
console.log = () => {};
console.debug = () => {};
console.info = () => {};
}
};

View File

@@ -5,12 +5,13 @@ const UNRAID_NET = new URL(sessionStorage.getItem('unraidPurchaseUrl') ?? import
const ACCOUNT_CALLBACK = new URL('c', ACCOUNT);
const FORUMS_BUG_REPORT = new URL('/bug-reports', FORUMS);
const CONNECT_DOCS = new URL('category/unraid-connect', DOCS);
const CONNECT_DOCS = new URL('/go/connect/', DOCS);
const CONNECT_DASHBOARD = new URL(import.meta.env.VITE_CONNECT ?? 'https://connect.myunraid.net');
const CONNECT_FORUMS = new URL('/forum/94-connect-plugin-support/', FORUMS);
const CONTACT = new URL('/contact', UNRAID_NET);
const DISCORD = new URL('https://discord.gg/unraid');
const DISCORD = new URL('https://discord.unraid.net');
const PURCHASE_CALLBACK = new URL('/c', UNRAID_NET);
const UNRAID_NET_SUPPORT = new URL('/support', UNRAID_NET);
const WEBGUI = new URL(import.meta.env.VITE_WEBGUI ?? window.location.origin);
const WEBGUI_GRAPHQL = new URL('/graphql', WEBGUI);
@@ -22,9 +23,22 @@ const WEBGUI_TOOLS_UPDATE = new URL('/Tools/Update', WEBGUI);
const OS_RELEASES = new URL(import.meta.env.VITE_OS_RELEASES ?? 'https://releases.unraid.net/os');
const DOCS_RELEASE_NOTES = new URL('/unraid-os/release-notes/', DOCS);
const DOCS_REGISTRATION_LICENSING = new URL('/unraid-os/faq/licensing-faq', DOCS);
const DOCS_REGISTRATION_REPLACE_KEY = new URL('/unraid-os/manual/changing-the-flash-device', DOCS);
const DOCS_RELEASE_NOTES = new URL('/go/release-notes/', DOCS);
/**
* @param version - An Unraid OS version string (x.x.x-suffix).
* Suffix indicates special releases, such as RCs or betas.
* @returns A URL object pointing to the release notes for the specified Unraid OS version.
*/
const getReleaseNotesUrl = (version: string): URL => {
const osVersion = version.split('-')[0];
return new URL(`/unraid-os/release-notes/${osVersion}`, DOCS);
}
const DOCS_REGISTRATION_LICENSING = new URL('/go/faq-licensing/', DOCS);
const DOCS_REGISTRATION_REPLACE_KEY = new URL('/go/changing-the-flash-device/', DOCS);
const SUPPORT = new URL('https://unraid.net');
export {
ACCOUNT,
@@ -40,6 +54,7 @@ export {
OS_RELEASES,
DOCS,
DOCS_RELEASE_NOTES,
getReleaseNotesUrl,
DOCS_REGISTRATION_LICENSING,
DOCS_REGISTRATION_REPLACE_KEY,
WEBGUI,
@@ -49,4 +64,6 @@ export {
WEBGUI_TOOLS_DOWNGRADE,
WEBGUI_TOOLS_REGISTRATION,
WEBGUI_TOOLS_UPDATE,
SUPPORT,
UNRAID_NET_SUPPORT,
};

View File

@@ -126,10 +126,10 @@
"Learn More": "",
"No Keyfile": "",
"Let's Unleash your Hardware!": "",
"<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 a Pro 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>": "",
"Trial": "",
"Thank you for choosing Unraid OS!": "",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</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>": "",
"Trial Expired": "",
"Your Trial has expired": "",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "",
@@ -186,7 +186,7 @@
"Starting your free 30 day trial": "",
"Trial Key Created": "",
"Please wait while the page reloads to install your trial key": "",
"A Trial key provides all the functionality of a Pro Registration key": "",
"A Trial key provides all the functionality of an Unleashed Registration key": "",
"Extension Installed": "",
"Recovered": "",
"Replaced": "",
@@ -196,5 +196,5 @@
"Install Extended": "",
"Install Recovered": "",
"Install Replaced": "",
"Your free Trial key provides all the functionality of a Pro Registration key": ""
"Your free Trial key provides all the functionality of an Unleashed Registration key": ""
}

View File

@@ -1,360 +1,372 @@
{
"LAN IP": "LAN IP",
"LAN IP {0}": "LAN IP {0}",
"LAN IP Copied": "LAN IP Copied",
"Click to Copy LAN IP {0}": "Click to Copy LAN IP {0}",
"Trial Key Expired at {0}": "Trial Key Expired at {0}",
"Trial Key Expires at {0}": "Trial Key Expires at {0}",
"Trial Key Expired {0}": "Trial Key Expired {0}",
"Trial Key Expires in {0}": "Trial Key Expires in {0}",
"Server Up Since {0}": "Server Up Since {0}",
"Uptime {0}": "Uptime {0}",
"year": "{n} year | {n} years",
"month": "{n} month | {n} months",
"day": "{n} day | {n} days",
"hour": "{n} hour | {n} hours",
"minute": "{n} minute | {n} minutes",
"second": "{n} second | {n} seconds",
"{0} {1} Key…": "{0} {1} Key…",
"{0} devices": "{0} devices",
"{0} out of {1} allowed devices upgrade your key to support more devices": "{0} out of {1} allowed devices upgrade your key to support more devices",
"{0} out of {1} devices": "{0} out of {1} devices",
"{0} Release Notes": "{0} Release Notes",
"{0} Signed In Successfully": "{0} Signed In Successfully",
"{0} Signed Out Successfully": "{0} Signed Out Successfully",
"{0} Update Available": "{0} Update Available",
"{1} Key {0} Successfully": "{1} Key {0} Successfully",
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>",
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>Please refresh the page to ensure you load your latest configuration</p>",
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Register for Connect by signing in to your Unraid.net account</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>",
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>",
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>There is a physical problem accessing your USB Flash boot device</p>",
"<p>There is a problem with your USB Flash device</p>": "<p>There is a problem with your USB Flash device</p>",
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>": "<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>",
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>",
"<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 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>",
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>",
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"A Trial key provides all the functionality of an Unleashed Registration key": "A Trial key provides all the functionality of an Unleashed Registration key.",
"A valid GUID is required to check for OS updates.": "A valid GUID is required to check for OS updates.",
"A valid keyfile and USB Flash boot device are required to check for OS updates.": "A valid keyfile and USB Flash boot device are required to check for OS updates.",
"A valid keyfile is required to check for OS updates.": "A valid keyfile is required to check for OS updates.",
"A valid OS version is required to check for OS updates.": "A valid OS version is required to check for OS updates.",
"Acklowledge that you have made a Flash Backup to enable this action": "Acklowledge that you have made a Flash Backup to enable this action",
"ago": "ago",
"Purchase": "Purchase",
"Upgrade": "Upgrade",
"Fix Error": "Fix Error",
"Get Started": "Get Started",
"Trial Expired, see options below": "Trial Expired, see options below",
"Learn more about the error": "Learn more about the error",
"Close Dropdown": "Close Dropdown",
"Open Dropdown": "Open Dropdown",
"Thank you for installing Connect!": "Thank you for installing Connect!",
"Sign In to your Unraid.net account to get started": "Sign In to your Unraid.net account to get started",
"Go to Connect": "Go to Connect",
"Opens Connect in new tab": "Opens Connect in new tab",
"Manage Unraid.net Account": "Manage Unraid.net Account",
"Manage Unraid.net Account in new tab": "Manage Unraid.net Account in new tab",
"Settings": "Settings",
"Go to Connect plugin settings": "Go to Connect plugin settings",
"Enhance your Unraid experience with Connect": "Enhance your Unraid experience with Connect",
"Beta": "Beta",
"Loading": "Loading",
"Restarting unraid-api…": "Restarting unraid-api…",
"unraid-api is offline": "unraid-api is offline",
"Introducing Unraid Connect": "Introducing Unraid Connect",
"Enhance your Unraid experience": "Enhance your Unraid experience",
"Connected": "Connected",
"Dynamic Remote Access": "Dynamic Remote Access",
"Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.": "Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.",
"Manage Your Server Within Connect": "Manage Your Server Within Connect",
"Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.": "Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.",
"Deep Linking": "Deep Linking",
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.",
"Online Flash Backup": "Online Flash Backup",
"Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.": "Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.",
"Real-time Monitoring": "Real-time Monitoring",
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "Get an overview of your server's state, storage space, apps and VMs status, and more.",
"Customizable Dashboard Tiles": "Customizable Dashboard Tiles",
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.",
"License Management": "License Management",
"Manage your license keys at any time via the My Keys section.": "Manage your license keys at any time via the My Keys section.",
"Plus more on the way": "Plus more on the way",
"All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.": "All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.",
"Attached Storage Devices": "Attached Storage Devices",
"Backing up...this may take a few minutes": "Backing up...this may take a few minutes",
"Basic": "Basic",
"Begin downgrade to {0}": "Begin downgrade to {0}",
"Beta": "Beta",
"Blacklisted USB Flash GUID": "Blacklisted USB Flash GUID",
"BLACKLISTED": "BLACKLISTED",
"Calculating OS Update Eligibility…": "Calculating OS Update Eligibility…",
"Calculating trial expiration…": "Calculating trial expiration…",
"Callback redirect type not present or incorrect": "Callback redirect type not present or incorrect",
"Cancel {0}": "Cancel {0}",
"Cancel": "Cancel",
"Cannot access your USB Flash boot device": "Cannot access your USB Flash boot device",
"Cannot validate Unraid Trial key": "Cannot validate Unraid Trial key",
"Check Eligibility": "Check Eligibility",
"check for OS updates": "check for OS updates",
"Check for OS Updates": "Check for OS Updates",
"Check for Prereleases": "Check for Prereleases",
"Check for Update": "Check for Update",
"Checking WAN IPs…": "Checking WAN IPs…",
"Checking...": "Checking...",
"Checkout the Connect Documentation": "Checkout the Connect Documentation",
"No thanks": "No thanks",
"Learn more": "Learn more",
"Install Connect": "Install Connect",
"Installing Connect": "Installing Connect",
"Click to close modal": "Click to close modal",
"Click to Copy LAN IP {0}": "Click to Copy LAN IP {0}",
"Close Dropdown": "Close Dropdown",
"Close Modal": "Close Modal",
"Close": "Close",
"Reload": "Reload",
"Reload Page": "Reload Page",
"Unraid logo animating with a wave like effect": "Unraid logo animating with a wave like effect",
"Click to close modal": "Click to close modal",
"Error": "Error",
"Performing actions": "Performing actions",
"Success!": "Success!",
"Something went wrong": "Something went wrong",
"Please keep this window open while we perform some actions": "Please keep this window open while we perform some actions",
"You're one step closer to enhancing your Unraid experience": "You're one step closer to enhancing your Unraid experience",
"Thank you for purchasing an Unraid {0} Key!": "Thank you for purchasing an Unraid {0} Key!",
"Thank you for upgrading to an Unraid {0} Key!": "Thank you for upgrading to an Unraid {0} Key!",
"Your {0} Key has been replaced!": "Your {0} Key has been replaced!",
"Your Trial key has been extended!": "Your Trial key has been extended!",
"Configure Connect Features": "Configure Connect Features",
"Confirm and start update": "Confirm and start update",
"Confirm to Install Unraid OS {0}": "Confirm to Install Unraid OS {0}",
"Connected": "Connected",
"Contact Support": "Contact Support",
"Continue": "Continue",
"Copied": "Copied",
"Copy Key URL": "Copy Key URL",
"Copy your Key URL: {0}": "Copy your Key URL: {0}",
"Then go to Tools > Registration to manually install it": "Then go to Tools > Registration to manually install it",
"Enhance your experience with Unraid Connect": "Enhance your experience with Unraid Connect",
"Sign In to utilize Unraid Connect": "Sign In to utilize Unraid Connect",
"Configure Connect Features": "Configure Connect Features",
"The primary method of support for Unraid Connect is through our forums and Discord.": "The primary method of support for Unraid Connect is through our forums and Discord.",
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.",
"The logs may contain sensitive information so do not post them publicly.": "The logs may contain sensitive information so do not post them publicly.",
"Download unraid-api Logs": "Download unraid-api Logs",
"Unraid Connect Forums": "Unraid Connect Forums",
"Unraid Discord": "Unraid Discord",
"Unraid Contact Page": "Unraid Contact Page",
"Create Flash Backup": "Create Flash Backup",
"Current Version {0}": "Current Version {0}",
"Current Version: Unraid {0}": "Current Version: Unraid {0}",
"Customizable Dashboard Tiles": "Customizable Dashboard Tiles",
"day": "{n} day | {n} days",
"Deep Linking": "Deep Linking",
"DNS issue, unable to resolve wanip4.unraid.net": "DNS issue, unable to resolve wanip4.unraid.net",
"Unable to fetch client WAN IPv4": "Unable to fetch client WAN IPv4",
"Checking WAN IPs…": "Checking WAN IPs…",
"Remark: your WAN IPv4 is {0}": "Remark: your WAN IPv4 is {0}",
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.",
"This may indicate a complex network that will not work with this Remote Access solution.": "This may indicate a complex network that will not work with this Remote Access solution.",
"Ignore this message if you are currently connected via Remote Access or VPN.": "Ignore this message if you are currently connected via Remote Access or VPN.",
"Ready to update Connect account configuration": "Ready to update Connect account configuration",
"Signing in {0}": "Signing in {0}",
"Signing out {0}": "Signing out {0}",
"{0} Signed In Successfully": "{0} Signed In Successfully",
"{0} Signed Out Successfully": "{0} Signed Out Successfully",
"Sign In Failed": "Sign In Failed",
"Sign Out Failed": "Sign Out Failed",
"Failed to update Connect account configuration": "Failed to update Connect account configuration",
"Callback redirect type not present or incorrect": "Callback redirect type not present or incorrect",
"Failed to install key": "Failed to install key",
"Ready to Install Key": "Ready to Install Key",
"Installing Extended Trial": "Installing Extended Trial",
"Installing Recovered": "Installing Recovered",
"Installing Replaced": "Installing Replaced",
"{0} {1} Key…": "{0} {1} Key…",
"{1} Key {0} Successfully": "{1} Key {0} Successfully",
"Failed to {0} {1} Key": "Failed to {0} {1} Key",
"Purchase Key": "Purchase Key",
"Upgrade Key": "Upgrade Key",
"Recover Key": "Recover Key",
"Redeem Activation Code": "Redeem Activation Code",
"Replace Key": "Replace Key",
"Sign In with Unraid.net Account": "Sign In with Unraid.net Account",
"Sign Out of Unraid.net": "Sign Out of Unraid.net",
"Downgrade Unraid OS to {0}": "Downgrade Unraid OS to {0}",
"Downgrade Unraid OS": "Downgrade Unraid OS",
"Downgrades are only recommended if you're unable to solve a critical issue.": "Downgrades are only recommended if you're unable to solve a critical issue.",
"Download Diagnostics": "Download Diagnostics",
"Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.": "Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.",
"Download unraid-api Logs": "Download unraid-api Logs",
"Dynamic Remote Access": "Dynamic Remote Access",
"Eligible for free feature updates for {0}": "Eligible for free feature updates for {0}",
"Eligible for free feature updates until {0}": "Eligible for free feature updates until {0}",
"Eligible": "Eligible",
"Enable update notifications": "Enable update notifications",
"Enhance your experience with Unraid Connect": "Enhance your experience with Unraid Connect",
"Enhance your Unraid experience with Connect": "Enhance your Unraid experience with Connect",
"Enhance your Unraid experience": "Enhance your Unraid experience",
"Error creating a trial key. Please try again later.": "Error creating a trial key. Please try again later.",
"Error Parsing Changelog • {0}": "Error Parsing Changelog • {0}",
"Error": "Error",
"Expired {0}": "Expired {0}",
"Expired": "Expired",
"Expires at {0}": "Expires at {0}",
"Expires in {0}": "Expires in {0}",
"Extend License to Enable OS Updates": "Extend License to Enable OS Updates",
"Extend License to Update": "Extend License to Update",
"Extend License": "Extend License",
"Extend Trial": "Extend Trial",
"Start Free 30 Day Trial": "Start Free 30 Day Trial",
"Go to Management Access Now": "Go to Management Access Now",
"Contact Support": "Contact Support",
"Learn More": "Learn More",
"No Keyfile": "No Keyfile",
"Let's Unleash your Hardware!": "Let's Unleash your Hardware!",
"<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 a Pro 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 a Pro 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>",
"Trial": "Trial",
"Thank you for choosing Unraid OS!": "Thank you for choosing Unraid OS!",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</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 a <em>Pro</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>",
"Trial Expired": "Trial Expired",
"Your Trial has expired": "Your Trial has expired",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</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>",
"Basic": "Basic",
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Register for Connect by signing in to your Unraid.net account</p>",
"<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>",
"Plus": "Plus",
"Pro": "Pro",
"Flash GUID Error": "Flash GUID Error",
"Registration key / USB Flash GUID mismatch": "Registration key / USB Flash GUID mismatch",
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>",
"Multiple License Keys Present": "Multiple License Keys Present",
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>",
"Missing key file": "Missing key file",
"<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 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>",
"Invalid installation": "Invalid installation",
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>",
"No USB flash configuration data": "No USB flash configuration data",
"<p>There is a problem with your USB Flash device</p>": "<p>There is a problem with your USB Flash device</p>",
"No Flash": "No Flash",
"Cannot access your USB Flash boot device": "Cannot access your USB Flash boot device",
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>There is a physical problem accessing your USB Flash boot device</p>",
"BLACKLISTED": "BLACKLISTED",
"Blacklisted USB Flash GUID": "Blacklisted USB Flash GUID",
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>": "<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>",
"USB Flash device error": "USB Flash device error",
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>",
"USB Flash has no serial number": "USB Flash has no serial number",
"Trial Requires Internet Connection": "Trial Requires Internet Connection",
"Cannot validate Unraid Trial key": "Cannot validate Unraid Trial key",
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>",
"Stale": "Stale",
"Stale Server": "Stale Server",
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>Please refresh the page to ensure you load your latest configuration</p>",
"Invalid API Key": "Invalid API Key",
"Please sign out then sign back in to refresh your API key.": "Please sign out then sign back in to refresh your API key.",
"Invalid API Key Format": "Invalid API Key Format",
"Too Many Devices": "Too Many Devices",
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.",
"Unraid Connect Install Failed": "Unraid Connect Install Failed",
"Rebooting will likely solve this.": "Rebooting will likely solve this.",
"SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated",
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.",
"Unraid Connect Error": "Unraid Connect Error",
"Trial Key Creation Failed": "Trial Key Creation Failed",
"Error creatiing a trial key. Please try again later.": "Error creatiing a trial key. Please try again later.",
"Extending your free trial by 15 days": "Extending your free trial by 15 days",
"Please keep this window open": "Please keep this window open",
"Starting your free 30 day trial": "Starting your free 30 day trial",
"Trial Key Created": "Trial Key Created",
"Please wait while the page reloads to install your trial key": "Please wait while the page reloads to install your trial key",
"A Trial key provides all the functionality of a Pro Registration key": "A Trial key provides all the functionality of a Pro Registration key.",
"Extension Installed": "Extension Installed",
"Recovered": "Recovered",
"Replaced": "Replaced",
"Installing": "Installing",
"Installed": "Installed",
"Install": "Install",
"Failed to {0} {1} Key": "Failed to {0} {1} Key",
"Failed to install key": "Failed to install key",
"Failed to update Connect account configuration": "Failed to update Connect account configuration",
"Fetching & parsing changelog…": "Fetching & parsing changelog…",
"Fix Error": "Fix Error",
"Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.": "Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.",
"Flash GUID Error": "Flash GUID Error",
"Flash GUID required to check replacement status": "Flash GUID required to check replacement status",
"Flash GUID": "Flash GUID",
"Flash Product": "Flash Product",
"Flash Vendor": "Flash Vendor",
"Get a Lifetime Key" : "Get a Lifetime Key",
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "Get an overview of your server's state, storage space, apps and VMs status, and more.",
"Get Started": "Get Started",
"Go to Connect plugin settings": "Go to Connect plugin settings",
"Go to Connect": "Go to Connect",
"Go to Management Access Now": "Go to Management Access Now",
"Go to Settings > Notifications to enable automatic OS update notifications for future releases.": "Go to Settings > Notifications to enable automatic OS update notifications for future releases.",
"Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.": "Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.",
"Go to Tools > Management Access to ensure your backup is up-to-date.": "Go to Tools > Management Access to ensure your backup is up-to-date.",
"Go to Tools > Registration to fix": "Go to Tools > Registration to fix",
"Go to Tools > Registration to Learn More": "Go to Tools > Registration to Learn More",
"Go to Tools > Update OS for more options.": "Go to Tools > Update OS for more options.",
"Go to Tools > Update": "Go to Tools > Update",
"hour": "{n} hour | {n} hours",
"I have made a Flash Backup": "I have made a Flash Backup",
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.",
"Ignore this message if you are currently connected via Remote Access or VPN.": "Ignore this message if you are currently connected via Remote Access or VPN.",
"Ignore this release until next reboot": "Ignore this release until next reboot",
"Ignored Releases": "Ignored Releases",
"In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.": "In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.",
"Ineligible as of {0}": "Ineligible as of {0}",
"Ineligible for feature updates released after {0}": "Ineligible for feature updates released after {0}",
"Ineligible for Unraid OS updates": "Ineligible for Unraid OS updates",
"Ineligible": "Ineligible",
"Ineligible for self-replacement": "Ineligible for self-replacement",
"Install Connect": "Install Connect",
"Install Extended": "Install Extended",
"Install Recovered": "Install Recovered",
"Install Replaced": "Install Replaced",
"Your free Trial key provides all the functionality of a Pro Registration key": "Your free Trial key provides all the functionality of a Pro Registration key",
"Calculating trial expiration…": "Calculating trial expiration…",
"Signing In": "Signing In",
"Signing Out": "Signing Out",
"Sign In requires the local unraid-api to be running": "Sign In requires the local unraid-api to be running",
"Sign Out requires the local unraid-api to be running": "Sign Out requires the local unraid-api to be running",
"Unraid OS {0} Released": "Unraid OS {0} Released",
"Unraid {0} Update Available": "Unraid {0} Update Available",
"{0} Update Available": "{0} Update Available",
"Unraid OS Update Available": "Unraid OS Update Available",
"Update Unraid OS confirmation required": "Update Unraid OS confirmation required",
"Please confirm the update details below": "Please confirm the update details below",
"Current Version {0}": "Current Version {0}",
"Current Version: Unraid {0}": "Current Version: Unraid {0}",
"New Version: {0}": "New Version: {0}",
"Version: {0}": "Version: {0}",
"This update will require a reboot": "This update will require a reboot",
"Confirm and start update": "Confirm and start update",
"Update Unraid OS": "Update Unraid OS",
"Install Unraid OS {0}": "Install Unraid OS {0}",
"Install": "Install",
"Installed": "Installed",
"Installing Connect": "Installing Connect",
"Installing Extended Trial": "Installing Extended Trial",
"Installing Extended": "Installing Extended",
"Installing Recovered": "Installing Recovered",
"Installing Replaced": "Installing Replaced",
"Installing": "Installing",
"Introducing Unraid Connect": "Introducing Unraid Connect",
"Invalid API Key Format": "Invalid API Key Format",
"Invalid API Key": "Invalid API Key",
"Invalid installation": "Invalid installation",
"It's highly recommended to review the changelog before continuing your update": "It's highly recommended to review the changelog before continuing your update",
"Key ineligible for {0}": "Key ineligible for {0}",
"Key ineligible for future releases": "Key ineligible for future releases",
"Key ineligible for new updates": "Key ineligible for new updates",
"Keyfile required to check replacement status": "Keyfile required to check replacement status",
"LAN IP {0}": "LAN IP {0}",
"LAN IP Copied": "LAN IP Copied",
"LAN IP": "LAN IP",
"Last checked: {0}": "Last checked: {0}",
"Downgrade Unraid OS": "Downgrade Unraid OS",
"Downgrade Unraid OS to {0}": "Downgrade Unraid OS to {0}",
"No downgrade available": "No downgrade available",
"Begin downgrade to {0}": "Begin downgrade to {0}",
"Version available for restore {0}": "Version available for restore {0}",
"check for OS updates": "check for OS updates",
"Check for Prereleases": "Check for Prereleases",
"Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes keeping your server up-to-date ensures the best experience that Unraid has to offer.": "Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes keeping your server up-to-date ensures the best experience that Unraid has to offer.",
"Check for OS Updates": "Check for OS Updates",
"Checking...": "Checking...",
"View release notes": "View release notes",
"View Changelog for {0}": "View Changelog for {0}",
"View Changelog & Update": "View Changelog & Update",
"{0} Release Notes": "{0} Release Notes",
"Unable to open release notes": "Unable to open release notes",
"Downgrades are only recommended if you're unable to solve a critical issue.": "Downgrades are only recommended if you're unable to solve a critical issue.",
"In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.": "In the rare event you need to downgrade we ask that you please provide us with Diagnostics so we can investigate your issue.",
"Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.": "Download the Diagnostics zip then please open a bug report on our forums with a description of the issue along with your diagnostics.",
"Reboot Now to Downgrade": "Reboot Now to Downgrade",
"Reboot Now to Update": "Reboot Now to Update",
"Reboot Now to Downgrade to {0}": "Reboot Now to Downgrade to {0}",
"Reboot Now to Update to {0}": "Reboot Now to Update to {0}",
"Reboot Required for Downgrade": "Reboot Required for Downgrade",
"Reboot Required for Update": "Reboot Required for Update",
"Reboot Required for Downgrade to {0}": "Reboot Required for Downgrade to {0}",
"Reboot Required for Update to {0}": "Reboot Required for Update to {0}",
"Updating 3rd party drivers": "Updating 3rd party drivers",
"Update Available": "Update Available",
"Up-to-date": "Up-to-date",
"Open a bug report": "Open a bug report",
"Go to Tools > Update": "Go to Tools > Update",
"A valid keyfile and USB Flash boot device are required to check for OS updates.": "A valid keyfile and USB Flash boot device are required to check for OS updates.",
"Please fix any errors and try again.": "Please fix any errors and try again.",
"Go to Tools > Registration to fix": "Go to Tools > Registration to fix",
"Original release date {0}": "Original release date {0}",
"Registered to": "Registered to",
"Registered on": "Registered on",
"Updates Expire": "Updates Expire",
"Flash GUID": "Flash GUID",
"Flash Vendor": "Flash Vendor",
"Flash Product": "Flash Product",
"Attached Storage Devices": "Attached Storage Devices",
"{0} out of {1} devices": "{0} out of {1} devices",
"{0} out of {1} allowed devices upgrade your key to support more devices": "{0} out of {1} allowed devices upgrade your key to support more devices",
"{0} devices": "{0} devices",
"unlimited": "unlimited",
"Unable to check for OS updates": "Unable to check for OS updates",
"Learn more about the error": "Learn more about the error",
"Learn more and fix": "Learn more and fix",
"Learn more and link your key to your account": "Learn more and link your key to your account",
"Learn more": "Learn more",
"Learn More": "Learn More",
"Let's Unleash your Hardware!": "Let's Unleash your Hardware!",
"License key actions": "License key actions",
"License key type": "License key type",
"OS Update Eligibility Expiration": "OS Update Eligibility Expiration",
"Ineligible for updates released after {0}": "Ineligible for updates released after {0}",
"Eligible for updates until {0}": "Eligible for updates until {0}",
"Ineligible as of {0}": "Ineligible as of {0}",
"Eligible for updates for {0}": "Eligible for updates for {0}",
"Renew your license key now": "Renew your license key now",
"Extend License to Enable OS Updates": "Extend License to Enable OS Updates",
"Check Eligibility": "Check Eligibility",
"Eligible": "Eligible",
"Ineligible": "Ineligible",
"Flash GUID required to check replacement status": "Flash GUID required to check replacement status",
"Keyfile required to check replacement status": "Keyfile required to check replacement status",
"Unraid {0}": "Unraid {0}",
"OS Update Eligibility": "OS Update Eligibility",
"Transfer License to New Flash": "Transfer License to New Flash",
"Starter": "Starter",
"Unleashed": "Unleashed",
"License Management": "License Management",
"Lifetime": "Lifetime",
"Link Key": "Link Key",
"Linked": "Linked",
"Linked to Unraid.net account": "Linked to Unraid.net account",
"Loading": "Loading",
"Manage Unraid.net Account in new tab": "Manage Unraid.net Account in new tab",
"Manage Unraid.net Account": "Manage Unraid.net Account",
"Manage your license keys at any time via the My Keys section.": "Manage your license keys at any time via the My Keys section.",
"Manage Your Server Within Connect": "Manage Your Server Within Connect",
"minute": "{n} minute | {n} minutes",
"Missing key file": "Missing key file",
"month": "{n} month | {n} months",
"More options": "More options",
"Multiple License Keys Present": "Multiple License Keys Present",
"Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.": "Never ever be left without a backup of your config. If you need to change flash drives, generate a backup from Connect and be up and running in minutes.",
"New Version: {0}": "New Version: {0}",
"No downgrade available": "No downgrade available",
"No Flash": "No Flash",
"No Keyfile": "No Keyfile",
"No thanks": "No thanks",
"No USB flash configuration data": "No USB flash configuration data",
"Not Linked": "Not Linked",
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.",
"Online Flash Backup": "Online Flash Backup",
"Open a bug report": "Open a bug report",
"Open Dropdown": "Open Dropdown",
"Opens Connect in new tab": "Opens Connect in new tab",
"Original release date {0}": "Original release date {0}",
"OS Update Eligibility Expiration": "OS Update Eligibility Expiration",
"OS Update Eligibility Expired": "OS Update Eligibility Expired",
"OS Update Eligibility": "OS Update Eligibility",
"Pay your annual fee to continue receiving OS updates.": "Pay your annual fee to continue receiving OS updates.",
"Renew Key": "Renew Key",
"A valid GUID is required to check for OS updates.": "A valid GUID is required to check for OS updates.",
"A valid keyfile is required to check for OS updates.": "A valid keyfile is required to check for OS updates.",
"A valid OS version is required to check for OS updates.": "A valid OS version is required to check for OS updates.",
"Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.": "Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.",
"Key ineligible for new updates": "Key ineligible for new updates",
"Ineligible for Unraid OS updates": "Ineligible for Unraid OS updates",
"Learn more and fix": "Learn more and fix",
"Expires at {0}": "Expires at {0}",
"Expires in {0}": "Expires in {0}",
"Expired": "Expired",
"Expired {0}": "Expired {0}",
"Create Flash Backup": "Create Flash Backup",
"Get a Lifetime Key" : "Get a Lifetime Key",
"We recommend backing up your USB Flash Boot Device before starting the update.": "We recommend backing up your USB Flash Boot Device before starting the update.",
"You have already activated the Flash Backup feature via the Unraid Connect plugin.": "You have already activated the Flash Backup feature via the Unraid Connect plugin.",
"Go to Tools > Management Access to ensure your backup is up-to-date.": "Go to Tools > Management Access to ensure your backup is up-to-date.",
"You have not activated the Flash Backup feature via the Unraid Connect plugin.": "You have not activated the Flash Backup feature via the Unraid Connect plugin.",
"Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.": "Go to Tools > Management Access to activate the Flash Backup feature and ensure your backup is up-to-date.",
"Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.": "Flash Backup is not available. Navigate to {0}/Main/Settings/Flash to try again then come back to this page.",
"Backing up...this may take a few minutes": "Backing up...this may take a few minutes",
"Acklowledge that you have made a Flash Backup to enable this action": "Acklowledge that you have made a Flash Backup to enable this action",
"You can also manually create a new backup by clicking the Create Flash Backup button.": "You can also manually create a new backup by clicking the Create Flash Backup button.",
"You can manually create a backup by clicking the Create Flash Backup button.": "You can manually create a backup by clicking the Create Flash Backup button.",
"I have made a Flash Backup": "I have made a Flash Backup",
"You may still update to releases dated prior to your update expiration date.": "You may still update to releases dated prior to your update expiration date.",
"View Available Updates": "View Available Updates",
"Your license key is not eligible for Unraid OS {0}": "Your license key is not eligible for Unraid OS {0}",
"Unraid {0} Available": "Unraid {0} Available",
"Key ineligible for {0}": "Key ineligible for {0}",
"Up-to-date with eligible releases": "Up-to-date with eligible releases",
"Key ineligible for future releases": "Key ineligible for future releases",
"View Changelog": "View Changelog",
"You are still eligible to access OS updates that were published on or before {1}.": "You are still eligible to access OS updates that were published on or before {1}.",
"Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.",
"Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.",
"Extend License": "Extend License",
"Calculating OS Update Eligibility…": "Calculating OS Update Eligibility…",
"Cancel": "Cancel",
"Unknown error": "Unknown error",
"Performing actions": "Performing actions",
"Please confirm the update details below": "Please confirm the update details below",
"Please finish the initiated downgrade to enable updates.": "Please finish the initiated downgrade to enable updates.",
"Please finish the initiated update to enable a downgrade.": "Please finish the initiated update to enable a downgrade.",
"Download Diagnostics": "Download Diagnostics",
"Requires the local unraid-api to be running successfully": "Requires the local unraid-api to be running successfully",
"Sign In": "Sign In",
"OS Update Eligibility Expired": "OS Update Eligibility Expired",
"Go to Tools > Registration to Learn More": "Go to Tools > Registration to Learn More",
"Installing Extended": "Installing Extended",
"Release requires verification to update": "Release requires verification to update",
"Error Parsing Changelog • {0}": "Error Parsing Changelog • {0}",
"It's highly recommended to review the changelog before continuing your update": "It's highly recommended to review the changelog before continuing your update",
"View Changelog on Docs": "View Changelog on Docs",
"Fetching & parsing changelog…": "Fetching & parsing changelog…",
"View on Docs": "View on Docs",
"Extend License to Update": "Extend License to Update",
"Install Unraid OS {0}": "Install Unraid OS {0}",
"View Changelog to Start Update": "View Changelog to Start Update",
"Unraid OS {0} Update Available": "Unraid OS {0} Update Available",
"Remove": "Remove",
"Remove from ignore list": "Remove from ignore list",
"Ignored Releases": "Ignored Releases",
"Ignore this release until next reboot": "Ignore this release until next reboot",
"Confirm to Install Unraid OS {0}": "Confirm to Install Unraid OS {0}",
"Continue": "Continue",
"Verify to Update": "Verify to Update",
"Please fix any errors and try again.": "Please fix any errors and try again.",
"Please keep this window open while we perform some actions": "Please keep this window open while we perform some actions",
"Please keep this window open": "Please keep this window open",
"Please sign out then sign back in to refresh your API key.": "Please sign out then sign back in to refresh your API key.",
"Please wait while the page reloads to install your trial key": "Please wait while the page reloads to install your trial key",
"Plus more on the way": "Plus more on the way",
"Plus": "Plus",
"Pro": "Pro",
"Purchase Key": "Purchase Key",
"Purchase": "Purchase",
"Ready to Install Key": "Ready to Install Key",
"Ready to update Connect account configuration": "Ready to update Connect account configuration",
"Real-time Monitoring": "Real-time Monitoring",
"Reboot Now to Downgrade to {0}": "Reboot Now to Downgrade to {0}",
"Reboot Now to Downgrade": "Reboot Now to Downgrade",
"Reboot Now to Update to {0}": "Reboot Now to Update to {0}",
"Reboot Now to Update": "Reboot Now to Update",
"Reboot Required for Downgrade to {0}": "Reboot Required for Downgrade to {0}",
"Reboot Required for Downgrade": "Reboot Required for Downgrade",
"Reboot Required for Update to {0}": "Reboot Required for Update to {0}",
"Reboot Required for Update": "Reboot Required for Update",
"Rebooting will likely solve this.": "Rebooting will likely solve this.",
"Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes keeping your server up-to-date ensures the best experience that Unraid has to offer.": "Receive the latest and greatest for Unraid OS. Whether it new features, security patches, or bug fixes keeping your server up-to-date ensures the best experience that Unraid has to offer.",
"Recover Key": "Recover Key",
"Recovered": "Recovered",
"Redeem Activation Code": "Redeem Activation Code",
"Refresh": "Refresh",
"Registered on": "Registered on",
"Registered to": "Registered to",
"Registration key / USB Flash GUID mismatch": "Registration key / USB Flash GUID mismatch",
"Release date {0}": "Release date {0}",
"Release requires verification to update": "Release requires verification to update",
"Reload Page": "Reload Page",
"Reload": "Reload",
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.",
"Remark: your WAN IPv4 is {0}": "Remark: your WAN IPv4 is {0}",
"Remove from ignore list": "Remove from ignore list",
"Remove": "Remove",
"Renew Key": "Renew Key",
"Renew your license key now": "Renew your license key now",
"Replace Key": "Replace Key",
"Replaced": "Replaced",
"Requires the local unraid-api to be running successfully": "Requires the local unraid-api to be running successfully",
"Restarting unraid-api…": "Restarting unraid-api…",
"second": "{n} second | {n} seconds",
"Server Up Since {0}": "Server Up Since {0}",
"Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.": "Servers equipped with a myunraid.net certificate can be managed directly from within the Connect web UI. Manage multiple servers from your phone, tablet, laptop, or PC in the same browser window.",
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.",
"Settings": "Settings",
"Sign In Failed": "Sign In Failed",
"Sign In requires the local unraid-api to be running": "Sign In requires the local unraid-api to be running",
"Sign In to utilize Unraid Connect": "Sign In to utilize Unraid Connect",
"Sign In to your Unraid.net account to get started": "Sign In to your Unraid.net account to get started",
"Sign In with Unraid.net Account": "Sign In with Unraid.net Account",
"Sign In": "Sign In",
"Sign Out Failed": "Sign Out Failed",
"Sign Out of Unraid.net": "Sign Out of Unraid.net",
"Sign Out requires the local unraid-api to be running": "Sign Out requires the local unraid-api to be running",
"Signing in {0}…": "Signing in {0}…",
"Signing In": "Signing In",
"Signing out {0}…": "Signing out {0}…",
"Signing Out": "Signing Out",
"Something went wrong": "Something went wrong",
"SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated",
"Stale Server": "Stale Server",
"Stale": "Stale",
"Start Free 30 Day Trial": "Start Free 30 Day Trial",
"Starter": "Starter",
"Starting your free 30 day trial": "Starting your free 30 day trial",
"Success!": "Success!",
"Thank you for choosing Unraid OS!": "Thank you for choosing Unraid OS!",
"Thank you for installing Connect!": "Thank you for installing Connect!",
"Thank you for purchasing an Unraid {0} Key!": "Thank you for purchasing an Unraid {0} Key!",
"Thank you for upgrading to an Unraid {0} Key!": "Thank you for upgrading to an Unraid {0} Key!",
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.",
"The logs may contain sensitive information so do not post them publicly.": "The logs may contain sensitive information so do not post them publicly.",
"The primary method of support for Unraid Connect is through our forums and Discord.": "The primary method of support for Unraid Connect is through our forums and Discord.",
"Then go to Tools > Registration to manually install it": "Then go to Tools > Registration to manually install it",
"This may indicate a complex network that will not work with this Remote Access solution.": "This may indicate a complex network that will not work with this Remote Access solution.",
"This update will require a reboot": "This update will require a reboot",
"Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.": "Toggle on/off server accessibility with dynamic remote access. Automatically turn on UPnP and open a random WAN port on your router at the click of a button and close off access in seconds.",
"Too Many Devices": "Too Many Devices",
"Transfer License to New Flash": "Transfer License to New Flash",
"Trial Expired, see options below": "Trial Expired, see options below",
"Trial Expired": "Trial Expired",
"Trial Key Created": "Trial Key Created",
"Trial Key Creation Failed": "Trial Key Creation Failed",
"Trial Key Expired {0}": "Trial Key Expired {0}",
"Trial Key Expired at {0}": "Trial Key Expired at {0}",
"Trial Key Expires at {0}": "Trial Key Expires at {0}",
"Trial Key Expires in {0}": "Trial Key Expires in {0}",
"Trial Requires Internet Connection": "Trial Requires Internet Connection",
"Trial": "Trial",
"Unable to check for OS updates": "Unable to check for OS updates",
"Unable to fetch client WAN IPv4": "Unable to fetch client WAN IPv4",
"Unable to open release notes": "Unable to open release notes",
"Unknown error": "Unknown error",
"Unknown": "Unknown",
"Unleashed": "Unleashed",
"unlimited": "unlimited",
"Unraid {0} Available": "Unraid {0} Available",
"Unraid {0} Update Available": "Unraid {0} Update Available",
"Unraid {0}": "Unraid {0}",
"Unraid Connect Error": "Unraid Connect Error",
"Unraid Connect Forums": "Unraid Connect Forums",
"Unraid Connect Install Failed": "Unraid Connect Install Failed",
"Unraid Contact Page": "Unraid Contact Page",
"Unraid Discord": "Unraid Discord",
"Unraid logo animating with a wave like effect": "Unraid logo animating with a wave like effect",
"Unraid OS {0} Released": "Unraid OS {0} Released",
"Unraid OS {0} Update Available": "Unraid OS {0} Update Available",
"Unraid OS is up-to-date": "Unraid OS is up-to-date",
"Unraid OS Update Available": "Unraid OS Update Available",
"unraid-api is offline": "unraid-api is offline",
"Up-to-date with eligible releases": "Up-to-date with eligible releases",
"Up-to-date": "Up-to-date",
"Update Available": "Update Available",
"Update Released": "Update Released",
"Go to Tools > Update OS for more options.": "Go to Tools > Update OS for more options.",
"Go to Settings > Notifications to enable automatic OS update notifications for future releases.": "Go to Settings > Notifications to enable automatic OS update notifications for future releases.",
"More options": "More options"
"Update Unraid OS confirmation required": "Update Unraid OS confirmation required",
"Update Unraid OS": "Update Unraid OS",
"Updates Expire": "Updates Expire",
"Updating 3rd party drivers": "Updating 3rd party drivers",
"Upgrade Key": "Upgrade Key",
"Upgrade": "Upgrade",
"Uptime {0}": "Uptime {0}",
"USB Flash device error": "USB Flash device error",
"USB Flash has no serial number": "USB Flash has no serial number",
"Verify to Update": "Verify to Update",
"Version available for restore {0}": "Version available for restore {0}",
"Version: {0}": "Version: {0}",
"View Available Updates": "View Available Updates",
"View Changelog & Update": "View Changelog & Update",
"View Changelog for {0}": "View Changelog for {0}",
"View Changelog on Docs": "View Changelog on Docs",
"View Changelog to Start Update": "View Changelog to Start Update",
"View Changelog": "View Changelog",
"View on Docs": "View on Docs",
"View release notes": "View release notes",
"We recommend backing up your USB Flash Boot Device before starting the update.": "We recommend backing up your USB Flash Boot Device before starting the update.",
"year": "{n} year | {n} years",
"You are still eligible to access OS updates that were published on or before {1}.": "You are still eligible to access OS updates that were published on or before {1}.",
"You can also manually create a new backup by clicking the Create Flash Backup button.": "You can also manually create a new backup by clicking the Create Flash Backup button.",
"You can manually create a backup by clicking the Create Flash Backup button.": "You can manually create a backup by clicking the Create Flash Backup button.",
"You have already activated the Flash Backup feature via the Unraid Connect plugin.": "You have already activated the Flash Backup feature via the Unraid Connect plugin.",
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.",
"You have not activated the Flash Backup feature via the Unraid Connect plugin.": "You have not activated the Flash Backup feature via the Unraid Connect plugin.",
"You may still update to releases dated prior to your update expiration date.": "You may still update to releases dated prior to your update expiration date.",
"You're one step closer to enhancing your Unraid experience": "You're one step closer to enhancing your Unraid experience",
"Your {0} Key has been replaced!": "Your {0} Key has been replaced!",
"Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates. You are still eligible to access OS updates that were published on or before {1}.",
"Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.": "Your {0} license included one year of free updates at the time of purchase. You are now eligible to extend your license and access the latest OS updates.",
"Your free Trial key provides all the functionality of an Unleashed Registration key": "Your free Trial key provides all the functionality of an Unleashed Registration key",
"Your license key is not eligible for Unraid OS {0}": "Your license key is not eligible for Unraid OS {0}",
"Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.": "Your license key's OS update eligibility has expired. Please renew your license key to enable updates released after your expiration date.",
"Your Trial has expired": "Your Trial has expired",
"Your Trial key has been extended!": "Your Trial key has been extended!"
}

View File

@@ -126,10 +126,10 @@
"Learn More": "もっと詳しく知る",
"No Keyfile": "キーファイルがありません",
"Let's Unleash your Hardware!": "ハードウェアを解き放ちましょう!",
"<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 a Pro 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>登録キーを購入するか、30 日間の無料の<em>トライアル</em>キーをインストールするまで、サーバーは使用できません。 \n<em>トライアル</em> キーは、Pro 登録キーのすべての機能を提供します。</p><p>登録キーは、USB フラッシュ ブート デバイスのシリアル番号 (GUID) にバインドされています。\nサイズが少なくとも 1 GB の高品質の有名ブランドのデバイスを使用してください。</p><p>注: USB メモリ カード リーダーのほとんどは固有のシリアル番号を提示していないため、通常はサポートされていません。</p><p><strong>\n重要:</strong></p><ul class='list-disc pl-16px'><li>サーバー時間が 5 分以内であることを確認してください。</li><li>\n指定された DNS サーバー</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>": "<p>登録キーを購入するか、30 日間の無料の<em>トライアル</em>キーをインストールするまで、サーバーは使用できません。 \n<em>トライアル</em> キーは、Pro 登録キーのすべての機能を提供します。</p><p>登録キーは、USB フラッシュ ブート デバイスのシリアル番号 (GUID) にバインドされています。\nサイズが少なくとも 1 GB の高品質の有名ブランドのデバイスを使用してください。</p><p>注: USB メモリ カード リーダーのほとんどは固有のシリアル番号を提示していないため、通常はサポートされていません。</p><p><strong>\n重要:</strong></p><ul class='list-disc pl-16px'><li>サーバー時間が 5 分以内であることを確認してください。</li><li>\n指定された DNS サーバー</li></ul>",
"Trial": "トライアル",
"Thank you for choosing Unraid OS!": "Unraid OS をお選びいただきありがとうございます。",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</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><em>トライアル</em> キーには、<em>プロ</em> キーのすべての機能とデバイス サポートが含まれています。</p><p><em>トライアル</em> の終了後\n有効期限に達しても、次回アレイを停止するかサーバーを再起動するまで、 サーバーは<strong>通常どおり機能</strong>します。</p><p>その時点で、ライセンス キーを購入するか、<em>ライセンス キーをリクエストすることができます。 \n> トライアル</em>拡張機能。</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><em>トライアル</em> キーには、<em>プロ</em> キーのすべての機能とデバイス サポートが含まれています。</p><p><em>トライアル</em> の終了後\n有効期限に達しても、次回アレイを停止するかサーバーを再起動するまで、 サーバーは<strong>通常どおり機能</strong>します。</p><p>その時点で、ライセンス キーを購入するか、<em>ライセンス キーをリクエストすることができます。 \n> トライアル</em>拡張機能。</p>",
"Trial Expired": "トライアル期間が終了しました",
"Your Trial has expired": "トライアル版の有効期限が切れました",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>Unraid OS を引き続き使用するには、ライセンス キーを購入することができます。\nあるいは、トライアルの延長をリクエストすることもできます。</p>",
@@ -186,7 +186,7 @@
"Starting your free 30 day trial": "30 日間の無料トライアルを開始する",
"Trial Key Created": "トライアルキーが作成されました",
"Please wait while the page reloads to install your trial key": "試用版キーをインストールするには、ページがリロードされるまでお待ちください。",
"A Trial key provides all the functionality of a Pro Registration key": "トライアル キーは、Pro 登録キーのすべての機能を提供します。",
"A Trial key provides all the functionality of an Unleashed Registration key": "トライアル キーは、Pro 登録キーのすべての機能を提供します。",
"Extension Installed": "拡張機能がインストールされました",
"Recovered": "回復しました",
"Replaced": "交換されました",
@@ -196,5 +196,5 @@
"Install Extended": "拡張インストール",
"Install Recovered": "インストールが回復しました",
"Install Replaced": "インストールと置き換え",
"Your free Trial key provides all the functionality of a Pro Registration key": "無料のトライアル キーは、プロ登録キーのすべての機能を提供します"
"Your free Trial key provides all the functionality of an Unleashed Registration key": "無料のトライアル キーは、プロ登録キーのすべての機能を提供します"
}

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