mirror of
https://github.com/unraid/api.git
synced 2026-01-02 14:40:01 -06:00
Compare commits
174 Commits
refactor/u
...
v3.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b33c86c99c | ||
|
|
cd0248e4c9 | ||
|
|
ecb3ed5003 | ||
|
|
0569339a41 | ||
|
|
3e9faead43 | ||
|
|
6e700b2385 | ||
|
|
464fc4993c | ||
|
|
4316c72809 | ||
|
|
ce0cebe09c | ||
|
|
23b90a0d56 | ||
|
|
3f8b3536b5 | ||
|
|
0dcf785b45 | ||
|
|
8cf4aff622 | ||
|
|
cefda7c42b | ||
|
|
0393b2382c | ||
|
|
23e900f7fd | ||
|
|
2d6aafc257 | ||
|
|
b191efece1 | ||
|
|
2a7f0043f5 | ||
|
|
607c7e3704 | ||
|
|
c246a443c5 | ||
|
|
fd495e1f5c | ||
|
|
621a06cafa | ||
|
|
f890b05151 | ||
|
|
567d8fdd6d | ||
|
|
7d996906ad | ||
|
|
b5ec076279 | ||
|
|
de8dfe3dba | ||
|
|
7249956d40 | ||
|
|
e6eb56466e | ||
|
|
8954700bcb | ||
|
|
eb595cea9e | ||
|
|
9a1a0a54e6 | ||
|
|
134396b602 | ||
|
|
2aa65fdb68 | ||
|
|
a9c4d7d5dd | ||
|
|
2cbbd5ee40 | ||
|
|
c84c55761c | ||
|
|
77eed36990 | ||
|
|
5c2d84d8b4 | ||
|
|
9883f0f82f | ||
|
|
e62b05b6f6 | ||
|
|
8e6ee8b770 | ||
|
|
666b51a28a | ||
|
|
1962097a66 | ||
|
|
7f010854b5 | ||
|
|
17288a4c02 | ||
|
|
ea48def9fc | ||
|
|
a1d5c29ffb | ||
|
|
bf99eb25c8 | ||
|
|
b35a440792 | ||
|
|
58f9eec8b1 | ||
|
|
26841aa10d | ||
|
|
e18a8d670e | ||
|
|
49d077db97 | ||
|
|
9dafe165b0 | ||
|
|
cce1953cb8 | ||
|
|
7e33b25593 | ||
|
|
78fb49a6fc | ||
|
|
f1e0d93bc5 | ||
|
|
195a178d15 | ||
|
|
b9257fce28 | ||
|
|
41eaf4ef1b | ||
|
|
93d0c08955 | ||
|
|
c5bc3454ff | ||
|
|
c33b4ef709 | ||
|
|
ce3ba7d070 | ||
|
|
639eb08291 | ||
|
|
6d109b4c4c | ||
|
|
6f3971dc47 | ||
|
|
2ccb503dc8 | ||
|
|
3cb9fdf102 | ||
|
|
40d81a4081 | ||
|
|
5a85f55be8 | ||
|
|
5455e211bc | ||
|
|
cb4cc989c7 | ||
|
|
037aa479bf | ||
|
|
08567f287a | ||
|
|
a57f1d890d | ||
|
|
3ab406e012 | ||
|
|
f36f4702a2 | ||
|
|
62697f7972 | ||
|
|
ec8d2bc0e8 | ||
|
|
d83664b6a3 | ||
|
|
6910a020d2 | ||
|
|
60e5c6e3e8 | ||
|
|
90b1432875 | ||
|
|
f1059aa381 | ||
|
|
01b4937f35 | ||
|
|
3e051815c5 | ||
|
|
e976daf8b0 | ||
|
|
422046dc03 | ||
|
|
9a270971d1 | ||
|
|
0742382ae1 | ||
|
|
763c38430e | ||
|
|
4d926bba8e | ||
|
|
4acc4ea9a9 | ||
|
|
565bf47818 | ||
|
|
176a0f30be | ||
|
|
6f4d983d89 | ||
|
|
6a0e258cf2 | ||
|
|
8d82064888 | ||
|
|
4300179b67 | ||
|
|
7e31ae2ebf | ||
|
|
7a27560b0d | ||
|
|
93655fef62 | ||
|
|
a581a95cb4 | ||
|
|
261fdda47c | ||
|
|
7a2a243a21 | ||
|
|
bead4256af | ||
|
|
e8dfd7e3b3 | ||
|
|
e456b7fcac | ||
|
|
fbe5e417ef | ||
|
|
5f80053a33 | ||
|
|
fa520a2d3e | ||
|
|
cf54f01945 | ||
|
|
44d2d58f12 | ||
|
|
daba2a352f | ||
|
|
d1ff2b1fad | ||
|
|
b1bd71f2e2 | ||
|
|
7f49816275 | ||
|
|
d73d460e88 | ||
|
|
ab1e852b6c | ||
|
|
117b7430db | ||
|
|
2e73f9e37a | ||
|
|
d3158983b4 | ||
|
|
dae7baa6ad | ||
|
|
e29f5e1adf | ||
|
|
e8d15c7dbb | ||
|
|
58be009da4 | ||
|
|
d4eb0ce3f2 | ||
|
|
d73324a141 | ||
|
|
7061be60f4 | ||
|
|
2a65f64ac1 | ||
|
|
120ba3e447 | ||
|
|
a527c7183a | ||
|
|
a0dfbb4e15 | ||
|
|
764f65ff61 | ||
|
|
1615e8623c | ||
|
|
d7bb9ff073 | ||
|
|
d896581e12 | ||
|
|
f833fa1fab | ||
|
|
2823517b26 | ||
|
|
3e0a8d0070 | ||
|
|
b3768d65aa | ||
|
|
d2e17c0051 | ||
|
|
d0354c2ef2 | ||
|
|
17fc1181c2 | ||
|
|
dbe7c5fb93 | ||
|
|
54b421d01f | ||
|
|
0e9611f802 | ||
|
|
4743a2439d | ||
|
|
242c167f82 | ||
|
|
e071b994cf | ||
|
|
60bb8aa0fa | ||
|
|
9f56f34ea4 | ||
|
|
d66b33e600 | ||
|
|
63b7c0361e | ||
|
|
dea66ff49d | ||
|
|
c29621741e | ||
|
|
6d7d013f7a | ||
|
|
ed8e2420a5 | ||
|
|
92ad66dd59 | ||
|
|
5699e34ae9 | ||
|
|
5f9dc26173 | ||
|
|
6d29ec2b90 | ||
|
|
f91ae5c7a0 | ||
|
|
33c69bf76f | ||
|
|
3e95bb259f | ||
|
|
64b6bee559 | ||
|
|
61cb029780 | ||
|
|
c02d823618 | ||
|
|
fec36919c2 | ||
|
|
072242704d |
10
.github/workflows/lint-test-build-web.yml
vendored
10
.github/workflows/lint-test-build-web.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -68,7 +68,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
|
||||
52
.github/workflows/main.yml
vendored
52
.github/workflows/main.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Reconfigure git to use HTTP authenti:cation
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
ssh://git@github.com/
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "api/.nvmrc"
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -93,7 +93,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -130,7 +130,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Add SSH deploy key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
known_hosts: ${{ secrets.KNOWN_HOSTS }}
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "api/.nvmrc"
|
||||
|
||||
@@ -170,7 +170,7 @@ jobs:
|
||||
echo "::set-output name=API_SHA256::${API_SHA256}"
|
||||
|
||||
- name: Upload tgz to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/release/*.tgz
|
||||
@@ -185,7 +185,7 @@ jobs:
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -197,7 +197,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -210,7 +210,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
@@ -227,9 +227,9 @@ jobs:
|
||||
with:
|
||||
timezoneLinux: "America/Los_Angeles"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Download unraid web components
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-web
|
||||
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
|
||||
@@ -242,7 +242,7 @@ jobs:
|
||||
bash ./pkg_build.sh s
|
||||
bash ./pkg_build.sh p
|
||||
- name: Upload binary txz and plg to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: connect-files
|
||||
path: |
|
||||
@@ -259,19 +259,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Make Staging Release Folder
|
||||
run: mkdir staging-release/
|
||||
|
||||
- name: Download unraid-api binary tgz
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
path: staging-release
|
||||
|
||||
- name: Download plugin binary tgz
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: connect-files
|
||||
|
||||
@@ -298,6 +298,18 @@ jobs:
|
||||
source: staging-release
|
||||
out_dir: unraid-api
|
||||
|
||||
- name: Upload Staging Plugin to Cloudflare Bucket
|
||||
uses: jakejarvis/s3-sync-action@v0.5.1
|
||||
env:
|
||||
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET_PREVIEW }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'auto'
|
||||
SOURCE_DIR: staging-release
|
||||
DEST_DIR: unraid-api
|
||||
|
||||
|
||||
create-draft-release:
|
||||
# Only create new draft if this is a version tag
|
||||
if: |
|
||||
@@ -307,15 +319,15 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download unraid-api binary tgz
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
|
||||
- name: Download plugin binary tgz
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: connect-files
|
||||
|
||||
|
||||
10
.github/workflows/pull-request-web.yml
vendored
10
.github/workflows/pull-request-web.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -51,7 +51,7 @@ jobs:
|
||||
needs: [lint-web]
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Create env file
|
||||
run: |
|
||||
@@ -63,7 +63,7 @@ jobs:
|
||||
cat .env
|
||||
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "web/package-lock.json"
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Upload build to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-web
|
||||
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
|
||||
|
||||
24
.github/workflows/pull-request.yml
vendored
24
.github/workflows/pull-request.yml
vendored
@@ -24,15 +24,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
@@ -57,16 +57,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
@@ -97,16 +97,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: true
|
||||
- uses: docker/setup-buildx-action@v2
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
# network=host driver-opt needed to push to local registry
|
||||
driver-opts: network=host
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: api
|
||||
target: builder
|
||||
@@ -127,7 +127,7 @@ jobs:
|
||||
echo "::set-output name=API_SHA256::${API_SHA256}"
|
||||
|
||||
- name: Upload tgz to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: unraid-api
|
||||
path: ${{ github.workspace }}/api/deploy/release/*.tgz
|
||||
@@ -155,7 +155,7 @@ jobs:
|
||||
with:
|
||||
timezoneLinux: "America/Los_Angeles"
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Build Plugin
|
||||
run: |
|
||||
cd source/dynamix.unraid.net
|
||||
@@ -173,7 +173,7 @@ jobs:
|
||||
# escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"${RELEASE_NOTES}")
|
||||
# sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "plugins/dynamix.unraid.net.staging.plg"
|
||||
- name: Upload binary txz and plg to Github artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: connect-files
|
||||
path: |
|
||||
|
||||
57
.github/workflows/release-production.yml
vendored
Normal file
57
.github/workflows/release-production.yml
vendored
Normal file
@@ -0,0 +1,57 @@
|
||||
name: Publish Release to Digital Ocean
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
publish-to-digital-ocean:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Download Release Artifacts (Plugins)
|
||||
uses: dsaltares/fetch-gh-release-asset@master
|
||||
with:
|
||||
file: ".*"
|
||||
regex: true
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
target: "./"
|
||||
version: "latest"
|
||||
|
||||
- uses: cardinalby/git-get-release-action@v1
|
||||
id: release-info
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
latest: true
|
||||
prerelease: false
|
||||
- name: Get Release Changelog
|
||||
run: |
|
||||
notes=$(cat << EOF
|
||||
${{ steps.release-info.outputs.body }}
|
||||
EOF
|
||||
)
|
||||
escapedNotes=$(sed -e 's/[&\\/]/\\&/g; s/$/\\/' -e '$s/\\$//' <<<"$notes")
|
||||
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.plg"
|
||||
sed -i -z -E "s/<CHANGES>(.*)<\/CHANGES>/<CHANGES>\n${escapedNotes}\n<\/CHANGES>/g" "dynamix.unraid.net.staging.plg"
|
||||
|
||||
- name: Upload All Release Files to DO Spaces
|
||||
uses: BetaHuhn/do-spaces-action@v2
|
||||
with:
|
||||
access_key: ${{ secrets.DO_ACCESS_KEY }}
|
||||
secret_key: ${{ secrets.DO_SECRET_KEY }}
|
||||
space_name: ${{ secrets.DO_SPACE_NAME }}
|
||||
space_region: ${{ secrets.DO_SPACE_REGION }}
|
||||
source: "."
|
||||
out_dir: unraid-api
|
||||
|
||||
- name: Upload Staging Plugin to Cloudflare Bucket
|
||||
uses: jakejarvis/s3-sync-action@v0.5.1
|
||||
env:
|
||||
AWS_S3_ENDPOINT: ${{ secrets.CF_ENDPOINT }}
|
||||
AWS_S3_BUCKET: ${{ secrets.CF_BUCKET }}
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.CF_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.CF_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: 'auto'
|
||||
SOURCE_DIR: "."
|
||||
DEST_DIR: unraid-api
|
||||
@@ -1 +1 @@
|
||||
18.17.1
|
||||
18.19.1
|
||||
226
api/CHANGELOG.md
226
api/CHANGELOG.md
@@ -2,6 +2,232 @@
|
||||
|
||||
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.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)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* build docker command updated to use dc.sh script ([0b40886](https://github.com/unraid/api/commit/0b40886e84f27a94dbf67ef4ca0cd8539ef3913e))
|
||||
* date format in UnraidCheck.php ([#852](https://github.com/unraid/api/issues/852)) ([6465f2d](https://github.com/unraid/api/commit/6465f2d7b2394090f35e29cdd680d98ce37f3728))
|
||||
* **deps:** update dependency @apollo/client to v3.9.5 ([#785](https://github.com/unraid/api/issues/785)) ([75b98bc](https://github.com/unraid/api/commit/75b98bc1cbca5b66ae72f52a0b6f5f58230a2473))
|
||||
* **deps:** update dependency @heroicons/vue to v2.1.1 ([#804](https://github.com/unraid/api/issues/804)) ([a0eb7ee](https://github.com/unraid/api/commit/a0eb7ee3ec459dbe1992a7f85bf194da30395a74))
|
||||
* **deps:** update dependency focus-trap to v7.5.4 ([#788](https://github.com/unraid/api/issues/788)) ([fe000e8](https://github.com/unraid/api/commit/fe000e83825e82cac558d3277664a440e59c0e4a))
|
||||
* **deps:** update dependency graphql-ws to v5.15.0 ([#790](https://github.com/unraid/api/issues/790)) ([4773b13](https://github.com/unraid/api/commit/4773b132167d740d4c996efe22e0f1b99576fb9b))
|
||||
* display dropdown for pro key no connect installed ([#848](https://github.com/unraid/api/issues/848)) ([b559604](https://github.com/unraid/api/commit/b55960429895b46627f1cd3ed1683ee527e62944))
|
||||
* dropdown reboot link text ([#849](https://github.com/unraid/api/issues/849)) ([a8ed5e5](https://github.com/unraid/api/commit/a8ed5e5628bc71fb783a03c3db92d21805243738))
|
||||
* os updates rc to stable ([bf1bd88](https://github.com/unraid/api/commit/bf1bd887d60ac085bf4aeae90f11be3b45ee1182))
|
||||
* state connect values without connect installed ([e47de6c](https://github.com/unraid/api/commit/e47de6c2c5db7a2a1a9b24099feb02023b3a7bbf))
|
||||
* state php breaking with double quotes in server description ([c6e92aa](https://github.com/unraid/api/commit/c6e92aa3157c9fe9e7b83580881ebcc1cbd03658))
|
||||
* state php special chars for html attributes ([#853](https://github.com/unraid/api/issues/853)) ([dd4139c](https://github.com/unraid/api/commit/dd4139cf1a7ae5c6f9b00111c33ae124bb17e630))
|
||||
* unraid-api missing start command + var defaults ([ceb4c58](https://github.com/unraid/api/commit/ceb4c587d20c7527f2b36a3278c310b0e657bfba))
|
||||
* unraid-api.php $param1 fallback ([909c79c](https://github.com/unraid/api/commit/909c79c8c82500aea1a0d4d00766f788103c5fe3))
|
||||
|
||||
## [3.5.0](https://github.com/unraid/api/compare/v3.4.0...v3.5.0) (2024-02-07)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add manage account link to all versions of upc dropdown ([678e620](https://github.com/unraid/api/commit/678e620c1902a376b1866265711d5722b4119d8e))
|
||||
* add new staging url for connect website ([#841](https://github.com/unraid/api/issues/841)) ([4cfc07b](https://github.com/unraid/api/commit/4cfc07b6763dbb79b68cf01f7eaf7cf33370d4db))
|
||||
* also ship to cloudflare ([#844](https://github.com/unraid/api/issues/844)) ([41c4210](https://github.com/unraid/api/commit/41c42103685209592b272f81a877702da04d0915))
|
||||
* button add underline-hover-red style option ([f2fa5fa](https://github.com/unraid/api/commit/f2fa5fa49675ef461330be7b7eb3e3e4106983b0))
|
||||
* changelog modal ([2ddbacd](https://github.com/unraid/api/commit/2ddbacd137cc5748244c3d25ac91f82e64d77f99))
|
||||
* check update response modal ([39678f0](https://github.com/unraid/api/commit/39678f0bb0ddc5f87ea7f5ed80a0472100ea8b5d))
|
||||
* create WebguiCheckForUpdate endpoint ([41d546e](https://github.com/unraid/api/commit/41d546eea5fcf6593d7b5047274c074bb89c1802))
|
||||
* getOsReleaseBySha256 cached endpoint with keyfile header ([cd2413a](https://github.com/unraid/api/commit/cd2413abe8c5baab40e4e5974e08a5d18dce8e0d))
|
||||
* new check update buttons in dropdown ([ef5fcb9](https://github.com/unraid/api/commit/ef5fcb96a324143da864df803acaa0da1cd00eb7))
|
||||
* ship preview to different bucket ([#845](https://github.com/unraid/api/issues/845)) ([8e5d247](https://github.com/unraid/api/commit/8e5d247bca83d9e50977c9b16b212841ac9f70ad))
|
||||
* ship production to different bucket ([#846](https://github.com/unraid/api/issues/846)) ([63c0875](https://github.com/unraid/api/commit/63c08758c76425e007b1779bb2f77b75bc45896e))
|
||||
* unraidcheck callable from webgui with altUrl & json output ([ba8a67e](https://github.com/unraid/api/commit/ba8a67edfa043f442b11724227129f8d3f6cae0a))
|
||||
* update modals ([8ad7d8b](https://github.com/unraid/api/commit/8ad7d8be9437e0caa0409da8f7322050919fbbaa))
|
||||
* update os ignore release ([1955eb2](https://github.com/unraid/api/commit/1955eb23a3cdc30f0a67bc5950a047f83a860d99))
|
||||
* update os notifications enabled usage + link to enable & more options to account app ([5c82aff](https://github.com/unraid/api/commit/5c82aff80dc7e6d8f4b23e52af29abc2b8576424))
|
||||
* updateOs check response determines if update auth is required ([a9816d9](https://github.com/unraid/api/commit/a9816d9ad48ff80d87b5aeb236ff60c4979ad298))
|
||||
* updateOs store call local server-side endpoint & add modal support ([be48447](https://github.com/unraid/api/commit/be48447f943828af281095c5a092ac686e729030))
|
||||
* upgrade a ton of dependencies ([#842](https://github.com/unraid/api/issues/842)) ([94c1746](https://github.com/unraid/api/commit/94c174620c2347a3cf3d100404635f99a5b47287))
|
||||
* WebguiCheckForUpdate using server-side check ([590deb1](https://github.com/unraid/api/commit/590deb130c301d4004fecdc211270583806b5593))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* backport _var() PHP function to older versions of Unraid ([f53150e](https://github.com/unraid/api/commit/f53150e1fa33b3f45b66ad0dc5eaabc470564d45))
|
||||
* changlog relative links and external links ([a789e20](https://github.com/unraid/api/commit/a789e204ce7b966e6c935923626538ac344aeefe))
|
||||
* check update response modal expired key button styles ([92993e3](https://github.com/unraid/api/commit/92993e3e0b6240c83a6a64efedd8ddb3be3f9ef7))
|
||||
* **deps:** update dependency ws to v8.16.0 ([#815](https://github.com/unraid/api/issues/815)) ([212020e](https://github.com/unraid/api/commit/212020e78d4de0576137058a3374837b4a43e02d))
|
||||
* extraLinks when no updates available ([853a991](https://github.com/unraid/api/commit/853a9911e3fd7eec9bbc88468de78f87b448d477))
|
||||
* ignore release localStorage ([62c45ec](https://github.com/unraid/api/commit/62c45ec9d7c68498bbcfe933a5b63e4759c7129c))
|
||||
* lint ([83235f9](https://github.com/unraid/api/commit/83235f9db726f4582b9d353a66f2f5e8925b8e34))
|
||||
* lint unused value ([2c7e53b](https://github.com/unraid/api/commit/2c7e53bf67d1f214201624b39786bfb7de6aa520))
|
||||
* marked-base-url install ([416ba71](https://github.com/unraid/api/commit/416ba716aa750a094e8cd521a79f6deebcd37864))
|
||||
* missing translations ([faf17e4](https://github.com/unraid/api/commit/faf17e41e81c11443bc062d8ce35a33d9ae9ebbc))
|
||||
* regTm format after key install without page refresh ([f3ddb31](https://github.com/unraid/api/commit/f3ddb31f994de9192f7203698ecc5d7de673c6a3))
|
||||
* regTm format when already set ([5ad911f](https://github.com/unraid/api/commit/5ad911f8133daa60de53da738d41c6a59e2f02cc))
|
||||
* ServerUpdateOsResponse type ([78bdae8](https://github.com/unraid/api/commit/78bdae86c907142d3ee32d6715eaa8f5a974a1ed))
|
||||
* State Class usage in other files ([4ad7f53](https://github.com/unraid/api/commit/4ad7f53ec145b2e6d2895619523e90c1daa3f68f))
|
||||
* state data humanReadable switch fallthrus ([9144e39](https://github.com/unraid/api/commit/9144e39d39aa56af0ad897735d1a3545330920d0))
|
||||
* state php usage from cli ([46fd321](https://github.com/unraid/api/commit/46fd321707c14cd1f265ee806f673500d87132dd))
|
||||
* translations ([3fabd57](https://github.com/unraid/api/commit/3fabd5756674c06fa803729cf13d19c592d8d46a))
|
||||
* type issue with changlelog modal visibility ([e3c3f6b](https://github.com/unraid/api/commit/e3c3f6bf0f1882788291db17bd74865fefc3abf6))
|
||||
|
||||
## [3.4.0](https://github.com/unraid/api/compare/v3.3.0...v3.4.0) (2024-01-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add logrotate to cron in nestjs ([#839](https://github.com/unraid/api/issues/839)) ([5c91524](https://github.com/unraid/api/commit/5c91524d849147c0ac7925f3a2f1cce67ffe75de))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* allow failure for log deletion ([eff3142](https://github.com/unraid/api/commit/eff31423927644be436a831126678719c2eb0621))
|
||||
* allowed origins check not working without spaces ([#838](https://github.com/unraid/api/issues/838)) ([b998b38](https://github.com/unraid/api/commit/b998b38355fab77ecc2f62bc64896766218db3d4))
|
||||
* excessive logging ([89cb254](https://github.com/unraid/api/commit/89cb2544ed0e0edd33b59f15d487487e22c0ae32))
|
||||
* run hourly ([0425794](https://github.com/unraid/api/commit/0425794356a01262222e7dff2645d3629e00d0f6))
|
||||
|
||||
## [3.3.0](https://github.com/unraid/api/compare/v3.2.3...v3.3.0) (2024-01-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add button to add current origin to extra origins setting ([8c15163](https://github.com/unraid/api/commit/8c15163b3b072122bff1f8f25de62594b1e67992))
|
||||
* add environment to docker-compose ([2ee4683](https://github.com/unraid/api/commit/2ee46839095e3b8ee287cfe10f29ae9a39dcff68))
|
||||
* add support for expiration in var.ini ([#833](https://github.com/unraid/api/issues/833)) ([0474c2e](https://github.com/unraid/api/commit/0474c2e14fa462d2e1ec6d9a7f974660385d073e))
|
||||
* always show DRA even if disabled ([ab708c0](https://github.com/unraid/api/commit/ab708c0df634e21bf81595412d7de0be3ff7c392))
|
||||
* change sort order of Update/Downgrade ([#754](https://github.com/unraid/api/issues/754)) ([be96b3a](https://github.com/unraid/api/commit/be96b3aac709682a6517fa6e84beb586b9d8bf5c))
|
||||
* check for OS updates via PHP ([#752](https://github.com/unraid/api/issues/752)) ([4496615](https://github.com/unraid/api/commit/44966157b80a51dfe01d927c2af2d010c04becc5))
|
||||
* close log on exit ([d6ede86](https://github.com/unraid/api/commit/d6ede86eca6301342cdf35bf1f9365896b5e5009))
|
||||
* disable account & key actions when unraid-api CORS error ([1d15406](https://github.com/unraid/api/commit/1d1540646a264038ae96f4063c31a40cd048d2f9))
|
||||
* extraOrigins public, remove origin listener ([91f96ba](https://github.com/unraid/api/commit/91f96ba818773d6e71dde1ff52a4c8ec21ba6b5d))
|
||||
* fix codegen ([d0bf5bb](https://github.com/unraid/api/commit/d0bf5bb8197b11f7a250ca5392890184a1dbeff7))
|
||||
* fix exit hook and cleanup docker scripts ([#758](https://github.com/unraid/api/issues/758)) ([a9ff73e](https://github.com/unraid/api/commit/a9ff73e0a04c67e9ec9d5551cf0b1f124be6f381))
|
||||
* fix logging format on start and stop ([c6720c3](https://github.com/unraid/api/commit/c6720c331df055480d2d65b37290f4978fe429da))
|
||||
* improve check for OS updates via PHP ([cde12b2](https://github.com/unraid/api/commit/cde12b247f9bba97644750cd95a2b0db320ca1d9))
|
||||
* local start command ([99b6007](https://github.com/unraid/api/commit/99b6007ba30353084a8bea54cc0e782fcc1bfea4))
|
||||
* log config recreation reason ([f36c72f](https://github.com/unraid/api/commit/f36c72f5ad44b7e41d1726fa181dc2b9f594c72c))
|
||||
* nestjs initial query implementation ([#748](https://github.com/unraid/api/issues/748)) ([075d7f2](https://github.com/unraid/api/commit/075d7f25785bf686779b7fee1d5ea39f09ff3ea8))
|
||||
* new key types in API ([e42f9dc](https://github.com/unraid/api/commit/e42f9dc95be03e8389aac443f2147c07a316d48d))
|
||||
* npm scripts to prevent webgui builds with wrong urls ([279966a](https://github.com/unraid/api/commit/279966afa3c218fbe85bafe91ee40fff2eb59ef2))
|
||||
* patch DefaultPageLayout for web component ([629fec6](https://github.com/unraid/api/commit/629fec64f911131e4ab3810c99028b484ce18b83))
|
||||
* **plg:** WIP extra origins support ([85acaae](https://github.com/unraid/api/commit/85acaaee02dad98eeef8a8c4a09b463e84d593b4))
|
||||
* regTy swapped ([564b25c](https://github.com/unraid/api/commit/564b25cf5ce0a62d40f8d63d44c81e9c8560e0be))
|
||||
* run codegen and update build script ([07512ad](https://github.com/unraid/api/commit/07512adc13ee0d819db45ff6c5c5f58a0ba31141))
|
||||
* server store isOsVersionStable ([b5ee4d4](https://github.com/unraid/api/commit/b5ee4d4ee632a7528e6f5df079cab0cb5ea656eb))
|
||||
* stretch downgrade component buttons ([fa4f63e](https://github.com/unraid/api/commit/fa4f63e8bfca525ccfedb16f19d395bf11a68561))
|
||||
* swap to fragement usage on webcomponent ([42733ab](https://github.com/unraid/api/commit/42733abf6e443516ff715569333422ce80d3b1d2))
|
||||
* **web:** caseModel ([4174d0b](https://github.com/unraid/api/commit/4174d0bf2cac99af5db48e5642e0037d7425c952))
|
||||
* **web:** create script to move build to webgui repo ([92df453](https://github.com/unraid/api/commit/92df453255fed45210d9a192c68bb27d3b0ee981))
|
||||
* **web:** downgrade os web component ([45496ab](https://github.com/unraid/api/commit/45496ab7685d4bbfe591be46489260bac9b03474))
|
||||
* **web:** finalize api cors error & settings field ([e1d9e16](https://github.com/unraid/api/commit/e1d9e16b8e80e0940a0078131ea629559e3238ec))
|
||||
* **web:** guidValidation if new keyfile auto install ([0abb196](https://github.com/unraid/api/commit/0abb196d2c57ead4dca2adb2981ab79cdd1647c4))
|
||||
* **web:** localStorage craftUrl for dev ([e646187](https://github.com/unraid/api/commit/e646187b04548c010cf26c7ae38a82ced6270394))
|
||||
* **web:** refactor generic updateOS with date comparison ([91a753c](https://github.com/unraid/api/commit/91a753cd7018b89d53e9cd2d7c429ce53e291336))
|
||||
* **web:** registration component ui / ux ([717d873](https://github.com/unraid/api/commit/717d8733bd4b8c87b6ae6f1cd66717056c5df876))
|
||||
* **web:** registration replace eligibility docs btn ([b69285f](https://github.com/unraid/api/commit/b69285ff8ca5b896082b5f0e1aeba70f9a2c5129))
|
||||
* **web:** registration too many devices messaging ([1c0b5a3](https://github.com/unraid/api/commit/1c0b5a317aadf6173405770878e6038d4d8b448f))
|
||||
* **web:** start prep for new key type support ([5c5035a](https://github.com/unraid/api/commit/5c5035a5446516999729ddc56d1077ee512f14d3))
|
||||
* **web:** update os create flash backup button ([50ba61c](https://github.com/unraid/api/commit/50ba61cf80b7df2d121962cf4ec4b10952e8eecb))
|
||||
* **web:** WIP key expiration ([24618fe](https://github.com/unraid/api/commit/24618fe09db2109c2eb57ab1655ab0fb7d79fc90))
|
||||
* **web:** WIP registration page UI UX ([559e5b8](https://github.com/unraid/api/commit/559e5b8698d5df80ca57f530a2bf2cb6f01e30c7))
|
||||
* **web:** WIP registration page web component ([bd772a9](https://github.com/unraid/api/commit/bd772a9c97d49b57a0b5a0e6a367c9a4e3732086))
|
||||
* **web:** WIP updateOs callback ([2ad55ed](https://github.com/unraid/api/commit/2ad55ed019155e46d8627ea5c1b82cd5e4351127))
|
||||
* WIP first pass at UpdateOs page replacement component ([3a5d871](https://github.com/unraid/api/commit/3a5d871f1fd054720c3693705484072ff567ff28))
|
||||
* WIP UpdateOs page component ([8e4c36d](https://github.com/unraid/api/commit/8e4c36d38ce4e70307f5d14c953d5103c8b7e8e4))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* 6.10 view release notes js ([254d894](https://github.com/unraid/api/commit/254d894f39e512d1b4a0472180cb27090de256a0))
|
||||
* add missing translation keys ([03b506c](https://github.com/unraid/api/commit/03b506cd4e68f23a85bbfd54205322a6a4f93e5b))
|
||||
* add serverName / description to dashboard payload ([9677aff](https://github.com/unraid/api/commit/9677aff1cd0942f36a2845f3f105601c494efd9e))
|
||||
* allow null for the local entry in the myservers cfg ([01157c8](https://github.com/unraid/api/commit/01157c86ea3838ca675d65528a882cf25d0019a6))
|
||||
* azure and gray theme custom colors ([92e552c](https://github.com/unraid/api/commit/92e552c9c7f7804902f18eb2d71f8483671fe048))
|
||||
* codegen on web run ([e2e67c2](https://github.com/unraid/api/commit/e2e67c21067a138d963f5f10760b84cf6a533542))
|
||||
* combinedKnownOrigins in state.php for UPC ([b550eea](https://github.com/unraid/api/commit/b550eeae7077cbdbd6d004506bdc96d04c04bc4c))
|
||||
* Connect settings myservers config parse ([1c1483a](https://github.com/unraid/api/commit/1c1483a5cc506deab9d858dabbb8388c8b1d1ec1))
|
||||
* dateTime system settings ([56ccbff](https://github.com/unraid/api/commit/56ccbff61fb61ab67277100c525b80adf95e9b72))
|
||||
* **deps:** update dependency graphql to v16.8.1 ([bff1b19](https://github.com/unraid/api/commit/bff1b19706bee1e3103e3a0a1d2fceb3154f9bba))
|
||||
* **deps:** update graphql-tools monorepo (major) ([#693](https://github.com/unraid/api/issues/693)) ([3447eb0](https://github.com/unraid/api/commit/3447eb047a1dcd575b88a96bbcef9946aca366a1))
|
||||
* **deps:** update nest monorepo ([#816](https://github.com/unraid/api/issues/816)) ([4af3699](https://github.com/unraid/api/commit/4af36991b8b376f816ed51fd503a66e99675a3e7))
|
||||
* downgrade remove erroneous file_get_contents ([df9c918](https://github.com/unraid/api/commit/df9c91867cf3f7cf6b424a386d7e68bd510ec20f))
|
||||
* exit with process.exit not process.exitcode ([dcb6def](https://github.com/unraid/api/commit/dcb6def1cf3365dca819feed101160c8ad0125dc))
|
||||
* graphQL CORS error detection ([e5ea67f](https://github.com/unraid/api/commit/e5ea67fe5224fd5aaf06e1e63e7efc01974a10ac))
|
||||
* header version thirdPartyDriversDownloading pill ([c2ff31c](https://github.com/unraid/api/commit/c2ff31c672bc30683062c6cefbd5e744a7a2a676))
|
||||
* lint unused param var prefixed ([8d103a9](https://github.com/unraid/api/commit/8d103a9ca89139d7b4f513318a67bcc64c0daa0c))
|
||||
* local container startup commands cleaned up ([6c0ccb2](https://github.com/unraid/api/commit/6c0ccb2b24f98282be4db2e0b2e6362f4a187def))
|
||||
* logrotate not working due to invalid ownership of unraid-api folder ([ec0581a](https://github.com/unraid/api/commit/ec0581abf58a217f698d52d5337f2b312e5a645b))
|
||||
* missing translation ([81a9380](https://github.com/unraid/api/commit/81a93802993e7d95fb587cbfe3b598136a89348b))
|
||||
* optional check on api.version to allow fallback to save value ([0ac4455](https://github.com/unraid/api/commit/0ac4455f78407eca7aa1d6ee360830067a1c5c3e))
|
||||
* patch ShowChanges.php in 6.10 ([92d09c2](https://github.com/unraid/api/commit/92d09c2846c1bf64276e140c4cf4635e8bbfa94b))
|
||||
* plg installer header version replacement ([7d0de2c](https://github.com/unraid/api/commit/7d0de2c8b3dc3c2d3c204e7846cf65d6df07545f))
|
||||
* plg remove reboot-details path ([d54d90e](https://github.com/unraid/api/commit/d54d90ec04c67ee532cbcb77c4c5890545899e5a))
|
||||
* **plg:** Downgrade & Update page file locations ([3fbb6b7](https://github.com/unraid/api/commit/3fbb6b70c1152d0691f3d74298908338e19cda53))
|
||||
* **plg:** third party reboot detection ([f0ee640](https://github.com/unraid/api/commit/f0ee640767e446a829fd2e60033560786e5f63b0))
|
||||
* plugin install should suppress output from `unraid-api stop` ([#757](https://github.com/unraid/api/issues/757)) ([3da5d95](https://github.com/unraid/api/commit/3da5d9573b499c84c25e33b26a2014e79bef40f7))
|
||||
* rearrange exit hook to try to fix closing ([843d3f4](https://github.com/unraid/api/commit/843d3f41162c5dbcfd7803912b1879d7a182231a))
|
||||
* refreshServerState check regExp ([7fca971](https://github.com/unraid/api/commit/7fca971cab40b6e5493e7e21baf85e3d6ba66b90))
|
||||
* remove var_dump Connect settings ([9425f8b](https://github.com/unraid/api/commit/9425f8b133d44ac759d09158eadd13c81e7796fb))
|
||||
* renew callback messaging in modal ([e98d065](https://github.com/unraid/api/commit/e98d0654237b111cf912eb5014dbcc5da0e92ca3))
|
||||
* replaceRenew response cache use & purge ([ca85199](https://github.com/unraid/api/commit/ca851991ecb09720d70135d302aa93ad10a96d3a))
|
||||
* set sha in test step as well. ([8af3367](https://github.com/unraid/api/commit/8af3367226f9a3bc51db65ffe5dd53d6c5aa0017))
|
||||
* state php version checking ([494f5e9](https://github.com/unraid/api/commit/494f5e9935bc207b81098e84a0fe3e259939cf39))
|
||||
* stop using username to determine reg status ([c5a6cd7](https://github.com/unraid/api/commit/c5a6cd7bf930d8bc94ccae45f5363c12fd1fccfc))
|
||||
* ThirdPartyDriver messaging on Update page ([f23ad76](https://github.com/unraid/api/commit/f23ad762c04c3da918429a376146fe096a5030d5))
|
||||
* try to set environment in docker build ([caece63](https://github.com/unraid/api/commit/caece63e7f180f94a7ee6b962c905296c6b987bb))
|
||||
* uninstall reboot-details include ([3849462](https://github.com/unraid/api/commit/3849462f572659a43157a49511075f2d8cd5dd4c))
|
||||
* unraid-api server state refresh after key extension use regExp ([490595f](https://github.com/unraid/api/commit/490595f9b420054e6c2fe40d868b902b262718af))
|
||||
* updateOs auth group usage ([52b1ad9](https://github.com/unraid/api/commit/52b1ad9a7d3c9cdc989dd729d7828b0678349c27))
|
||||
* updateOs type check ([ba230e2](https://github.com/unraid/api/commit/ba230e2643399fbfa1612059f235ccdf61f7f486))
|
||||
* web component translations class ([6c81f6f](https://github.com/unraid/api/commit/6c81f6f70dcbe4f055a0041863fe275d6e01d6b9))
|
||||
* **web:** azure & gray theme header font colors ([8a5c7c9](https://github.com/unraid/api/commit/8a5c7c9304a063b26d7ff2df5c174aa9f1c0f53c))
|
||||
* **web:** card wrapper error border styles ([c71f420](https://github.com/unraid/api/commit/c71f420a4c9f7325127e3f38157dbc6255b3e139))
|
||||
* **web:** connect graph error handling ([c239937](https://github.com/unraid/api/commit/c239937c407cfea0defde1994809a5c0a196cca2))
|
||||
* **web:** default time format include am/pm ([31694cd](https://github.com/unraid/api/commit/31694cd7141e2ec0b0c3b4e4480d34d19c80adae))
|
||||
* **web:** downgrade status pill for no downgrade available ([9d9ebb1](https://github.com/unraid/api/commit/9d9ebb1c6efd486a90dcd78ba63766e24be26d55))
|
||||
* **web:** downgrade-not-available when downgrade initiated ([d060359](https://github.com/unraid/api/commit/d0603592596a3173889e9d06d57cfaa602eb80bb))
|
||||
* **web:** installPlugin composable for os updates ([9fb024a](https://github.com/unraid/api/commit/9fb024a68d65905e5351cfa71ca64cdffa0fa74c))
|
||||
* **web:** lint fixes ([224d637](https://github.com/unraid/api/commit/224d63773d505b8d65c9455fb94260ae617d9fe5))
|
||||
* **web:** localStorage craftUrl for dev ([2e108da](https://github.com/unraid/api/commit/2e108da0db7de01d03ee3b0657a614355a61b208))
|
||||
* **web:** missing translation ([74a8f27](https://github.com/unraid/api/commit/74a8f27643d7ba9c9d5dcd6a43b189a936dae648))
|
||||
* **web:** missing translation for update ([cb46a94](https://github.com/unraid/api/commit/cb46a94c7238bf381fbfc48109b1dd648d2e4949))
|
||||
* **web:** missing translations ([8ea733b](https://github.com/unraid/api/commit/8ea733b295a5f3bd922e867f544e5538873a5088))
|
||||
* **web:** missing translations ([d2eed92](https://github.com/unraid/api/commit/d2eed9291de9297aa0d556f06b9b8f5f09734250))
|
||||
* **web:** no plugin, don't show restart api button ([e628a8b](https://github.com/unraid/api/commit/e628a8b64fab4d1a5ce84af62abde3cd4c53ba96))
|
||||
* **web:** preview and test releases usage ([4b8cfb4](https://github.com/unraid/api/commit/4b8cfb464e8296ce20d6ff3870949d739a86ca1b))
|
||||
* **web:** reboot required disable update check link ([f029652](https://github.com/unraid/api/commit/f0296528bae52227ecbe281786ddf4d3a0cc940f))
|
||||
* **web:** reg component conditional keyActions ([730dff2](https://github.com/unraid/api/commit/730dff2e6344f7ee076e1c67d82ef0783a5931b2))
|
||||
* **web:** Registration key actions ([f7b1016](https://github.com/unraid/api/commit/f7b1016980c3f576b007a1d01184bf35f0eef311))
|
||||
* **web:** regTy on account payload ([64b0b5e](https://github.com/unraid/api/commit/64b0b5eb5767d41012f6bcb9536030ec39e45af9))
|
||||
* **web:** regUpdatesExpired use .isAfter ([5d67adf](https://github.com/unraid/api/commit/5d67adf4625a108e3374eb72714cdc1747b2a9c5))
|
||||
* **web:** replace check request error handling ([c1491fe](https://github.com/unraid/api/commit/c1491fecdc327d78f8de7c0f04fda481fb47cb56))
|
||||
* **web:** replaceCheck type ([1bd9729](https://github.com/unraid/api/commit/1bd9729b0197b49ca460912bbc56cd3b206d00dc))
|
||||
* **web:** replaceCheck type ([8cc6020](https://github.com/unraid/api/commit/8cc602019a2c8a718b59590d166644a1cb4d16cc))
|
||||
* **web:** state $_SESSION usage ([412392d](https://github.com/unraid/api/commit/412392dc1c5e612199e76ee7e1cae03705957e3d))
|
||||
* **web:** state php warnings ([1460cab](https://github.com/unraid/api/commit/1460cabe6b041f9f9fb89ca474a7d7e872d31c39))
|
||||
* **web:** translation ([cc85a49](https://github.com/unraid/api/commit/cc85a4903178999dbb80da50aa3b02ff38012172))
|
||||
* **web:** type errors ([e6c57eb](https://github.com/unraid/api/commit/e6c57eb910a1c1f948a3104c4e7fc04ac8b2d327))
|
||||
* **web:** upc dropdown updates external icon ([13936bb](https://github.com/unraid/api/commit/13936bb157f9097a19c7498fce252f3f86526ccb))
|
||||
* **web:** update CallbackButton import ([eabfeca](https://github.com/unraid/api/commit/eabfeca618d3bf682a331c6d9e1f17b5facdcdca))
|
||||
* **web:** Update OS auto redirect loop with account ([9b56fc3](https://github.com/unraid/api/commit/9b56fc3883f51942de9b1c8d1d1f30595fee7fa5))
|
||||
* **web:** updateOs lint ([bd9e9d5](https://github.com/unraid/api/commit/bd9e9d55cc7bba432f65d78feee83526dbfff059))
|
||||
* **web:** use dateTime format from server ([7090f38](https://github.com/unraid/api/commit/7090f38a9ab8b2d1dfce4095f4e2669d4d78a3e1))
|
||||
|
||||
### [3.2.3](https://github.com/unraid/api/compare/v3.2.2...v3.2.3) (2023-09-08)
|
||||
|
||||
|
||||
|
||||
@@ -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 \
|
||||
@@ -15,8 +15,6 @@ RUN apt-get update -y && apt-get install -y \
|
||||
git \
|
||||
build-essential
|
||||
|
||||
RUN mkdir /var/log/unraid-api/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Set app env
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[api]
|
||||
version="3.2.3+075d7f25"
|
||||
extraOrigins=""
|
||||
version="3.4.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
[notifier]
|
||||
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[api]
|
||||
version="3.2.3+075d7f25"
|
||||
extraOrigins=""
|
||||
version="3.4.0"
|
||||
extraOrigins="https://google.com,https://test.com"
|
||||
[local]
|
||||
[notifier]
|
||||
apikey="unnotify_30994bfaccf839c65bae75f7fa12dd5ee16e69389f754c3b98ed7d5"
|
||||
@@ -16,7 +16,7 @@ regWizTime="1611175408732_0951-1653-3509-FBA155FA23C0"
|
||||
idtoken=""
|
||||
accesstoken=""
|
||||
refreshtoken=""
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
allowedOrigins="/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, http://localhost:8080, https://localhost:4443, https://tower.local:4443, https://192.168.1.150:4443, https://tower:4443, https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443, https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443, https://10-252-0-1.hash.myunraid.net:4443, https://10-252-1-1.hash.myunraid.net:4443, https://10-253-3-1.hash.myunraid.net:4443, https://10-253-4-1.hash.myunraid.net:4443, https://10-253-5-1.hash.myunraid.net:4443, https://google.com, https://test.com, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000, https://studio.apollographql.com"
|
||||
dynamicRemoteAccessType="DISABLED"
|
||||
[upc]
|
||||
apikey="unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810"
|
||||
|
||||
143
api/dev/states/var.ini
Normal file
143
api/dev/states/var.ini
Normal file
@@ -0,0 +1,143 @@
|
||||
version="6.11.2"
|
||||
MAX_ARRAYSZ="30"
|
||||
MAX_CACHESZ="30"
|
||||
NAME="Tower"
|
||||
timeZone="Australia/Adelaide"
|
||||
COMMENT="Dev Server"
|
||||
SECURITY="user"
|
||||
WORKGROUP="WORKGROUP"
|
||||
DOMAIN=""
|
||||
DOMAIN_SHORT=""
|
||||
hideDotFiles="no"
|
||||
localMaster="yes"
|
||||
enableFruit="no"
|
||||
USE_NETBIOS="yes"
|
||||
USE_WSD="no"
|
||||
WSD_OPT=""
|
||||
USE_NTP="yes"
|
||||
NTP_SERVER1="time1.google.com"
|
||||
NTP_SERVER2="time2.google.com"
|
||||
NTP_SERVER3="time3.google.com"
|
||||
NTP_SERVER4="time4.google.com"
|
||||
DOMAIN_LOGIN="Administrator"
|
||||
SYS_MODEL="Dell R710"
|
||||
SYS_ARRAY_SLOTS="24"
|
||||
SYS_FLASH_SLOTS="1"
|
||||
USE_SSL="auto"
|
||||
PORT="80"
|
||||
PORTSSL="443"
|
||||
LOCAL_TLD="local"
|
||||
BIND_MGT="no"
|
||||
USE_TELNET="yes"
|
||||
PORTTELNET="23"
|
||||
USE_SSH="yes"
|
||||
PORTSSH="22"
|
||||
USE_UPNP="yes"
|
||||
START_PAGE="Main"
|
||||
startArray="no"
|
||||
spindownDelay="0"
|
||||
spinupGroups="no"
|
||||
defaultFsType="xfs"
|
||||
shutdownTimeout="90"
|
||||
luksKeyfile="/tmp/unraid/keyfile"
|
||||
poll_attributes="1800"
|
||||
poll_attributes_default="1800"
|
||||
poll_attributes_status="default"
|
||||
queueDepth="auto"
|
||||
nr_requests="Auto"
|
||||
nr_requests_default="Auto"
|
||||
nr_requests_status="default"
|
||||
md_scheduler="auto"
|
||||
md_scheduler_default="auto"
|
||||
md_scheduler_status="default"
|
||||
md_num_stripes="1280"
|
||||
md_num_stripes_default="1280"
|
||||
md_num_stripes_status="default"
|
||||
md_queue_limit="80"
|
||||
md_queue_limit_default="80"
|
||||
md_queue_limit_status="default"
|
||||
md_sync_limit="5"
|
||||
md_sync_limit_default="5"
|
||||
md_sync_limit_status="default"
|
||||
md_write_method="auto"
|
||||
md_write_method_default="auto"
|
||||
md_write_method_status="default"
|
||||
shareDisk="yes"
|
||||
shareUser="e"
|
||||
shareUserInclude=""
|
||||
shareUserExclude=""
|
||||
shareSMBEnabled="yes"
|
||||
shareNFSEnabled="no"
|
||||
shareInitialOwner="Administrator"
|
||||
shareInitialGroup="Domain Users"
|
||||
shareCacheEnabled="yes"
|
||||
shareCacheFloor="2000000"
|
||||
shareMoverSchedule="40 3 * * *"
|
||||
shareMoverLogging="no"
|
||||
fuse_remember="330"
|
||||
fuse_remember_default="330"
|
||||
fuse_remember_status="default"
|
||||
fuse_directio="auto"
|
||||
fuse_directio_default="auto"
|
||||
fuse_directio_status="default"
|
||||
fuse_useino="yes"
|
||||
shareAvahiEnabled="yes"
|
||||
shareAvahiSMBName="%h"
|
||||
shareAvahiSMBModel="Xserve"
|
||||
shfs_logging="1"
|
||||
safeMode="no"
|
||||
startMode="Normal"
|
||||
configValid="yes"
|
||||
joinStatus="Not joined"
|
||||
deviceCount="4"
|
||||
flashGUID="0000-0000-0000-000000000000"
|
||||
flashProduct="DataTraveler_3.0"
|
||||
flashVendor="KINGSTON"
|
||||
regCheck=""
|
||||
regFILE="/app/dev/Unraid.net/Pro.key"
|
||||
regGUID="13FE-4200-C300-58C372A52B19"
|
||||
regTy="Pro"
|
||||
regTo="Eli Bosley"
|
||||
regTm="1833409182"
|
||||
regTm2="0"
|
||||
regExp=""
|
||||
regGen="0"
|
||||
sbName="/boot/config/super.dat"
|
||||
sbVersion="2.9.13"
|
||||
sbUpdated="1596079143"
|
||||
sbEvents="173"
|
||||
sbState="1"
|
||||
sbClean="yes"
|
||||
sbSynced="1586819259"
|
||||
sbSyncErrs="0"
|
||||
sbSynced2="1586822456"
|
||||
sbSyncExit="0"
|
||||
sbNumDisks="5"
|
||||
mdColor="green-blink"
|
||||
mdNumDisks="4"
|
||||
mdNumDisabled="1"
|
||||
mdNumInvalid="1"
|
||||
mdNumMissing="0"
|
||||
mdNumNew="0"
|
||||
mdNumErased="0"
|
||||
mdResync="0"
|
||||
mdResyncCorr="0"
|
||||
mdResyncPos="0"
|
||||
mdResyncDb="0"
|
||||
mdResyncDt="0"
|
||||
mdResyncAction="check P"
|
||||
mdResyncSize="438960096"
|
||||
mdState="STOPPED"
|
||||
mdVersion="2.9.14"
|
||||
fsState="Stopped"
|
||||
fsProgress="Autostart disabled"
|
||||
fsCopyPrcnt="0"
|
||||
fsNumMounted="0"
|
||||
fsNumUnmountable="0"
|
||||
fsUnmountableMask=""
|
||||
shareCount="0"
|
||||
shareSMBCount="1"
|
||||
shareNFSCount="0"
|
||||
shareMoverActive="no"
|
||||
reservedNames="parity,parity2,parity3,diskP,diskQ,diskR,disk,disks,flash,boot,user,user0,disk0,disk1,disk2,disk3,disk4,disk5,disk6,disk7,disk8,disk9,disk10,disk11,disk12,disk13,disk14,disk15,disk16,disk17,disk18,disk19,disk20,disk21,disk22,disk23,disk24,disk25,disk26,disk27,disk28,disk29,disk30,disk31"
|
||||
csrf_token="0000000000000000"
|
||||
@@ -50,6 +50,24 @@ services:
|
||||
profiles:
|
||||
- builder
|
||||
|
||||
local:
|
||||
networks:
|
||||
- mothership_default
|
||||
image: unraid-api:development
|
||||
ports:
|
||||
- "3001:3001"
|
||||
build:
|
||||
context: .
|
||||
target: development
|
||||
dockerfile: Dockerfile
|
||||
<<: *volumes
|
||||
command: npm run start:dev
|
||||
environment:
|
||||
- IS_DOCKER=true
|
||||
- GIT_SHA=${GIT_SHA:?err}
|
||||
- IS_TAGGED=${IS_TAGGED}
|
||||
profiles:
|
||||
- builder
|
||||
|
||||
builder:
|
||||
image: unraid-api:builder
|
||||
|
||||
9065
api/package-lock.json
generated
9065
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
163
api/package.json
163
api/package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@unraid/api",
|
||||
"version": "3.2.3",
|
||||
"version": "3.5.3",
|
||||
"main": "dist/index.js",
|
||||
"bin": "dist/unraid-api.cjs",
|
||||
"type": "module",
|
||||
@@ -26,7 +26,7 @@
|
||||
"compile": "tsup --config ./tsup.config.ts",
|
||||
"bundle": "pkg . --public",
|
||||
"build": "npm run compile && npm run bundle",
|
||||
"build:docker": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder",
|
||||
"build:docker": "./scripts/dc.sh run --rm builder",
|
||||
"build-pkg": "./scripts/build.mjs",
|
||||
"codegen": "MOTHERSHIP_GRAPHQL_LINK='https://staging.mothership.unraid.net/ws' graphql-codegen --config codegen.yml -r dotenv/config './.env.staging'",
|
||||
"codegen:watch": "DOTENV_CONFIG_PATH='./.env.staging' graphql-codegen-esm --config codegen.yml --watch -r dotenv/config",
|
||||
@@ -34,8 +34,8 @@
|
||||
"tsc": "tsc --noEmit",
|
||||
"lint": "DEBUG=eslint:cli-engine eslint . --config .eslintrc.cjs",
|
||||
"lint:fix": "DEBUG=eslint:cli-engine eslint . --fix --config .eslintrc.cjs",
|
||||
"test:watch": "vitest --segfault-retry=3 --no-threads",
|
||||
"test": "vitest run --segfault-retry=3 --no-threads",
|
||||
"test:watch": "vitest --segfault-retry=3 --pool=forks",
|
||||
"test": "vitest run --segfault-retry=3 --pool=forks",
|
||||
"coverage": "vitest run --segfault-retry=3 --coverage",
|
||||
"patch:subscriptions-transport-ws": "node ./.scripts/patches/subscriptions-transport-ws.cjs",
|
||||
"release": "standard-version",
|
||||
@@ -45,12 +45,12 @@
|
||||
"start:plugin-verbose": "LOG_CONTEXT=true LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty LOG_LEVEL=trace unraid-api start --debug",
|
||||
"start:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs start --debug'",
|
||||
"restart:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs restart --debug'",
|
||||
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
|
||||
"stop:dev": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development tsup --config ./tsup.config.ts --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs stop --debug'",
|
||||
"start:report": "LOG_MOTHERSHIP_MESSAGES=true LOG_TYPE=pretty NODE_ENV=development LOG_LEVEL=trace NODE_ENV=development LOG_CONTEXT=true tsup --config ./tsup.config.ts --watch --onSuccess 'DOTENV_CONFIG_PATH=./.env.development node -r dotenv/config dist/unraid-api.cjs report --debug'",
|
||||
"start:docker": "docker compose run --rm builder-interactive",
|
||||
"build:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose build dev",
|
||||
"docker:dev": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm --service-ports dev",
|
||||
"docker:test": "GIT_SHA=$(git rev-parse --short HEAD) IS_TAGGED=$(git describe --tags --abbrev=0 --exact-match || echo '') docker-compose run --rm builder npm run test"
|
||||
"build:dev": "./scripts/dc.sh build dev",
|
||||
"start:local": "./scripts/dc.sh run --rm --service-ports local",
|
||||
"start:ddev": "./scripts/dc.sh run --rm --service-ports dev",
|
||||
"start:dtest": "./scripts/dc.sh run --rm builder npm run test"
|
||||
},
|
||||
"files": [
|
||||
".env.staging",
|
||||
@@ -59,20 +59,21 @@
|
||||
"unraid-api"
|
||||
],
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.7.12",
|
||||
"@apollo/server": "^4.6.0",
|
||||
"@apollo/client": "^3.8.9",
|
||||
"@apollo/server": "^4.10.0",
|
||||
"@as-integrations/fastify": "^2.1.1",
|
||||
"@graphql-codegen/client-preset": "^4.0.0",
|
||||
"@graphql-tools/load-files": "^6.6.1",
|
||||
"@graphql-tools/merge": "^8.4.0",
|
||||
"@graphql-tools/schema": "^9.0.17",
|
||||
"@graphql-tools/utils": "^9.2.1",
|
||||
"@graphql-codegen/client-preset": "^4.1.0",
|
||||
"@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.2.9",
|
||||
"@nestjs/core": "^10.3.0",
|
||||
"@nestjs/graphql": "^12.0.11",
|
||||
"@nestjs/passport": "^10.0.2",
|
||||
"@nestjs/platform-fastify": "^10.2.9",
|
||||
"@reduxjs/toolkit": "^1.9.5",
|
||||
"@nestjs/passport": "^10.0.3",
|
||||
"@nestjs/platform-fastify": "^10.3.0",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@reflet/cron": "^1.3.1",
|
||||
"@runonflux/nat-upnp": "^1.0.2",
|
||||
"accesscontrol": "^2.2.1",
|
||||
@@ -85,27 +86,27 @@
|
||||
"catch-exit": "^1.2.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"class-validator": "^0.14.1",
|
||||
"cli-table": "^0.3.11",
|
||||
"command-exists": "^1.2.9",
|
||||
"convert": "^4.10.0",
|
||||
"convert": "^4.14.1",
|
||||
"cors": "^2.8.5",
|
||||
"cross-fetch": "^4.0.0",
|
||||
"docker-event-emitter": "^0.3.0",
|
||||
"dockerode": "^3.3.5",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"find-process": "^1.4.7",
|
||||
"graphql": "^16.8.1",
|
||||
"graphql-fields": "^2.0.3",
|
||||
"graphql-scalars": "^1.21.3",
|
||||
"graphql-scalars": "^1.22.4",
|
||||
"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.2",
|
||||
"graphql-ws": "^5.14.3",
|
||||
"htpasswd-js": "^1.0.2",
|
||||
"ini": "^4.1.0",
|
||||
"ini": "^4.1.1",
|
||||
"ip": "^1.1.8",
|
||||
"jose": "^4.14.2",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -113,96 +114,96 @@
|
||||
"mustache": "^4.2.0",
|
||||
"nanobus": "^4.5.0",
|
||||
"nest-access-control": "^3.1.0",
|
||||
"nestjs-pino": "^3.5.0",
|
||||
"nestjs-pino": "^4.0.0",
|
||||
"node-cache": "^5.1.2",
|
||||
"node-window-polyfill": "^1.0.2",
|
||||
"openid-client": "^5.4.0",
|
||||
"openid-client": "^5.6.4",
|
||||
"p-iteration": "^1.1.8",
|
||||
"p-retry": "^4.6.2",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"pidusage": "^3.0.2",
|
||||
"pino": "^8.16.2",
|
||||
"pino-http": "^8.5.1",
|
||||
"pino-pretty": "^10.2.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"pino": "^8.17.2",
|
||||
"pino-http": "^9.0.0",
|
||||
"pino-pretty": "^10.3.1",
|
||||
"reflect-metadata": "^0.1.14",
|
||||
"request": "^2.88.2",
|
||||
"semver": "^7.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"stoppable": "^1.1.0",
|
||||
"systeminformation": "^5.21.2",
|
||||
"ts-command-line-args": "^2.5.0",
|
||||
"uuid": "^9.0.0",
|
||||
"systeminformation": "^5.21.22",
|
||||
"ts-command-line-args": "^2.5.1",
|
||||
"uuid": "^9.0.1",
|
||||
"ws": "^8.13.0",
|
||||
"wtfnode": "^0.9.1",
|
||||
"xhr2": "^0.2.1",
|
||||
"zod": "^3.22.2"
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/runtime": "^7.21.0",
|
||||
"@babel/runtime": "^7.23.8",
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/fragment-matcher": "^5.0.0",
|
||||
"@graphql-codegen/import-types-preset": "^3.0.0",
|
||||
"@graphql-codegen/typed-document-node": "^5.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.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-typed-document-node/core": "^3.2.0",
|
||||
"@nestjs/testing": "^10.2.10",
|
||||
"@swc/core": "^1.3.81",
|
||||
"@types/async-exit-hook": "^2.0.0",
|
||||
"@types/btoa": "^1.2.3",
|
||||
"@types/bytes": "^3.1.1",
|
||||
"@types/cli-table": "^0.3.1",
|
||||
"@types/command-exists": "^1.2.0",
|
||||
"@nestjs/testing": "^10.3.0",
|
||||
"@swc/core": "^1.3.102",
|
||||
"@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/express": "^4.17.17",
|
||||
"@types/graphql-fields": "^1.3.5",
|
||||
"@types/graphql-type-uuid": "^0.2.3",
|
||||
"@types/ini": "^1.3.31",
|
||||
"@types/lodash": "^4.14.192",
|
||||
"@types/mustache": "^4.2.2",
|
||||
"@types/node": "^18.17.12",
|
||||
"@types/pidusage": "^2.0.2",
|
||||
"@types/pify": "^5.0.1",
|
||||
"@types/semver": "^7.3.13",
|
||||
"@types/sendmail": "^1.4.4",
|
||||
"@types/stoppable": "^1.1.1",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@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/mustache": "^4.2.5",
|
||||
"@types/node": "^20.11.0",
|
||||
"@types/pidusage": "^2.0.5",
|
||||
"@types/pify": "^5.0.4",
|
||||
"@types/semver": "^7.5.6",
|
||||
"@types/sendmail": "^1.4.7",
|
||||
"@types/stoppable": "^1.1.3",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"@types/ws": "^8.5.4",
|
||||
"@types/wtfnode": "^0.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.58.0",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"@types/wtfnode": "^0.7.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.1",
|
||||
"@typescript-eslint/parser": "^6.18.1",
|
||||
"@unraid/eslint-config": "github:unraid/eslint-config",
|
||||
"@vitest/coverage-v8": "^0.34.1",
|
||||
"@vitest/ui": "^0.34.0",
|
||||
"@vitest/coverage-v8": "^1.2.0",
|
||||
"@vitest/ui": "^1.2.0",
|
||||
"camelcase-keys": "^8.0.2",
|
||||
"cz-conventional-changelog": "3.3.0",
|
||||
"eslint": "^8.38.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^48.0.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.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",
|
||||
"execa": "^7.1.1",
|
||||
"filter-obj": "^5.1.0",
|
||||
"got": "^13.0.0",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.11.0",
|
||||
"graphql-codegen-typescript-validation-schema": "^0.12.1",
|
||||
"ip-regex": "^5.0.0",
|
||||
"json-difference": "^1.9.1",
|
||||
"json-difference": "^1.16.0",
|
||||
"map-obj": "^5.0.2",
|
||||
"p-props": "^5.0.0",
|
||||
"path-exists": "^5.0.0",
|
||||
"path-type": "^5.0.0",
|
||||
"pkg": "^5.8.1",
|
||||
"pretty-bytes": "^6.1.0",
|
||||
"pretty-bytes": "^6.1.1",
|
||||
"pretty-ms": "^8.0.0",
|
||||
"standard-version": "^9.5.0",
|
||||
"tsup": "^7.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"typesync": "^0.11.0",
|
||||
"vite-tsconfig-paths": "^4.2.0",
|
||||
"vitest": "^0.34.0",
|
||||
"zx": "^7.2.1"
|
||||
"tsup": "^8.0.1",
|
||||
"typescript": "^5.3.3",
|
||||
"typesync": "^0.12.1",
|
||||
"vite-tsconfig-paths": "^4.2.3",
|
||||
"vitest": "^1.2.0",
|
||||
"zx": "^7.2.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@vmngr/libvirt": "github:unraid/libvirt"
|
||||
|
||||
4
api/scripts/dc.sh
Executable file
4
api/scripts/dc.sh
Executable file
@@ -0,0 +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 "$@"
|
||||
43
api/src/__test__/common/allowed-origins.test.ts
Normal file
43
api/src/__test__/common/allowed-origins.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { expect, test } from 'vitest';
|
||||
|
||||
// Preloading imports for faster tests
|
||||
import '@app/common/allowed-origins';
|
||||
import '@app/store/modules/emhttp';
|
||||
import '@app/store';
|
||||
|
||||
test('Returns allowed origins', async () => {
|
||||
const { store } = await import('@app/store');
|
||||
const { loadStateFiles } = await import('@app/store/modules/emhttp');
|
||||
const { getAllowedOrigins } = await import('@app/common/allowed-origins');
|
||||
const { loadConfigFile } = await import('@app/store/modules/config');
|
||||
|
||||
// Load state files into store
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
// Get allowed origins
|
||||
expect(getAllowedOrigins()).toMatchInlineSnapshot(`
|
||||
[
|
||||
"/var/run/unraid-notifications.sock",
|
||||
"/var/run/unraid-php.sock",
|
||||
"/var/run/unraid-cli.sock",
|
||||
"http://localhost:8080",
|
||||
"https://localhost:4443",
|
||||
"https://tower.local:4443",
|
||||
"https://192.168.1.150:4443",
|
||||
"https://tower:4443",
|
||||
"https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443",
|
||||
"https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443",
|
||||
"https://10-252-0-1.hash.myunraid.net:4443",
|
||||
"https://10-252-1-1.hash.myunraid.net:4443",
|
||||
"https://10-253-3-1.hash.myunraid.net:4443",
|
||||
"https://10-253-4-1.hash.myunraid.net:4443",
|
||||
"https://10-253-5-1.hash.myunraid.net:4443",
|
||||
"https://google.com",
|
||||
"https://test.com",
|
||||
"https://connect.myunraid.net",
|
||||
"https://connect-staging.myunraid.net",
|
||||
"https://dev-my.myunraid.net:4000",
|
||||
]
|
||||
`);
|
||||
});
|
||||
@@ -54,7 +54,7 @@ test('it creates a MEMORY config with NO OPTIONAL values', () => {
|
||||
},
|
||||
"remote": {
|
||||
"accesstoken": "",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
@@ -146,7 +146,7 @@ test('it creates a MEMORY config with OPTIONAL values', () => {
|
||||
"remote": {
|
||||
"2Fa": "yes",
|
||||
"accesstoken": "",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://staging.connect.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"allowedOrigins": "/var/run/unraid-notifications.sock, /var/run/unraid-php.sock, /var/run/unraid-cli.sock, https://connect.myunraid.net, https://connect-staging.myunraid.net, https://dev-my.myunraid.net:4000",
|
||||
"apikey": "",
|
||||
"avatar": "",
|
||||
"dynamicRemoteAccessType": "DISABLED",
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parse } from 'ini';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
import { Serializer } from 'multi-ini';
|
||||
|
||||
test('MultiIni breaks when serializing an object with a boolean inside', async () => {
|
||||
const objectToSerialize = {
|
||||
root: {
|
||||
anonMode: false,
|
||||
},
|
||||
};
|
||||
const serializer = new Serializer({ keep_quotes: false });
|
||||
expect(serializer.serialize(objectToSerialize)).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
anonMode=false
|
||||
"
|
||||
`)
|
||||
});
|
||||
|
||||
test('MultiIni can safely serialize an object with a boolean inside', async () => {
|
||||
const objectToSerialize = {
|
||||
root: {
|
||||
anonMode: false,
|
||||
},
|
||||
};
|
||||
expect(safelySerializeObjectToIni(objectToSerialize)).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
anonMode="false"
|
||||
"
|
||||
`);
|
||||
const result = safelySerializeObjectToIni(objectToSerialize);
|
||||
expect(parse(result)).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"anonMode": false,
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
114
api/src/__test__/core/utils/misc/parse-config.test.ts
Normal file
114
api/src/__test__/core/utils/misc/parse-config.test.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { test, expect } from 'vitest';
|
||||
import { parseConfig } from '@app/core/utils/misc/parse-config';
|
||||
import { Parser as MultiIniParser } from 'multi-ini';
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { parse } from 'ini';
|
||||
import { safelySerializeObjectToIni } from '@app/core/utils/files/safe-ini-serializer';
|
||||
|
||||
const iniTestData = `["root"]
|
||||
idx="0"
|
||||
name="root"
|
||||
desc="Console and webGui login account"
|
||||
passwd="yes"
|
||||
["xo"]
|
||||
idx="1"
|
||||
name="xo"
|
||||
desc=""
|
||||
passwd="yes"
|
||||
["test_user"]
|
||||
idx="2"
|
||||
name="test_user"
|
||||
desc=""
|
||||
passwd="no"`;
|
||||
|
||||
test('it loads a config from a passed in ini file successfully', () => {
|
||||
const res = parseConfig<any>({
|
||||
file: iniTestData,
|
||||
type: 'ini',
|
||||
});
|
||||
expect(res).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
"idx": "0",
|
||||
"name": "root",
|
||||
"passwd": "yes",
|
||||
},
|
||||
"testUser": {
|
||||
"desc": "",
|
||||
"idx": "2",
|
||||
"name": "test_user",
|
||||
"passwd": "no",
|
||||
},
|
||||
"xo": {
|
||||
"desc": "",
|
||||
"idx": "1",
|
||||
"name": "xo",
|
||||
"passwd": "yes",
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(res?.root.desc).toEqual('Console and webGui login account');
|
||||
});
|
||||
|
||||
test('it loads a config from disk properly', () => {
|
||||
const path = './dev/states/var.ini';
|
||||
const res = parseConfig<any>({ filePath: path, type: 'ini' });
|
||||
expect(res.DOMAIN_SHORT).toEqual(undefined);
|
||||
expect(res.domainShort).toEqual('');
|
||||
expect(res.shareCount).toEqual('0');
|
||||
});
|
||||
|
||||
test('Confirm Multi-Ini Parser Still Broken', () => {
|
||||
const parser = new MultiIniParser();
|
||||
const res = parser.parse(iniTestData);
|
||||
expect(res).toMatchInlineSnapshot('{}');
|
||||
});
|
||||
|
||||
test('Combine Ini and Multi-Ini to read and then write a file with quotes', async () => {
|
||||
const parsedFile = parse(iniTestData);
|
||||
expect(parsedFile).toMatchInlineSnapshot(`
|
||||
{
|
||||
"root": {
|
||||
"desc": "Console and webGui login account",
|
||||
"idx": "0",
|
||||
"name": "root",
|
||||
"passwd": "yes",
|
||||
},
|
||||
"test_user": {
|
||||
"desc": "",
|
||||
"idx": "2",
|
||||
"name": "test_user",
|
||||
"passwd": "no",
|
||||
},
|
||||
"xo": {
|
||||
"desc": "",
|
||||
"idx": "1",
|
||||
"name": "xo",
|
||||
"passwd": "yes",
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const ini = safelySerializeObjectToIni(parsedFile);
|
||||
await writeFile('/tmp/test.ini', ini);
|
||||
const file = await readFile('/tmp/test.ini', 'utf-8');
|
||||
expect(file).toMatchInlineSnapshot(`
|
||||
"[root]
|
||||
idx="0"
|
||||
name="root"
|
||||
desc="Console and webGui login account"
|
||||
passwd="yes"
|
||||
[xo]
|
||||
idx="1"
|
||||
name="xo"
|
||||
desc=""
|
||||
passwd="yes"
|
||||
[test_user]
|
||||
idx="2"
|
||||
name="test_user"
|
||||
desc=""
|
||||
passwd="no"
|
||||
"
|
||||
`);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { checkMothershipAuthentication } from "@app/graphql/resolvers/query/cloud/check-mothership-authentication";
|
||||
import { expect, test } from "vitest";
|
||||
import packageJson from '@app/../package.json'
|
||||
|
||||
test('It fails to authenticate with mothership with no credentials', async () => {
|
||||
await expect(checkMothershipAuthentication('BAD', 'BAD')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Failed to connect to https://mothership.unraid.net/ws with a "426" HTTP error.]`);
|
||||
expect(packageJson.version).not.toBeNull();
|
||||
await expect(checkMothershipAuthentication(packageJson.version, 'BAD_API_KEY')).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Invalid credentials]`);
|
||||
}, 15_000)
|
||||
188
api/src/__test__/graphql/resolvers/subscription/network.test.ts
Normal file
188
api/src/__test__/graphql/resolvers/subscription/network.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { expect, test } from 'vitest';
|
||||
import { type Nginx } from '../../../../core/types/states/nginx';
|
||||
import { getUrlForField, getUrlForServer, getServerIps, type NginxUrlFields } from '@app/graphql/resolvers/subscription/network';
|
||||
import { store } from '@app/store';
|
||||
import { loadStateFiles } from '@app/store/modules/emhttp';
|
||||
import { loadConfigFile } from '@app/store/modules/config';
|
||||
|
||||
test.each([
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 123, httpsPort: 443, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 12_345, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 212, httpsPort: 3_233, url: 'my-default-url.com' }],
|
||||
[{ httpPort: 80, httpsPort: 443, url: 'https://BROKEN_URL' }],
|
||||
|
||||
])('getUrlForField', ({ httpPort, httpsPort, url }) => {
|
||||
const responseInsecure = getUrlForField({
|
||||
port: httpPort,
|
||||
url,
|
||||
});
|
||||
|
||||
const responseSecure = getUrlForField({
|
||||
portSsl: httpsPort,
|
||||
url,
|
||||
});
|
||||
if (httpPort === 80) {
|
||||
expect(responseInsecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseInsecure.port).toBe(httpPort.toString());
|
||||
}
|
||||
|
||||
if (httpsPort === 443) {
|
||||
expect(responseSecure.port).toBe('');
|
||||
} else {
|
||||
expect(responseSecure.port).toBe(httpsPort.toString());
|
||||
}
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl disabled', () => {
|
||||
const result = getUrlForServer({ nginx: { lanIp: '192.168.1.1', sslEnabled: false, httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"http://192.168.1.1:123/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl yes, port empty', () => {
|
||||
const result = getUrlForServer(
|
||||
{ nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://192.168.1.1/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field exists, ssl auto', () => {
|
||||
const getResult = async () => getUrlForServer({
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: true, sslMode: 'auto', httpPort: 123, httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanIp',
|
||||
});
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Cannot get IP Based URL for field: "lanIp" SSL mode auto]`);
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', () => {
|
||||
const getResult = async () => getUrlForServer(
|
||||
{
|
||||
nginx: { lanIp: '192.168.1.1', sslEnabled: false, sslMode: 'no' } as const as Nginx,
|
||||
ports: {
|
||||
port: ':123', portSsl: ':445', defaultUrl: new URL('https://my-default-url.unraid.net'),
|
||||
},
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist',
|
||||
});
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port non-empty', () => {
|
||||
const result = getUrlForServer({
|
||||
nginx: { lanFqdn: 'my-fqdn.unraid.net', httpsPort: 445 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net:445/"');
|
||||
});
|
||||
|
||||
test('getUrlForServer - FQDN - field exists, port empty', () => {
|
||||
const result = getUrlForServer({ nginx: { lanFqdn: 'my-fqdn.unraid.net', httpPort: 80, httpsPort: 443 } as const as Nginx,
|
||||
field: 'lanFqdn',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot('"https://my-fqdn.unraid.net/"');
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ nginx: { lanFqdn: 'my-fqdn.unraid.net', sslEnabled: false, sslMode: 'no', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'lanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'yes', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn' as NginxUrlFields }],
|
||||
[{ nginx: { wanFqdn6: 'my-fqdn.unraid.net', sslEnabled: true, sslMode: 'auto', httpPort: 80, httpsPort: 443 } as const as Nginx, field: 'wanFqdn6' as NginxUrlFields }],
|
||||
|
||||
])('getUrlForServer - FQDN', ({ nginx, field }) => {
|
||||
const result = getUrlForServer({ nginx, field });
|
||||
expect(result.toString()).toBe('https://my-fqdn.unraid.net/');
|
||||
});
|
||||
|
||||
test('getUrlForServer - field does not exist, ssl disabled', () => {
|
||||
const getResult = async () => getUrlForServer({ nginx:
|
||||
{ lanFqdn: 'my-fqdn.unraid.net' } as const as Nginx,
|
||||
ports: { portSsl: '', port: '', defaultUrl: new URL('https://my-default-url.unraid.net') },
|
||||
// @ts-expect-error Field doesn't exist
|
||||
field: 'idontexist' });
|
||||
void expect(getResult).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: IP URL Resolver: Could not resolve any access URL for field: "idontexist", is FQDN?: false]`);
|
||||
});
|
||||
|
||||
test('integration test, loading nginx ini and generating all URLs', async () => {
|
||||
await store.dispatch(loadStateFiles());
|
||||
await store.dispatch(loadConfigFile());
|
||||
|
||||
const urls = getServerIps();
|
||||
expect(urls.urls).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"ipv4": "https://tower.local:4443/",
|
||||
"ipv6": "https://tower.local:4443/",
|
||||
"name": "Default",
|
||||
"type": "DEFAULT",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://192.168.1.150:4443/",
|
||||
"name": "LAN IPv4",
|
||||
"type": "LAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://tower:4443/",
|
||||
"name": "LAN Name",
|
||||
"type": "MDNS",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://tower.local:4443/",
|
||||
"name": "LAN MDNS",
|
||||
"type": "MDNS",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://192-168-1-150.thisisfourtyrandomcharacters012345678900.myunraid.net:4443/",
|
||||
"name": "LAN FQDN",
|
||||
"type": "LAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://85-121-123-122.thisisfourtyrandomcharacters012345678900.myunraid.net:8443/",
|
||||
"name": "WAN FQDN",
|
||||
"type": "WAN",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-0-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 0",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-252-1-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 1",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-3-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 3",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-4-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 4",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
{
|
||||
"ipv4": "https://10-253-5-1.hash.myunraid.net:4443/",
|
||||
"name": "WG FQDN 55",
|
||||
"type": "WIREGUARD",
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(urls.errors).toMatchInlineSnapshot(`
|
||||
[
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanIp6", is FQDN?: false],
|
||||
[Error: IP URL Resolver: Could not resolve any access URL for field: "lanFqdn6", is FQDN?: true],
|
||||
[Error: No URL Provided],
|
||||
]
|
||||
`);
|
||||
});
|
||||
111
api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts
Normal file
111
api/src/__test__/mothership/api-key/api-key-check-jobs.test.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { API_KEY_STATUS } from '@app/mothership/api-key/api-key-types';
|
||||
import * as apiKeyCheckJobs from '@app/mothership/jobs/api-key-check-jobs';
|
||||
import * as apiKeyValidator from '@app/mothership/api-key/validate-api-key-with-keyserver';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { type RecursivePartial } from '@app/types/index';
|
||||
import { type RootState } from '@app/store/index';
|
||||
import { logoutUser } from '@app/store/modules/config';
|
||||
|
||||
describe('apiKeyCheckJob Tests', () => {
|
||||
it('API Check Job (with success)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(true);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenLastCalledWith({
|
||||
payload: API_KEY_STATUS.API_KEY_VALID,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
});
|
||||
|
||||
it('API Check Job (with invalid length key)', async () => {
|
||||
// Setup state
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: 'too-short-key' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer').mockResolvedValue(API_KEY_STATUS.API_KEY_VALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
|
||||
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
|
||||
expect(validationSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('API Check Job (with a failure that throws an error - NETWORK_ERROR)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValueOnce(API_KEY_STATUS.NETWORK_ERROR);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: API_KEY_STATUS.NETWORK_ERROR,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
});
|
||||
|
||||
it('API Check Job (with a failure that throws an error - INVALID_RESPONSE)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValueOnce(API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: Keyserver Failure, must retry]`);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith({
|
||||
payload: API_KEY_STATUS.INVALID_KEYSERVER_RESPONSE,
|
||||
type: 'apiKey/setApiKeyState',
|
||||
});
|
||||
}, 10_000);
|
||||
|
||||
it('API Check Job (with failure that results in a log out)', async () => {
|
||||
const getState = vi.fn<[], RecursivePartial<RootState>>().mockReturnValue({
|
||||
apiKey: { status: API_KEY_STATUS.PENDING_VALIDATION },
|
||||
config: { remote: { apikey: '_______________________BIG_API_KEY_HERE_________________________' } },
|
||||
emhttp: { var: { flashGuid: 'my-flash-guid', version: '6.11.5' } },
|
||||
});
|
||||
|
||||
const dispatch = vi.fn();
|
||||
|
||||
const validationSpy = vi.spyOn(apiKeyValidator, 'validateApiKeyWithKeyServer')
|
||||
.mockResolvedValue(API_KEY_STATUS.API_KEY_INVALID);
|
||||
|
||||
await expect(apiKeyCheckJobs.apiKeyCheckJob(getState, dispatch)).resolves.toBe(false);
|
||||
|
||||
expect(validationSpy).toHaveBeenCalledOnce();
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(expect.any(Function));
|
||||
}, 10_000);
|
||||
});
|
||||
@@ -56,7 +56,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
expect(state).toMatchObject(
|
||||
expect.objectContaining({
|
||||
api: {
|
||||
extraOrigins: '',
|
||||
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
|
||||
version: expect.any(String),
|
||||
},
|
||||
connectionStatus: {
|
||||
@@ -114,7 +114,7 @@ test('updateUserConfig merges in changes to current state', async () => {
|
||||
expect(state).toMatchObject(
|
||||
expect.objectContaining({
|
||||
api: {
|
||||
extraOrigins: '',
|
||||
extraOrigins: expect.stringMatching('https://google.com,https://test.com'),
|
||||
version: expect.any(String),
|
||||
},
|
||||
connectionStatus: {
|
||||
|
||||
@@ -984,6 +984,7 @@ test('After init returns values from cfg file for all fields', async () => {
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
|
||||
@@ -103,6 +103,7 @@ test('Returns parsed state file', async () => {
|
||||
"porttelnet": 23,
|
||||
"queueDepth": "auto",
|
||||
"regCheck": "Valid",
|
||||
"regExp": "",
|
||||
"regFile": "/app/dev/Unraid.net/Pro.key",
|
||||
"regGen": "0",
|
||||
"regGuid": "13FE-4200-C300-58C372A52B19",
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { start } from '@app/cli/commands/start';
|
||||
import { stop } from '@app/cli/commands/stop';
|
||||
|
||||
@@ -6,7 +5,6 @@ import { stop } from '@app/cli/commands/stop';
|
||||
* Stop a running API process and then start it again.
|
||||
*/
|
||||
export const restart = async () => {
|
||||
setEnv('LOG_TRANSPORT', 'stdout');
|
||||
await stop();
|
||||
await start();
|
||||
};
|
||||
|
||||
@@ -6,14 +6,12 @@ import { logToSyslog } from '@app/cli/log-to-syslog';
|
||||
import { getters } from '@app/store';
|
||||
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
|
||||
import { API_VERSION } from '@app/environment';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
|
||||
/**
|
||||
* Start a new API process.
|
||||
*/
|
||||
export const start = async () => {
|
||||
// Set process title
|
||||
setEnv('LOG_TRANSPORT', 'stdout');
|
||||
|
||||
process.title = 'unraid-api';
|
||||
const runningProcesses = await getAllUnraidApiPids();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { cliLogger } from '@app/core/log';
|
||||
import { getAllUnraidApiPids } from '@app/cli/get-unraid-api-pid';
|
||||
import { setEnv } from '@app/cli/set-env';
|
||||
import { sleep } from '@app/core/utils/misc/sleep';
|
||||
import pRetry from 'p-retry';
|
||||
|
||||
@@ -9,8 +8,6 @@ import pRetry from 'p-retry';
|
||||
*/
|
||||
|
||||
export const stop = async () => {
|
||||
setEnv('LOG_TRANSPORT', 'stdout');
|
||||
|
||||
try {
|
||||
await pRetry(async (attempts) => {
|
||||
const runningApis = await getAllUnraidApiPids();
|
||||
@@ -18,7 +15,7 @@ export const stop = async () => {
|
||||
if (runningApis.length > 0) {
|
||||
cliLogger.info('Stopping %s unraid-api process(es)...', runningApis.length);
|
||||
runningApis.forEach(pid => process.kill(pid, 'SIGTERM'));
|
||||
|
||||
await sleep(50);
|
||||
const newPids = await getAllUnraidApiPids();
|
||||
|
||||
if (newPids.length > 0) {
|
||||
@@ -31,7 +28,7 @@ export const stop = async () => {
|
||||
return true;
|
||||
}, {
|
||||
retries: 2,
|
||||
minTimeout: 2_000,
|
||||
minTimeout: 1_000,
|
||||
factor: 1,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -11,11 +11,7 @@ export const main = async (...argv: string[]) => {
|
||||
cliLogger.debug(env, 'Loading env file');
|
||||
|
||||
// Set envs
|
||||
setEnv(
|
||||
'LOG_TYPE',
|
||||
process.env.LOG_TYPE ??
|
||||
(command === 'start' || mainOptions.debug ? 'pretty' : 'raw')
|
||||
);
|
||||
setEnv('LOG_TYPE', 'pretty');
|
||||
cliLogger.debug({ paths: getters.paths() }, 'Starting CLI');
|
||||
|
||||
setEnv('DEBUG', mainOptions.debug ?? false);
|
||||
@@ -28,7 +24,7 @@ export const main = async (...argv: string[]) => {
|
||||
if (!process.env.LOG_TRANSPORT) {
|
||||
if (process.env.ENVIRONMENT === 'production' && !mainOptions.debug) {
|
||||
setEnv('LOG_TRANSPORT', 'file');
|
||||
setEnv('LOG_LEVEL', 'DEBUG');
|
||||
setEnv('LOG_LEVEL', 'INFO');
|
||||
} else if (!mainOptions.debug) {
|
||||
// Staging Environment, backgrounded plugin
|
||||
setEnv('LOG_TRANSPORT', 'file');
|
||||
|
||||
@@ -1,93 +1,112 @@
|
||||
import { getters, type RootState, store } from '@app/store';
|
||||
import { uniq } from 'lodash';
|
||||
import { getServerIps, getUrlForField } from '@app/graphql/resolvers/subscription/network';
|
||||
import {
|
||||
getServerIps,
|
||||
getUrlForField,
|
||||
} from '@app/graphql/resolvers/subscription/network';
|
||||
import { FileLoadStatus } from '@app/store/types';
|
||||
import { logger } from '../core';
|
||||
import { GRAPHQL_INTROSPECTION } from '@app/environment';
|
||||
|
||||
const getAllowedSocks = (): string[] => [
|
||||
// Notifier bridge
|
||||
'/var/run/unraid-notifications.sock',
|
||||
// Notifier bridge
|
||||
'/var/run/unraid-notifications.sock',
|
||||
|
||||
// Unraid PHP scripts
|
||||
'/var/run/unraid-php.sock',
|
||||
// Unraid PHP scripts
|
||||
'/var/run/unraid-php.sock',
|
||||
|
||||
// CLI
|
||||
'/var/run/unraid-cli.sock',
|
||||
// CLI
|
||||
'/var/run/unraid-cli.sock',
|
||||
];
|
||||
|
||||
const getLocalAccessUrlsForServer = (state: RootState = store.getState()): string[] => {
|
||||
const { emhttp } = state;
|
||||
if (emhttp.status !== FileLoadStatus.LOADED) {
|
||||
return [];
|
||||
}
|
||||
const getLocalAccessUrlsForServer = (
|
||||
state: RootState = store.getState()
|
||||
): string[] => {
|
||||
const { emhttp } = state;
|
||||
if (emhttp.status !== FileLoadStatus.LOADED) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { nginx } = emhttp;
|
||||
try {
|
||||
return [
|
||||
getUrlForField({ url: 'localhost', port: nginx.httpPort }).toString(),
|
||||
getUrlForField({ url: 'localhost', portSsl: nginx.httpsPort }).toString(),
|
||||
];
|
||||
} catch (error: unknown) {
|
||||
logger.debug('Caught error in getLocalAccessUrlsForServer: \n%o', error);
|
||||
return [];
|
||||
}
|
||||
const { nginx } = emhttp;
|
||||
try {
|
||||
return [
|
||||
getUrlForField({
|
||||
url: 'localhost',
|
||||
port: nginx.httpPort,
|
||||
}).toString(),
|
||||
getUrlForField({
|
||||
url: 'localhost',
|
||||
portSsl: nginx.httpsPort,
|
||||
}).toString(),
|
||||
];
|
||||
} catch (error: unknown) {
|
||||
logger.debug(
|
||||
'Caught error in getLocalAccessUrlsForServer: \n%o',
|
||||
error
|
||||
);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getRemoteAccessUrlsForAllowedOrigins = (state: RootState = store.getState()): string[] => {
|
||||
const { urls } = getServerIps(state);
|
||||
const getRemoteAccessUrlsForAllowedOrigins = (
|
||||
state: RootState = store.getState()
|
||||
): string[] => {
|
||||
const { urls } = getServerIps(state);
|
||||
|
||||
if (urls) {
|
||||
return urls.reduce<string[]>((acc, curr) => {
|
||||
if (curr.ipv4 && curr.ipv6) {
|
||||
acc.push(curr.ipv4.toString());
|
||||
} else if (curr.ipv4) {
|
||||
acc.push(curr.ipv4.toString());
|
||||
} else if (curr.ipv6) {
|
||||
acc.push(curr.ipv6.toString());
|
||||
}
|
||||
if (urls) {
|
||||
return urls.reduce<string[]>((acc, curr) => {
|
||||
if (curr.ipv4 && curr.ipv6 || curr.ipv4) {
|
||||
acc.push(curr.ipv4.toString());
|
||||
} else if (curr.ipv6) {
|
||||
acc.push(curr.ipv6.toString());
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return [];
|
||||
return [];
|
||||
};
|
||||
|
||||
const getExtraOrigins = (): string[] => {
|
||||
const { extraOrigins } = getters.config().api;
|
||||
if (extraOrigins) {
|
||||
return extraOrigins.split(', ').filter(origin => origin.startsWith('http://') || origin.startsWith('https://'));
|
||||
}
|
||||
const { extraOrigins } = getters.config().api;
|
||||
if (extraOrigins) {
|
||||
return extraOrigins
|
||||
.replaceAll(' ', '')
|
||||
.split(',')
|
||||
.filter(
|
||||
(origin) =>
|
||||
origin.startsWith('http://') ||
|
||||
origin.startsWith('https://')
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
return [];
|
||||
};
|
||||
|
||||
const getConnectOrigins = () : string[] => {
|
||||
const connectMain = 'https://connect.myunraid.net';
|
||||
const connectStaging = 'https://staging.connect.myunraid.net';
|
||||
const connectDev = 'https://dev-my.myunraid.net:4000';
|
||||
const getConnectOrigins = (): string[] => {
|
||||
const connectMain = 'https://connect.myunraid.net';
|
||||
const connectStaging = 'https://connect-staging.myunraid.net';
|
||||
const connectDev = 'https://dev-my.myunraid.net:4000';
|
||||
|
||||
return [
|
||||
connectMain,
|
||||
connectStaging,
|
||||
connectDev
|
||||
]
|
||||
}
|
||||
return [connectMain, connectStaging, connectDev];
|
||||
};
|
||||
|
||||
const getApolloSandbox = (): string[] => {
|
||||
if (GRAPHQL_INTROSPECTION) {
|
||||
return ['https://studio.apollographql.com'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (GRAPHQL_INTROSPECTION) {
|
||||
return ['https://studio.apollographql.com'];
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getAllowedOrigins = (state: RootState = store.getState()): string[] => uniq([
|
||||
...getAllowedSocks(),
|
||||
...getLocalAccessUrlsForServer(),
|
||||
...getRemoteAccessUrlsForAllowedOrigins(state),
|
||||
...getExtraOrigins(),
|
||||
...getConnectOrigins(),
|
||||
...getApolloSandbox()
|
||||
|
||||
]).map(url => url.endsWith('/') ? url.slice(0, -1) : url);
|
||||
export const getAllowedOrigins = (
|
||||
state: RootState = store.getState()
|
||||
): string[] =>
|
||||
uniq([
|
||||
...getAllowedSocks(),
|
||||
...getLocalAccessUrlsForServer(),
|
||||
...getRemoteAccessUrlsForAllowedOrigins(state),
|
||||
...getExtraOrigins(),
|
||||
...getConnectOrigins(),
|
||||
...getApolloSandbox(),
|
||||
]).map((url) => (url.endsWith('/') ? url.slice(0, -1) : url));
|
||||
|
||||
@@ -2,6 +2,33 @@ import { pino } from 'pino';
|
||||
import { LOG_TRANSPORT, LOG_TYPE } from '@app/environment';
|
||||
|
||||
import pretty from 'pino-pretty';
|
||||
import { chmodSync, existsSync, mkdirSync, rmSync, statSync } from 'node:fs';
|
||||
import { getters } from '@app/store/index';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const makeLoggingDirectoryIfNotExists = () => {
|
||||
if (!existsSync(getters.paths()['log-base'])) {
|
||||
console.log('Creating logging directory');
|
||||
mkdirSync(getters.paths()['log-base']);
|
||||
}
|
||||
|
||||
chmodSync(getters.paths()['log-base'], 0o644);
|
||||
if (
|
||||
existsSync(`${getters.paths()['log-base']}/stdout.log`) &&
|
||||
statSync(`${getters.paths()['log-base']}/stdout.log`).size > 5_000_000
|
||||
) {
|
||||
rmSync(`${getters.paths()['log-base']}/stdout.log`);
|
||||
}
|
||||
try {
|
||||
rmSync(`${getters.paths()['log-base']}/stdout.log.*`);
|
||||
} catch (e) {
|
||||
// Ignore Error
|
||||
}
|
||||
};
|
||||
|
||||
if (LOG_TRANSPORT === 'file') {
|
||||
makeLoggingDirectoryIfNotExists();
|
||||
}
|
||||
|
||||
export const levels = [
|
||||
'trace',
|
||||
@@ -20,9 +47,12 @@ const level =
|
||||
] ?? 'info';
|
||||
|
||||
export const logDestination = pino.destination({
|
||||
dest: LOG_TRANSPORT === 'file' ? '/var/log/unraid-api/stdout.log' : 1,
|
||||
dest:
|
||||
LOG_TRANSPORT === 'file'
|
||||
? join(getters.paths()['log-base'], 'stdout.log')
|
||||
: 1,
|
||||
minLength: 1_024,
|
||||
sync: false
|
||||
sync: false,
|
||||
});
|
||||
|
||||
const stream =
|
||||
|
||||
@@ -9,7 +9,7 @@ export const setupLogRotation = async () => {
|
||||
'/etc/logrotate.d/unraid-api',
|
||||
`
|
||||
/var/log/unraid-api/*.log {
|
||||
rotate 2
|
||||
rotate 1
|
||||
missingok
|
||||
size 5M
|
||||
}
|
||||
|
||||
@@ -5,14 +5,12 @@ import {
|
||||
diskLayout,
|
||||
} from 'systeminformation';
|
||||
import { map as asyncMap } from 'p-iteration';
|
||||
import { ensurePermission } from '@app/core/utils/permissions/ensure-permission';
|
||||
import { type Context } from '@app/graphql/schema/utils';
|
||||
import {
|
||||
type Disk,
|
||||
DiskInterfaceType,
|
||||
DiskSmartStatus,
|
||||
DiskFsType,
|
||||
} from '@app/graphql/generated/api/types';
|
||||
import { DiskFsType } from '@app/graphql/generated/api/types';
|
||||
import { graphqlLogger } from '@app/core/log';
|
||||
|
||||
const getTemperature = async (
|
||||
|
||||
@@ -113,8 +113,12 @@ export type Var = {
|
||||
regFile: string;
|
||||
regGen: string;
|
||||
regGuid: string;
|
||||
/** Registration time for key */
|
||||
regTm: string;
|
||||
/** Expiration of license for Trial Keys */
|
||||
regTm2: string;
|
||||
/** Expiration of Updates for non-legacy keys */
|
||||
regExp: string | null;
|
||||
/** Who the current Unraid key is registered to. */
|
||||
regTo: string;
|
||||
/** Which type of key this is. */
|
||||
|
||||
@@ -15,6 +15,6 @@ export const PORT = process.env.PORT ?? '/var/run/unraid-api.sock';
|
||||
export const DRY_RUN = process.env.DRY_RUN === 'true';
|
||||
export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS === 'true';
|
||||
export const LOG_CORS = process.env.LOG_CORS === 'true';
|
||||
export const LOG_TYPE = process.env.LOG_TYPE as 'pretty' | 'raw';
|
||||
export const LOG_TYPE = process.env.LOG_TYPE as 'pretty' | 'raw' ?? 'pretty';
|
||||
export const LOG_LEVEL = process.env.LOG_LEVEL as 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
|
||||
export const LOG_TRANSPORT = process.env.LOG_TRANSPORT as 'file' | 'stdout';
|
||||
@@ -688,7 +688,8 @@ export function RegistrationSchema(): z.ZodObject<Properties<Registration>> {
|
||||
guid: z.string().nullish(),
|
||||
keyFile: KeyFileSchema().nullish(),
|
||||
state: RegistrationStateSchema.nullish(),
|
||||
type: registrationTypeSchema.nullish()
|
||||
type: registrationTypeSchema.nullish(),
|
||||
updateExpiration: z.string().nullish()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -327,7 +327,8 @@ export type Disk = {
|
||||
export enum DiskFsType {
|
||||
BTRFS = 'btrfs',
|
||||
VFAT = 'vfat',
|
||||
XFS = 'xfs'
|
||||
XFS = 'xfs',
|
||||
ZFS = 'zfs'
|
||||
}
|
||||
|
||||
export enum DiskInterfaceType {
|
||||
@@ -938,6 +939,7 @@ export type Registration = {
|
||||
keyFile?: Maybe<KeyFile>;
|
||||
state?: Maybe<RegistrationState>;
|
||||
type?: Maybe<registrationType>;
|
||||
updateExpiration?: Maybe<Scalars['String']['output']>;
|
||||
};
|
||||
|
||||
export enum RegistrationState {
|
||||
@@ -2334,6 +2336,7 @@ export type RegistrationResolvers<ContextType = Context, ParentType extends Reso
|
||||
keyFile?: Resolver<Maybe<ResolversTypes['KeyFile']>, ParentType, ContextType>;
|
||||
state?: Resolver<Maybe<ResolversTypes['RegistrationState']>, ParentType, ContextType>;
|
||||
type?: Resolver<Maybe<ResolversTypes['registrationType']>, ParentType, ContextType>;
|
||||
updateExpiration?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ enum DiskFsType {
|
||||
xfs
|
||||
btrfs
|
||||
vfat
|
||||
zfs
|
||||
}
|
||||
|
||||
enum DiskInterfaceType {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
type Query {
|
||||
registration: Registration
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
registration: Registration!
|
||||
}
|
||||
|
||||
type KeyFile {
|
||||
location: String
|
||||
contents: String
|
||||
}
|
||||
|
||||
type Registration {
|
||||
guid: String
|
||||
type: registrationType
|
||||
keyFile: KeyFile
|
||||
state: RegistrationState
|
||||
expiration: String
|
||||
updateExpiration: String
|
||||
}
|
||||
@@ -100,7 +100,8 @@ void am(
|
||||
unlinkUnixPort();
|
||||
|
||||
shutdownApiEvent();
|
||||
process.exitCode = 0;
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
},
|
||||
async (error: NodeJS.ErrnoException) => {
|
||||
@@ -110,6 +111,6 @@ void am(
|
||||
}
|
||||
shutdownApiEvent();
|
||||
// Kill application
|
||||
process.exitCode = 1;
|
||||
process.exit(1);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export const shutdownApiEvent = () => {
|
||||
logger.debug('Writing final configs');
|
||||
writeConfigSync('flash');
|
||||
writeConfigSync('memory');
|
||||
|
||||
logger.debug('Shutting down log destination');
|
||||
logDestination.flushSync();
|
||||
logDestination.destroy();
|
||||
logDestination.destroy();
|
||||
};
|
||||
|
||||
@@ -104,6 +104,7 @@ export type VarIni = {
|
||||
regGuid: string;
|
||||
regTm: string;
|
||||
regTm2: string;
|
||||
regExp?: string;
|
||||
regTo: string;
|
||||
regTy: string;
|
||||
regState: string;
|
||||
@@ -249,6 +250,7 @@ export const parse: StateFileToIniParserMap['var'] = (iniFile) => {
|
||||
regTy:
|
||||
registrationType[iniFile.regTy?.toUpperCase()] ??
|
||||
registrationType.INVALID,
|
||||
regExp: iniFile.regExp ?? null,
|
||||
// Make sure to use a || not a ?? as regCheck can be an empty string
|
||||
regState:
|
||||
RegistrationState[
|
||||
|
||||
@@ -8,17 +8,19 @@ import { Module } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ACGuard, AccessControlModule } from 'nest-access-control';
|
||||
import { LoggerModule } from 'nestjs-pino';
|
||||
import { CronModule } from '@app/unraid-api/cron/cron.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
LoggerModule.forRoot({
|
||||
pinoHttp: {
|
||||
logger: apiLogger,
|
||||
autoLogging: false
|
||||
autoLogging: false,
|
||||
},
|
||||
}),
|
||||
AccessControlModule.forRoles(setupPermissions()),
|
||||
AuthModule,
|
||||
CronModule,
|
||||
GraphModule,
|
||||
RestModule,
|
||||
],
|
||||
@@ -26,10 +28,7 @@ import { LoggerModule } from 'nestjs-pino';
|
||||
providers: [
|
||||
{
|
||||
provide: 'APP_GUARD',
|
||||
useFactory: () =>
|
||||
new GraphqlAuthGuard(
|
||||
new Reflector(),
|
||||
),
|
||||
useFactory: () => new GraphqlAuthGuard(new Reflector()),
|
||||
},
|
||||
{
|
||||
provide: 'APP_GUARD',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { apiLogger } from '@app/core/log';
|
||||
import { BYPASS_PERMISSION_CHECKS } from '@app/environment';
|
||||
import { ServerHeaderStrategy } from '@app/unraid-api/auth/header.strategy';
|
||||
import { IS_PUBLIC_KEY } from '@app/unraid-api/auth/public.decorator';
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('AuthService', () => {
|
||||
|
||||
9
api/src/unraid-api/cron/cron.module.ts
Normal file
9
api/src/unraid-api/cron/cron.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { LogCleanupService } from '@app/unraid-api/cron/log-cleanup.service';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
|
||||
@Module({
|
||||
imports: [ScheduleModule.forRoot()],
|
||||
providers: [LogCleanupService],
|
||||
})
|
||||
export class CronModule {}
|
||||
18
api/src/unraid-api/cron/log-cleanup.service.ts
Normal file
18
api/src/unraid-api/cron/log-cleanup.service.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import { execa } from 'execa';
|
||||
|
||||
@Injectable()
|
||||
export class LogCleanupService {
|
||||
private readonly logger = new Logger(LogCleanupService.name);
|
||||
|
||||
@Cron('0 * * * *')
|
||||
async handleCron() {
|
||||
try {
|
||||
this.logger.debug('Running logrotate');
|
||||
await execa(`/usr/sbin/logrotate`, ['/etc/logrotate.conf']);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { ArrayResolver } from './array.resolver';
|
||||
|
||||
describe('ArrayResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { CloudResolver } from './cloud.resolver';
|
||||
|
||||
describe('CloudResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { ConfigResolver } from './config.resolver';
|
||||
|
||||
describe('ConfigResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DisksResolver } from './disks.resolver';
|
||||
|
||||
describe('DisksResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DisplayResolver } from './display.resolver';
|
||||
|
||||
describe('DisplayResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { DockerContainersResolver } from './docker-containers.resolver';
|
||||
|
||||
describe('DockerContainersResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { FlashResolver } from './flash.resolver';
|
||||
|
||||
describe('FlashResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { InfoResolver } from './info.resolver';
|
||||
|
||||
describe('InfoResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { NotificationsResolver } from './notifications.resolver';
|
||||
|
||||
describe('NotificationsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { OnlineResolver } from './online.resolver';
|
||||
|
||||
describe('OnlineResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { OwnerResolver } from './owner.resolver';
|
||||
|
||||
describe('OwnerResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { RegistrationResolver } from './registration.resolver';
|
||||
|
||||
describe('RegistrationResolver', () => {
|
||||
|
||||
@@ -34,6 +34,9 @@ export class RegistrationResolver {
|
||||
expiration: (
|
||||
1_000 * (isTrial || isExpired ? Number(emhttp.var.regTm2) : 0)
|
||||
).toString(),
|
||||
updateExpiration: emhttp.var.regExp
|
||||
? (Number(emhttp.var.regExp) * 1_000).toString()
|
||||
: null,
|
||||
keyFile: {
|
||||
location: emhttp.var.regFile,
|
||||
contents: await getKeyFile(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { VarsResolver } from './vars.resolver';
|
||||
|
||||
describe('VarsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { VmsResolver } from './vms.resolver';
|
||||
|
||||
describe('VmsResolver', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
import { RestService } from './rest.service';
|
||||
|
||||
describe('RestService', () => {
|
||||
|
||||
@@ -7,7 +7,6 @@ export default defineConfig(() => {
|
||||
// Manually set NODE_ENV to make sure we always run tests in test mode
|
||||
process.env.NODE_ENV = 'test';
|
||||
return {
|
||||
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
globals: true,
|
||||
|
||||
@@ -301,7 +301,7 @@ if [ -e /etc/rc.d/rc.unraid-api ]; then
|
||||
# stop flash backup
|
||||
/etc/rc.d/rc.flash_backup stop &>/dev/null
|
||||
# stop the api gracefully
|
||||
/etc/rc.d/rc.unraid-api stop
|
||||
/etc/rc.d/rc.unraid-api stop &>/dev/null
|
||||
# forcibly stop older clients
|
||||
kill -9 `pidof unraid-api` &>/dev/null
|
||||
# uninstall the api
|
||||
@@ -313,10 +313,13 @@ if [ -e /etc/rc.d/rc.unraid-api ]; then
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/Registration.page && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/DefaultPageLayout.php && [[ -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/include/Wrappers.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/include/ShowChanges.php && [[ -f "$FILE-" ]] && mv -f "$FILE-" "$FILE"
|
||||
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/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"
|
||||
@@ -410,6 +413,7 @@ FILE=/usr/local/emhttp/plugins/dynamix/include/UpdateDNS.php && [[ -f "$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-"
|
||||
@@ -449,15 +453,18 @@ if [[ -n $LINENUM ]]; then
|
||||
mv -f "$FILE~" "$FILE"
|
||||
fi
|
||||
|
||||
# patch showchanges script, starting with 6.11.0-rc1
|
||||
# patch: showchanges, starting with 6.11.0-rc1
|
||||
# ShowChanges.php, in 6.10
|
||||
# search text: $valid = ['/var/tmp/','/tmp/plugins/'];
|
||||
# replacement text: $valid = ['/var/tmp/','/tmp/plugins/','/boot/previous'];
|
||||
FILE=/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges
|
||||
if test -f "${FILE}" && ! grep -q "'/boot/previous'" "${FILE}" &>/dev/null; then
|
||||
# backup the file so it can be restored later
|
||||
cp -f "$FILE" "$FILE-"
|
||||
sed -i '/$valid = \[/c$valid = ['"'/var/tmp/'"','"'/tmp/plugins/'"','"'/boot/previous'"'];' "$FILE"
|
||||
fi
|
||||
FILES=(/usr/local/emhttp/plugins/dynamix.plugin.manager/scripts/showchanges /usr/local/emhttp/plugins/dynamix.plugin.manager/include/ShowChanges.php)
|
||||
for FILE in "${FILES[@]}"; do
|
||||
if test -f "${FILE}" && ! grep -q "'/boot/previous'" "${FILE}" &>/dev/null; then
|
||||
# backup the file so it can be restored later
|
||||
cp -f "$FILE" "$FILE-"
|
||||
sed -i '/$valid = \[/c$valid = ['"'/var/tmp/'"','"'/tmp/plugins/'"','"'/boot/previous'"'];' "$FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
# move settings on flash drive
|
||||
CFG_OLD=/boot/config/plugins/Unraid.net
|
||||
@@ -495,6 +502,26 @@ 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
|
||||
FILE=/usr/local/emhttp/plugins/dynamix/include/Wrappers.php
|
||||
if test -f "${FILE}" && ! grep -q "_var" "${FILE}" &>/dev/null; then
|
||||
TEXT=$(
|
||||
cat <<'END_HEREDOC'
|
||||
function _var(&$name, $key=null, $default='') {
|
||||
return is_null($key) ? ($name ?? $default) : ($name[$key] ?? $default);
|
||||
}
|
||||
?>
|
||||
END_HEREDOC
|
||||
)
|
||||
cp -f "$FILE" "$FILE-"
|
||||
# delete last line of the file if it contains `?>`
|
||||
if test $( tail -n 1 "${FILE}" ) = '?>' ; then
|
||||
sed -i '$ d' "${FILE}"
|
||||
fi
|
||||
echo "${TEXT}" >>"${FILE}"
|
||||
fi
|
||||
|
||||
# install the main txz
|
||||
upgradepkg --install-new --reinstall "${MAINTXZ}"
|
||||
|
||||
@@ -562,7 +589,7 @@ if [ "${PLGTYPE}" = "staging" ]; then
|
||||
CHANGED=yes
|
||||
[[ ! -f "$FILE-" ]] && cp "$FILE" "$FILE-"
|
||||
OLD="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/\";"
|
||||
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
|
||||
NEW="add_header Content-Security-Policy \"frame-ancestors 'self' https://connect-staging.myunraid.net https://connect.myunraid.net/ https://dev-my.myunraid.net:4000/\";"
|
||||
sed -i "s#${OLD}#${NEW}#" "${FILE}"
|
||||
fi
|
||||
fi
|
||||
|
||||
79
plugin/source/dynamix.unraid.net/pkg_build.sh
Executable file
79
plugin/source/dynamix.unraid.net/pkg_build.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
# passes `shellcheck` and `shfmt -i 2`
|
||||
|
||||
[[ "$1" == "s" ]] && env=staging
|
||||
[[ "$1" == "p" ]] && env=production
|
||||
[[ -z "${env}" ]] && echo "usage: [s|p]" && exit 1
|
||||
|
||||
DIR=$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")
|
||||
MAINDIR=$(dirname "$(dirname "${DIR}")")
|
||||
tmpdir=/tmp/tmp.$((RANDOM * 19318203981230 + 40))
|
||||
pluginSrc=$(basename "${DIR}")
|
||||
plugin="${pluginSrc}"
|
||||
[[ "${env}" == "staging" ]] && plugin="${plugin}.staging" && cp "${MAINDIR}/plugins/${pluginSrc}.plg" "${MAINDIR}/plugins/${plugin}.plg"
|
||||
version=$(date +"%Y.%m.%d.%H%M")
|
||||
plgfile="${MAINDIR}/plugins/${plugin}.plg"
|
||||
txzfile="${MAINDIR}/archive/${plugin}-${version}.txz"
|
||||
|
||||
# create txz package
|
||||
mkdir -p "$(dirname "${txzfile}")"
|
||||
mkdir -p "${tmpdir}"
|
||||
# shellcheck disable=SC2046
|
||||
cp --parents -f $(find . -type f ! \( -iname ".DS_Store" -o -iname "pkg_build.sh" -o -iname "makepkg" -o -iname "explodepkg" -o -iname "sftp-config.json" \)) "${tmpdir}/"
|
||||
cd "${tmpdir}" || exit 1
|
||||
if [[ "${env}" == "staging" ]]; then
|
||||
# create README.md for staging plugin
|
||||
mv "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging"
|
||||
sed -i "s@\*\*Unraid Connect\*\*@\*\*Unraid Connect \(staging\)\*\*@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.unraid.net.staging/README.md"
|
||||
sed -i "s@dynamix.unraid.net.plg@dynamix.unraid.net.staging.plg@" "${tmpdir}/usr/local/emhttp/plugins/dynamix.my.servers/Connect.page"
|
||||
fi
|
||||
chmod 0755 -R .
|
||||
sudo chown root:root -R .
|
||||
sudo "${MAINDIR}/source/dynamix.unraid.net/makepkg" -l y -c y "${txzfile}"
|
||||
sudo rm -rf "${tmpdir}"
|
||||
md5=$(md5sum "${txzfile}" | cut -f 1 -d ' ')
|
||||
echo "MD5: ${md5}"
|
||||
sha256=$(sha256sum "${txzfile}" | cut -f 1 -d ' ')
|
||||
echo "SHA256: ${sha256}"
|
||||
|
||||
# test txz package
|
||||
mkdir -p "${tmpdir}"
|
||||
cd "${tmpdir}" || exit 1
|
||||
RET=$(sudo "${MAINDIR}/source/dynamix.unraid.net/explodepkg" "${txzfile}" 2>&1 >/dev/null)
|
||||
sudo rm -rf "${tmpdir}"
|
||||
[[ "${RET}" != "" ]] && echo "Error: invalid txz package created: ${txzfile}" && exit 1
|
||||
cd "${DIR}" || exit 1
|
||||
|
||||
# define vars for plg
|
||||
pluginURL="https://stable.dl.unraid.net/unraid-api/\&name;.plg"
|
||||
downloadserver="https://stable.dl.unraid.net"
|
||||
js_dl_server="https://registration.unraid.net"
|
||||
if [[ "${env}" == "staging" ]]; then
|
||||
pluginURL="https://preview.dl.unraid.net/unraid-api/\&name;.plg"
|
||||
downloadserver="https://preview.dl.unraid.net"
|
||||
js_dl_server="https://registration-dev.unraid.net"
|
||||
fi
|
||||
|
||||
# update plg file
|
||||
sed -i -E "s#(ENTITY name\s*)\".*\"#\1\"${plugin}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY env\s*)\".*\"#\1\"${env}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY version\s*)\".*\"#\1\"${version}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY pluginURL\s*)\".*\"#\1\"${pluginURL}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY MD5\s*)\".*\"#\1\"${md5}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY SHA256\s*)\".*\"#\1\"${sha256}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY downloadserver\s*)\".*\"#\1\"${downloadserver}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY js_dl_server\s*)\".*\"#\1\"${js_dl_server}\"#g" "${plgfile}"
|
||||
|
||||
# set from environment vars
|
||||
sed -i -E "s#(ENTITY API_version\s*)\".*\"#\1\"${API_VERSION}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY API_MD5\s*)\".*\"#\1\"${API_MD5}\"#g" "${plgfile}"
|
||||
sed -i -E "s#(ENTITY API_SHA256\s*)\".*\"#\1\"${API_SHA256}\"#g" "${plgfile}"
|
||||
|
||||
# add changelog for major versions
|
||||
# sed -i "/<CHANGES>/a ###${version}\n" ${plgfile}
|
||||
|
||||
echo
|
||||
grep -E "ENTITY (name|pluginURL|env|version|MD5|SHA256|node_api_version)" "${plgfile}"
|
||||
echo
|
||||
echo "${env} plugin: ${plgfile}"
|
||||
echo "${env} txz: ${txzfile}"
|
||||
@@ -140,7 +140,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();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Menu="About"
|
||||
Menu="About:30"
|
||||
Type="xmenu"
|
||||
Title="Registration"
|
||||
Icon="icon-registration"
|
||||
|
||||
@@ -30,8 +30,7 @@ if (!document.getElementsByTagName(modalsWebComponent).length) {
|
||||
$i18nHost.appendChild($modals);
|
||||
}
|
||||
</script>
|
||||
<?
|
||||
echo "
|
||||
|
||||
<unraid-i18n-host>
|
||||
<unraid-user-profile server='" . $serverState->getServerStateJson() . "'></unraid-user-profile>
|
||||
</unraid-i18n-host>";
|
||||
<unraid-user-profile server="<?= $serverState->getServerStateJsonForHtmlAttr() ?>"></unraid-user-profile>
|
||||
</unraid-i18n-host>
|
||||
|
||||
@@ -15,6 +15,7 @@ $webguiGlobals = $GLOBALS;
|
||||
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
|
||||
|
||||
require_once "$docroot/plugins/dynamix.my.servers/include/reboot-details.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
/**
|
||||
* ServerState class encapsulates server-related information and settings.
|
||||
*
|
||||
@@ -33,7 +34,15 @@ class ServerState
|
||||
protected $webguiGlobals;
|
||||
|
||||
private $var;
|
||||
private $flashbackupCfg;
|
||||
private $apiKey = '';
|
||||
private $apiVersion = '';
|
||||
private $avatar = '';
|
||||
private $email = '';
|
||||
private $extraOrigins = [];
|
||||
private $flashBackupActivated = '';
|
||||
private $hasRemoteApikey = false;
|
||||
private $registeredTime = '';
|
||||
private $username = '';
|
||||
private $connectPluginInstalled = '';
|
||||
private $connectPluginVersion;
|
||||
private $configErrorEnum = [
|
||||
@@ -47,16 +56,19 @@ class ServerState
|
||||
private $rebootDetails;
|
||||
private $caseModel = '';
|
||||
private $keyfileBase64UrlSafe = '';
|
||||
private $updateOsCheck;
|
||||
private $updateOsNotificationsEnabled = false;
|
||||
private $updateOsResponse;
|
||||
private $updateOsIgnoredReleases = [];
|
||||
|
||||
public $myServersFlashCfg = [];
|
||||
public $myServersMemoryCfg = [];
|
||||
public $host = 'unknown';
|
||||
public $combinedKnownOrigins = [];
|
||||
|
||||
public $nginxCfg;
|
||||
public $flashbackupStatus;
|
||||
public $registered;
|
||||
public $nginxCfg = [];
|
||||
public $flashbackupStatus = [];
|
||||
public $registered = false;
|
||||
public $myServersMiniGraphConnected = false;
|
||||
public $keyfileBase64 = '';
|
||||
|
||||
@@ -74,11 +86,42 @@ class ServerState
|
||||
// echo "<pre>" . json_encode($this->webguiGlobals, JSON_PRETTY_PRINT) . "</pre>";
|
||||
|
||||
$this->var = (array)parse_ini_file('state/var.ini');
|
||||
$this->nginxCfg = parse_ini_file('/var/local/emhttp/nginx.ini');
|
||||
$this->nginxCfg = @parse_ini_file('/var/local/emhttp/nginx.ini') ?? [];
|
||||
|
||||
$this->flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
|
||||
$this->flashbackupStatus = (file_exists($this->flashbackupCfg)) ? @parse_ini_file($this->flashbackupCfg) : [];
|
||||
$this->osVersion = $this->var['version'];
|
||||
$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->rebootDetails = new RebootDetails();
|
||||
|
||||
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
|
||||
if ($this->keyfileBase64 !== false) {
|
||||
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
|
||||
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
|
||||
}
|
||||
|
||||
$this->updateOsCheck = new UnraidOsCheck();
|
||||
$this->updateOsIgnoredReleases = $this->updateOsCheck->getIgnoredReleases();
|
||||
$this->updateOsNotificationsEnabled = !empty(@$this->getWebguiGlobal('notify', 'unraidos'));
|
||||
$this->updateOsResponse = $this->updateOsCheck->getUnraidOSCheckResult();
|
||||
|
||||
$this->setConnectValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of a webgui global setting.
|
||||
*/
|
||||
public function getWebguiGlobal(string $key, string $subkey = null) {
|
||||
if (!$subkey) {
|
||||
return _var($this->webguiGlobals, $key, '');
|
||||
}
|
||||
$keyArray = _var($this->webguiGlobals, $key, []);
|
||||
return _var($keyArray, $subkey, '');
|
||||
}
|
||||
|
||||
private function setConnectValues() {
|
||||
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) {
|
||||
$this->connectPluginInstalled = 'dynamix.unraid.net.plg';
|
||||
}
|
||||
@@ -89,11 +132,29 @@ class ServerState
|
||||
$this->connectPluginInstalled .= '_installFailed';
|
||||
}
|
||||
|
||||
// exit early if the plugin is not installed
|
||||
if (!$this->connectPluginInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connectPluginVersion = 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-' . $this->var['version']);
|
||||
|
||||
$this->getMyServersCfgValues();
|
||||
$this->getConnectKnownOrigins();
|
||||
$this->getFlashBackupStatus();
|
||||
}
|
||||
|
||||
private function getFlashBackupStatus() {
|
||||
$flashbackupCfg = '/var/local/emhttp/flashbackup.ini';
|
||||
$this->flashbackupStatus = (file_exists($flashbackupCfg)) ? @parse_ini_file($flashbackupCfg) : [];
|
||||
$this->flashBackupActivated = empty($this->flashbackupStatus['activated']) ? '' : 'true';
|
||||
}
|
||||
|
||||
private function getMyServersCfgValues() {
|
||||
/**
|
||||
* @todo can we read this from somewhere other than the flash? Connect page uses this path and /boot/config/plugins/dynamix.my.servers/myservers.cfg…
|
||||
* - $myservers_memory_cfg_path ='/var/local/emhttp/myservers.cfg';
|
||||
@@ -118,15 +179,17 @@ class ServerState
|
||||
$this->myServersFlashCfg['remote']['dynamicRemoteAccessType'] = "DISABLED";
|
||||
}
|
||||
|
||||
$this->osVersion = $this->var['version'];
|
||||
$this->osVersionBranch = trim(@exec('plugin category /var/log/plugins/unRAIDServer.plg') ?? 'stable');
|
||||
$this->apiKey = $this->myServersFlashCfg['upc']['apikey'] ?? '';
|
||||
$this->apiVersion = $this->myServersFlashCfg['api']['version'] ?? '';
|
||||
$this->avatar = (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '';
|
||||
$this->email = $this->myServersFlashCfg['remote']['email'] ?? '';
|
||||
$this->hasRemoteApikey = !empty($this->myServersFlashCfg['remote']['apikey']);
|
||||
$this->registered = !empty($this->myServersFlashCfg['remote']['apikey']) && $this->connectPluginInstalled;
|
||||
$this->registeredTime = $this->myServersFlashCfg['remote']['regWizTime'] ?? '';
|
||||
$this->username = $this->myServersFlashCfg['remote']['username'] ?? '';
|
||||
}
|
||||
|
||||
$caseModelFile = '/boot/config/plugins/dynamix/case-model.cfg';
|
||||
$this->caseModel = file_exists($caseModelFile) ? file_get_contents($caseModelFile) : '';
|
||||
|
||||
$this->rebootDetails = new RebootDetails();
|
||||
|
||||
private function getConnectKnownOrigins() {
|
||||
/**
|
||||
* Allowed origins warning displayed when the current webGUI URL is NOT included in the known lists of allowed origins.
|
||||
* Include localhost in the test, but only display HTTP(S) URLs that do not include localhost.
|
||||
@@ -141,6 +204,11 @@ class ServerState
|
||||
$combinedOrigins = $allowedOrigins . "," . $extraOrigins; // combine the two strings for easier searching
|
||||
$combinedOrigins = str_replace(" ", "", $combinedOrigins); // replace any spaces with nothing
|
||||
$hostNotKnown = stripos($combinedOrigins, $this->host) === false; // check if the current host is in the combined list of origins
|
||||
|
||||
if ($extraOrigins) {
|
||||
$this->extraOrigins = explode(",", $extraOrigins);
|
||||
}
|
||||
|
||||
if ($hostNotKnown) {
|
||||
$this->combinedKnownOrigins = explode(",", $combinedOrigins);
|
||||
|
||||
@@ -157,25 +225,8 @@ class ServerState
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->keyfileBase64 = empty($this->var['regFILE']) ? null : @file_get_contents($this->var['regFILE']);
|
||||
if ($this->keyfileBase64 !== false) {
|
||||
$this->keyfileBase64 = @base64_encode($this->keyfileBase64);
|
||||
$this->keyfileBase64UrlSafe = str_replace(['+', '/', '='], ['-', '_', ''], trim($this->keyfileBase64));
|
||||
}
|
||||
|
||||
/**
|
||||
* updateOsResponse is provided by the dynamix.plugin.manager/scripts/unraidcheck script saving to /tmp/unraidcheck/result.json
|
||||
*/
|
||||
$this->updateOsResponse = @json_decode(@file_get_contents('/tmp/unraidcheck/result.json'), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of a webgui global setting.
|
||||
*/
|
||||
public function getWebguiGlobal(string $key) {
|
||||
return $this->webguiGlobals[$key];
|
||||
}
|
||||
/**
|
||||
* Retrieve the server information as an associative array
|
||||
*
|
||||
@@ -184,9 +235,9 @@ class ServerState
|
||||
public function getServerState()
|
||||
{
|
||||
$serverState = [
|
||||
"apiKey" => $this->myServersFlashCfg['upc']['apikey'] ?? '',
|
||||
"apiVersion" => $this->myServersFlashCfg['api']['version'] ?? '',
|
||||
"avatar" => (!empty($this->myServersFlashCfg['remote']['avatar']) && $this->connectPluginInstalled) ? $this->myServersFlashCfg['remote']['avatar'] : '',
|
||||
"apiKey" => $this->apiKey,
|
||||
"apiVersion" => $this->apiVersion,
|
||||
"avatar" => $this->avatar,
|
||||
"caseModel" => $this->caseModel,
|
||||
"config" => [
|
||||
'valid' => ($this->var['configValid'] === 'yes'),
|
||||
@@ -196,59 +247,67 @@ class ServerState
|
||||
"connectPluginVersion" => $this->connectPluginVersion,
|
||||
"csrf" => $this->var['csrf_token'],
|
||||
"dateTimeFormat" => [
|
||||
"date" => @$this->getWebguiGlobal('display')['date'] ?? '',
|
||||
"time" => @$this->getWebguiGlobal('display')['time'] ?? '',
|
||||
"date" => @$this->getWebguiGlobal('display', 'date') ?? '',
|
||||
"time" => @$this->getWebguiGlobal('display', 'time') ?? '',
|
||||
],
|
||||
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT']) : '',
|
||||
"description" => $this->var['COMMENT'] ? htmlspecialchars($this->var['COMMENT'], ENT_HTML5, 'UTF-8') : '',
|
||||
"deviceCount" => $this->var['deviceCount'],
|
||||
"email" => $this->myServersFlashCfg['remote']['email'] ?? '',
|
||||
"email" => $this->email,
|
||||
"expireTime" => 1000 * (($this->var['regTy'] === 'Trial' || strstr($this->var['regTy'], 'expired')) ? $this->var['regTm2'] : 0),
|
||||
"extraOrigins" => explode(',', $this->myServersFlashCfg['api']['extraOrigins'] ?? ''),
|
||||
"extraOrigins" => $this->extraOrigins,
|
||||
"flashProduct" => $this->var['flashProduct'],
|
||||
"flashVendor" => $this->var['flashVendor'],
|
||||
"flashBackupActivated" => empty($this->flashbackupStatus['activated']) ? '' : 'true',
|
||||
"flashBackupActivated" => $this->flashBackupActivated,
|
||||
"guid" => $this->var['flashGUID'],
|
||||
"hasRemoteApikey" => !empty($this->myServersFlashCfg['remote']['apikey']),
|
||||
"internalPort" => $_SERVER['SERVER_PORT'],
|
||||
"hasRemoteApikey" => $this->hasRemoteApikey,
|
||||
"internalPort" => _var($_SERVER, 'SERVER_PORT'),
|
||||
"keyfile" => $this->keyfileBase64UrlSafe,
|
||||
"lanIp" => ipaddr(),
|
||||
"locale" => (!empty($_SESSION) && $_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
|
||||
"model" => $this->var['SYS_MODEL'],
|
||||
"name" => htmlspecialchars($this->var['NAME']),
|
||||
"model" => $this->var['SYS_MODEL'] ? htmlspecialchars($this->var['SYS_MODEL'], ENT_HTML5, 'UTF-8') : '',
|
||||
"name" => htmlspecialchars($this->var['NAME'], ENT_HTML5, 'UTF-8'),
|
||||
"osVersion" => $this->osVersion,
|
||||
"osVersionBranch" => $this->osVersionBranch,
|
||||
"protocol" => $_SERVER['REQUEST_SCHEME'],
|
||||
"protocol" => _var($_SERVER, 'REQUEST_SCHEME'),
|
||||
"rebootType" => $this->rebootDetails->getRebootType(),
|
||||
"regDev" => @(int)$this->var['regDev'] ?? 0,
|
||||
"regDevs" => @(int)$this->var['regDevs'] ?? 0,
|
||||
"regGen" => @(int)$this->var['regGen'],
|
||||
"regGuid" => @$this->var['regGUID'] ?? '',
|
||||
"regTo" => @htmlspecialchars($this->var['regTo']) ?? '',
|
||||
"regTo" => @htmlspecialchars($this->var['regTo'], ENT_HTML5, 'UTF-8') ?? '',
|
||||
"regTm" => $this->var['regTm'] ? @$this->var['regTm'] * 1000 : '', // JS expects milliseconds
|
||||
"regTy" => @$this->var['regTy'] ?? '',
|
||||
"regExp" => $this->var['regExp'] ? @$this->var['regExp'] * 1000 : '', // JS expects milliseconds
|
||||
"registered" => $this->registered,
|
||||
"registeredTime" => $this->myServersFlashCfg['remote']['regWizTime'] ?? '',
|
||||
"site" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'],
|
||||
"registeredTime" => $this->registeredTime,
|
||||
"site" => _var($_SERVER, 'REQUEST_SCHEME') . "://" . _var($_SERVER, 'HTTP_HOST'),
|
||||
"state" => strtoupper(empty($this->var['regCheck']) ? $this->var['regTy'] : $this->var['regCheck']),
|
||||
"theme" => [
|
||||
"banner" => !empty($this->getWebguiGlobal('display')['banner']),
|
||||
"bannerGradient" => $this->getWebguiGlobal('display')['showBannerGradient'] === 'yes' ?? false,
|
||||
"bgColor" => ($this->getWebguiGlobal('display')['background']) ? '#' . $this->getWebguiGlobal('display')['background'] : '',
|
||||
"descriptionShow" => (!empty($this->getWebguiGlobal('display')['headerdescription']) && $this->getWebguiGlobal('display')['headerdescription'] !== 'no'),
|
||||
"metaColor" => ($this->getWebguiGlobal('display')['headermetacolor'] ?? '') ? '#' . $this->getWebguiGlobal('display')['headermetacolor'] : '',
|
||||
"name" => $this->getWebguiGlobal('display')['theme'],
|
||||
"textColor" => ($this->getWebguiGlobal('display')['header']) ? '#' . $this->getWebguiGlobal('display')['header'] : '',
|
||||
"banner" => !empty($this->getWebguiGlobal('display', 'banner')),
|
||||
"bannerGradient" => $this->getWebguiGlobal('display', 'showBannerGradient') === 'yes' ?? false,
|
||||
"bgColor" => ($this->getWebguiGlobal('display', 'background')) ? '#' . $this->getWebguiGlobal('display', 'background') : '',
|
||||
"descriptionShow" => (!empty($this->getWebguiGlobal('display', 'headerdescription')) && $this->getWebguiGlobal('display', 'headerdescription') !== 'no'),
|
||||
"metaColor" => ($this->getWebguiGlobal('display', 'headermetacolor') ?? '') ? '#' . $this->getWebguiGlobal('display', 'headermetacolor') : '',
|
||||
"name" => $this->getWebguiGlobal('display', 'theme'),
|
||||
"textColor" => ($this->getWebguiGlobal('display', 'header')) ? '#' . $this->getWebguiGlobal('display', 'header') : '',
|
||||
],
|
||||
"ts" => time(),
|
||||
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
|
||||
"username" => $this->myServersFlashCfg['remote']['username'] ?? '',
|
||||
"wanFQDN" => $this->nginxCfg['NGINX_WANFQDN'] ?? '',
|
||||
"username" => $this->username,
|
||||
"wanFQDN" => @$this->nginxCfg['NGINX_WANFQDN'] ?? '',
|
||||
];
|
||||
|
||||
if ($this->combinedKnownOrigins) {
|
||||
$serverState['combinedKnownOrigins'] = $this->combinedKnownOrigins;
|
||||
}
|
||||
|
||||
if ($this->updateOsIgnoredReleases) {
|
||||
$serverState['updateOsIgnoredReleases'] = $this->updateOsIgnoredReleases;
|
||||
}
|
||||
|
||||
if ($this->updateOsNotificationsEnabled) {
|
||||
$serverState['updateOsNotificationsEnabled'] = $this->updateOsNotificationsEnabled;
|
||||
}
|
||||
|
||||
if ($this->updateOsResponse) {
|
||||
$serverState['updateOsResponse'] = $this->updateOsResponse;
|
||||
}
|
||||
@@ -257,11 +316,21 @@ class ServerState
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server information as a JSON string
|
||||
* Retrieve the server information as JSON
|
||||
*
|
||||
* @return string A JSON string containing server information.
|
||||
* @return string
|
||||
*/
|
||||
public function getServerStateJson() {
|
||||
return json_encode($this->getServerState());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the server information as JSON string with converted special characters to HTML entities
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getServerStateJsonForHtmlAttr() {
|
||||
$json = json_encode($this->getServerState());
|
||||
return htmlspecialchars($json, ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
* 3a. This is done so that the translation is available to the rest of the Unraid webgui.
|
||||
* 3b. Unfortunately there are numerous "special characters" that aren't allowed in Unraid's translation keys as they're automatically stripped out.
|
||||
* 3c. This means that we have to create a new translation key that is a "safe" version of the translation key used in the web components.
|
||||
* 3d. Special characters that are not allowed are: . ( ) { } [ ] ; ' " < > * /
|
||||
* 3e. There are likely more but I'm unable to find the documentation PDF. Go figure.
|
||||
* 3d. Special characters that are not allowed are: ? { } | & ~ ! [ ] ( ) / : * ^ . " '
|
||||
* 3e. There are likely more but I'm unable to find the documentation PDF - updated list of invalid characters above as mentioned in the language guide document.
|
||||
*
|
||||
* Usage example:
|
||||
* ```
|
||||
@@ -49,336 +49,299 @@ class WebComponentTranslations
|
||||
private function initializeTranslations()
|
||||
{
|
||||
$this->translations[$_SESSION['locale'] ?? 'en_US'] = [
|
||||
'LAN IP' => _('LAN IP'),
|
||||
'LAN IP {0}' => sprintf(_('LAN IP %s'), '{0}'),
|
||||
'LAN IP Copied' => _('LAN IP Copied'),
|
||||
'Click to Copy LAN IP {0}' => sprintf(_('Click to copy LAN IP %s'), '{0}'),
|
||||
'Trial Key Expired at {0}' => sprintf(_('Trial Key Expired at %s'), '{0}'),
|
||||
'Trial Key Expires at {0}' => sprintf(_('Trial Key Expires at %s'), '{0}'),
|
||||
'Trial Key Expired {0}' => sprintf(_('Trial Key Expired %s'), '{0}'),
|
||||
'Trial Key Expires in {0}' => sprintf(_('Trial Key Expires in %s'), '{0}'),
|
||||
'Server Up Since {0}' => sprintf(_('Server Up Since %s'), '{0}'),
|
||||
'Uptime {0}' => sprintf(_('Uptime %s'), '{0}'),
|
||||
'year' => sprintf(_('%s year'), '{n}') . ' | ' . sprintf(_('%s years'), '{n}'),
|
||||
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
|
||||
'day' => sprintf(_('%s day'), '{n}') . ' | ' . sprintf(_('%s days'), '{n}'),
|
||||
'hour' => sprintf(_('%s hour'), '{n}') . ' | ' . sprintf(_('%s hours'), '{n}'),
|
||||
'minute' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
|
||||
'second' => sprintf(_('%s second'), '{n}') . ' | ' . sprintf(_('%s seconds'), '{n}'),
|
||||
'{0} {1} Key…' => sprintf(_('%1s %2s Key…'), '{0}', '{1}'),
|
||||
'{0} devices' => sprintf(_('%s devices'), '{0}'),
|
||||
'{0} out of {1} allowed devices – upgrade your key to support more devices' => sprintf(_('%1s out of %2s allowed devices – upgrade your key to support more devices'), '{0}', '{1}'),
|
||||
'{0} out of {1} devices' => sprintf(_('%1s out of %2s devices'), '{0}', '{1}'),
|
||||
'{0} Release Notes' => sprintf(_('%s Release Notes'), '{0}'),
|
||||
'{0} Signed In Successfully' => sprintf(_('%s Signed In Successfully'), '{0}'),
|
||||
'{0} Signed Out Successfully' => sprintf(_('%s Signed Out Successfully'), '{0}'),
|
||||
'{0} Update Available' => sprintf(_('%s Update Available'), '{0}'),
|
||||
'{1} Key {0} Successfully' => sprintf(_('%2s Key %1s Successfully'), '{0}', '{1}'),
|
||||
'<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 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 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 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'),
|
||||
'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}' => sprintf(_('Begin downgrade to %s'), '{0}'),
|
||||
'Beta' => _('Beta'),
|
||||
'Blacklisted USB Flash GUID' => _('Blacklisted USB Flash GUID'),
|
||||
'BLACKLISTED' => _('BLACKLISTED'),
|
||||
'Calculating trial expiration…' => _('Calculating trial expiration…'),
|
||||
'Callback redirect type not present or incorrect' => _('Callback redirect type not present or incorrect'),
|
||||
'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 for OS Updates' => _('Check for OS Updates'),
|
||||
'check for OS updates' => _('check for OS updates'),
|
||||
'Check for Prereleases' => _('Check for Prereleases'),
|
||||
'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'),
|
||||
'Click to close modal' => _('Click to close modal'),
|
||||
'Click to Copy LAN IP {0}' => sprintf(_('Click to copy LAN IP %s'), '{0}'),
|
||||
'Close Dropdown' => _('Close Dropdown'),
|
||||
'Close Modal' => _('Close Modal'),
|
||||
'Close' => _('Close'),
|
||||
'Reload' => _('Reload'),
|
||||
'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!' => sprintf(_('Thank you for purchasing an Unraid %s Key!'), '{0}'),
|
||||
'Thank you for upgrading to an Unraid {0} Key!' => sprintf(_('Thank you for upgrading to an Unraid %s Key!'), '{0}'),
|
||||
'Your {0} Key has been replaced!' => sprintf(_('Your %s Key has been replaced!'), '{0}'),
|
||||
'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'),
|
||||
'Connected' => _('Connected'),
|
||||
'Contact Support' => _('Contact Support'),
|
||||
'Copied' => _('Copied'),
|
||||
'Copy Key URL' => _('Copy Key URL'),
|
||||
'Copy your Key URL: {0}' => sprintf(_('Copy your Key URL: %s'), '{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'),
|
||||
'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}' => sprintf(_('Remark: your WAN IPv4 is %s'), '{0}'),
|
||||
'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}'),
|
||||
'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}…' => sprintf(_('Signing in %s…'), '{0}'),
|
||||
'Signing out {0}…' => sprintf(_('Signing out %s…'), '{0}'),
|
||||
'{0} Signed In Successfully' => sprintf(_('%s Signed In Successfully'), '{0}'),
|
||||
'{0} Signed Out Successfully' => sprintf(_('%s Signed Out Successfully'), '{0}'),
|
||||
'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…' => sprintf(_('%1s %2s Key…'), '{0}', '{1}'),
|
||||
'{1} Key {0} Successfully' => sprintf(_('%2s Key %1s Successfully'), '{0}', '{1}'),
|
||||
'Failed to {0} {1} Key' => sprintf(_('Failed to %1s %2s Key'), '{0}', '{1}'),
|
||||
'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'),
|
||||
'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>' . _('*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>',
|
||||
'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 **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>',
|
||||
'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>' . _('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>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>',
|
||||
'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'),
|
||||
'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' => sprintf(_('Unraid OS %s Released'), '{0}'),
|
||||
'Unraid OS {0} Update Available' => sprintf(_('Unraid OS %s Update Available'), '{0}'),
|
||||
'Unraid {0} Update Available' => sprintf(_('Unraid %s Update Available'), '{0}'),
|
||||
'{0} Update Available' => sprintf(_('%s Update Available'), '{0}'),
|
||||
'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'),
|
||||
'Create Flash Backup' => _('Create Flash Backup'),
|
||||
'Current Version {0}' => sprintf(_('Current Version %s'), '{0}'),
|
||||
'Current Version: Unraid {0}' => sprintf(_('Current Version: Unraid %s'), '{0}'),
|
||||
'New Version: {0}' => sprintf(_('New Version: %s'), '{0}'),
|
||||
'Version: {0}' => sprintf(_('Version: %s'), '{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'),
|
||||
'Last checked: {0}' => sprintf(_('Last checked: %s'), '{0}'),
|
||||
'Downgrade Unraid OS' => _('Downgrade Unraid OS'),
|
||||
'Customizable Dashboard Tiles' => _('Customizable Dashboard Tiles'),
|
||||
'day' => sprintf(_('%s day'), '{n}') . ' | ' . sprintf(_('%s days'), '{n}'),
|
||||
'Deep Linking' => _('Deep Linking'),
|
||||
'DNS issue, unable to resolve wanip4.unraid.net' => _('DNS issue, unable to resolve wanip4.unraid.net'),
|
||||
'Downgrade Unraid OS to {0}' => sprintf(_('Downgrade Unraid OS to %s'), '{0}'),
|
||||
'No downgrade available' => _('No downgrade available'),
|
||||
'Begin downgrade to {0}' => sprintf(_('Begin downgrade to %s'), '{0}'),
|
||||
'Version available for restore {0}' => sprintf(_('Version available for restore %s'), '{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}' => sprintf(_('View Changelog for %s'), '{0}'),
|
||||
'View Changelog & Update' => _('View Changelog & Update'),
|
||||
'{0} Release Notes' => sprintf(_('%s Release Notes'), '{0}'),
|
||||
'Unable to open release notes' => _('Unable to open release notes'),
|
||||
'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.'),
|
||||
'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}' => sprintf(_('Reboot Now to Downgrade to %s'), '{0}'),
|
||||
'Reboot Now to Update to {0}' => sprintf(_('Reboot Now to Update to %s'), '{0}'),
|
||||
'Reboot Required for Downgrade' => _('Reboot Required for Downgrade'),
|
||||
'Reboot Required for Update' => _('Reboot Required for Update'),
|
||||
'Reboot Required for Downgrade to {0}' => sprintf(_('Reboot Required for Downgrade to %s'), '{0}'),
|
||||
'Reboot Required for Update to {0}' => sprintf(_('Reboot Required for Update to %s'), '{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}' => sprintf(_('Original release date %s'), '{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' => sprintf(_('%1s out of %2s devices'), '{0}', '{1}'),
|
||||
'{0} out of {1} allowed devices – upgrade your key to support more devices' => sprintf(_('%1s out of %2s allowed devices – upgrade your key to support more devices'), '{0}', '{1}'),
|
||||
'{0} devices' => sprintf(_('%s devices'), '{0}'),
|
||||
'unlimited' => _('unlimited'),
|
||||
'Unable to check for OS updates' => _('Unable to check for OS updates'),
|
||||
'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}' => sprintf(_('Ineligible for updates released after %s'), '{0}'),
|
||||
'Eligible for updates until {0}' => sprintf(_('Eligible for updates until %s'), '{0}'),
|
||||
'Ineligible as of {0}' => sprintf(_('Ineligible as of %s'), '{0}'),
|
||||
'Eligible for updates for {0}' => sprintf(_('Eligible for updates for %s'), '{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}' => sprintf(_('Unraid %s'), '{0}'),
|
||||
'OS Update Eligibility' => _('OS Update Eligibility'),
|
||||
'Transfer License to New Flash' => _('Transfer License to New Flash'),
|
||||
'Starter' => _('Starter'),
|
||||
'Unleashed' => _('Unleashed'),
|
||||
'Lifetime' => _('Lifetime'),
|
||||
'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'),
|
||||
'Download unraid-api Logs' => _('Download unraid-api Logs'),
|
||||
'Dynamic Remote Access' => _('Dynamic Remote Access'),
|
||||
'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' => _('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}'),
|
||||
'Expired' => _('Expired'),
|
||||
'Expired {0}' => sprintf(_('Expired %s'), '{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.'),
|
||||
'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'),
|
||||
'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}'),
|
||||
'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'),
|
||||
'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 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 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 > 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.'),
|
||||
'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' => _('Install'),
|
||||
'Installed' => _('Installed'),
|
||||
'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'),
|
||||
'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'),
|
||||
'LAN IP' => _('LAN IP'),
|
||||
'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' => _('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'),
|
||||
'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' => sprintf(_('%s minute'), '{n}') . ' | ' . sprintf(_('%s minutes'), '{n}'),
|
||||
'Missing key file' => _('Missing key file'),
|
||||
'month' => sprintf(_('%s month'), '{n}') . ' | ' . sprintf(_('%s months'), '{n}'),
|
||||
'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}'),
|
||||
'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'),
|
||||
'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}'),
|
||||
'Performing actions' => _('Performing actions'),
|
||||
'Please confirm the update details below' => _('Please confirm the update details below'),
|
||||
'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}' => sprintf(_('Reboot Now to Downgrade to %s'), '{0}'),
|
||||
'Reboot Now to Downgrade' => _('Reboot Now to Downgrade'),
|
||||
'Reboot Now to Update to {0}' => sprintf(_('Reboot Now to Update to %s'), '{0}'),
|
||||
'Reboot Now to Update' => _('Reboot Now to Update'),
|
||||
'Reboot Required for Downgrade to {0}' => sprintf(_('Reboot Required for Downgrade to %s'), '{0}'),
|
||||
'Reboot Required for Downgrade' => _('Reboot Required for Downgrade'),
|
||||
'Reboot Required for Update to {0}' => sprintf(_('Reboot Required for Update to %s'), '{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'),
|
||||
'Registered on' => _('Registered on'),
|
||||
'Registered to' => _('Registered to'),
|
||||
'Registration key / USB Flash GUID mismatch' => _('Registration key / USB Flash GUID mismatch'),
|
||||
'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}'),
|
||||
'Replace Key' => _('Replace Key'),
|
||||
'Replaced' => _('Replaced'),
|
||||
'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}'),
|
||||
'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 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}…' => sprintf(_('Signing in %s…'), '{0}'),
|
||||
'Signing In' => _('Signing In'),
|
||||
'Signing out {0}…' => sprintf(_('Signing out %s…'), '{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'),
|
||||
'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!' => sprintf(_('Thank you for purchasing an Unraid %s Key!'), '{0}'),
|
||||
'Thank you for upgrading to an Unraid {0} Key!' => sprintf(_('Thank you for upgrading to an Unraid %s Key!'), '{0}'),
|
||||
'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}' => sprintf(_('Trial Key Expired %s'), '{0}'),
|
||||
'Trial Key Expired at {0}' => sprintf(_('Trial Key Expired at %s'), '{0}'),
|
||||
'Trial Key Expires at {0}' => sprintf(_('Trial Key Expires at %s'), '{0}'),
|
||||
'Trial Key Expires in {0}' => sprintf(_('Trial Key Expires in %s'), '{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'),
|
||||
'unlimited' => _('unlimited'),
|
||||
'Unraid {0} Available' => sprintf(_('Unraid %s Available'), '{0}'),
|
||||
'Unraid {0} Update Available' => sprintf(_('Unraid %s Update Available'), '{0}'),
|
||||
'Unraid {0}' => sprintf(_('Unraid %s'), '{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' => sprintf(_('Unraid OS %s Released'), '{0}'),
|
||||
'Unraid OS {0} Update Available' => sprintf(_('Unraid OS %s Update Available'), '{0}'),
|
||||
'Unraid OS Update Available' => _('Unraid OS Update Available'),
|
||||
'unraid-api is offline' => _('unraid-api is offline'),
|
||||
'Up-to-date' => _('Up-to-date'),
|
||||
'Update Available' => _('Update Available'),
|
||||
'Update Unraid OS confirmation required' => _('Update Unraid OS confirmation required'),
|
||||
'Update Unraid OS' => _('Update Unraid OS'),
|
||||
'Updating 3rd party drivers' => _('Updating 3rd party drivers'),
|
||||
'Upgrade Key' => _('Upgrade Key'),
|
||||
'Upgrade' => _('Upgrade'),
|
||||
'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'),
|
||||
'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' => _('View Changelog'),
|
||||
'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 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 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.'),
|
||||
'View Available Updates' => _('View Available Updates'),
|
||||
'Your license key is not eligible for Unraid OS {0}' => sprintf(_('Your license key is not eligible for Unraid OS %s'), '{0}'),
|
||||
'Unraid {0} Available' => sprintf(_('Unraid %s Available'), '{0}'),
|
||||
'Key ineligible for {0}' => sprintf(_('Key ineligible for %s'), '{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 {0}.' => sprintf(_('You are still eligible to access OS updates that were published on or before %s'), '{0}'),
|
||||
'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 {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.'), '{0}'),
|
||||
'Extend License' => _('Extend License'),
|
||||
'Calculating OS Update Eligibility…' => _('Calculating OS Update Eligibility…'),
|
||||
'Cancel' => _('Cancel'),
|
||||
'Unknown error' => _('Unknown error'),
|
||||
'Installing Extended' => _('Installing Extended'),
|
||||
'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.'),
|
||||
'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 Trial has expired' => _('Your Trial has expired'),
|
||||
'Your Trial key has been extended!' => _('Your Trial key has been extended!'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -49,16 +49,20 @@ if ($cli) {
|
||||
if ($argc > 2) $param1 = $argv[2];
|
||||
} else {
|
||||
$command = $_POST['command'];
|
||||
$param1 = $_POST['param1'];
|
||||
$param1 = $_POST['param1'] ?? '';
|
||||
}
|
||||
if (!in_array($command, $validCommands)) $command = 'none';
|
||||
|
||||
if (!file_exists('/usr/local/sbin/unraid-api') || !file_exists('/usr/local/bin/unraid-api/unraid-api')) {
|
||||
response_complete(406, array('error' => 'Please reinstall the My Servers plugin'));
|
||||
response_complete(406, array('error' => 'Please reinstall the Unraid Connect plugin'));
|
||||
}
|
||||
|
||||
$output = [];
|
||||
$retval = null;
|
||||
|
||||
switch ($command) {
|
||||
case 'start':
|
||||
exec('unraid-api start 2>/dev/null', $output, $retval);
|
||||
$output = implode(PHP_EOL, $output);
|
||||
response_complete(200, array('result' => $output), $output);
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Menu="About"
|
||||
Menu="About:20"
|
||||
Title="Downgrade OS"
|
||||
Icon="icon-update"
|
||||
Tag="upload"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Menu="About"
|
||||
Menu="About:10"
|
||||
Title="Update OS"
|
||||
Icon="icon-update"
|
||||
Tag="upload"
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
/* Copyright 2005-2023, Lime Technology
|
||||
* Copyright 2012-2023, 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,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*/
|
||||
/**
|
||||
* Abstracting this code into a separate file allows us to use it in multiple places without duplicating code.
|
||||
* 1. unraidcheck script can call this
|
||||
* require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
* $unraidOsCheck = new UnraidOsCheck();
|
||||
* $unraidOsCheck->checkForUpdate();
|
||||
*
|
||||
* 2. Unraid webgui web components can GET this file with action params to get updates, ignore updates, etc.
|
||||
* - EX: Unraid webgui web components can check for updates via a GET request and receive a response with the json file directly
|
||||
* - this is useful for the UPC to check for updates and display a model based on the value
|
||||
* - `/plugins/dynamix.plugin.manager/scripts/unraidcheck.php?json=true`
|
||||
* - note the json=true query param to receive a json response
|
||||
*
|
||||
* @param action {'check'|'removeAllIgnored'|'removeIgnoredVersion'|'ignoreVersion'} - the action to perform
|
||||
* @param version {string} - the version to ignore or remove
|
||||
* @param json {string} - if set to true, will return the json response from the external request
|
||||
* @param altUrl {URL} - if set, will use this url instead of the default
|
||||
*/
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
|
||||
|
||||
class UnraidOsCheck
|
||||
{
|
||||
private const BASE_RELEASES_URL = 'https://releases.unraid.net/os';
|
||||
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';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
|
||||
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
|
||||
|
||||
if ($isGetRequest && $getHasAction) {
|
||||
$this->handleGetRequestWithActions();
|
||||
}
|
||||
}
|
||||
|
||||
private function handleGetRequestWithActions()
|
||||
{
|
||||
switch ($_GET['action']) {
|
||||
case 'check':
|
||||
$this->checkForUpdate();
|
||||
break;
|
||||
|
||||
case 'removeAllIgnored':
|
||||
$this->removeAllIgnored();
|
||||
break;
|
||||
|
||||
case 'removeIgnoredVersion':
|
||||
if (isset($_GET['version'])) {
|
||||
$this->removeIgnoredVersion($_GET['version']);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ignoreVersion':
|
||||
if (isset($_GET['version'])) {
|
||||
$this->ignoreVersion($_GET['version']);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->respondWithError(400, "Unhandled action");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function getUnraidOSCheckResult()
|
||||
{
|
||||
if (file_exists(self::JSON_FILE_RESULT)) {
|
||||
return $this->readJsonFile(self::JSON_FILE_RESULT);
|
||||
}
|
||||
}
|
||||
|
||||
public function getIgnoredReleases()
|
||||
{
|
||||
if (!file_exists(self::JSON_FILE_IGNORED)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ignoredData = $this->readJsonFile(self::JSON_FILE_IGNORED);
|
||||
|
||||
if (is_array($ignoredData) && array_key_exists(self::JSON_FILE_IGNORED_KEY, $ignoredData)) {
|
||||
return $ignoredData[self::JSON_FILE_IGNORED_KEY];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/** @todo clean up this method to be more extensible */
|
||||
public function checkForUpdate()
|
||||
{
|
||||
// Multi-language support
|
||||
if (!function_exists('_')) {
|
||||
function _($text) {return $text;}
|
||||
}
|
||||
|
||||
extract(parse_plugin_cfg('dynamix', true));
|
||||
|
||||
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
|
||||
|
||||
$params = [];
|
||||
$params['branch'] = plugin('category', self::PLG_PATH, 'stable');
|
||||
$params['current_version'] = plugin('version', self::PLG_PATH) ?: _var($var,'version');
|
||||
if (_var($var,'regExp')) $params['update_exp'] = date('Y-m-d', _var($var,'regExp')*1);
|
||||
$defaultUrl = self::BASE_RELEASES_URL;
|
||||
// pass a param of altUrl to use the provided url instead of the default
|
||||
$parsedAltUrl = (array_key_exists('altUrl',$_GET) && $_GET['altUrl']) ? $_GET['altUrl'] : null;
|
||||
// if $parsedAltUrl pass to params
|
||||
if ($parsedAltUrl) $params['altUrl'] = $parsedAltUrl;
|
||||
|
||||
$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);
|
||||
}
|
||||
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);
|
||||
$responseMutated = json_decode($response, true);
|
||||
}
|
||||
|
||||
// add params that were used for debugging
|
||||
$responseMutated['params'] = $params;
|
||||
|
||||
// store locally for UPC to access
|
||||
$this->writeJsonFile(self::JSON_FILE_RESULT, $responseMutated);
|
||||
|
||||
// if we have a query param of json=true then just output the json
|
||||
if (array_key_exists('json',$_GET) && $_GET['json']) {
|
||||
header('Content-Type: application/json');
|
||||
echo $response;
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// 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());
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
private function removeAllIgnored()
|
||||
{
|
||||
if (file_exists(self::JSON_FILE_IGNORED)) {
|
||||
$this->deleteJsonFile(self::JSON_FILE_IGNORED);
|
||||
$this->respondWithSuccess([]);
|
||||
}
|
||||
// fail silently if file doesn't exist
|
||||
}
|
||||
|
||||
private function removeIgnoredVersion($removeVersion)
|
||||
{
|
||||
if ($this->isValidSemVerFormat($removeVersion)) {
|
||||
if (file_exists(self::JSON_FILE_IGNORED)) {
|
||||
$existingData = $this->readJsonFile(self::JSON_FILE_IGNORED);
|
||||
|
||||
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY] = array_diff($existingData[self::JSON_FILE_IGNORED_KEY], [$removeVersion]);
|
||||
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
|
||||
$this->respondWithSuccess($existingData);
|
||||
} else {
|
||||
$this->respondWithError(400, "No versions to remove in the JSON file");
|
||||
}
|
||||
} else {
|
||||
$this->respondWithError(400, "No JSON file found");
|
||||
}
|
||||
} else {
|
||||
$this->respondWithError(400, "Invalid removeVersion format");
|
||||
}
|
||||
}
|
||||
|
||||
private function ignoreVersion($version)
|
||||
{
|
||||
if ($this->isValidSemVerFormat($version)) {
|
||||
$newData = [$this::JSON_FILE_IGNORED_KEY => [$version]];
|
||||
$existingData = file_exists(self::JSON_FILE_IGNORED) ? $this->readJsonFile(self::JSON_FILE_IGNORED) : [];
|
||||
|
||||
if (isset($existingData[self::JSON_FILE_IGNORED_KEY])) {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY][] = $version;
|
||||
} else {
|
||||
$existingData[self::JSON_FILE_IGNORED_KEY] = [$version];
|
||||
}
|
||||
|
||||
$this->writeJsonFile(self::JSON_FILE_IGNORED, $existingData);
|
||||
$this->respondWithSuccess($existingData);
|
||||
} else {
|
||||
$this->respondWithError(400, "Invalid version format");
|
||||
}
|
||||
}
|
||||
|
||||
private function isValidSemVerFormat($version)
|
||||
{
|
||||
return preg_match('/^\d+\.\d+(\.\d+)?(-.+)?$/', $version);
|
||||
}
|
||||
|
||||
private function readJsonFile($file)
|
||||
{
|
||||
return @json_decode(@file_get_contents($file), true) ?? [];
|
||||
}
|
||||
|
||||
private function writeJsonFile($file, $data)
|
||||
{
|
||||
if (!is_dir(dirname($file))) { // prevents errors when directory doesn't exist
|
||||
mkdir(dirname($file));
|
||||
}
|
||||
file_put_contents($file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
private function deleteJsonFile($file)
|
||||
{
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
private function respondWithError($statusCode, $message)
|
||||
{
|
||||
http_response_code($statusCode);
|
||||
echo $message;
|
||||
}
|
||||
|
||||
private function respondWithSuccess($data)
|
||||
{
|
||||
http_response_code(200);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode($data, JSON_PRETTY_PRINT);
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate and handle the request for GET requests with actions – vars are duplicated here for multi-use of this file
|
||||
$isGetRequest = !empty($_SERVER) && isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] === 'GET';
|
||||
$getHasAction = $_GET !== null && !empty($_GET) && isset($_GET['action']);
|
||||
if ($isGetRequest && $getHasAction) {
|
||||
new UnraidOsCheck();
|
||||
}
|
||||
@@ -13,60 +13,7 @@
|
||||
?>
|
||||
<?
|
||||
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
|
||||
require_once "$docroot/webGui/include/Wrappers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/PluginHelpers.php";
|
||||
require_once "$docroot/plugins/dynamix.plugin.manager/include/UnraidCheck.php";
|
||||
|
||||
// Multi-language support
|
||||
if (!function_exists('_')) {
|
||||
function _($text) {return $text;}
|
||||
}
|
||||
|
||||
extract(parse_plugin_cfg('dynamix', true));
|
||||
|
||||
$var = (array)@parse_ini_file('/var/local/emhttp/var.ini');
|
||||
$script = "$docroot/webGui/scripts/notify";
|
||||
$server = strtoupper(_var($var,'NAME','server'));
|
||||
$output = _var($notify,'plugin');
|
||||
$plg = '/var/log/plugins/unRAIDServer.plg';
|
||||
|
||||
$params = [];
|
||||
$params['branch'] = plugin('category', $plg, 'stable');
|
||||
$params['current_version'] = plugin('version', $plg) ?: _var($var,'version');
|
||||
if (_var($var,'regExp')) $params['update_exp'] = date('m-d-Y', _var($var,'regExp')*1);
|
||||
$urlbase = 'https://releases.unraid.net/os';
|
||||
$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);
|
||||
}
|
||||
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();
|
||||
|
||||
$json = json_decode($response, true);
|
||||
if (!$json) {
|
||||
$response = json_encode(array('error' => 'Invalid response from '.$urlbase), JSON_PRETTY_PRINT);
|
||||
$json = json_decode($response, true);
|
||||
}
|
||||
|
||||
// add params that were sent to $urlbase
|
||||
$json['params'] = $params;
|
||||
|
||||
// store locally for UPC to access
|
||||
$file = '/tmp/unraidcheck/result.json';
|
||||
if (!is_dir(dirname($file))) mkdir(dirname($file));
|
||||
file_put_contents($file, json_encode($json, JSON_PRETTY_PRINT));
|
||||
|
||||
// send notification if a newer version is available
|
||||
if ($json && array_key_exists('isNewer',$json) && $json['isNewer']) {
|
||||
$newver = (array_key_exists('version',$json) && $json['version']) ? $json['version'] : 'unknown';
|
||||
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");
|
||||
}
|
||||
exit(0);
|
||||
?>
|
||||
$unraidOsCheck = new UnraidOsCheck();
|
||||
$unraidOsCheck->checkForUpdate();
|
||||
|
||||
@@ -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);
|
||||
?>
|
||||
|
||||
@@ -1 +1 @@
|
||||
18.17.1
|
||||
18.19.1
|
||||
@@ -33,7 +33,8 @@ const staticGuid = '1111-1111-5GDB-123412341234';
|
||||
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
|
||||
|
||||
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
|
||||
const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
|
||||
// const twentyDaysAgo = Date.now() - 20 * 24 * 60 * 60 * 1000; // 20 days ago
|
||||
const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000; // 90 days ago
|
||||
const twoDaysAgo = Date.now() - 2 * 24 * 60 * 60 * 1000; // 2 days ago
|
||||
// const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000; // 1 day ago
|
||||
const oneHourFromNow = Date.now() + 60 * 60 * 1000; // 1 hour from now
|
||||
@@ -57,8 +58,8 @@ let regExp: number | undefined;
|
||||
// EBLACKLISTED1
|
||||
// EBLACKLISTED2
|
||||
// ENOCONN
|
||||
const state: ServerState = 'STARTER';
|
||||
let regDev = 0;
|
||||
const state: ServerState = 'TRIAL';
|
||||
let regDevs = 0;
|
||||
let regTy = '';
|
||||
switch (state) {
|
||||
// @ts-ignore
|
||||
@@ -72,18 +73,18 @@ switch (state) {
|
||||
regTy = 'Trial';
|
||||
// @ts-ignore
|
||||
case 'BASIC':
|
||||
regDev = 6;
|
||||
regDevs = 6;
|
||||
// @ts-ignore
|
||||
case 'PLUS':
|
||||
regDev = 12;
|
||||
regDevs = 12;
|
||||
// @ts-ignore
|
||||
case 'PRO':
|
||||
// @ts-ignore
|
||||
case 'STARTER':
|
||||
regDev = 4;
|
||||
regDevs = 6;
|
||||
// regExp = oneHourFromNow;
|
||||
// regExp = oneDayFromNow;
|
||||
regExp = twentyDaysAgo;
|
||||
regExp = ninetyDaysAgo;
|
||||
// regExp = uptime;
|
||||
// regExp = 1696363920000; // nori.local's expiration
|
||||
// @ts-ignore
|
||||
@@ -95,7 +96,7 @@ switch (state) {
|
||||
// regExp = 1696363920000; // nori.local's expiration
|
||||
// @ts-ignore
|
||||
case 'LIFETIME':
|
||||
if (regDev === 0) { regDev = 99999; }
|
||||
if (regDevs === 0) { regDevs = -1; }
|
||||
if (regTy === '') { regTy = state.charAt(0).toUpperCase() + state.substring(1).toLowerCase(); } // title case
|
||||
break;
|
||||
}
|
||||
@@ -103,8 +104,8 @@ switch (state) {
|
||||
const connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
|
||||
// const connectPluginInstalled = '';
|
||||
|
||||
const osVersion = '6.13.0-beta0.22';
|
||||
const osVersionBranch = 'preview';
|
||||
const osVersion = '6.12.5';
|
||||
const osVersionBranch = 'stable';
|
||||
// const parsedRegExp = regExp ? dayjs(regExp).format('YYYY-MM-DD') : undefined;
|
||||
|
||||
// const mimicWebguiUnraidCheck = async (): Promise<ServerUpdateOsResponse | undefined> => {
|
||||
@@ -173,10 +174,13 @@ export const serverState: Server = {
|
||||
textColor: ''
|
||||
},
|
||||
updateOsResponse: {
|
||||
version: '6.13.0-beta0.27',
|
||||
name: 'Unraid 6.13.0-beta0.27',
|
||||
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',
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
type Component,
|
||||
} from 'vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import type { ButtonProps } from '~/types/ui/button';
|
||||
|
||||
export interface ButtonProps {
|
||||
btnStyle?: 'black' | 'fill' | 'gray' | 'outline' | 'outline-black' | 'outline-white' | 'underline' | 'white';
|
||||
btnType?: 'button' | 'submit' | 'reset';
|
||||
click?: () => void;
|
||||
disabled?: boolean;
|
||||
download?: boolean;
|
||||
external?: boolean;
|
||||
href?: string;
|
||||
icon?: Component;
|
||||
iconRight?: Component;
|
||||
iconRightHoverDisplay?: boolean;
|
||||
// iconRightHoverAnimate?: boolean;
|
||||
size?: '12px' | '14px' | '16px' | '18px' | '20px' | '24px';
|
||||
text?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
btnStyle: 'fill',
|
||||
btnType: 'button',
|
||||
@@ -30,6 +14,7 @@ const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
// iconRightHoverAnimate: true,
|
||||
size: '16px',
|
||||
text: '',
|
||||
title: '',
|
||||
});
|
||||
|
||||
defineEmits(['click']);
|
||||
@@ -63,6 +48,9 @@ const classes = computed(() => {
|
||||
case 'underline':
|
||||
buttonColors = 'opacity-75 underline border-transparent transition hover:text-alpha hover:bg-beta hover:border-beta focus:text-alpha focus:bg-beta focus:border-beta hover:opacity-100 focus:opacity-100';
|
||||
break;
|
||||
case 'underline-hover-red':
|
||||
buttonColors = 'opacity-75 underline border-transparent transition hover:text-white hover:bg-unraid-red hover:border-unraid-red focus:text-white focus:bg-unraid-red focus:border-unraid-red hover:opacity-100 focus:opacity-100';
|
||||
break;
|
||||
case 'white':
|
||||
buttonColors = 'text-black bg-white transition hover:bg-grey focus:bg-grey';
|
||||
break;
|
||||
@@ -111,6 +99,7 @@ const classes = computed(() => {
|
||||
:target="external ? '_blank' : ''"
|
||||
:type="!href ? btnType : ''"
|
||||
:class="classes.button"
|
||||
:title="title"
|
||||
@click="click ?? $emit('click')"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -21,20 +21,39 @@ const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
|
||||
const { osVersion, rebootType } = storeToRefs(serverStore);
|
||||
const { available } = storeToRefs(updateOsStore);
|
||||
const { ineligibleText, rebootTypeText } = storeToRefs(updateOsActionsStore);
|
||||
const { osVersion, rebootType, stateDataError } = storeToRefs(serverStore);
|
||||
const { available, availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const { rebootTypeText } = storeToRefs(updateOsActionsStore);
|
||||
|
||||
const showUpdateAvailable = computed(() => !ineligibleText.value && available.value && rebootType.value === '');
|
||||
const updateOsStatus = computed(() => {
|
||||
if (stateDataError.value) { // only allowed to update when server is does not have a state error
|
||||
return null;
|
||||
}
|
||||
|
||||
const rebootRequiredLink = computed(() => {
|
||||
if (rebootType.value === 'downgrade') {
|
||||
return WEBGUI_TOOLS_DOWNGRADE.toString();
|
||||
if (rebootTypeText.value) {
|
||||
return {
|
||||
badgeColor: 'yellow',
|
||||
badgeIcon: ExclamationTriangleIcon,
|
||||
href: rebootType.value === 'downgrade'
|
||||
? WEBGUI_TOOLS_DOWNGRADE.toString()
|
||||
: WEBGUI_TOOLS_UPDATE.toString(),
|
||||
text: t(rebootTypeText.value),
|
||||
};
|
||||
}
|
||||
if (rebootType.value === 'thirdPartyDriversDownloading' || rebootType.value === 'update') {
|
||||
return WEBGUI_TOOLS_UPDATE.toString();
|
||||
|
||||
if (availableWithRenewal.value || available.value) {
|
||||
return {
|
||||
click: () => { updateOsStore.setModalOpen(true); },
|
||||
text: availableWithRenewal.value
|
||||
? t('Update Released')
|
||||
: t('Update Available'),
|
||||
title: availableWithRenewal.value
|
||||
? t('Unraid OS {0} Released', [availableWithRenewal.value])
|
||||
: t('Unraid OS {0} Update Available', [available.value]),
|
||||
};
|
||||
}
|
||||
return '';
|
||||
|
||||
return null;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -56,33 +75,22 @@ const rebootRequiredLink = computed(() => {
|
||||
</UiBadge>
|
||||
</button>
|
||||
|
||||
<a
|
||||
v-if="showUpdateAvailable"
|
||||
:href="WEBGUI_TOOLS_UPDATE.toString()"
|
||||
<component
|
||||
:is="updateOsStatus.href ? 'a' : 'button'"
|
||||
v-if="updateOsStatus"
|
||||
:href="updateOsStatus.href ?? undefined"
|
||||
:title="updateOsStatus.title ?? undefined"
|
||||
class="group"
|
||||
:title="t('Unraid OS {0} Update Available', [available])"
|
||||
@click="updateOsStatus.click ? updateOsStatus.click() : undefined"
|
||||
>
|
||||
<UiBadge
|
||||
color="orange"
|
||||
:icon="BellAlertIcon"
|
||||
:color="updateOsStatus.badgeColor ?? 'orange'"
|
||||
:icon="updateOsStatus.badgeIcon ?? BellAlertIcon"
|
||||
size="12px"
|
||||
>
|
||||
{{ t('Update Available') }}
|
||||
{{ updateOsStatus.text }}
|
||||
</UiBadge>
|
||||
</a>
|
||||
<a
|
||||
v-else-if="rebootRequiredLink"
|
||||
:href="rebootRequiredLink"
|
||||
class="group"
|
||||
>
|
||||
<UiBadge
|
||||
:color="'yellow'"
|
||||
:icon="ExclamationTriangleIcon"
|
||||
size="12px"
|
||||
>
|
||||
{{ t(rebootTypeText) }}
|
||||
</UiBadge>
|
||||
</a>
|
||||
</component>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { XMarkIcon } from '@heroicons/vue/24/outline';
|
||||
import useFocusTrap from '~/composables/useFocusTrap';
|
||||
|
||||
export interface Props {
|
||||
centerContent?: boolean;
|
||||
description?: string;
|
||||
error?: boolean;
|
||||
maxWidth?: string;
|
||||
@@ -11,15 +12,18 @@ export interface Props {
|
||||
showCloseX?: boolean;
|
||||
success?: boolean;
|
||||
t: any;
|
||||
tallContent?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
centerContent: true,
|
||||
description: '',
|
||||
error: false,
|
||||
maxWidth: 'sm:max-w-lg',
|
||||
open: false,
|
||||
showCloseX: false,
|
||||
success: false,
|
||||
tallContent: false,
|
||||
title: '',
|
||||
});
|
||||
watchEffect(() => {
|
||||
@@ -54,23 +58,29 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
tabindex="-1"
|
||||
@keyup.esc="closeModal"
|
||||
>
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
<div
|
||||
class="fixed inset-0 flex min-h-screen w-screen justify-center p-8px sm:p-16px overflow-y-auto"
|
||||
:class="{
|
||||
'items-start sm:items-center': tallContent,
|
||||
'items-center': !tallContent,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||
:title="t('Click to close modal')"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<div class="text-center flex min-h-full items-center justify-center p-4 md:p-0">
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
enter="duration-300 ease-out"
|
||||
enter-from="opacity-0"
|
||||
enter-to="opacity-100"
|
||||
leave="duration-200 ease-in"
|
||||
leave-from="opacity-100"
|
||||
leave-to="opacity-0"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-0 bg-black bg-opacity-80 transition-opacity"
|
||||
:title="t('Click to close modal')"
|
||||
@click="closeModal"
|
||||
/>
|
||||
</TransitionChild>
|
||||
<TransitionChild
|
||||
appear
|
||||
as="template"
|
||||
@@ -88,30 +98,49 @@ const ariaLablledById = computed((): string|undefined => props.title ? `ModalTit
|
||||
success ? 'shadow-green-600/30 border-green-600/10' : '',
|
||||
!error && !success ? 'shadow-orange/10 border-white/10' : '',
|
||||
]"
|
||||
class="text-16px text-beta bg-alpha text-left relative flex flex-col justify-around p-16px my-24px sm:p-24px border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
|
||||
class="text-16px text-beta bg-alpha text-left relative z-10 flex flex-col justify-around border-2 border-solid shadow-xl transform overflow-hidden rounded-lg transition-all sm:w-full"
|
||||
>
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 hidden pt-2 pr-2 sm:block">
|
||||
<button type="button" class="rounded-md text-beta bg-alpha p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2" @click="closeModal">
|
||||
<div v-if="showCloseX" class="absolute z-20 right-0 top-0 pt-4px pr-4px hidden sm:block">
|
||||
<button
|
||||
class="rounded-md text-beta bg-transparent p-2 hover:text-white focus:text-white hover:bg-unraid-red focus:bg-unraid-red focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
type="button"
|
||||
@click="closeModal"
|
||||
>
|
||||
<span class="sr-only">{{ t('Close') }}</span>
|
||||
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<header class="text-center">
|
||||
<header
|
||||
class="relative z-0 grid items-start gap-2 p-16px md:p-24px rounded-t"
|
||||
:class="{
|
||||
'sm:pr-40px': showCloseX,
|
||||
'justify-between': $slots['header'],
|
||||
'justify-center': !$slots['header'],
|
||||
}"
|
||||
>
|
||||
<div class="absolute -z-10 inset-0 opacity-10 bg-beta" />
|
||||
<template v-if="!$slots['header']">
|
||||
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
|
||||
<h1 v-if="title" :id="ariaLablledById" class="text-center text-20px sm:text-24px font-semibold flex flex-wrap justify-center gap-x-4px">
|
||||
{{ title }}
|
||||
<slot name="headerTitle" />
|
||||
</h1>
|
||||
<h2 v-if="description" class="text-20px opacity-75">
|
||||
{{ description }}
|
||||
</h2>
|
||||
</template>
|
||||
<slot name="header" />
|
||||
</header>
|
||||
<slot name="main" />
|
||||
|
||||
<footer v-if="$slots['footer']" class="text-14px relative -mx-16px -mb-16px sm:-mx-24px sm:-mb-24px p-4 sm:p-6">
|
||||
<div
|
||||
v-if="$slots['main'] || description"
|
||||
class="relative max-h-[65vh] tall:max-h-[75vh] flex flex-col gap-y-16px sm:gap-y-24px p-16px md:p-24px overflow-y-auto shadow-inner"
|
||||
:class="centerContent && 'text-center'"
|
||||
>
|
||||
<h2 v-if="description" class="text-18px sm:text-20px opacity-75" v-html="description" />
|
||||
<div v-if="$slots['main']">
|
||||
<slot name="main" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer v-if="$slots['footer']" class="text-14px relative p-16px md:p-24px">
|
||||
<div class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<div class="relative z-10">
|
||||
<slot name="footer" />
|
||||
|
||||
@@ -7,11 +7,15 @@ import '~/assets/main.css';
|
||||
|
||||
import { useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useTrialStore } from '~/store/trial';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
|
||||
const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
const { modalOpen: updateOsModalVisible } = storeToRefs(useUpdateOsStore());
|
||||
const { releaseForUpdate: updateOsChangelogModalVisible } = storeToRefs(useUpdateOsChangelogStore());
|
||||
// import { usePromoStore } from '~/store/promo';
|
||||
// const { promoVisible } = storeToRefs(usePromoStore());
|
||||
// <UpcPromo :t="t" :open="promoVisible" />
|
||||
@@ -21,6 +25,8 @@ const { trialModalVisible } = storeToRefs(useTrialStore());
|
||||
<div class="relative z-[99999]">
|
||||
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
|
||||
<UpcTrial :t="t" :open="trialModalVisible" />
|
||||
<UpdateOsCheckUpdateResponseModal :t="t" :open="updateOsModalVisible" />
|
||||
<UpdateOsChangelogModal :t="t" :open="!!updateOsChangelogModalVisible" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ 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';
|
||||
|
||||
@@ -35,6 +36,7 @@ import UserProfileUptimeExpire from '~/components/UserProfile/UptimeExpire.vue';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const replaceRenewCheckStore = useReplaceRenewStore();
|
||||
const serverStore = useServerStore();
|
||||
const {
|
||||
authAction,
|
||||
@@ -44,6 +46,8 @@ const {
|
||||
flashVendor,
|
||||
flashProduct,
|
||||
keyActions,
|
||||
keyfile,
|
||||
computedRegDevs,
|
||||
regGuid,
|
||||
regTm,
|
||||
regTo,
|
||||
@@ -53,27 +57,33 @@ const {
|
||||
state,
|
||||
stateData,
|
||||
stateDataError,
|
||||
tooManyDevices,
|
||||
} = storeToRefs(serverStore);
|
||||
|
||||
const { outputDateTimeFormatted: formattedRegTm } = useDateTimeHelper(dateTimeFormat.value, t, false, regTm.value);
|
||||
const formattedRegTm = ref<any>();
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const setFormattedRegTm = () => {
|
||||
if (!regTm.value) { return; }
|
||||
|
||||
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 { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, t, true, regTm.value);
|
||||
formattedRegTm.value = outputDateTimeFormatted.value;
|
||||
};
|
||||
watch(regTm, (_newV) => {
|
||||
setFormattedRegTm();
|
||||
});
|
||||
onBeforeMount(() => {
|
||||
setFormattedRegTm();
|
||||
});
|
||||
|
||||
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));
|
||||
// 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 [
|
||||
@@ -83,7 +93,7 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
text: regTy.value,
|
||||
}]
|
||||
: []),
|
||||
...(state.value === 'TRIAL' || state.value === 'EEXPIRED'
|
||||
...(showTrialExpiration.value
|
||||
? [{
|
||||
error: state.value === 'EEXPIRED',
|
||||
label: t('Trial expiration'),
|
||||
@@ -102,13 +112,13 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
text: regTo.value,
|
||||
}]
|
||||
: []),
|
||||
...(regTo.value && regTm.value
|
||||
...(regTo.value && regTm.value && formattedRegTm.value
|
||||
? [{
|
||||
label: t('Registered on'),
|
||||
text: formattedRegTm.value,
|
||||
}]
|
||||
: []),
|
||||
...(regExp.value && (state.value === 'STARTER' || state.value === 'UNLEASHED')
|
||||
...(showUpdateEligibility.value
|
||||
? [{
|
||||
label: t('OS Update Eligibility'),
|
||||
warning: regUpdatesExpired.value,
|
||||
@@ -141,26 +151,24 @@ const items = computed((): RegistrationItemProps[] => {
|
||||
text: flashProduct.value,
|
||||
}]
|
||||
: []),
|
||||
...(!stateDataError.value
|
||||
...(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]),
|
||||
}]
|
||||
: []),
|
||||
...(!stateDataError.value && guid.value
|
||||
...(showTransferStatus.value
|
||||
? [{
|
||||
label: t('Transfer License to New Flash'),
|
||||
component: RegistrationReplaceCheck,
|
||||
componentProps: { t },
|
||||
}]
|
||||
: []),
|
||||
// filter out renew action and only display other key actions…renew is displayed in RegistrationUpdateExpirationAction
|
||||
...(keyActions.value && keyActions.value?.filter(action => !['renew'].includes(action.name)).length > 0
|
||||
...(showFilteredKeyActions.value
|
||||
? [{
|
||||
label: t('License key actions'),
|
||||
component: KeyActions,
|
||||
componentProps: {
|
||||
filterOut: ['renew'],
|
||||
@@ -170,6 +178,13 @@ 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>
|
||||
|
||||
@@ -25,13 +25,16 @@ 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-3 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-start rounded"
|
||||
>
|
||||
<dt class="font-semibold flex flex-row justify-start items-center gap-x-8px">
|
||||
<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">
|
||||
<ShieldExclamationIcon v-if="error" class="w-16px h-16px fill-current" />
|
||||
<span>{{ label }}</span>
|
||||
<span v-html="label" />
|
||||
</dt>
|
||||
<dd class="leading-normal sm:col-span-2">
|
||||
<dd
|
||||
class="leading-normal sm:col-span-3"
|
||||
:class="!label && 'sm:col-start-2'"
|
||||
>
|
||||
<span v-if="text" class="select-all" :class="!error ? 'opacity-75' : ''">
|
||||
{{ text }}
|
||||
</span>
|
||||
|
||||
34
web/components/Ui/Switch.vue
Normal file
34
web/components/Ui/Switch.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
|
||||
export interface Props {
|
||||
label?: string;
|
||||
// propChecked?: boolean;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
label: '',
|
||||
// propChecked: false,
|
||||
});
|
||||
|
||||
const checked = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SwitchGroup>
|
||||
<div class="flex items-center gap-8px p-8px rounded">
|
||||
<Switch
|
||||
v-model="checked"
|
||||
:class="checked ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
|
||||
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
|
||||
>
|
||||
<span v-show="!checked" class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<span
|
||||
:class="checked ? 'translate-x-[26px]' : 'translate-x-[2px]'"
|
||||
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
|
||||
/>
|
||||
</Switch>
|
||||
<SwitchLabel>{{ label }}</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</template>
|
||||
@@ -19,8 +19,8 @@ import { storeToRefs } from 'pinia';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { WEBGUI_TOOLS_UPDATE } from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
|
||||
import 'tailwindcss/tailwind.css';
|
||||
import '~/assets/main.css';
|
||||
@@ -34,8 +34,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
rebootVersion: '',
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const { rebootType } = storeToRefs(serverStore);
|
||||
|
||||
const subtitle = computed(() => {
|
||||
@@ -50,7 +50,7 @@ const showLoader = computed(() => window.location.pathname === WEBGUI_TOOLS_UPDA
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (showLoader.value) {
|
||||
updateOsActionsStore.executeUpdateOsCallback(true);
|
||||
accountStore.updateOs(true);
|
||||
}
|
||||
serverStore.setRebootVersion(props.rebootVersion);
|
||||
});
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
|
||||
defineProps<{
|
||||
t: any;
|
||||
}>();
|
||||
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const accountStore = useAccountStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,7 +17,7 @@ const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
:text="t('Check for OS Updates')"
|
||||
class="flex-0"
|
||||
@click="updateOsActionsStore.executeUpdateOsCallback()"
|
||||
@click="accountStore.updateOs()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
139
web/components/UpdateOs/ChangelogModal.vue
Normal file
139
web/components/UpdateOs/ChangelogModal.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
ArrowSmallRightIcon,
|
||||
EyeIcon,
|
||||
KeyIcon,
|
||||
ServerStackIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
// import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
// const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const { availableWithRenewal } = storeToRefs(updateOsStore);
|
||||
const {
|
||||
releaseForUpdate,
|
||||
mutatedParsedChangelog,
|
||||
parseChangelogFailed,
|
||||
parsedChangelogTitle,
|
||||
} = storeToRefs(updateOsChangelogStore);
|
||||
|
||||
const showExtendKeyButton = computed(() => {
|
||||
return availableWithRenewal.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:center-content="false"
|
||||
:error="!!parseChangelogFailed"
|
||||
max-width="max-w-800px"
|
||||
:open="!!releaseForUpdate"
|
||||
:show-close-x="true"
|
||||
:t="t"
|
||||
:tall-content="true"
|
||||
:title="parsedChangelogTitle ?? undefined"
|
||||
@close="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
>
|
||||
<template #main>
|
||||
<div
|
||||
v-if="mutatedParsedChangelog"
|
||||
class="text-16px sm:text-18px prose prose-a:text-unraid-red hover:prose-a:no-underline hover:prose-a:text-unraid-red/60 dark:prose-a:text-orange hover:dark:prose-a:text-orange/60"
|
||||
v-html="mutatedParsedChangelog"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-else-if="parseChangelogFailed"
|
||||
class="text-center flex flex-col gap-4 prose"
|
||||
>
|
||||
<h2 class="text-lg text-unraid-red italic font-semibold">
|
||||
{{ props.t(`Error Parsing Changelog • {0}`, [parseChangelogFailed]) }}
|
||||
</h2>
|
||||
<p>
|
||||
{{ props.t(`It's highly recommended to review the changelog before continuing your update`) }}
|
||||
</p>
|
||||
<div
|
||||
v-if="releaseForUpdate?.changelogPretty"
|
||||
class="flex self-center"
|
||||
>
|
||||
<BrandButton
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t("View Changelog on Docs") }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="text-center flex flex-col justify-center w-full min-h-[250px] min-w-[280px] sm:min-w-[400px]"
|
||||
>
|
||||
<BrandLoading class="w-[150px] mx-auto mt-24px" />
|
||||
<p>{{ props.t("Fetching & parsing changelog…") }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex flex-col-reverse xs:flex-row justify-between gap-12px md:gap-16px">
|
||||
<div class="flex flex-col-reverse xs:flex-row xs:justify-start gap-12px md:gap-16px">
|
||||
<BrandButton
|
||||
btn-style="underline-hover-red"
|
||||
:icon="XMarkIcon"
|
||||
@click="updateOsChangelogStore.setReleaseForUpdate(null)"
|
||||
>
|
||||
{{ props.t("Close") }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
v-if="releaseForUpdate?.changelogPretty"
|
||||
btn-style="underline"
|
||||
:external="true"
|
||||
:href="releaseForUpdate?.changelogPretty"
|
||||
:icon="EyeIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
>
|
||||
{{ props.t("View on Docs") }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
<BrandButton
|
||||
v-if="showExtendKeyButton"
|
||||
btn-style="fill"
|
||||
:icon="KeyIcon"
|
||||
:icon-right="ArrowTopRightOnSquareIcon"
|
||||
@click="purchaseStore.renew()"
|
||||
>
|
||||
{{ props.t("Extend License to Update") }}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
v-else-if="releaseForUpdate?.sha256"
|
||||
:icon="ServerStackIcon"
|
||||
:icon-right="ArrowSmallRightIcon"
|
||||
@click="updateOsChangelogStore.fetchAndConfirmInstall(releaseForUpdate.sha256)"
|
||||
>
|
||||
{{ props.t('Continue') }}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
334
web/components/UpdateOs/CheckUpdateResponseModal.vue
Normal file
334
web/components/UpdateOs/CheckUpdateResponseModal.vue
Normal file
@@ -0,0 +1,334 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ArrowTopRightOnSquareIcon,
|
||||
CogIcon,
|
||||
EyeIcon,
|
||||
IdentificationIcon,
|
||||
KeyIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
|
||||
import useDateTimeHelper from '~/composables/dateTime';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { usePurchaseStore } from '~/store/purchase';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsChangelogStore } from '~/store/updateOsChangelog';
|
||||
import type { ButtonProps } from '~/types/ui/button';
|
||||
|
||||
export interface Props {
|
||||
open?: boolean;
|
||||
t: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
open: false,
|
||||
});
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const purchaseStore = usePurchaseStore();
|
||||
const serverStore = useServerStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
const updateOsChangelogStore = useUpdateOsChangelogStore();
|
||||
|
||||
const {
|
||||
regExp,
|
||||
regUpdatesExpired,
|
||||
dateTimeFormat,
|
||||
updateOsIgnoredReleases,
|
||||
updateOsNotificationsEnabled,
|
||||
updateOsResponse,
|
||||
} = storeToRefs(serverStore);
|
||||
const {
|
||||
available,
|
||||
availableWithRenewal,
|
||||
availableReleaseDate,
|
||||
availableRequiresAuth,
|
||||
checkForUpdatesLoading,
|
||||
} = storeToRefs(updateOsStore);
|
||||
|
||||
/**
|
||||
* 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<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();
|
||||
});
|
||||
|
||||
const ignoreThisRelease = ref(false);
|
||||
// if we had a release ignored and now we don't set ignoreThisRelease to false
|
||||
watch(updateOsIgnoredReleases, (newVal, oldVal) => {
|
||||
if (oldVal.length > 0 && newVal.length === 0) {
|
||||
ignoreThisRelease.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const notificationsSettings = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value
|
||||
? props.t('Go to Settings > Notifications to enable automatic OS update notifications for future releases.')
|
||||
: undefined;
|
||||
});
|
||||
|
||||
interface ModalCopy {
|
||||
title: string;
|
||||
description?: string;
|
||||
}
|
||||
const modalCopy = computed((): ModalCopy | null => {
|
||||
if (checkForUpdatesLoading.value) {
|
||||
return {
|
||||
title: props.t('Checking for OS updates...'),
|
||||
};
|
||||
}
|
||||
|
||||
// Use the release date
|
||||
let formattedReleaseDate = '';
|
||||
if (availableReleaseDate.value) {
|
||||
// build string with prefix
|
||||
formattedReleaseDate = props.t('Release date {0}', [userFormattedReleaseDate.value]);
|
||||
}
|
||||
|
||||
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]);
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Released', [availableWithRenewal.value]),
|
||||
description: `<p>${formattedReleaseDate}</p><p>${description}</p>`,
|
||||
};
|
||||
} else if (available.value) {
|
||||
const description = availableRequiresAuth.value
|
||||
? props.t('Release requires verification to update')
|
||||
: undefined;
|
||||
return {
|
||||
title: props.t('Unraid OS {0} Update Available', [available.value]),
|
||||
description: description ? `<p>${formattedReleaseDate}</p><p>${description}</p>` : formattedReleaseDate,
|
||||
};
|
||||
} else if (!available.value && !availableWithRenewal.value) {
|
||||
return {
|
||||
title: props.t('Unraid OS is up-to-date'),
|
||||
description: notificationsSettings.value ?? undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const showNotificationsSettingsLink = computed(() => {
|
||||
return !updateOsNotificationsEnabled.value && !available.value && !availableWithRenewal.value;
|
||||
});
|
||||
|
||||
const extraLinks = computed((): ButtonProps[] => {
|
||||
const buttons: ButtonProps[] = [];
|
||||
|
||||
if (showNotificationsSettingsLink.value) {
|
||||
buttons.push({
|
||||
btnStyle: 'outline',
|
||||
href: '/Settings/Notifications',
|
||||
icon: CogIcon,
|
||||
text: props.t('Enable update notifications'),
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
});
|
||||
|
||||
const actionButtons = computed((): ButtonProps[] | null => {
|
||||
// update not available or no action buttons default closing
|
||||
if (!available.value || ignoreThisRelease.value) { return null; }
|
||||
|
||||
const buttons: ButtonProps[] = [];
|
||||
|
||||
// update available but not stable branch - should link out to account update callback
|
||||
// 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,
|
||||
text: props.t('Verify to Update'),
|
||||
});
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
// update available - open changelog to commence update
|
||||
if (available.value && updateOsResponse.value?.changelog) {
|
||||
buttons.push({
|
||||
btnStyle: availableWithRenewal.value
|
||||
? 'outline'
|
||||
: undefined,
|
||||
click: async () => await updateOsChangelogStore.setReleaseForUpdate(updateOsResponse.value ?? null),
|
||||
icon: EyeIcon,
|
||||
text: availableWithRenewal.value
|
||||
? props.t('View Changelog')
|
||||
: props.t('View Changelog to Start Update'),
|
||||
});
|
||||
}
|
||||
|
||||
// update available with renewal - open changelog and Extend License options
|
||||
if (availableWithRenewal.value) {
|
||||
buttons.push({
|
||||
click: async () => await purchaseStore.renew(),
|
||||
icon: KeyIcon,
|
||||
iconRight: ArrowTopRightOnSquareIcon,
|
||||
iconRightHoverDisplay: false,
|
||||
text: props.t('Extend License'),
|
||||
title: props.t('Pay your annual fee to continue receiving OS updates.'),
|
||||
});
|
||||
}
|
||||
|
||||
return buttons;
|
||||
});
|
||||
|
||||
const close = () => {
|
||||
// close it
|
||||
updateOsStore.setModalOpen(false);
|
||||
// then ignore the release if applicable
|
||||
if (ignoreThisRelease.value && (availableWithRenewal.value || available.value)) {
|
||||
setTimeout(() => {
|
||||
serverStore.updateOsIgnoreRelease(availableWithRenewal.value ?? available.value ?? '');
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
const renderMainSlot = computed(() => {
|
||||
return checkForUpdatesLoading.value || available.value || availableWithRenewal.value || extraLinks.value?.length > 0 || updateOsIgnoredReleases.value.length > 0;
|
||||
});
|
||||
|
||||
const userFormattedReleaseDate = ref<any>();
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
const setUserFormattedReleaseDate = () => {
|
||||
if (!availableReleaseDate.value) { return; }
|
||||
|
||||
const { outputDateTimeFormatted } = useDateTimeHelper(dateTimeFormat.value, props.t, true, availableReleaseDate.value.valueOf());
|
||||
userFormattedReleaseDate.value = outputDateTimeFormatted.value;
|
||||
};
|
||||
watch(availableReleaseDate, (_newV) => {
|
||||
setUserFormattedReleaseDate();
|
||||
});
|
||||
onBeforeMount(() => {
|
||||
if (availableReleaseDate.value) {
|
||||
setUserFormattedReleaseDate();
|
||||
}
|
||||
setFormattedRegExp();
|
||||
});
|
||||
|
||||
const modalWidth = computed(() => {
|
||||
if (availableWithRenewal.value) { // wider since we'll have four buttons
|
||||
return 'max-w-800px';
|
||||
}
|
||||
return 'max-w-640px';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
:t="t"
|
||||
:open="open"
|
||||
:title="modalCopy?.title"
|
||||
:description="modalCopy?.description"
|
||||
:show-close-x="!checkForUpdatesLoading"
|
||||
:max-width="modalWidth"
|
||||
@close="close"
|
||||
>
|
||||
<template v-if="renderMainSlot" #main>
|
||||
<BrandLoading v-if="checkForUpdatesLoading" class="w-[150px] mx-auto" />
|
||||
<div v-else class="flex flex-col gap-y-16px">
|
||||
<div v-if="extraLinks.length > 0" class="flex flex-col xs:flex-row justify-center gap-8px">
|
||||
<BrandButton
|
||||
v-for="item in extraLinks"
|
||||
:key="item.text"
|
||||
:btn-style="item.btnStyle ?? undefined"
|
||||
:href="item.href ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text)"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click ? item.click() : undefined"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="available || availableWithRenewal" class="mx-auto">
|
||||
<SwitchGroup>
|
||||
<div class="flex justify-center items-center gap-8px p-8px rounded">
|
||||
<Switch
|
||||
v-model="ignoreThisRelease"
|
||||
:class="ignoreThisRelease ? 'bg-gradient-to-r from-unraid-red to-orange' : 'bg-transparent'"
|
||||
class="relative inline-flex h-24px w-[48px] items-center rounded-full overflow-hidden"
|
||||
>
|
||||
<span v-show="!ignoreThisRelease" class="absolute z-0 inset-0 opacity-10 bg-beta" />
|
||||
<span
|
||||
:class="ignoreThisRelease ? 'translate-x-[26px]' : 'translate-x-[2px]'"
|
||||
class="inline-block h-20px w-20px transform rounded-full bg-white transition"
|
||||
/>
|
||||
</Switch>
|
||||
<SwitchLabel class="text-16px">
|
||||
{{ t('Ignore this release until next reboot') }}
|
||||
</SwitchLabel>
|
||||
</div>
|
||||
</SwitchGroup>
|
||||
</div>
|
||||
<div v-else-if="updateOsIgnoredReleases.length > 0" class="w-full max-w-640px mx-auto flex flex-col gap-8px">
|
||||
<h3 class="text-left text-16px font-semibold italic">
|
||||
{{ t('Ignored Releases') }}
|
||||
</h3>
|
||||
<UpdateOsIgnoredRelease
|
||||
v-for="ignoredRelease in updateOsIgnoredReleases"
|
||||
:key="ignoredRelease"
|
||||
:label="ignoredRelease"
|
||||
:t="t"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="!checkForUpdatesLoading" #footer>
|
||||
<div
|
||||
class="w-full flex gap-8px mx-auto"
|
||||
:class="{
|
||||
'flex-col-reverse xs:flex-row justify-between': actionButtons,
|
||||
'justify-center': !actionButtons,
|
||||
}"
|
||||
>
|
||||
<div class="flex flex-col-reverse xs:flex-row justify-start gap-8px">
|
||||
<BrandButton
|
||||
btn-style="underline-hover-red"
|
||||
:icon="XMarkIcon"
|
||||
:text="t('Close')"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:icon="ArrowTopRightOnSquareIcon"
|
||||
:text="t('More options')"
|
||||
@click="accountStore.updateOs()"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="actionButtons" class="flex flex-col xs:flex-row justify-end gap-8px">
|
||||
<BrandButton
|
||||
v-for="item in actionButtons"
|
||||
:key="item.text"
|
||||
:btn-style="item.btnStyle ?? undefined"
|
||||
:icon="item.icon"
|
||||
:icon-right="item.iconRight"
|
||||
:icon-right-hover-display="item.iconRightHoverDisplay"
|
||||
:text="t(item.text)"
|
||||
:title="item.title ? t(item.title) : undefined"
|
||||
@click="item.click ? item.click() : undefined"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -80,7 +80,7 @@ const downgradeButton = ref<UserProfileLink>({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="downgradeButton" class="flex flex-col sm:flex-shrink-0 items-center gap-16px">
|
||||
<div v-if="downgradeButton" class="flex flex-col flex-shrink-0 gap-16px flex-grow items-stretch">
|
||||
<BrandButton
|
||||
:btn-style="'underline'"
|
||||
:icon="InformationCircleIcon"
|
||||
|
||||
38
web/components/UpdateOs/IgnoredRelease.vue
Normal file
38
web/components/UpdateOs/IgnoredRelease.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<script setup lang="ts">
|
||||
import { XMarkIcon } from '@heroicons/vue/24/solid';
|
||||
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
t: any;
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
label: '',
|
||||
});
|
||||
|
||||
const serverStore = useServerStore();
|
||||
const { darkMode } = storeToRefs(useThemeStore());
|
||||
|
||||
const evenBgColor = computed(() => {
|
||||
return darkMode.value ? 'even:bg-grey-darkest' : 'even:bg-black/5';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="text-16px p-12px flex flex-row gap-4px sm:px-20px sm:gap-16px items-center justify-between rounded"
|
||||
:class="evenBgColor"
|
||||
>
|
||||
<span class="font-semibold">{{ label }}</span>
|
||||
<BrandButton
|
||||
:btn-style="'underline'"
|
||||
:icon-right="XMarkIcon"
|
||||
:text="t('Remove')"
|
||||
:title="t('Remove from ignore list')"
|
||||
@click="serverStore.updateOsRemoveIgnoredRelease(label)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
13
web/components/UpdateOs/IgnoredReleases.vue
Normal file
13
web/components/UpdateOs/IgnoredReleases.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
@@ -117,9 +117,9 @@ const disableCallbackButton = computed(() => !acknowledgeBackup.value || flashBa
|
||||
|
||||
watchEffect(() => {
|
||||
if (available.value) {
|
||||
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
|
||||
updateButton.value = updateOsActionsStore.updateCallbackButton();
|
||||
} else {
|
||||
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
|
||||
updateButton.value = updateOsActionsStore.updateCallbackButton();
|
||||
}
|
||||
if (flashBackupBasicStatus.value === 'complete') {
|
||||
acknowledgeBackup.value = true; // auto check the box
|
||||
|
||||
@@ -47,9 +47,9 @@ const updateButton = ref<UserProfileLink | undefined>();
|
||||
|
||||
watchEffect(() => {
|
||||
if (availableWithRenewal.value) {
|
||||
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
|
||||
updateButton.value = updateOsActionsStore.updateCallbackButton();
|
||||
} else {
|
||||
updateButton.value = updateOsActionsStore.initUpdateOsCallback();
|
||||
updateButton.value = updateOsActionsStore.updateCallbackButton();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { useCallbackStore, useCallbackActionsStore } from '~/store/callbackActions';
|
||||
import { useDropdownStore } from '~/store/dropdown';
|
||||
import { useReplaceRenewStore } from '~/store/replaceRenew';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useThemeStore } from '~/store/theme';
|
||||
import type { Server } from '~/types/server';
|
||||
@@ -22,7 +21,6 @@ const { t } = useI18n();
|
||||
|
||||
const callbackStore = useCallbackStore();
|
||||
const dropdownStore = useDropdownStore();
|
||||
const replaceRenewCheckStore = useReplaceRenewStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { callbackData } = storeToRefs(useCallbackActionsStore());
|
||||
@@ -33,12 +31,8 @@ const {
|
||||
guid,
|
||||
keyfile,
|
||||
lanIp,
|
||||
state,
|
||||
connectPluginInstalled,
|
||||
} = storeToRefs(serverStore);
|
||||
const { bannerGradient, theme, altTheme } = storeToRefs(useThemeStore());
|
||||
|
||||
const hideDropdown = computed(() => state.value === 'PRO' && !connectPluginInstalled.value);
|
||||
const { bannerGradient, theme } = storeToRefs(useThemeStore());
|
||||
|
||||
/**
|
||||
* Close dropdown when clicking outside
|
||||
@@ -94,8 +88,6 @@ onBeforeMount(() => {
|
||||
if (callbackData.value) {
|
||||
return console.debug('Renew callback detected, skipping auto check for key replacement, renewal eligibility, and OS Update.');
|
||||
}
|
||||
// automatically check for replacement and renewal eligibility…will prompt user if eligible for a renewal / key re-roll for legacy keys
|
||||
replaceRenewCheckStore.check();
|
||||
} else {
|
||||
console.warn('A valid keyfile and USB Flash boot device are required to check for key renewals, key replacement eligibiliy, and OS update availability.');
|
||||
}
|
||||
@@ -105,11 +97,7 @@ onBeforeMount(() => {
|
||||
<template>
|
||||
<div
|
||||
id="UserProfile"
|
||||
:class="{
|
||||
'text-alpha': !altTheme,
|
||||
'text-beta': altTheme,
|
||||
}"
|
||||
class="relative z-20 flex flex-col h-full gap-y-4px pt-4px pr-16px pl-40px"
|
||||
class="text-alpha relative z-20 flex flex-col h-full gap-y-4px pt-4px pr-16px pl-40px"
|
||||
>
|
||||
<div v-if="bannerGradient" class="absolute z-0 w-[125%] top-0 bottom-0 right-0" :style="bannerGradient" />
|
||||
|
||||
@@ -122,7 +110,7 @@ onBeforeMount(() => {
|
||||
<div class="relative z-10 flex flex-row items-center justify-end gap-x-16px h-full">
|
||||
<h1 class="text-14px sm:text-18px relative flex flex-col-reverse items-end md:flex-row border-0">
|
||||
<template v-if="description && theme?.descriptionShow">
|
||||
<span class="text-right text-12px sm:text-18px hidden 2xs:block">{{ description }}</span>
|
||||
<span class="text-right text-12px sm:text-18px hidden 2xs:block" v-html="description" />
|
||||
<span class="text-gamma hidden md:inline-block px-8px">•</span>
|
||||
</template>
|
||||
<button :title="t('Click to Copy LAN IP {0}', [lanIp])" class="opacity-100 hover:opacity-75 focus:opacity-75 transition-opacity" @click="copyLanIp()">
|
||||
@@ -137,14 +125,12 @@ onBeforeMount(() => {
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<template v-if="!hideDropdown">
|
||||
<div class="block w-2px h-24px bg-gamma" />
|
||||
<div class="block w-2px h-24px bg-gamma" />
|
||||
|
||||
<OnClickOutside class="flex items-center justify-end h-full" :options="{ ignore: [clickOutsideIgnoreTarget] }" @trigger="outsideDropdown">
|
||||
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" :t="t" />
|
||||
<UpcDropdown ref="clickOutsideTarget" :t="t" />
|
||||
</OnClickOutside>
|
||||
</template>
|
||||
<OnClickOutside class="flex items-center justify-end h-full" :options="{ ignore: [clickOutsideIgnoreTarget] }" @trigger="outsideDropdown">
|
||||
<UpcDropdownTrigger ref="clickOutsideIgnoreTarget" :t="t" />
|
||||
<UpcDropdown ref="clickOutsideTarget" :t="t" />
|
||||
</OnClickOutside>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
// @todo with multiple actions of key install and update after successful key install, rather than showing default success message, show a message to have them confirm the update
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDoubleDownIcon,
|
||||
ClipboardIcon,
|
||||
CogIcon,
|
||||
WrenchScrewdriverIcon,
|
||||
XMarkIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import 'tailwindcss/tailwind.css';
|
||||
@@ -149,7 +152,7 @@ const keyInstallStatusCopy = computed((): { text: string; } => {
|
||||
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
|
||||
};
|
||||
case 'success':
|
||||
if (keyActionType.value === 'trialExtend') { txt2 = props.t('Extension Installed'); }
|
||||
if (keyActionType.value === 'renew' || keyActionType.value === 'trialExtend') { txt2 = props.t('Extension Installed'); }
|
||||
if (keyActionType.value === 'recover') { txt2 = props.t('Recovered'); }
|
||||
if (keyActionType.value === 'replace') { txt2 = props.t('Replaced'); }
|
||||
return {
|
||||
@@ -199,6 +202,16 @@ const accountActionStatusCopy = computed((): { text: string; } => {
|
||||
});
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
|
||||
/**
|
||||
* Ideally we'd show this based off of regExp.value, but we will not have that value yet.
|
||||
* So we'll use the keyType.value that we get from the keyInstall store.
|
||||
*/
|
||||
const showUpdateEligibility = computed(() => {
|
||||
// rather than specifically targeting 'Starter' and 'Unleashed' we'll target all keys that are not 'Basic', 'Plus', 'Pro', 'Lifetime', or 'Trial'
|
||||
if (!keyType.value) { return false; }
|
||||
return !['Basic', 'Plus', 'Pro', 'Lifetime', 'Trial'].includes(keyType.value);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -216,7 +229,7 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
<template #main>
|
||||
<div
|
||||
v-if="keyInstallStatus !== 'ready' || accountActionStatus !== 'ready'"
|
||||
class="text-center relative w-full flex flex-col justify-center gap-y-16px py-24px sm:py-32px"
|
||||
class="text-center relative w-full flex flex-col justify-center gap-y-16px py-24px"
|
||||
>
|
||||
<BrandLoading v-if="callbackStatus === 'loading'" class="w-[110px] mx-auto" />
|
||||
|
||||
@@ -236,7 +249,7 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
{{ t('Calculating trial expiration…') }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="keyType === 'Starter' || keyType === 'Unleashed'" class="opacity-75 italic mt-4px">
|
||||
<div v-if="showUpdateEligibility" class="opacity-75 italic mt-4px">
|
||||
<RegistrationUpdateExpiration
|
||||
v-if="refreshServerStateStatus === 'done'"
|
||||
:t="t"
|
||||
@@ -316,6 +329,7 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
<template v-if="callbackStatus === 'success'">
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:icon="XMarkIcon"
|
||||
:text="closeText"
|
||||
@click="close"
|
||||
/>
|
||||
@@ -323,17 +337,17 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
<template v-if="connectPluginInstalled && accountActionType === 'signIn'">
|
||||
<BrandButton
|
||||
v-if="isSettingsPage"
|
||||
class="grow-0"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
@click="close"
|
||||
/>
|
||||
<BrandButton
|
||||
v-else
|
||||
class="grow-0"
|
||||
:href="WEBGUI_CONNECT_SETTINGS.toString()"
|
||||
:icon="CogIcon"
|
||||
:text="t('Configure Connect Features')"
|
||||
class="grow-0"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -347,10 +361,12 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
<template v-if="updateOsStatus === 'confirming' && !stateDataError">
|
||||
<BrandButton
|
||||
btn-style="underline"
|
||||
:icon="XMarkIcon"
|
||||
:text="t('Cancel')"
|
||||
@click="cancelUpdateOs"
|
||||
/>
|
||||
<BrandButton
|
||||
:icon="CheckIcon"
|
||||
:text="t('Confirm and start update')"
|
||||
@click="confirmUpdateOs"
|
||||
/>
|
||||
@@ -359,6 +375,7 @@ const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
|
||||
<template v-if="stateDataError">
|
||||
<BrandButton
|
||||
:href="WEBGUI_TOOLS_REGISTRATION.toString()"
|
||||
:icon="WrenchScrewdriverIcon"
|
||||
:text="t('Fix Error')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,38 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
ArrowTopRightOnSquareIcon,
|
||||
BellAlertIcon,
|
||||
CogIcon,
|
||||
KeyIcon,
|
||||
UserIcon,
|
||||
} from '@heroicons/vue/24/solid';
|
||||
|
||||
import {
|
||||
ACCOUNT,
|
||||
CONNECT_DASHBOARD,
|
||||
WEBGUI_CONNECT_SETTINGS,
|
||||
WEBGUI_TOOLS_REGISTRATION,
|
||||
} from '~/helpers/urls';
|
||||
import { useAccountStore } from '~/store/account';
|
||||
import { useErrorsStore } from '~/store/errors';
|
||||
import { useServerStore } from '~/store/server';
|
||||
import { useUpdateOsStore } from '~/store/updateOs';
|
||||
import { useUpdateOsActionsStore } from '~/store/updateOsActions';
|
||||
import type { UserProfileLink } from '~/types/userProfile';
|
||||
|
||||
const props = defineProps<{ t: any; }>();
|
||||
|
||||
const accountStore = useAccountStore();
|
||||
const errorsStore = useErrorsStore();
|
||||
const updateOsActionsStore = useUpdateOsActionsStore();
|
||||
const updateOsStore = useUpdateOsStore();
|
||||
|
||||
const { errors } = storeToRefs(errorsStore);
|
||||
const {
|
||||
keyActions,
|
||||
connectPluginInstalled,
|
||||
rebootType,
|
||||
rebootVersion,
|
||||
registered,
|
||||
regUpdatesExpired,
|
||||
stateData,
|
||||
stateDataError,
|
||||
} = storeToRefs(useServerStore());
|
||||
const { available: osUpdateAvailable } = storeToRefs(useUpdateOsStore());
|
||||
const {
|
||||
available: osUpdateAvailable,
|
||||
availableWithRenewal: osUpdateAvailableWithRenewal,
|
||||
} = storeToRefs(updateOsStore);
|
||||
|
||||
const signInAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signIn') ?? []);
|
||||
const signOutAction = computed(() => stateData.value.actions?.filter((act: { name: string; }) => act.name === 'signOut') ?? []);
|
||||
@@ -42,6 +50,58 @@ const signOutAction = computed(() => stateData.value.actions?.filter((act: { nam
|
||||
*/
|
||||
const filteredKeyActions = computed(() => keyActions.value?.filter(action => !['renew'].includes(action.name)));
|
||||
|
||||
const manageUnraidNetAccount = computed(() => {
|
||||
return {
|
||||
external: true,
|
||||
click: () => { accountStore.manage(); },
|
||||
icon: UserIcon,
|
||||
text: props.t('Manage Unraid.net Account'),
|
||||
title: props.t('Manage Unraid.net Account in new tab'),
|
||||
};
|
||||
});
|
||||
|
||||
const updateOsCheckForUpdatesButton = computed(() => {
|
||||
return {
|
||||
click: () => {
|
||||
updateOsStore.localCheckForUpdate();
|
||||
},
|
||||
icon: ArrowPathIcon,
|
||||
text: props.t('Check for Update'),
|
||||
};
|
||||
});
|
||||
const updateOsResponseModalOpenButton = computed(() => {
|
||||
return {
|
||||
click: () => {
|
||||
updateOsStore.setModalOpen(true);
|
||||
},
|
||||
emphasize: true,
|
||||
icon: BellAlertIcon,
|
||||
text: osUpdateAvailableWithRenewal.value
|
||||
? props.t('Unraid OS {0} Released', [osUpdateAvailableWithRenewal.value])
|
||||
: props.t('Unraid OS {0} Update Available', [osUpdateAvailable.value]),
|
||||
};
|
||||
});
|
||||
const updateOsToolsUpdatePageButton = computed(() => {
|
||||
return {
|
||||
external: true,
|
||||
href: WEBGUI_TOOLS_REGISTRATION.toString(),
|
||||
icon: KeyIcon,
|
||||
text: rebootType.value === 'downgrade'
|
||||
? props.t('Reboot Now to Downgrade to {0}', [rebootVersion.value])
|
||||
: props.t('Reboot Now to Update to {0}', [rebootVersion.value]),
|
||||
};
|
||||
});
|
||||
|
||||
const updateOsButton = computed(() => {
|
||||
if (rebootType.value === 'downgrade' || rebootType.value === 'update') {
|
||||
return updateOsToolsUpdatePageButton.value;
|
||||
}
|
||||
if (osUpdateAvailable.value) {
|
||||
return updateOsResponseModalOpenButton.value;
|
||||
}
|
||||
return updateOsCheckForUpdatesButton.value;
|
||||
});
|
||||
|
||||
const links = computed(():UserProfileLink[] => {
|
||||
return [
|
||||
...(regUpdatesExpired.value
|
||||
@@ -52,8 +112,10 @@ const links = computed(():UserProfileLink[] => {
|
||||
title: props.t('Go to Tools > Registration to Learn More'),
|
||||
}]
|
||||
: []),
|
||||
// callback to account.unraid.net/server/update-os
|
||||
updateOsActionsStore.initUpdateOsCallback(),
|
||||
|
||||
// ensure we only show the update button when we don't have an error
|
||||
...(!stateDataError.value ? [updateOsButton.value] : []),
|
||||
|
||||
// connect plugin links
|
||||
...(registered.value && connectPluginInstalled.value
|
||||
? [
|
||||
@@ -65,13 +127,7 @@ const links = computed(():UserProfileLink[] => {
|
||||
text: props.t('Go to Connect'),
|
||||
title: props.t('Opens Connect in new tab'),
|
||||
},
|
||||
{
|
||||
external: true,
|
||||
href: ACCOUNT.toString(),
|
||||
icon: ArrowTopRightOnSquareIcon,
|
||||
text: props.t('Manage Unraid.net Account'),
|
||||
title: props.t('Manage Unraid.net Account in new tab'),
|
||||
},
|
||||
...([manageUnraidNetAccount.value]),
|
||||
{
|
||||
href: WEBGUI_CONNECT_SETTINGS.toString(),
|
||||
icon: CogIcon,
|
||||
@@ -80,7 +136,9 @@ const links = computed(():UserProfileLink[] => {
|
||||
},
|
||||
...(signOutAction.value),
|
||||
]
|
||||
: []
|
||||
: [
|
||||
...([manageUnraidNetAccount.value]),
|
||||
]
|
||||
),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -20,7 +20,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"
|
||||
|
||||
@@ -35,6 +35,8 @@ const title = computed((): string => {
|
||||
if (showErrorIcon.value) { return props.t('Learn more about the error'); }
|
||||
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
|
||||
});
|
||||
|
||||
// const hideAvatar = computed(() => state.value === 'PRO' && !connectPluginInstalled.value);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -53,7 +55,7 @@ const title = computed((): string => {
|
||||
<span class="absolute bottom-[-3px] inset-x-0 h-2px w-full bg-gradient-to-r from-unraid-red to-orange rounded opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity" />
|
||||
</span>
|
||||
|
||||
<BellAlertIcon v-if="osUpdateAvailable && !rebootType" class="hover:animate-pulse text-alpha fill-current relative w-16px h-16px" />
|
||||
<BellAlertIcon v-if="osUpdateAvailable && !rebootType" class="hover:animate-pulse fill-current relative w-16px h-16px" />
|
||||
|
||||
<Bars3Icon v-if="!dropdownVisible" class="w-20px" />
|
||||
<Bars3BottomRightIcon v-else class="w-20px" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user