Compare commits

...

271 Commits

Author SHA1 Message Date
Zack Spear
105dac5a17 test: web build action v2 final v5 2023-08-08 13:45:21 -07:00
Zack Spear
694c3077b1 test: web build action 2023-08-08 13:33:41 -07:00
Zack Spear
498583207c test: web build action 2023-08-08 13:32:20 -07:00
Zack Spear
a9f76a2deb test: web build action 2023-08-08 13:31:16 -07:00
Zack Spear
a91bef59c2 test: web build action 2023-08-08 13:29:12 -07:00
Zack Spear
25b501675f test: web actions branch name 2023-08-08 13:26:31 -07:00
Zack Spear
cc44fbfbe3 test: web builds 2023-08-08 13:22:36 -07:00
Zack Spear
07136ad235 test: web action 2023-08-08 13:20:56 -07:00
Zack Spear
aea90134ca test: abstracted Lint, Test, and Build Web 2023-08-08 13:19:25 -07:00
Zack Spear
e2bb6348eb test: pull req web envs 2023-08-08 13:09:41 -07:00
Zack Spear
7c925dc7f6 chore: remove env logs 2023-08-08 13:05:14 -07:00
Zack Spear
99eedc793d test: action web pr 2023-08-08 13:01:08 -07:00
Zack Spear
3940a43b4f test: pull-req-web 2023-08-08 12:58:12 -07:00
Zack Spear
aa247694db test: first pass web components build action 2023-08-08 12:55:54 -07:00
Zack Spear
56ba2f1b0b chore: lint 2023-08-08 12:15:03 -07:00
Zack Spear
4674d48ffb feat: unraid-components .gitkeep 2023-08-08 11:19:16 -07:00
Zack Spear
88fbb52606 chore: web .env.example 2023-08-08 09:51:14 -07:00
Zack Spear
18f80390d5 chore: remove i18n ally review yml 2023-08-07 17:54:53 -07:00
Zack Spear
51f5032db2 move into web for api repo merging 2023-08-07 17:52:23 -07:00
Zack Spear
12bb6595b4 feat: implement .env usage 2023-08-07 17:51:37 -07:00
Zack Spear
96d8c80ca5 chore: promo comment out reorg 2023-08-07 17:51:37 -07:00
Zack Spear
6edefb0365 refactor: comment out promo 2023-08-07 17:51:37 -07:00
Zack Spear
2dd89fad4c fix: invalid api key error only w/ plg 2023-08-07 17:51:37 -07:00
Zack Spear
7e75272991 refactor: ENOKEYFILE2 copy 2023-08-07 17:51:37 -07:00
Zack Spear
2b464c2d1d refactor: allow key recover w/o plugin 2023-08-07 17:51:37 -07:00
Zack Spear
9dba19e7f6 feat: disable sign out w/o a key 2023-08-07 17:51:37 -07:00
Zack Spear
c29244e5f0 refactor: improve type readability 2023-08-07 17:51:37 -07:00
Zack Spear
2bdc718e0a refactor: comment out connect promo dropdown item 2023-08-07 17:51:37 -07:00
Zack Spear
2fbd2f1304 refactor: urls 2023-08-07 17:51:37 -07:00
Zack Spear
5c58bde027 refactor: WIP trial requires account 2023-08-07 17:51:37 -07:00
Zack Spear
33d2e4ba63 test: ja locale 2023-08-07 17:51:37 -07:00
Zack Spear
23fc148755 chore: clean up 2023-08-07 17:51:37 -07:00
Zack Spear
ff2a5e5b2f chore: update test locales 2023-08-07 17:51:37 -07:00
Zack Spear
91b234918d fix: prevent api client from starting to early 2023-08-07 17:51:37 -07:00
Zack Spear
d5c3fb25bf feat: gql retrylink 2023-08-07 17:51:37 -07:00
Zack Spear
7b64e5e08b chore: fix log copy 2023-08-07 17:51:37 -07:00
Zack Spear
fefeb59103 fix: translation keys for errors 2023-08-07 17:51:37 -07:00
Zack Spear
ec42ff8674 chore: @todo devEnv 2023-08-07 17:51:37 -07:00
Zack Spear
5367272a6b fix: missing translation key 2023-08-07 17:51:37 -07:00
Zack Spear
9f67c49bdd fix: locale data ts 2023-08-07 17:51:36 -07:00
Zack Spear
1e4f449bb1 chore: update lint script 2023-08-07 17:51:36 -07:00
Zack Spear
3f6305d35a chore: lint fixes 2023-08-07 17:51:36 -07:00
Zack Spear
ff30595d25 fix: translation key issue 2023-08-07 17:51:36 -07:00
Zack Spear
31c8bc68ae refactor: uptime expire time formatting 2023-08-07 17:51:36 -07:00
Zack Spear
4ec1d58fa8 chore: comment 2023-08-07 17:51:36 -07:00
Zack Spear
ea2de14d18 refactor: install key callback action i18n 2023-08-07 17:51:36 -07:00
Zack Spear
828f50a4cd refactor: account callback action copy for i18n 2023-08-07 17:51:36 -07:00
Zack Spear
0e00c5baae refactor: upc error spacing 2023-08-07 17:51:36 -07:00
Zack Spear
e2fd05edd4 refactor: clean up i18n host unused props 2023-08-07 17:51:36 -07:00
Zack Spear
d21ca3e68d feat: injecting translations from webgui's php 2023-08-07 17:51:36 -07:00
Zack Spear
29ca3b1baa feat: WIP messages from php to i18n 2023-08-07 17:51:36 -07:00
Zack Spear
18624d080d fix: connect status icon color online 2023-08-07 17:51:36 -07:00
Zack Spear
780dce53f9 fix: connect status icon color online 2023-08-07 17:51:36 -07:00
Zack Spear
3658eb0a0c refactor: translation message variables 2023-08-07 17:51:36 -07:00
Zack Spear
06d6fe357a refactor: uniform line height in web components 2023-08-07 17:51:36 -07:00
Zack Spear
9390ded8b6 fix: upc text vertical centering 2023-08-07 17:51:36 -07:00
Zack Spear
9ec014ae54 refactor: svg mark remove title 2023-08-07 17:51:36 -07:00
Zack Spear
600d6f3655 refactor: server state data type 2023-08-07 17:51:36 -07:00
Zack Spear
acadc5417d feat: vue components pass t prop 2023-08-07 17:51:36 -07:00
Zack Spear
43210e7b9b feat: server state i18n 2023-08-07 17:51:36 -07:00
Zack Spear
0e2d535f96 feat: i18n web components 2023-08-07 17:51:36 -07:00
Zack Spear
2e39a5bceb feat: basic vue-i18n 2023-08-07 17:51:35 -07:00
Zack Spear
4284dff515 refactor: simplify callback handler component 2023-08-07 17:51:35 -07:00
Zack Spear
8f9e664534 chore: lint 2023-08-07 17:51:35 -07:00
Zack Spear
28e408c989 refactor: download logs component copy for translations 2023-08-07 17:51:35 -07:00
Zack Spear
72edac039e chore: lint 2023-08-07 17:51:35 -07:00
Zack Spear
0df33d4237 refactor: simplify WebguiUnraidApiCommand request 2023-08-07 17:51:35 -07:00
Zack Spear
6722d4f50c refactor: WebguiUnraidApiCommand response handling 2023-08-07 17:51:35 -07:00
Zack Spear
ea97456020 refactor: WebguiUnraidApiCommand response handling 2023-08-07 17:51:35 -07:00
Zack Spear
f409e33adb refactor: restart logic creates new client then gets server details 2023-08-07 17:51:35 -07:00
Zack Spear
31f29229da refactor: connect status component 2023-08-07 17:51:35 -07:00
Zack Spear
e2d5dbb155 feat: api offline restart button 2023-08-07 17:51:35 -07:00
Zack Spear
daf5933a84 refactor: clean up dropdown 2023-08-07 17:51:35 -07:00
Zack Spear
ada1fe615e fix: auth component button 2023-08-07 17:51:35 -07:00
Zack Spear
c89696f3d5 chore: lint 2023-08-07 17:51:35 -07:00
Zack Spear
ee7e49e929 chore: lint 2023-08-07 17:51:35 -07:00
Zack Spear
5edd1c7bf2 refactor: rename to built components to unraid- 2023-08-07 17:51:35 -07:00
Zack Spear
46077f2641 refactor: remove state polling and only refetch after action 2023-08-07 17:51:35 -07:00
Zack Spear
bb7a4ccdb1 chore: clean up 2023-08-07 17:51:35 -07:00
Zack Spear
47168ee946 chore: lint 2023-08-07 17:51:35 -07:00
Zack Spear
6004d727de refactor: connect status error 2023-08-07 17:51:35 -07:00
Zack Spear
d430b1566d refactor: remove unused callback store value 2023-08-07 17:51:35 -07:00
Zack Spear
e1e7472a39 refactor: connect status 2023-08-07 17:51:35 -07:00
Zack Spear
3598823810 refactor: replace key action ordering 2023-08-07 17:51:35 -07:00
Zack Spear
64024bc54d refactor: unraidApi store start restart logic 2023-08-07 17:51:35 -07:00
Zack Spear
dde1af071c refactor: WebguiUnraidApiCommand 2023-08-07 17:51:35 -07:00
Zack Spear
f5498c8b52 chore: lint serverState 2023-08-07 17:51:34 -07:00
Zack Spear
376ede6458 refactor: connectPluginVersion 2023-08-07 17:51:34 -07:00
Zack Spear
5c9c05c5e8 refactor: query online for connect status 2023-08-07 17:51:34 -07:00
Zack Spear
b76d2e4bbc chore: eslint ignore codegen 2023-08-07 17:51:34 -07:00
Zack Spear
17f5f0a936 chore: lint 2023-08-07 17:51:34 -07:00
Zack Spear
33763b5809 fix: eslint fixes rd.3 2023-08-07 17:51:34 -07:00
Zack Spear
1e0cb2ca68 fix: eslint fixes stores rd.2 2023-08-07 17:51:34 -07:00
Zack Spear
2191cd8e9e fix: eslint fixes stores 2023-08-07 17:51:34 -07:00
Zack Spear
9c7229e923 fix: eslint fixes rd.1 2023-08-07 17:51:34 -07:00
Zack Spear
15efdeb554 feat: eslint setup 2023-08-07 17:51:34 -07:00
Zack Spear
0417e43c2d test: remove debugs for description in upc 2023-08-07 17:51:34 -07:00
Zack Spear
472e3fdba2 refactor: callback finished refreshServerState 2023-08-07 17:51:34 -07:00
Zack Spear
775b7fcd1e refactor: apollo client creation + WIP subscriptions 2023-08-07 17:51:34 -07:00
Zack Spear
8cc86525e2 refactor: dropdown error styles 2023-08-07 17:51:34 -07:00
Zack Spear
cdaadaf700 refactor: upc error styles 2023-08-07 17:51:34 -07:00
Zack Spear
3b4c4ed552 feat: working unraid-api gql 2023-08-07 17:51:34 -07:00
Zack Spear
c68d5e17f0 test: WIP apollo 2023-08-07 17:51:34 -07:00
Zack Spear
7f9412a758 fix: dropdown content keyline conditional display 2023-08-07 17:51:34 -07:00
Zack Spear
a1b7ee0800 refactor: troubleshoot modal opening 2023-08-07 17:51:33 -07:00
Zack Spear
b5fb0860eb refactor: errors 2023-08-07 17:51:33 -07:00
Zack Spear
71768e05b7 refactor: api, os, plugin versions in server store 2023-08-07 17:51:33 -07:00
Zack Spear
5fc4d939bb feat: contact support using webgui feedback modal 2023-08-07 17:51:33 -07:00
Zack Spear
b6324e41a3 refactor: errors for server 2023-08-07 17:51:33 -07:00
Zack Spear
04c17da21e feat: WIP error store progress with server data 2023-08-07 17:51:33 -07:00
Zack Spear
312178129e feat: WIP global error handling 2023-08-07 17:51:33 -07:00
Zack Spear
efb838c87c chore: package updates 2023-08-07 17:51:33 -07:00
Zack Spear
3f32b532f4 refactor: dropdown connect status vertical spacing 2023-08-07 17:51:33 -07:00
Zack Spear
410cb5b9ee refactor: reorder content in upc dropdown 2023-08-07 17:51:33 -07:00
Zack Spear
706c71df10 refactor: ServerState type 2023-08-07 17:51:33 -07:00
Zack Spear
a775026bf5 refactor: server state message formatting 2023-08-07 17:51:33 -07:00
Zack Spear
43b5293d97 fix: PRO state remove upgrade btn 2023-08-07 17:51:33 -07:00
Zack Spear
f4f1b1b32c refactor: serverData to show recover when pluginInstalled 2023-08-07 17:51:33 -07:00
Zack Spear
33fa5075e7 refactor: launchpad copy conditionals 2023-08-07 17:51:33 -07:00
Zack Spear
839664d1f5 refactor: trial copy 2023-08-07 17:51:33 -07:00
Zack Spear
330b86f227 chore: remove unused type import 2023-08-07 17:51:33 -07:00
Zack Spear
d5b0efd1e4 refactor: trial extension to happen in modal 2023-08-07 17:51:33 -07:00
Zack Spear
58735fd807 refactor: key actions to use button component 2023-08-07 17:51:33 -07:00
Zack Spear
de9b76a6a6 feat: start trial from upc 2023-08-07 17:51:33 -07:00
Zack Spear
77ede914a2 refactor: responsiveness 2023-08-07 17:51:33 -07:00
Zack Spear
eb2342f718 refactor: upc style responsive support 2023-08-07 17:51:33 -07:00
Zack Spear
ccd776b319 chore: clean unused imports 2023-08-07 17:51:33 -07:00
Zack Spear
2b73977a81 fix: sign in / out only allowed with plg installed 2023-08-07 17:51:33 -07:00
Zack Spear
7b61f1ee54 refactor: dropdown launchpad 2023-08-07 17:51:32 -07:00
Zack Spear
00a50d317d refactor: auth use button component 2023-08-07 17:51:32 -07:00
Zack Spear
115c60c44e fix: download api logs sizing 2023-08-07 17:51:32 -07:00
Zack Spear
57adbee68e refactor: outline button style border-2 2023-08-07 17:51:32 -07:00
Zack Spear
6e58578de8 refactor: modal font size increase 2023-08-07 17:51:32 -07:00
Zack Spear
04655e1854 refactor: theme usage and dropdown logo color 2023-08-07 17:51:32 -07:00
Zack Spear
d5811e72e1 chore: @todo modal color swap 2023-08-07 17:51:32 -07:00
Zack Spear
fa83b23875 style: dropdown wrapper default shadow color 2023-08-07 17:51:32 -07:00
Zack Spear
6efb003056 chore: add todo 2023-08-07 17:51:32 -07:00
Zack Spear
8b1b93cf64 fix: upgrades 2023-08-07 17:51:32 -07:00
Zack Spear
747bacb901 refactor: improved CTAs on callbackfeedback modal 2023-08-07 17:51:32 -07:00
Zack Spear
a39f879b3a refactor: account callback text 2023-08-07 17:51:32 -07:00
Zack Spear
dc4a05916e refactor: improved callbackfeedback and modal usage 2023-08-07 17:51:32 -07:00
Zack Spear
6b2d75dd9e chore: callback feedback @todos 2023-08-07 17:51:32 -07:00
Zack Spear
75c583f012 refactor: callback feedback trial expire time 2023-08-07 17:51:32 -07:00
Zack Spear
3d0dbb1695 refactor: callback feedback status 2023-08-07 17:51:32 -07:00
Zack Spear
4f1e353d06 refactor: code readability for callbacks feedback 2023-08-07 17:51:32 -07:00
Zack Spear
874a375779 refactor: dropdown logo + header 2023-08-07 17:51:32 -07:00
Zack Spear
3cd611ff38 refactor: purchase init callback 2023-08-07 17:51:32 -07:00
Zack Spear
7d3ed1d535 refactor: date format 2023-08-07 17:51:32 -07:00
Zack Spear
564456d91f refactor: position state data error above key actions in dropdown 2023-08-07 17:51:32 -07:00
Zack Spear
4079c81128 fix: expired state 2023-08-07 17:51:32 -07:00
Zack Spear
079c54ece4 fix: format time 2023-08-07 17:51:32 -07:00
Zack Spear
a740d029b0 test: setup for troubleshooting 2023-08-07 17:51:32 -07:00
Zack Spear
e8de3c95b1 test: extend trial 2023-08-07 17:51:32 -07:00
Zack Spear
5446d28405 test: callback feedback modal 2023-08-07 17:51:31 -07:00
Zack Spear
0afb6333ac refactor: remove avatar hover loader 2023-08-07 17:51:31 -07:00
Zack Spear
81cb61c785 refactor: style tweaks 2023-08-07 17:51:31 -07:00
Zack Spear
0cc53be948 fix: WanIpCheck web component 2023-08-07 17:51:31 -07:00
Zack Spear
45b3d07d61 refactor: account callback server payload 2023-08-07 17:51:31 -07:00
Zack Spear
c894f2bae5 refactor: DropdownTrigger hover/focus underline 2023-08-07 17:51:31 -07:00
Zack Spear
99ed6d67ac refactor: improve callbackFeedback modal 2023-08-07 17:51:31 -07:00
Zack Spear
6009d91d8e fix: button component 2023-08-07 17:51:31 -07:00
Zack Spear
4e452b486e refactor: modal shadow styles for error / success 2023-08-07 17:51:31 -07:00
Zack Spear
5b819fa409 refactor: theme, colors 2023-08-07 17:51:31 -07:00
Zack Spear
9f2c857646 refactor: theme store 2023-08-07 17:51:31 -07:00
Zack Spear
7b4afb86ba refactor: callback modal width 2023-08-07 17:51:31 -07:00
Zack Spear
1180430d23 refactor: test deploy script to play os sound 2023-08-07 17:51:31 -07:00
Zack Spear
a952cd14ca test: server state error 2023-08-07 17:51:31 -07:00
Zack Spear
6f6342a60b refactor: upc dropdown error styles 2023-08-07 17:51:31 -07:00
Zack Spear
2b8ec1f661 refactor: uptime expire to show expire for ENOCONN 2023-08-07 17:51:31 -07:00
Zack Spear
3e8b617677 feat: build with deploy to local unraid server 2023-08-07 17:51:31 -07:00
Zack Spear
e7da6d4bbb fix: UptimeExpire 2023-08-07 17:51:31 -07:00
Zack Spear
7d04507f57 fix: purchase payloads 2023-08-07 17:51:31 -07:00
Zack Spear
23e0423093 test: setup test callbacks 2023-08-07 17:51:31 -07:00
Zack Spear
ecfe0ec2f9 fix: sign in post working 2023-08-07 17:51:31 -07:00
Zack Spear
10a573fb4d refactor: callbacks and progress on actions 2023-08-07 17:51:30 -07:00
Zack Spear
069924b2d2 feat: install plugin 2023-08-07 17:51:30 -07:00
Zack Spear
8c7bf0e190 refactor: callback feedback 2023-08-07 17:51:30 -07:00
Zack Spear
1a239b6914 feat: install key and account config webgui requests 2023-08-07 17:51:30 -07:00
Zack Spear
0638120af8 refactor: improve modal animation and ux 2023-08-07 17:51:30 -07:00
Zack Spear
59cda3a865 refactor: modal animation 2023-08-07 17:51:30 -07:00
Zack Spear
2042d8962b feat: theme setting 2023-08-07 17:51:30 -07:00
Zack Spear
a9c859f022 fix: web component modals 2023-08-07 17:51:30 -07:00
Zack Spear
f4b4271c91 refactor: callback progress 2023-08-07 17:51:30 -07:00
Zack Spear
4263749486 test: update callbackTest page for wanIp prop 2023-08-07 17:51:30 -07:00
Zack Spear
05dd10a38e feat: rebuild manifest 2023-08-07 17:51:30 -07:00
Zack Spear
cea10fceb2 refactor: first pass at stateData 2023-08-07 17:51:30 -07:00
Zack Spear
122cd03427 chore: @todo idea for promo 2023-08-07 17:51:30 -07:00
Zack Spear
274ca97d59 refactor: resize menu icon 2023-08-07 17:51:30 -07:00
Zack Spear
c1afb728df refactor: dropdown trigger errorIcon 2023-08-07 17:51:30 -07:00
Zack Spear
fec3390880 refactor: dropdown trigger errorIcon 2023-08-07 17:51:30 -07:00
Zack Spear
7d316fc1db fix: authAction server getter 2023-08-07 17:51:29 -07:00
Zack Spear
8db52be416 feat: transition dropdown
refactor: attempt to fix some bugs
2023-08-07 17:51:29 -07:00
Zack Spear
379fe69813 refactor: promo styles 2023-08-07 17:51:29 -07:00
Zack Spear
0e86c3c071 refactor: convert promo from dropdown to modal 2023-08-07 17:51:29 -07:00
Zack Spear
9e2f6a8607 refactor: dropdown and promo store 2023-08-07 17:51:29 -07:00
Zack Spear
57bd93b3a2 refactor: remove Launchpad web component 2023-08-07 17:51:29 -07:00
Zack Spear
67ae4dab05 feat: open in upc dropdown 2023-08-07 17:51:29 -07:00
Zack Spear
2a2a16d2f4 fix: launchpad width 2023-08-07 17:51:29 -07:00
Zack Spear
52bbcbc984 feat: KeyActions component & general progress 2023-08-07 17:51:29 -07:00
Zack Spear
d7829aadd1 feat: auth web component 2023-08-07 17:51:29 -07:00
Zack Spear
b7be649326 refactor: rename download logs component 2023-08-07 17:51:29 -07:00
Zack Spear
7aa6e606f5 feat: download api logs web component 2023-08-07 17:51:29 -07:00
Zack Spear
e1f1e3e72e chore: README notes 2023-08-07 17:51:29 -07:00
Zack Spear
1d53fedf11 refactor: finalize WanIpCheck web component 2023-08-07 17:51:29 -07:00
Zack Spear
1daf2e8b1f refactor: WIP WanIpCheck 2023-08-07 17:51:29 -07:00
Zack Spear
1e5904b92a refactor: server store and types 2023-08-07 17:51:29 -07:00
Zack Spear
27aed8186b feat: WIP promo component 2023-08-07 17:51:29 -07:00
Zack Spear
834691f12b refactor(upc): trigger arrow size 2023-08-07 17:51:29 -07:00
Zack Spear
38ddd972ce fix: avoid Vue bug remove component styles 2023-08-07 17:51:29 -07:00
Zack Spear
9baca3a2a3 fix: server state buy component 2023-08-07 17:51:29 -07:00
Zack Spear
44ccb7abf0 feat(upc): avatar & brand components 2023-08-07 17:51:29 -07:00
Zack Spear
314160a9ea refactor(upc): dropdown progress 2023-08-07 17:51:29 -07:00
Zack Spear
ca2abadcd2 refactor: add & organize server store 2023-08-07 17:51:29 -07:00
Zack Spear
dbf8d14810 refactor(types): UserProfileLink 2023-08-07 17:51:29 -07:00
Zack Spear
2e9145372e refactor: clean up Dropdown 2023-08-07 17:51:28 -07:00
Zack Spear
a633e2ff82 refactor: dropdown components 2023-08-07 17:51:28 -07:00
Zack Spear
2d7e00bc3a refactor: dropdown item component and usage 2023-08-07 17:51:28 -07:00
Zack Spear
fc5b6a03c6 refactor: WIP progress on UPC 2023-08-07 17:51:28 -07:00
Zack Spear
f1130576a0 feat: user profile dropdown components 2023-08-07 17:51:28 -07:00
Zack Spear
0a13756fa3 refactor: removed old versions of meta info components 2023-08-07 17:51:28 -07:00
Zack Spear
471721eaa9 feat: create UptimeExpire component 2023-08-07 17:51:28 -07:00
Zack Spear
0b81add407 feat: create meta info ServerState component 2023-08-07 17:51:28 -07:00
Zack Spear
42f94550ae feat: create beta component 2023-08-07 17:51:28 -07:00
Zack Spear
107f4d1cf4 feat: create keyline component 2023-08-07 17:51:28 -07:00
Zack Spear
95acaa25dd refactor: server state and types 2023-08-07 17:51:28 -07:00
Zack Spear
262d3d1edd feat: url helpers 2023-08-07 17:51:28 -07:00
Zack Spear
f07342f8d2 refactor: tailwind config with custom sizes 2023-08-07 17:51:28 -07:00
Zack Spear
2c7e70b21e feat: create main css for default vars 2023-08-07 17:51:28 -07:00
Zack Spear
eac33e1a4e fix: web component styles 2023-08-07 17:51:28 -07:00
Zack Spear
df7c5fc950 chore: add lanIp to serverState seed data 2023-08-07 17:51:28 -07:00
Zack Spear
027d4b37f2 chore: add heroicons and vueuse components 2023-08-07 17:51:28 -07:00
Zack Spear
e6745b0ddb refactor: nuxt config components 2023-08-07 17:51:28 -07:00
Zack Spear
481e0a6e41 refactor: custom css for components w/ tailwind 2023-08-07 17:51:28 -07:00
Zack Spear
15b0277191 chore: vscode settings 2023-08-07 17:51:28 -07:00
Zack Spear
7cb3abeaeb refactor(UserProfile): add UptimeExpire and server state component 2023-08-07 17:51:27 -07:00
Zack Spear
00f5c8072e refactor: abstract serverState data to seed dev data 2023-08-07 17:51:27 -07:00
Zack Spear
16a3e7faf0 chore: nuxt config 2023-08-07 17:51:27 -07:00
Zack Spear
8c279aef07 feat: server state component 2023-08-07 17:51:27 -07:00
Zack Spear
ae1013ca40 refactor: add more data to server store 2023-08-07 17:51:27 -07:00
Zack Spear
9404bdb580 feat: uptime and expire time component 2023-08-07 17:51:27 -07:00
Zack Spear
4b27d54302 chore: tailwind customizations 2023-08-07 17:51:27 -07:00
Zack Spear
dc659a0c40 chore: README update 2023-08-07 17:51:27 -07:00
Zack Spear
bf5b104e28 chore: .env.example 2023-08-07 17:51:27 -07:00
Zack Spear
7d95552f33 chore: connect-web-components > connect-components 2023-08-07 17:51:27 -07:00
Zack Spear
6c4d5c63a0 feat: init commit w/ callback prototype components 2023-08-07 17:51:27 -07:00
Zack Spear
fb1c9e074d refactor: ENOKEYFILE2 copy 2023-08-07 17:07:41 -07:00
Zack Spear
d28b7aeac1 refactor: upc ENOKEYFILE2 copy 2023-08-07 16:43:59 -07:00
Zack Spear
d8f165e234 refactor: recover copy to not include connect 2023-08-07 16:18:06 -07:00
Zack Spear
a864c49f01 refactor: copy 30 day 2023-08-07 15:00:21 -07:00
Zack Spear
2217abbe26 refactor: copy 30 day 2023-08-07 13:37:53 -07:00
Zack Spear
ec56aa0f6c refactor: upc trial copy 2023-08-07 11:51:54 -07:00
Zack Spear
4912ceaca0 feat: vue3 web component translations 2023-08-03 18:27:51 -07:00
Zack Spear
d46b9e5ec8 refactor: web components renamed to unraid- 2023-07-24 16:51:36 -07:00
Zack Spear
3303549565 fix(plg): server-state parse dynamix.cfg 2023-07-21 15:21:00 -07:00
Zack Spear
3c69b1f0ad refactor(plg): state var improvement 2023-07-21 15:20:41 -07:00
Zack Spear
17893d78f9 refactor(plg): upc server state data simplify 2023-07-20 14:12:01 -07:00
Zack Spear
23130f901a refactor: pluginInstalled to connectPluginInstalled 2023-07-12 17:43:51 -07:00
Zack Spear
96d62618d6 refactor: registration page EEXPIRED conditional extension copy 2023-07-07 12:58:52 -07:00
Zack Spear
e0a5978de7 refactor: connect settings – move sign in to bottom 2023-07-03 14:07:40 -07:00
Zack Spear
d9788bbb22 refactor: deploy-dev macos sound 2023-06-29 10:15:12 -07:00
Zack Spear
ccbf9469e4 refactor: remove web components remote manifest checking 2023-06-26 13:43:38 -07:00
Zack Spear
55f4a13ad4 fix: myservers2 var usage for plugin version 2023-06-26 13:43:11 -07:00
Zack Spear
bb4df5cf9b refactor(plg): test deploy script 2023-06-21 16:06:08 -05:00
Zack Spear
a27117a45d refactor(plg): theme props for user profile 2023-06-21 15:46:28 -05:00
Zack Spear
6498598553 refactor(plg): user profile prop simplification 2023-06-21 14:08:01 -05:00
Zack Spear
551210d458 refactor(plg): console.error for unfound js file 2023-06-21 14:07:37 -05:00
Zack Spear
7239bc73da refactor: deploy-dev script progress 2023-06-21 14:07:10 -05:00
Zack Spear
264fc11479 style: readability in includes 2023-06-21 11:03:44 -05:00
Zack Spear
b6c95b2863 refactor(myservers1): improved manifest usage w/ remote comparison 2023-06-21 10:56:01 -05:00
Zack Spear
920c992834 feat: script to deploy working changes to server 2023-06-21 10:54:52 -05:00
Zack Spear
b36828ca3b refactor: web components vue3 2023-06-19 17:45:37 -05:00
104 changed files with 30663 additions and 669 deletions

View File

@@ -0,0 +1,74 @@
name: Lint, Test, and Build Web Components
on:
workflow_dispatch:
jobs:
lint-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Lint files
run: npm run lint
build-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Build
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components

View File

@@ -86,6 +86,37 @@ jobs:
- name: Run Docker Compose
run: docker-compose run builder npm run coverage
lint-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Lint files
run: npm run lint
build-api:
defaults:
run:
@@ -144,6 +175,44 @@ jobs:
name: unraid-api
path: ${{ github.workspace }}/api/deploy/release/*.tgz
build-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Build
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components
build-plugin:
needs: [lint-api, test-api, build-api]
defaults:
@@ -157,6 +226,11 @@ jobs:
timezoneLinux: "America/Los_Angeles"
- name: Checkout repo
uses: actions/checkout@v3
- name: Download unraid web components
uses: actions/download-artifact@v3
with:
name: unraid-web
path: ./plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components
- name: Build Plugin
run: |
cd source/dynamix.unraid.net

80
.github/workflows/pull-request-web.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: Pull Request Web
on:
pull_request:
paths:
- 'web/**'
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}-web
cancel-in-progress: true
jobs:
lint-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Lint files
run: npm run lint
build-web:
defaults:
run:
working-directory: web
runs-on: ubuntu-latest
needs: [lint-web]
steps:
- name: Checkout repo
uses: actions/checkout@v3
- name: Create env file
run: |
touch .env
echo VITE_ACCOUNT=${{ vars.VITE_ACCOUNT }} >> .env
echo VITE_CONNECT=${{ vars.VITE_CONNECT }} >> .env
echo VITE_UNRAID_NET=${{ vars.VITE_UNRAID_NET }} >> .env
echo VITE_CALLBACK_KEY=${{ vars.VITE_CALLBACK_KEY }} >> .env
cat .env
- name: Install node
uses: actions/setup-node@v3
with:
cache: "npm"
cache-dependency-path: "web/package-lock.json"
node-version-file: "web/.nvmrc"
- name: Installing node deps
run: npm install
- name: Build
run: npm run build
- name: Upload build to Github artifacts
uses: actions/upload-artifact@v3
with:
name: unraid-web
path: web/.nuxt/nuxt-custom-elements/dist/unraid-components

87
.gitignore vendored Normal file
View File

@@ -0,0 +1,87 @@
# Logs
./logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
coverage-ts
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Visual Studio Code workspace
.vscode/*
!.vscode/extensions.json
# OSX
.DS_Store
# Temp dir for tests
test/__temp__/*
# Built files
dist
# Typescript
typescript
# Ultra runner
.ultra.cache.json
# Github actions
RELEASE_NOTES.md
# Docker Deploy Folder
deploy/*
!deploy/.gitkeep
# pkg cache
.pkg-cache
*.log*
.nuxt
.nitro
.cache
.output
.env*
!.env.example

30
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,30 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": false,
"source.fixAll.eslint": true
},
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#78797d",
"activityBar.background": "#78797d",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#df9fac",
"activityBarBadge.foreground": "#15202b",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#78797d",
"statusBar.background": "#5f6063",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#78797d",
"statusBarItem.remoteBackground": "#5f6063",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#5f6063",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#5f606399",
"titleBar.inactiveForeground": "#e7e7e799"
},
"peacock.color": "#5f6063",
"i18n-ally.localesPaths": [
"locales"
],
"i18n-ally.keystyle": "flat"
}

54
plugin/scripts/deploy-dev.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# Path to store the last used server name
state_file="$HOME/.deploy_state"
# Read the last used server name from the state file
if [[ -f "$state_file" ]]; then
last_server_name=$(cat "$state_file")
else
last_server_name=""
fi
# Read the server name from the command-line argument or use the last used server name as the default
server_name="${1:-$last_server_name}"
# Check if the server name is provided
if [[ -z "$server_name" ]]; then
echo "Please provide the SSH server name."
exit 1
fi
# Save the current server name to the state file
echo "$server_name" > "$state_file"
# Source directory path
source_directory="plugin/source/dynamix.unraid.net/usr/local/emhttp/plugins/dynamix.my.servers"
# Destination directory path
destination_directory="/usr/local/emhttp/plugins/dynamix.my.servers"
# Replace the value inside the rsync command with the user's input
rsync_command="rsync -avz --progress --stats -m -e ssh \"$source_directory/\" \"root@${server_name}.local:$destination_directory/\""
echo "Executing the following command:"
echo "$rsync_command"
# Execute the rsync command and capture the exit code
eval "$rsync_command"
exit_code=$?
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
afplay /System/Library/Sounds/Submarine.aiff
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
# Linux
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
# Windows
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
fi
# Exit with the rsync command's exit code
exit $exit_code

View File

@@ -457,8 +457,6 @@ const isCommaSeparatedURLs = input =>
</script>
<form id="UnraidNetSettings" markdown="1" name="UnraidNetSettings" method="POST" action="/update.htm" target="progressFrame">
<div markdown="1" class="<?=$shade?>"><!-- begin Account section -->
<?
/**
* Allowed origins warning displayed when the current webGUI URL is NOT included in the known lists of allowed origins.
@@ -480,6 +478,7 @@ if (stripos($allowedOrigins.",", "/".$host.",") === false) {
}
?>
<? if ($allowedOriginsArr): ?>
<div markdown="1" class="<?=$shade?>"><!-- begin allowedOrigins warning -->
<dl>
<div style="margin-bottom: 2rem;">
<span class="orange-text"><i class='fa fa-warning fa-fw'></i> <strong>_(Warning)_</strong></span> <?= sprintf(_('Your current url **%s** is not in the list of allowed origins for this server'), $host) ?>.
@@ -493,25 +492,8 @@ if (stripos($allowedOrigins.",", "/".$host.",") === false) {
<dd>
</div>
</dl>
<? endif ?>
&nbsp;
: <span>_(Questions? See <a href="https://docs.unraid.net/category/unraid-connect" target="_blank">the documentation</a>.)_</span>
_(Account status)_:
: <unraid-authed prop-registered="<? echo $isRegistered ?>"></unraid-authed>
<?if($isRegistered):?>
_(Connected to Unraid Connect Cloud)_:
<?if($isConnected):?>
: _(Yes)_
<?else:?>
: <i class="fa fa-warning icon warning"></i> _(No)_
<?endif // end check for ($isConnected) ?>
<?endif // end check for ($isRegistered) ?>
</div><!-- end Account section -->
<? endif ?>
<div markdown="1" class="<?=$shade?>"><!-- begin Remote Access section -->
@@ -544,7 +526,7 @@ _(Allow Remote Access)_:
<?endif?>
&nbsp;
: <unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check>
: <unraid-i18n-host><unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></unraid-wan-ip-check></unraid-i18n-host>
<div markdown="1" id="wanpanel" style="display:'none'">
@@ -705,8 +687,26 @@ $(function() {
<!-- start unraid-api section -->
<div markdown="1" class="js-unraidApiLogs <?=$shade?>">
&nbsp;
: <span>_(Questions? See <a href="https://docs.unraid.net/category/unraid-connect" target="_blank">the documentation</a>.)_</span>
_(Account status)_:
: <unraid-i18n-host><unraid-auth></unraid-auth></unraid-i18n-host>
<?if($isRegistered):?>
_(Connected to Unraid Connect Cloud)_:
<?if($isConnected):?>
: _(Yes)_
<?else:?>
: <i class="fa fa-warning icon warning"></i> _(No)_
<?endif // end check for ($isConnected) ?>
<?endif // end check for ($isRegistered) ?>
_(Download unraid-api Logs)_:
: <unraid-api-logs></unraid-api-logs>
: <unraid-i18n-host><unraid-download-api-logs></unraid-download-api-logs></unraid-i18n-host>
</div>
<!-- end unraid-api section -->
<? require_once("$docroot/plugins/dynamix.my.servers/include/translations.php"); ?>
$webComponentTranslations:
: <pre><code><?= var_dump($webComponentTranslations); ?></code></pre>

View File

@@ -0,0 +1,262 @@
Menu="About"
Type="xmenu"
Title="Registration"
Icon="icon-registration"
Tag="pencil"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
*
* 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.
*/
?>
<?
function my_time_any($time) {
return $time ? _(my_time($time),0) : _('Anytime');
}
function my_time_now($time) {
return $time ? _(my_time($time),0) : _('Unknown');
}
$regGen = (int)$var['regGen'] ?? 0;
$trialExtensionEligible = $regGen === 0 || $regGen === 1;
?>
<style>
span.thanks{padding-left:12px;color:#6FA239;font-weight:bold;}
span.thanks.red{color:#F0000C;}
div.device{padding:0 12px;font-weight:normal;font-style:italic;}
div.remark{padding:0 12px;text-align:justify;}
</style>
<?if ( (strstr($var['regTy'], "unregistered")) or ($var['regTy']=="Trial") or (strstr($var['regTy'], "no connection")) or (strstr($var['regTy'], "withdrawn")) or (strstr($var['regTy'], "expired")) ):?>
<span class="thanks">_(Thank you for trying Unraid OS)_!</span>
<?elseif ( ($var['regTy']=="Basic") or ($var['regTy']=="Plus") or ($var['regTy']=="Pro") ):?>
<span class="thanks">_(Thank you for choosing Unraid OS)_!</span>
<?endif;?>
<?if (strstr($var['regTy'], "unregistered")):?>
<div markdown="1" class="remark">
:registration_1_plug:
Your server will not be usable until you purchase a Registration key or install a free 30 day *Trial* key. A *Trial*
key provides all the functionality of a *Pro* Registration key.
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 (min 4GB recommended).
Note: USB memory card readers are generally **not** supported because most do not present unique serial numbers.
:end
</div>
<?endif;?>
<?if ($var['regTy']=="Trial"):?>
<div markdown="1" class="remark">
:registration_3_plug:
Your *Trial* key includes all the functionality and device support of a *Pro* Registration key.
After your *Trial* key has reached expiration, your server **still functions normally** until the next time you **Stop** the array.
At that point, you may either purchase a Registration key, or request a *Trial* extension.
:end
</div>
<?endif;?>
<?if (strstr($var['regTy'], "no connection")):?>
<div markdown="1" class="remark">
<span class='red-text'>_(Cannot connect to key-server)_!</span>
_(Your *Trial* key requires an internet connection)_. _(Please check your)_ [_(Network Settings)_](NetworkSettings).
</div>
<?endif;?>
<?if (strstr($var['regTy'], "withdrawn")):?>
<div markdown="1" class="remark">
<span class='red-text'>_(Release has been withdrawn)_!</span>
_(This release has been withdrawn for use with *Trial* keys)_.
</div>
<?endif;?>
<?if (strstr($var['regTy'], "expired")):?>
<div markdown="1" class="remark">
<span class='red-text'>_(Your *Trial* key has expired)_.</span>
<? if ($trialExtensionEligible): ?>
:registration_4_plug:
To continue using Unraid OS you may purchase a Registration key. Alternately, you may request a *Trial* extension key.
:end
<? else: ?>
:registration_trial_extension_ineligible_plug:
You have used all your Trial extensions. To continue using Unraid OS you may purchase a Registration key.
:end
<? endif; ?>
</div>
<?endif;?>
<?if (strstr($var['regTy'], "invalid installation")):?>
<span class='thanks red'>_(Invalid *Trial* Installation)_</span>
<div markdown="1" class="remark">
:registration_5_plug:
It is not possible to use a *Trial* key with an existing Unraid OS installation.
You may purchase a Registration key corresponding to this USB Flash device to continue using this installation.
For more information, please [Contact Support](https://lime-technology.com/contact).
:end
</div>
<?endif;?>
<?if (strstr($var['regTy'], "missing")):?>
<span class='thanks red'>_(Missing Key File)_</span>
<div markdown="1" class="remark">
:registration_6_plug:
It appears that your Registration key file is corrupted or missing. The key file should be located in the
[config](/Registration/Browse?dir&#61;/boot/config) directory on your USB Flash boot device.
If you do not have a backup copy of your Registration key file, [Contact Support](https://lime-technology.com/contact).
If this was a *Trial* installation, you may purchase a Registration key.
:end
</div>
<?endif;?>
<?if (strstr($var['regTy'], "invalid key")):?>
<span class='thanks red'>_(The registered GUID does not match the USB Flash boot device GUID)_</span>
<?if (strstr($var['regTy'], "Trial")):?>
<div markdown="1" class="remark">
:registration_7_plug:
*Trial* installations are only valid with the originally registered USB Flash device.
To continue using this installation with this USB Flash device, you may purchase a Registration key.
:end
</div>
<?else:?>
<div markdown="1" class="remark">
:registration_8_plug:
The Registration key file does not correspond to the USB Flash boot device.
Please copy the correct key file to the [config](/Registration/Browse?dir&#61;/boot/config) directory
on your USB Flash boot device. If you do not have a backup copy of your key file, [Contact Support](https://lime-technology.com/contact).
If you want to replace your Registration key with a new key bound to this USB Flash device, click Replace Key below. An original key may be
replaced anytime. Thereafter, a replacement key may be replaced again after one year has passed. If you require
another replacement key sooner, [Contact Support](https://lime-technology.com/contact).
**Note:** Replacing a Registration key results in permanently *blacklisting* the previous USB Flash GUID.
:end
</div>
<?endif;?>
<?endif;?>
<?if (strstr($var['regTy'], "blacklisted")):?>
<span class='thanks red'>_(Blacklisted USB Flash GUID)_</span>
<div markdown="1" class="remark">
:registration_9_plug:
This USB Flash boot device has been *blacklisted*. This can occur as a result of transfering your Registration key to
a replacement USB Flash device, and you are currently booted from your old USB Flash device.
A USB Flash device may also be *blacklisted* if there is no serial number, or if we discover the serial number
is not unique (this is common with USB card readers).
For more information, please [Contact Support](https://lime-technology.com/contact).
:end
</div>
<?endif;?>
<?if ( ( !(strstr($var['regTy'], "invalid key")) and ((strstr($var['regTy'], "Trial"))) ) || (strstr($var['regTy'], "no connection")) || (strstr($var['regTy'], "withdrawn")) ):?>
_(***Trial*** key expires on)_:
: <?=my_time_now($var['regTm2'])?>
<?endif;?>
<?if ( strstr($var['regTy'], "invalid installation") || ( (strstr($var['regTy'], "invalid key")) && (strstr($var['regTy'], "Trial")) )):?>
_(Expiration)_:
: <?=my_time_now($var['regTm2'])?>
<?endif;?>
<?if ( (strstr($var['regTy'], "invalid installation")) || (strstr($var['regTy'], "invalid key")) || ($var['regTy']=="Basic") || ($var['regTy']=="Plus") || ($var['regTy']=="Pro") ):?>
_(Registered to)_:
: <?=htmlspecialchars($var['regTo'])?>
_(Registered on)_:
: <?=my_time_now($var['regTm'])?>
<?endif;?>
<?if ( (strstr($var['regTy'], "invalid installation")) or ( (strstr($var['regTy'], "invalid key")) and (!(strstr($var['regTy'], "Trial")))) ):?>
_(Registered GUID)_:
: <?=$var['regGUID']?>
<?endif;?>
<?if (strstr($var['regTy'], "flash device error")):?>
<span class='thanks red'>_(Error accessing your physical USB Flash boot device)_</span>
<div markdown="1" class="remark">
_(There is a physical problem accessing your USB Flash boot device)_. _(Please)_ [Contact Support](https://lime-technology.com/contact).
_(Flash GUID)_:
: _(Error code)_: <?=$var['regCheck']?>
<?else:?>
_(Flash GUID)_:
: <?=$var['flashGUID']?>
<?endif;?>
_(Flash Vendor)_:
: <?=$var['flashVendor']?>
_(Flash Product)_:
: <?=$var['flashProduct']?>
<?if ( ((strstr($var['regTy'], "invalid key")) and !(strstr($var['regTy'], "Trial"))) || ($var['regTy']=="Basic") || ($var['regTy']=="Plus") || ($var['regTy']=="Pro") ):?>
_(Replaceable)_:
: <?=my_time_any($var['regTm2'])?>
<?endif;?>
<?if ( !(strstr($var['regTy'], "flash device error")) || !(strstr($var['regTy'], "blacklisted")) ):?>
<div class="device"><?=sprintf(_("This server has %s attached storage device".($var['deviceCount']==1?'.':'s.')),$var['deviceCount'])?></div>
<?endif;?>
&nbsp;
: <unraid-i18n-host><unraid-key-actions></unraid-key-actions></unraid-i18n-host>

View File

@@ -0,0 +1,12 @@
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
$var = (array)parse_ini_file('state/var.ini');
require_once "$docroot/webGui/include/Wrappers.php";
require_once "$docroot/webGui/include/Helpers.php";
extract(parse_plugin_cfg('dynamix',true));
require_once "$docroot/plugins/dynamix.my.servers/include/state.php";
header('Content-type: application/json');
echo json_encode($serverState);

View File

@@ -0,0 +1,42 @@
<style>
#header {
z-index: 102 !important;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
#header unraid-i18n-host {
font-size: 16px;
margin-left: auto;
height: 100%;
}
</style>
<?php
// Set the path for the local manifest file
$localManifestFile = '/usr/local/emhttp/plugins/dynamix.my.servers/unraid-components/manifest.json';
// Load the local manifest
$localManifest = json_decode(file_get_contents($localManifestFile), true);
$searchText = 'unraid-components.client.mjs';
$fileValue = null;
foreach ($localManifest as $key => $value) {
if (strpos($key, $searchText) !== false && isset($value["file"])) {
$fileValue = $value["file"];
break;
}
}
if ($fileValue !== null) {
$prefixedPath = '/plugins/dynamix.my.servers/unraid-components/';
echo '<script src="' . $prefixedPath . $fileValue . '"></script>';
} else {
echo '<script>console.error("%cNo matching key containing \'' . $searchText . '\' found.", "font-weight: bold; color: white; background-color: red");</script>';
}

View File

@@ -1,648 +1,25 @@
<!-- myservers2 -->
<?
// add 'ipaddr' function for 6.9 backwards compatibility
if (!function_exists('ipaddr')) {
function ipaddr($ethX='eth0', $prot=4) {
global $$ethX;
switch ($$ethX['PROTOCOL:0']) {
case 'ipv4':
return $$ethX['IPADDR:0'];
case 'ipv6':
return $$ethX['IPADDR6:0'];
case 'ipv4+ipv6':
switch ($prot) {
case 4: return $$ethX['IPADDR:0'];
case 6: return $$ethX['IPADDR6:0'];
default:return [$$ethX['IPADDR:0'],$$ethX['IPADDR6:0']];}
default:
return $$ethX['IPADDR:0'];
}
}
}
$upc_translations = [
($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US' => [
'getStarted' => _('Get Started'),
'signIn' => _('Sign In'),
'signUp' => _('Sign Up'),
'signInUp' => _('Sign In / Up'),
'signInUnraidNetAccount' => _('Sign In with Unraid.net Account'),
'signOut' => _('Sign Out'),
'error' => _('Error'),
'fixError' => _('Fix Error'),
'closeLaunchpad' => _('Close Launchpad and continue to webGUI'),
'installPlugin' => _('Install Plugin'),
'noThanks' => _('No thanks'),
'closePromo' => _('Close Connect details and continue to webGUI'),
'promoHeading' => _('Enhance your Unraid experience with these<br> Connect (BETA) features'),
'learnMore' => _('Learn more'),
'checkoutTheMyServersDocs' => _('Checkout the Connect docs'),
'popUp' => _('Pop-up'),
'close' => _('Close'),
'backToPopUp' => sprintf(_('Back to %s'), _('Pop-up')),
'closePopUp' => sprintf(_('Close %s'), _('Pop-up')),
'contactSupport' => _('Contact Support'),
'lanIp' => sprintf(_('Click to copy LAN IP %s'), '{0}'),
'lanIpCopied' => _('LAN IP Copied'),
'continueToUnraid' => _('Continue to Unraid'),
'description' => _('Description'),
'year' => _('year'),
'years' => _('years'),
'month' => _('month'),
'months' => _('months'),
'day' => _('day'),
'days' => _('days'),
'hour' => _('hour'),
'hours' => _('hours'),
'minute' => _('minute'),
'minutes' => _('minutes'),
'second' => _('second'),
'seconds' => _('seconds'),
'ago' => _('ago'),
'basicPlusPro' => [
'heading' => _('Thank you for choosing Unraid OS and Connect').'!',
'message' => [
'registered' => _('Register for Connect by signing in to Unraid.net'),
'upgradeEligible' => _('To support more storage devices as your server grows click Upgrade Key'),
],
],
'actions' => [
'purchase' => _('Purchase Key'),
'upgrade' => _('Upgrade Key'),
'recover' => _('Recover Key'),
'replace' => _('Replace Key'),
'replaceIneligible' => _('Replace Key Ineligible'),
'extend' => _('Extend Trial'),
'startTrial' => _('Start Trial'),
'signOutUnraidNet' => _('Sign Out of Unraid.net'),
'redeemActivationCode' => _('Redeem Activation Code'),
],
'upc' => [
'avatarAlt' => '{0} '._('Avatar'),
'confirmClosure' => _('Confirm closure then continue to webGUI'),
'closeDropdown' => _('Close dropdown'),
'openDropdown' => _('Open dropdown'),
'pleaseConfirmClosureYouHaveOpenPopUp' => _('Please confirm closure').'. '._('You have an open pop-up').'.',
'trialHasExpiredSeeOptions' => _('Trial has expired see options below'),
'errorCertRequiresSignIn' => _('Sign In before your Unraid.net SSL certificate expires'),
'removeMyServersPlugin' => _('Remove Connect plugin'),
'continueUsingMyServers' => _('Continue using Connect'),
'confirmMyServersPluginRemoval' => _('Confirm Connect plugin removal'),
'removingMyServersPlugin' => _('Removing Connect plugin…'),
'enhanceYourExperienceWithMyServers' => _('Enhance your experience with Connect'),
'lanIpCopied' => _('LAN IP Copied'),
'installingMyServers' => _('Installing Connect (beta)'),
"updatePlugin" => _('Update Plugin'),
"updatingMyServers" => _('Updating Connect (beta)'),
'thankYouForInstallingMyServers' => _('Thank you installing Connect') . '!',
'connectYourUnraidnetAccountToGetStarted' => _('Connect your Unraid.net account to get started'),
'noRemoteApikeyRegisteredWithPlg' => [
'heading' => _('Connect Error'),
'msg' => _('Unraid.net re-authentication required'),
],
'errorTooManyDisks' => [
'heading' => 'Too many devices',
'msg' => [
'base' => 'You must upgrade your key to support more devices.',
'basic' => 'Your Basic key supports 6 devices.',
'plus' => 'Your Plus key supports 12 devices.',
],
],
'extraLinks' => [
'newTab' => sprintf(_('Opens %s in new tab'), '{0}'),
'myServers' => _('Go to Connect'),
'forums' => _('Unraid Forums'),
'settings' => [
'text' => _('Settings'),
'title' => _('Settings > Management Access • Unraid.net'),
],
],
'meta' => [
'trial' => [
'active' => [
'date' => sprintf(_('Trial key expires at %s'), '{date}'),
'timeDiff' => sprintf(_('Trial expires in %s'), '{timeDiff}'),
],
'expired' => [
'date' => sprintf(_('Trial key expired at %s'), '{date}'),
'timeDiff' => sprintf(_('Trial expired %s'), '{timeDiff}'),
],
],
'uptime' => [
'date' => sprintf(_('Server up since %s'), '{date}'),
'readable' => sprintf(_('Uptime %s'), '{timeDiff}'),
],
],
'myServers' => [
'heading' => _('Connect'),
'beta' => _('beta'),
'restarting' => _('Restarting…'),
'errors' => [
'unraidApi' => [
'heading' => _('Unraid API Error'),
'message' => _('Failed to connect to Unraid API'),
],
'myServers' => [
'heading' => _('Connect Error'),
'message' => _('Please wait a moment and reload the page'),
],
],
'closeDetails' => _('Close Details'),
'loading' => _('Loading Connect data'),
'displayingLastKnown' => _('Displaying last known server data'),
'mothership' => [
'connected' => _('Connected'),
'notConnected' => _('Disconnected'),
],
'accessLabels' => [
'current' => _('Current server'),
'local' => _('Local access'),
'offline' => _('Server Offline'),
'remote' => _('Remote access'),
'unavailable' => _('Access unavailable'),
],
'api' => [
'start' => _('Restart unraid-api'),
'startTitle' => _('Executes `unraid-api start`; no terminal needed'),
'stop' => _('Stop unraid-api'),
],
],
'opensNewHttpsWindow' => [
'base' => sprintf(_('Opens new HTTPS window to %s'), '{0}'),
'signIn' => sprintf(_('Opens new HTTPS window to %s'), _('Sign In')),
'signOut' => sprintf(_('Opens new HTTPS window to %s'), _('Sign Out')),
'purchase' => sprintf(_('Opens new HTTPS window to %s'), _('Purchase Key')),
'upgrade' => sprintf(_('Opens new HTTPS window to %s'), _('Upgrade Key')),
],
'signInActions' => [
'resolve' => _('Sign In to resolve'),
'purchaseKey' => _('Sign In to Purchase Key'),
'purchaseKeyOrExtendTrial' => '@:upc.signInActions.purchaseKey or @:actions.extend',
],
],
'stateData' => [
'ENOKEYFILE' => [
'humanReadable' => _('No Keyfile'),
'heading' => _("Let's unleash your hardware").'!',
'message' => '<p>'._('Your server will not be usable until you purchase a Registration key or install a free 30-day Trial key').'. '._('A Trial 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 (min 4GB recommended)').'.</p><p>'._('Note: USB memory card readers are generally not supported because most do not present unique serial numbers').'.</p>',
],
'TRIAL' => [
'humanReadable' => _('Trial'),
'heading' => _('Thank you for choosing Unraid OS').'!',
'message' => _('Your Trial key includes all the functionality and device support of a Pro key').'. '._('After your Trial has reached expiration your server still functions normally until the next time you Stop the array or reboot your server').'. '._('At that point you may either purchase a license key or request a Trial extension').'.',
'_extraMsg' => sprintf(_('You have %s remaining on your Trial key'), '{parsedExpireTime}'),
],
'EEXPIRED' => [
'humanReadable' => _('Trial Expired'),
'heading' => _('Your Trial has expired'),
'message' => [
'base' => _('To continue using Unraid OS you may purchase a license key').'. ',
'extensionNotEligible' => _('You have used all your Trial extensions').'. @:stateData.EEXPIRED.message.base',
'extensionEligible' => '@:stateData.EEXPIRED.message.base '._('Alternately, you may request a Trial extension').'.',
],
],
'BASIC' => [
'humanReadable' => _('Basic'),
],
'PLUS' => [
'humanReadable' => _('Plus'),
],
'PRO' => [
'humanReadable' => _('Pro'),
],
'EGUID' => [
'humanReadable' => _('GUID Error'),
'error' => [
'heading' => _('Registration key / GUID mismatch'),
'message' => [
'default' => _('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').'.',
'replacementIneligible' => _('Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months').'.',
'replacementEligible' => _('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 or Replace Key').'.',
'blacklisted' => _('Your Unraid registration key is ineligible for replacement as it is blacklisted') . '.',
],
],
],
'ENOKEYFILE2' => [
'humanReadable' => _('Missing key file'),
'error' => [
'heading' => '@:stateData.ENOKEYFILE2.humanReadable',
'message' => _('It appears that your license key file is corrupted or missing').'. '._('The key file should be located in the */config* directory on your USB Flash boot device').'. '._('If you do not have a backup copy of your license key file you may install the Connect (beta) plugin to attempt to recover your key').'. '._('If this was an expired Trial installation, you may purchase a license key').'.',
],
],
'ETRIAL' => [
'humanReadable' => _('Invalid installation'),
'error' => [
'heading' => '@:stateData.ETRIAL.humanReadable',
'message' => _('It is not possible to use a Trial key with an existing Unraid OS installation').'. '._('You may purchase a license key corresponding to this USB Flash device to continue using this installation').'.',
],
],
'ENOKEYFILE1' => [
'humanReadable' => _('No Keyfile'),
'error' => [
'heading' => _('No USB flash configuration data'),
'message' => _('There is a problem with your USB Flash device'),
],
],
'ENOFLASH' => [
'humanReadable' => _('No Flash'),
'error' => [
'heading' => _('Cannot access your USB Flash boot device'),
'message' => _('There is a physical problem accessing your USB Flash boot device'),
],
],
'EGUID1' => [
'humanReadable' => _('Multiple License Keys Present'),
'error' => [
'heading' => '@:stateData.EGUID1.humanReadable',
'message' => _('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').'. '._('Alternately you may purchase a license key for this USB flash device').'. '._('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').'.',
],
],
'EBLACKLISTED' => [
'humanReadable' => _('BLACKLISTED'),
'error' => [
'heading' => _('Blacklisted USB Flash GUID'),
'message' => _('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').'. '._('A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers').'.',
],
],
'EBLACKLISTED1' => [
'humanReadable' =>'@:stateData.EBLACKLISTED.humanReadable',
'error' => [
'heading' => _('USB Flash device error'),
'message' => _('This USB Flash device has an invalid GUID').'. '._('Please try a different USB Flash device').'.',
],
],
'EBLACKLISTED2' => [
'humanReadable' => '@:stateData.EBLACKLISTED.humanReadable',
'error' => [
'heading' => _('USB Flash has no serial number'),
'message' => '@:stateData.EBLACKLISTED.error.message',
],
],
'ENOCONN' => [
'humanReadable' => _('Trial Requires Internet Connection'),
'error' => [
'heading' => _('Cannot validate Unraid Trial key'),
'message' => _('Your Trial key requires an internet connection').'. '._('Please check Settings > Network').'.',
],
],
'STALE' => [
'humanReadable' => _('Stale'),
'error' => [
'heading' => _('Stale Server'),
'message' => _('Please refresh the page to ensure you load your latest configuration'),
],
],
],
'regWizPopUp' => [
'regWiz' => _('Registration Wizard'),
'toHome' => _('To Registration Wizard Home'),
'continueTrial' => _('Continue Trial'),
'serverInfoToggle' => _('Toggle server info visibility'),
'youCanSafelyCloseThisWindow' => _('You can safely close this window'),
'automaticallyClosingIn' => sprintf(_('Auto closing in %s'), '{0}'),
'byeBye' => _('bye bye 👋'),
'browserWillSelfDestructIn' => sprintf(_('Browser will self destruct in %s'), '{0}'),
'closingPopUpMayLeadToErrors' => _('Closing this pop-up window while actions are being preformed may lead to unintended errors'),
'goBack' => _('Go Back'),
'shutDown' => _('Shut Down'),
'haveAccountSignIn' => _('Already have an account').'? '._('Sign In'),
'noAccountSignUp' => _('Do not have an account').'? '._('Sign Up'),
'willConnectYourServerToMyServers' => _('This will register your server with Connect <sup>BETA</sup>'),
'serverInfo' => [
'flash' => _('Flash'),
'product' => _('Product'),
'GUID' => _('GUID'),
'name' => _('Name'),
'ip' => _('IP'),
],
'forms' => [
'displayName' => _('Display Name'),
'emailAddress' => _('Email Address'),
'displayNameOrEmailAddress' => _('Display Name or Email Address'),
'displayNameRootMessage' => _('Use your Unraid.net credentials, not your local server credentials'),
'honeyPotCopy' => _('If you fill this field out then your email will not be sent'),
'fieldRequired' => _('This field is required'),
'submit' => _('Submit'),
'submitting' => _('Submitting'),
'notValid' => _('Form not valid'),
'cancel' => _('Cancel'),
'confirm' => _('Confirm'),
'createMyAccount' => _('Create My Account'),
'subject' => _('Subject'),
'password' => _('Password'),
'togglePasswordVisibility' => _('Toggle Password Visibility'),
'message' => _('Message'),
'confirmPassword' => _('Confirm Password'),
'passwordMustMatch' => _('Password confirmation must match'),
'passwordMinimum' => _('8 or more characters'),
'comments' => _('comments'),
'newsletterCopy' => _('Sign me up for the monthly Unraid newsletter').': '._('a digest of recent blog posts, community videos, popular forum threads, product announcements, and more'),
'terms' => [
'iAgree' => _('I agree to the'),
'text' => _('Terms of Use'),
],
],
'routes' => [
'extendTrial' => [
'heading' => [
'loading' => _('Extending Trial'),
'error' => _('Trial Extension Failed'),
],
'message' => _('Not ready to purchase?').'<br>'._('Receive an additional 15 days for your trial').'.',
],
'forgotPassword' => [
'heading' => _('Forgot Password'),
'subheading' => _("After resetting your password come back to the Registration Wizard pop-up window to Sign In and complete your server's registration"),
'resetPasswordNow' => _('Reset Password Now'),
'backToSignIn' => _('Back to Sign In'),
],
'signIn' => [
'heading' => [
'signIn' => _('Unraid.net Sign In'),
'recover' => _('Unraid.net Sign In to Recover Key'),
'replace' => _('Unraid.net Sign In to Replace Key'),
],
'form' => [
'replacementConditions' => [
'name' => _('Acknowledge Replacement Conditions'),
'label' => _('I acknowledge that replacing a license key results in permanently blacklisting the previous USB Flash GUID'),
],
'label' => [
'password' => [
'replace' => _('Unraid.net account password'),
],
],
],
],
'signUp' => [
'heading' => _('Create Unraid.net Account'),
],
'signOut' => [
'heading' => _('Unraid.net Sign Out'),
'warnings' => [
'remoteAccessDisabled' => _('Remote access will be disabled'),
'remoteAccessInaccessible' => sprintf(_('You will no longer have access to this server using <abbr title="%s" class="italic">this url</abbr>'), '{0}'),
'disablingFlashBackup' => _('Automated flash backups will be disabled until you sign in again'),
'downloadFlashBackup' => _('Download latest backup from Connect Dashboard before signing out'),
],
],
'success' => [
'heading' => [
'username' => sprintf(_('Hi %s'), '{0}'),
'default' => _('Success'),
],
'subheading' => [
'extention' => _('Your trial will expire in 15 days'),
'newTrial' => _('Your trial will expire in 30 days'),
],
'signIn' => [
'tileTitle' => [
'actionFail' => sprintf(_('%s was not signed in to your Unraid.net account'), '{0}'),
'actionSuccess' => sprintf(_('%s is signed in to your Unraid.net account'), '{0}'),
'loading' => sprintf(_('Signing in %s to Unraid.net account'), '{0}'),
],
],
'signOut' => [
'tileTitle' => [
'actionFail' => sprintf(_('%s was not signed out of your Unraid.net account'), '{0}'),
'actionSuccess' => sprintf(_('%s was signed out of your Unraid.net account'), '{0}'),
'loading' => sprintf(_('Signing out %s from Unraid.net account'), '{0}'),
],
],
'keys' => [
'trial' => _('Trial'),
'basic' => _('Basic'),
'plus' => _('Plus'),
'pro' => _('Pro'),
],
'extended' => sprintf(_('%s Key Extended'), '{0}'),
'recovered' => sprintf(_('%s Key Recovered'), '{0}'),
'replaced' => sprintf(_('%s Key Replaced'), '{0}'),
'created' => sprintf(_('%s Key Created'), '{0}'),
'install' => [
'loading' => sprintf(_('Installing %s Key'), '{0}'),
'error' => sprintf(_('%s Key Install Error'), '{0}'),
'success' => sprintf(_('Installed %s Key'), '{0}'),
'manualInstructions' => _('To manually install the key paste the key file url into the Key file URL field on the webGUI Tools > Registration page and then click Install Key') . '.',
'copyFail' => _('Unable to copy'),
'copySuccess' => _('Copied key url') . '!',
'copyButton' => _('Copy Key URL'),
'copyBeforeClose' => _('Please copy the Key URL before closing this window'),
],
'timeout' => sprintf(_('Communication with %s has timed out'), '{0}'),
'loading1' => _('Please keep this window open'),
'loading2' => _('Still working our magic'),
'countdown' => [
'success' => [
'prefix' => sprintf(_('Auto closing in %s'), '{0}'),
'text' => _('You can safely close this window'),
],
'error' => [
'prefix' => sprintf(_('Auto redirecting in %s'), '{0}'),
'text' => _('Back to Registration Home'),
'complete' => _('Back in a flash ⚡️'),
],
],
],
'troubleshoot' => [
'heading' => [
'default' => _('Troubleshoot'),
'success' => _('Thank you for contacting Unraid'),
],
'subheading' => [
'default' => _("Forgot what Unraid.net account you used").'? '._("Have a USB flash device that already has an account associated with it").'? '._("Just give us the details about what happened and we'll do our best to get you up and running again").'.',
'success' => _('We have received your e-mail and will respond in the order it was received').'. '._('While we strive to respond to all requests as quickly as possible please allow for up to 3 business days for a response').'.',
],
'relevantServerData' => _('Your USB Flash GUID and other relevant server data will also be sent'),
],
'verifyEmail' => [
'heading' => _('Verify Email'),
'subheading' => sprintf(_('We have sent a verifcation email to %s'), '{0}'),
'form' => [
'verificationCode' => _('verification code'),
'verifyCode' => _('Paste or Enter code'),
],
'noCode' => _("Didn't get code?"),
],
'verifyEmailResend' => [
'heading' => _('Resend Email Verification Code'),
'goBack' => _("Have the code now? Go Back"),
'resend' => _("Resend Code"),
],
'whatIsMyServers' => [
'heading' => _('What is Unraid.net?'),
'subheading' => _('Expand your servers capabilities'),
'copy' => _('With an Unraid.net account you can start using Connect (beta) which gives you access to the following features:'),
'features' => [
"dynamicRemoteAccess" => [
'heading' => _('Dynamic Remote Access'),
'copy' => _('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').'.',
],
"manageWithinConnect" => [
'heading' => _('Manage Your Server Within Connect'),
'copy' => _('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').'.',
],
"deepLinking" => [
'heading' => _('Deep Linking'),
'copy' => _('The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections').'.',
],
"onlineFlashBackup" => [
'heading' => _('Online Flash Backup'),
'copy' => _('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').'.',
],
"realTimeMonitoring" => [
'heading' => _('Real-time Monitoring'),
'copy' => _("Get an overview of your server's state, storage space, apps and VMs status, and more").'.',
],
"customizableDashboardTitles" => [
'heading' => _('Customizable Dashboard Tiles'),
'copy' => _("Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard").'.',
],
"licenseManagement" => [
'heading' => _('License Management'),
'copy' => _('Manage your license keys at any time via the My Keys section').'.',
],
'plusMore' => [
'heading' => _('Plus more on the way'),
'copy' => _('All you need is an active internet connection, an Unraid.net account, and the Connect plugin').'. '._('Get started by installing the plugin') . '.',
],
],
],
'replaceKey' => [
'subheading' => [
'registered' => 'A record of your replacement will be sent to your Unraid.net account email address',
'notRegistered' => 'A record of your replacement will be sent to this email',
],
],
'notFound' => [
'subheading' => _('Page Not Found'),
],
'notAllowed' => [
'subheading' => _('Page Not Allowed'),
],
],
],
'wanIpCheck' => [
'checking' => _('Checking Wan IPs'),
'match' => sprintf(_('Remark: your WAN IPv4 is **%s**'), '{0}'),
'mismatch' => 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').'. '._('Ignore this message if you are currently connected via Remote Access or VPN').'.',
'resolveError' => _('DNS issue, unable to resolve wanip4.unraid.net'),
],
'upcTrigger' => [
'upgrade' => _('To support more storage devices as your server grows click the *Open Dropdown* button').'.',
'default' => _('Key management is done via the dropdown in the top right of the webGUI on every page').'.',
'open' => _('Open Dropdown'),
],
'yargYePirate' => _('Oh no! Are you pirating Unraid OS?<br>Are you ready to buy a real license?'),
'keyFileNotValid' => _('Key file not valid'),
'installFailed' => [
'heading' => _('Connect plugin install failed'),
'message' => _('The Connect plugin install is incomplete').'. '._('Please uninstall and reinstall the Connect plugin').'. '._('Be sure to let the install complete before you close the window').'.',
],
"downloadUnraidApiLogs" => _('Download unraid-api Logs'),
"download" => _('Download'),
"pleaseWait" => _('Please wait…')
],
];
// note: $myservers variable defined in myservers1.php, by parsing myservers.cfg
$configErrorEnum = [ // used to map $var['configValid'] value to mimic unraid-api's `configError` ENUM
"error" => 'UNKNOWN_ERROR',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
];
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
$plgInstalled = '';
if (!file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net') && !file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) {
$plgInstalled = ''; // base OS only, plugin not installed • show ad for plugin
} else {
// plugin is installed but if the unraid-api file doesn't fully install it's a failed install
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $plgInstalled = 'dynamix.unraid.net.plg';
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $plgInstalled = 'dynamix.unraid.net.staging.plg';
// plugin install failed • append failure detected so we can show warning about failed install via UPC
if (!file_exists('/usr/local/sbin/unraid-api')) $plgInstalled = $plgInstalled . '_installFailed';
}
// read flashbackup ini file
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
$serverstate = [ // feeds server vars to Vuex store in a slightly different array than state.php
"avatar" => (!empty($myservers['remote']['avatar']) && $plgInstalled) ? $myservers['remote']['avatar'] : '',
"config" => [
'valid' => $var['configValid'] === 'yes',
'error' => $var['configValid'] !== 'yes'
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
: null,
],
"deviceCount" => $var['deviceCount'],
"email" => $myservers['remote']['email'] ?? '',
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
"flashproduct" => $var['flashProduct'],
"flashvendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"internalip" => ipaddr(),
"internalport" => $_SERVER['SERVER_PORT'],
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
"osVersion" => $var['version'],
"plgVersion" => $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'] ),
"plgInstalled" => $plgInstalled,
"protocol" => $_SERVER['REQUEST_SCHEME'],
"reggen" => (int)$var['regGen'],
"regGuid" => $var['regGUID'],
"registered" => (!empty($myservers['remote']['username']) && $plgInstalled),
"servername" => $var['NAME'],
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
"ts" => time(),
"username" => (!empty($myservers['remote']['username']) && $plgInstalled) ? $myservers['remote']['username'] : '',
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
];
/** @TODO - prop refactor needed. The issue is because the prop names share the same name as the vuex store variables
* if we remove the props and deployed a UPC that doesn't rely on props anymore uses that don't have an updated version
* of this file will have a non-working UPC.
* apikey
* apiVersion
* csrf
* expiretime
* hideMyServers
* plgPath
* regWizTime
* sendCrashInfo
* serverdesc
* servermodel
* serverupdate
* uptime
*/
<?php
$docroot = $docroot ?? $_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp';
require_once("$docroot/plugins/dynamix.my.servers/include/state.php");
require_once("$docroot/plugins/dynamix.my.servers/include/translations.php");
?>
<unraid-user-profile
apikey="<?=$myservers['upc']['apikey'] ?? ''?>"
api-version="<?=$myservers['api']['version'] ?? ''?>"
banner="<?=$display['banner'] ?? ''?>"
bgcolor="<?=($backgnd) ? '#'.$backgnd : ''?>"
csrf="<?=$var['csrf_token']?>"
displaydesc="<?=($display['headerdescription']??''!='no') ? 'true' : ''?>"
expiretime="<?=1000*($var['regTy']=='Trial'||strstr($var['regTy'],'expired')?$var['regTm2']:0)?>"
hide-my-servers="<?=$plgInstalled ? '' : 'yes' ?>"
locale="<?=($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US'?>"
locale-messages="<?=rawurlencode(json_encode($upc_translations, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE))?>"
metacolor="<?=($display['headermetacolor']??'') ? '#'.$display['headermetacolor'] : ''?>"
plg-path="dynamix.my.servers"
reg-wiz-time="<?=$myservers['remote']['regWizTime'] ?? ''?>"
serverdesc="<?=$var['COMMENT']?>"
servermodel="<?=$var['SYS_MODEL']?>"
serverstate="<?=rawurlencode(json_encode($serverstate, JSON_UNESCAPED_SLASHES))?>"
show-banner-gradient="<?=$display['showBannerGradient'] ?? 'yes'?>"
textcolor="<?=($header) ? '#'.$header : ''?>"
theme="<?=$display['theme']?>"
uptime="<?=1000*(time() - round(strtok(exec("cat /proc/uptime"),' ')))?>"
></unraid-user-profile>
<!-- /myservers2 -->
<script>
window.LOCALE_DATA = '<?= rawurlencode(json_encode($webComponentTranslations, JSON_UNESCAPED_SLASHES, JSON_UNESCAPED_UNICODE)) ?>';
/**
* So we're not needing to modify DefaultLayout with an additional include, we'll add the Modals web component to the bottom of the body.
*/
const i18nHostWebComponent = 'unraid-i18n-host';
const modalsWebComponent = 'unraid-modals';
if (!document.getElementsByTagName(modalsWebComponent).length) {
const $body = document.getElementsByTagName('body')[0];
const $i18nHost = document.createElement(i18nHostWebComponent);
const $modals = document.createElement(modalsWebComponent);
$body.appendChild($i18nHost);
$i18nHost.appendChild($modals);
}
</script>
<?
echo "
<unraid-i18n-host>
<unraid-user-profile server='" . json_encode($serverState) . "'></unraid-user-profile>
</unraid-i18n-host>";

View File

@@ -0,0 +1,81 @@
<?php
// read flashbackup ini file
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
// base OS only, plugin not installed • show ad for plugin
$connectPluginInstalled = '';
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $connectPluginInstalled = 'dynamix.unraid.net.plg';
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
// plugin install failed if the unraid-api file doesn't fully install • append failure detected so we can show warning about failed install via UPC
if ($connectPluginInstalled && !file_exists('/usr/local/sbin/unraid-api')) $connectPluginInstalled .= '_installFailed';
$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-' . $var['version']);
$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) : [];
$configErrorEnum = [
"error" => 'UNKNOWN_ERROR',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
];
$registered = !empty($myservers['remote']['username']) && $connectPluginInstalled;
$serverState = [
"apiKey" => $registered ? $myservers['upc']['apikey'] ?? '' : '',
"apiVersion" => $myservers['api']['version'] ?? '',
"avatar" => (!empty($myservers['remote']['avatar']) && $connectPluginInstalled) ? $myservers['remote']['avatar'] : '',
"config" => [
'valid' => ($var['configValid'] === 'yes'),
'error' => isset($configErrorEnum[$var['configValid']]) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR',
],
"connectPluginInstalled" => $connectPluginInstalled,
"connectPluginVersion" => $connectPluginVersion,
"csrf" => $var['csrf_token'],
"description" => $var['COMMENT'] ?? '',
"deviceCount" => $var['deviceCount'],
"email" => $myservers['remote']['email'] ?? '',
"expireTime" => 1000 * (($var['regTy'] === 'Trial' || strstr($var['regTy'], 'expired')) ? $var['regTm2'] : 0),
"extraOrigins" => explode(',', $myservers['api']['extraOrigins'] ?? ''),
"flashProduct" => $var['flashProduct'],
"flashVendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"internalPort" => $_SERVER['SERVER_PORT'],
"keyfile" => empty($var['regFILE']) ? '' : str_replace(['+', '/', '='], ['-', '_', ''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
"lanIp" => ipaddr(),
"locale" => ($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
"model" => $var['SYS_MODEL'],
"name" => $var['NAME'],
"osVersion" => $var['version'],
"protocol" => $_SERVER['REQUEST_SCHEME'],
"regGen" => (int)$var['regGen'],
"regGuid" => $var['regGUID'],
"registered" => $registered,
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
"site" => $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
"theme" => [
"banner" => !empty($display['banner']),
"bannerGradient" => $display['showBannerGradient'] === 'yes' ?? false,
"bgColor" => ($display['background']) ? '#' . $display['background'] : '',
"descriptionShow" => (!empty($display['headerdescription']) && $display['headerdescription'] !== 'no'),
"metaColor" => ($display['headermetacolor'] ?? '') ? '#' . $display['headermetacolor'] : '',
"name" => $display['theme'],
"textColor" => ($display['header']) ? '#' . $display['header'] : '',
],
"ts" => time(),
"uptime" => 1000 * (time() - round(strtok(exec("cat /proc/uptime"), ' '))),
"username" => $registered ? $myservers['remote']['username'] : '',
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
];

View File

@@ -0,0 +1,204 @@
<?php
$webComponentTranslations = [
($_SESSION['locale']) ? $_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}'),
'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.'),
'Checkout the Connect Documentation' => _('Checkout the Connect Documentation'),
'No thanks' => _('No thanks'),
'Learn more' => _('Learn more'),
'Install Connect' => _('Install Connect'),
'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}'),
'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!'),
'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'),
],
];

4
web/.env.example Normal file
View File

@@ -0,0 +1,4 @@
VITE_ACCOUNT=https://localhost:8008
VITE_CONNECT=https://connect.myunraid.net
VITE_UNRAID_NET=https://unraid.ddev.site
VITE_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams

25
web/.eslintrc.cjs Normal file
View File

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

2
web/.npmrc Normal file
View File

@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

1
web/.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.16.0

43
web/README.md Normal file
View File

@@ -0,0 +1,43 @@
# connect-components via Nuxt 3
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# npm
npm install
```
## Development Server
Start the development server on `http://localhost:4321`
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
## Build for Unraid webgui [@TODO]
Instructions to come
## Interfacing with `unraid-api`
@todo https://v4.apollo.vuejs.org/guide-composable/fragments.html#colocating-fragments

78
web/_data/serverState.ts Normal file
View File

@@ -0,0 +1,78 @@
import type { Server, ServerState } from '~/types/server';
function makeid (length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
const charactersLength = characters.length;
let result = '';
for (let i = 0; i < length; i++) { result += characters.charAt(Math.floor(Math.random() * charactersLength)); }
return result;
}
const randomGuid = `1111-1111-${makeid(4)}-123412341234`; // this guid is registered in key server
// const newGuid = `1234-1234-${makeid(4)}-123412341234`; // this is a new USB, not registered
// const regWizTime = `1616711990500_${randomGuid}`;
// const blacklistedGuid = '154B-00EE-0700-9B50CF819816';
// ENOKEYFILE
// TRIAL
// BASIC
// PLUS
// PRO
// EEXPIRED
// EGUID
// EGUID1
// ETRIAL
// ENOKEYFILE2
// ENOKEYFILE1
// ENOFLASH
// EBLACKLISTED
// EBLACKLISTED1
// EBLACKLISTED2
// ENOCONN
const state: ServerState = 'TRIAL';
const uptime = Date.now() - 60 * 60 * 1000; // 1 hour ago
let expireTime = 0;
if (state === 'TRIAL') { expireTime = Date.now() + 60 * 60 * 1000; } // in 1 hour
if (state === 'EEXPIRED') { expireTime = uptime; } // 1 hour ago
export const serverState: Server = {
apiKey: 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
avatar: 'https://source.unsplash.com/300x300/?portrait',
config: {
// error: 'INVALID',
valid: true,
},
description: 'DevServer9000',
deviceCount: 3,
expireTime,
flashProduct: 'SanDisk_3.2Gen1',
flashVendor: 'USB',
guid: randomGuid,
// "guid": "0781-5583-8355-81071A2B0211",
inIframe: false,
keyfile: 'DUMMY_KEYFILE',
lanIp: '192.168.254.36',
license: '',
locale: 'en_US', // en_US, ja
name: 'fuji',
// connectPluginInstalled: 'dynamix.unraid.net.staging.plg',
connectPluginInstalled: '',
registered: true,
regGen: 0,
// "regGuid": "0781-5583-8355-81071A2B0211",
site: 'http://localhost:4321',
state,
theme: {
banner: false,
bannerGradient: false,
bgColor: '',
descriptionShow: true,
metaColor: '',
name: 'black',
textColor: ''
},
uptime,
username: 'zspearmint',
wanFQDN: ''
};

9
web/_webGui/setup.sh Normal file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
cd /usr/local/emhttp/plugins/dynamix.my.servers/ || exit
# replace Connect.page
# replace includes/myservers{1|2}.php
mkdir webComponents
mkdir webComponents/_nuxt
cd webComponents || exit
touch manifest.json
# create _nuxt/{filename}.js

View File

@@ -0,0 +1,159 @@
Menu="ManagementAccess:200"
Title="Callback Tests"
Icon="icon-u-globe"
Tag="globe"
---
<?php
/**
* @todo create web component env switcher liker upcEnv(). If we utilize manifest.json then we'll be switching its path.
*/
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
// print_r($mystatus);
// extract web component JS file from manifest
$jsonManifest = file_get_contents('/usr/local/emhttp/plugins/dynamix.my.servers/webComponents/manifest.json');
$jsonManifestData = json_decode($jsonManifest, true);
$webComponentJsFile = $jsonManifestData["connect-components.client.mjs"]["file"];
// web component
$localSourceBasePath = '/plugins/dynamix.my.servers/webComponents/';
$localSourceJs = $localSourceBasePath . $webComponentJsFile;
// add the web component source to the DOM
echo '<script id="unraid-webcomponents" defer src="' . $localSourceJs . '"></script>';
/**
* Build vars for user profile prop
*/
// add 'ipaddr' function for 6.9 backwards compatibility
if (!function_exists('ipaddr')) {
function ipaddr($ethX='eth0', $prot=4) {
global $$ethX;
switch ($$ethX['PROTOCOL:0']) {
case 'ipv4':
return $$ethX['IPADDR:0'];
case 'ipv6':
return $$ethX['IPADDR6:0'];
case 'ipv4+ipv6':
switch ($prot) {
case 4: return $$ethX['IPADDR:0'];
case 6: return $$ethX['IPADDR6:0'];
default:return [$$ethX['IPADDR:0'],$$ethX['IPADDR6:0']];}
default:
return $$ethX['IPADDR:0'];
}
}
}
$configErrorEnum = [ // used to map $var['configValid'] value to mimic unraid-api's `configError` ENUM
"error" => 'UNKNOWN_ERROR',
"invalid" => 'INVALID',
"nokeyserver" => 'NO_KEY_SERVER',
"withdrawn" => 'WITHDRAWN',
];
// read flashbackup ini file
$flashbackup_ini = '/var/local/emhttp/flashbackup.ini';
$flashbackup_status = (file_exists($flashbackup_ini)) ? @parse_ini_file($flashbackup_ini) : [];
$nginx = parse_ini_file('/var/local/emhttp/nginx.ini');
$connectPluginInstalled = '';
if (!file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net') && !file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) {
$connectPluginInstalled = ''; // base OS only, plugin not installed • show ad for plugin
} else {
// plugin is installed but if the unraid-api file doesn't fully install it's a failed install
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net')) $connectPluginInstalled = 'dynamix.unraid.net.plg';
if (file_exists('/var/lib/pkgtools/packages/dynamix.unraid.net.staging')) $connectPluginInstalled = 'dynamix.unraid.net.staging.plg';
// plugin install failed • append failure detected so we can show warning about failed install via UPC
if (!file_exists('/usr/local/sbin/unraid-api')) $connectPluginInstalled = $connectPluginInstalled . '_installFailed';
}
$serverData = [
"apiKey" => $myservers['upc']['apikey'] ?? '',
"apiVersion" => $myservers['api']['version'] ?? '',
"avatar" => (!empty($myservers['remote']['avatar']) && $connectPluginInstalled) ? $myservers['remote']['avatar'] : '',
"banner" => $display['banner'] ?? '',
"bannerGradient" => $display['showBannerGradient'] ?? 'yes',
"bgColor" => ($backgnd) ? '#'.$backgnd : '',
"config" => [
'valid' => $var['configValid'] === 'yes',
'error' => $var['configValid'] !== 'yes'
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
: null,
],
"csrf" => $var['csrf_token'],
"description" => $var['COMMENT'],
"descriptionShow" => ($display['headerdescription']??''!='no') ? 'true' : '',
"deviceCount" => $var['deviceCount'],
"email" => $myservers['remote']['email'] ?? '',
"expireTime" => 1000*($var['regTy']=='Trial'||strstr($var['regTy'],'expired')?$var['regTm2']:0),
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
"flashProduct" => $var['flashProduct'],
"flashVendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"lanIp" => ipaddr(),
"internalPort" => $_SERVER['SERVER_PORT'],
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
"locale" => ($_SESSION['locale']) ? $_SESSION['locale'] : 'en_US',
"metaColor" => ($display['headermetacolor']??'') ? '#'.$display['headermetacolor'] : '',
"model" => $var['SYS_MODEL'],
"name" => $var['NAME'],
"osVersion" => $var['version'],
"plgVersion" => $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'] ),
"connectPluginInstalled" => $connectPluginInstalled,
"protocol" => $_SERVER['REQUEST_SCHEME'],
"regGen" => (int)$var['regGen'],
"regGuid" => $var['regGUID'],
"registered" => (!empty($myservers['remote']['username']) && $connectPluginInstalled),
"registeredTime" => $myservers['remote']['regWizTime'] ?? '',
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
"textColor" => ($header) ? '#'.$header : '',
"theme" => $display['theme'],
"ts" => time(),
"uptime" => 1000*(time() - round(strtok(exec("cat /proc/uptime"),' '))),
"username" => (!empty($myservers['remote']['username']) && $connectPluginInstalled) ? $myservers['remote']['username'] : '',
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
];
$themeBg = '#111';
if ($display['theme'] === 'black' || $display['theme'] === 'azure') {
$themeBg = '#fff';
}
?>
<style>
.ComponentWrapper {
padding: 16px;
}
</style>
<div class="ComponentWrapper" style="background-color: <?=$themeBg?>;">
<?="<unraid-user-profile server='" . json_encode($serverData) . "'></connect-user-profile>"?>
</div>
<div class="ComponentWrapper">
<unraid-auth></connect-auth>
</div>
<div class="ComponentWrapper">
<unraid-download-api-logs></connect-download-api-logs>
</div>
<div class="ComponentWrapper">
<unraid-key-actions></connect-key-actions>
</div>
<div class="ComponentWrapper">
<unraid-wan-ip-check php-wan-ip="<?=@file_get_contents('https://wanip4.unraid.net/')?>"></connect-wan-ip-check>
</div>
<script>
/**
* So we're not needing to modify DefaultLayout with an additional include,
* we'll add the Modals web component to the bottom of the body
*/
const modalsWebComponent = 'connect-modals';
if (!document.getElementsByTagName(modalsWebComponent).length) {
const $body = document.getElementsByTagName('body')[0];
const $modals = document.createElement(modalsWebComponent);
$body.appendChild($modals);
}
</script>

12
web/app.vue Normal file
View File

@@ -0,0 +1,12 @@
<script lang="ts" setup>
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('UnraidComponents');
});
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</template>

28
web/assets/main.css Normal file
View File

@@ -0,0 +1,28 @@
@tailwind base;
@tailwind components;
/*
darkTheme
alpha: '#1c1b1b',
beta: '#f2f2f2',
gamma: '#999999',
lightTheme
alpha: '#f2f2f2',
beta: '#1c1b1b',
gamma: '#999999',
*/
body {
--color-alpha: #1c1b1b;
--color-beta: #f2f2f2;
--color-gamma: #999999;
--color-customgradient-start: rgba(242, 242, 242, .0);
--color-customgradient-end: rgba(242, 242, 242, .85);
--shadow-beta: 0 25px 50px -12px rgba(242, 242, 242, .15);
--ring-offset-shadow: 0 0 --var(--color-beta);
--ring-shadow: 0 0 --var(--color-beta);
}
/* Ensure this is always at the bottom @see https://tailwindcss.com/docs/content-configuration#working-with-third-party-libraries */
@tailwind utilities;

47
web/codegen.ts Normal file
View File

@@ -0,0 +1,47 @@
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
overwrite: true,
documents: ['./**/**/*.ts'],
ignoreNoDocuments: false,
config: {
namingConvention: {
typeNames: './fix-array-type.ts',
},
scalars: {
DateTime: 'string',
Long: 'number',
JSON: 'string',
URL: 'URL',
Port: 'number',
UUID: 'string',
},
},
generates: {
'composables/gql/': {
preset: 'client',
config: {
useTypeImports: true,
},
schema: [
{
'http://localhost:3001/graphql': {
headers: {
origin: '/var/run/unraid-php.sock',
'x-api-key': 'unupc_fab6ff6ffe51040595c6d9ffb63a353ba16cc2ad7d93f813a2e80a5810',
},
},
},
],
plugins: [
{
add: {
content: '/* eslint-disable */',
},
},
],
},
},
};
export default config;

View File

@@ -0,0 +1,36 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
const { t } = useI18n();
const serverStore = useServerStore();
const { authAction, stateData } = storeToRefs(serverStore);
</script>
<template>
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
<span v-if="stateData.error" class="text-unraid-red font-semibold">
<h3 class="text-16px mb-8px">{{ t(stateData.heading) }}</h3>
<span class="text-14px" v-html="t(stateData.message)" />
</span>
<span v-if="authAction">
<BrandButton
:disabled="authAction?.disabled"
:icon="authAction.icon"
:text="t(authAction.text)"
@click="authAction.click()"
/>
</span>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useServerStore } from '~/store/server';
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
const serverStore = useServerStore();
const { avatar, connectPluginInstalled, registered, username } = storeToRefs(serverStore);
</script>
<template>
<figure class="group relative z-0 flex items-center justify-center w-36px h-36px rounded-full bg-gradient-to-r from-unraid-red to-orange">
<img
v-if="avatar && connectPluginInstalled && registered"
:src="avatar"
:alt="username"
class="absolute z-10 inset-0 w-36px h-36px rounded-full overflow-hidden"
>
<template v-else>
<BrandMark gradient-start="#fff" gradient-stop="#fff" class="opacity-100 absolute z-10 w-36px px-4px" />
</template>
</figure>
</template>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import { XCircleIcon } from '@heroicons/vue/24/solid';
export interface ButtonProps {
btnStyle?: 'fill' | 'outline' | 'underline';
btnType?: 'button' | 'submit' | 'reset';
click?: () => void;
disabled?: boolean;
download?: boolean;
external?: boolean;
href?: string;
icon?: typeof XCircleIcon;
text?: string;
}
const props = withDefaults(defineProps<ButtonProps>(), {
btnStyle: 'fill',
btnType: 'button',
click: undefined,
href: undefined,
icon: undefined,
text: undefined,
});
defineEmits(['click']);
const classes = computed(() => {
switch (props.btnStyle) {
case 'fill':
return 'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60';
case 'outline':
return 'text-orange bg-gradient-to-r from-transparent to-transparent border-2 border-solid border-orange hover:text-white focus:text-white hover:from-unraid-red hover:to-orange focus:from-unraid-red focus:to-orange hover:border-transparent focus:border-transparent';
case 'underline':
return 'opacity-75 hover:opacity-100 focus:opacity-100 underline transition hover:text-alpha hover:bg-beta focus:text-alpha focus:bg-beta';
}
});
</script>
<template>
<component
:is="href ? 'a' : 'button'"
:disabled="disabled ?? null"
:href="href"
:rel="external ? 'noopener noreferrer' : ''"
:target="external ? '_blank' : ''"
:type="!href ? btnType : ''"
class="text-14px text-center font-semibold flex-none flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed"
:class="classes"
@click="click ?? $emit('click')"
>
<component :is="icon" v-if="icon" class="flex-shrink-0 w-14px" />
{{ text }}
</component>
</template>

View File

@@ -0,0 +1,142 @@
<script setup lang="ts">
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
gradientStart?: string;
gradientStop?: string;
title?: string,
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
title: 'Loading',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 133.52 76.97"
:class="`unraid_mark`"
role="img"
>
<title>{{ title }}</title>
<desc>Unraid logo animating with a wave like effect</desc>
<defs>
<linearGradient
id="unraidLoadingGradient"
x1="23.76"
y1="81.49"
x2="109.76"
y2="-4.51"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<path
d="m70,19.24zm57,0l6.54,0l0,38.49l-6.54,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_9"
/>
<path
d="m70,19.24zm47.65,11.9l-6.55,0l0,-23.79l6.55,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_8"
/>
<path
d="m70,19.24zm31.77,-4.54l-6.54,0l0,-14.7l6.54,0l0,14.7z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_7"
/>
<path
d="m70,19.24zm15.9,11.9l-6.54,0l0,-23.79l6.54,0l0,23.79z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_6"
/>
<path
d="m63.49,19.24l6.51,0l0,38.49l-6.51,0l0,-38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_5"
/>
<path
d="m70,19.24zm-22.38,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_4"
/>
<path
d="m70,19.24zm-38.26,43.03l6.55,0l0,14.73l-6.55,0l0,-14.73z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_3"
/>
<path
d="m70,19.24zm-54.13,26.6l6.54,0l0,23.78l-6.54,0l0,-23.78z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_2"
/>
<path
d="m70,19.24zm-63.46,38.49l-6.54,0l0,-38.49l6.54,0l0,38.49z"
fill="url(#unraidLoadingGradient)"
class="unraid_mark_1"
/>
</svg>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
@tailwind utilities;
</style>

View File

@@ -0,0 +1,38 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 222.36 39.04"
>
<defs>
<linearGradient
id="unraidLogo"
x1="47.53"
y1="79.1"
x2="170.71"
y2="-44.08"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<title>Unraid Logo</title>
<path
d="M146.7,29.47H135l-3,9h-6.49L138.93,0h8l13.41,38.49h-7.09L142.62,6.93l-5.83,16.88h8ZM29.69,0V25.4c0,8.91-5.77,13.64-14.9,13.64S0,34.31,0,25.4V0H6.54V25.4c0,5.17,3.19,7.92,8.25,7.92s8.36-2.75,8.36-7.92V0ZM50.86,12v26.5H44.31V0h6.11l17,26.5V0H74V38.49H67.9ZM171.29,0h6.54V38.49h-6.54Zm51.07,24.69c0,9-5.88,13.8-15.17,13.8H192.67V0H207.3c9.18,0,15.06,4.78,15.06,13.8ZM215.82,13.8c0-5.28-3.3-8.14-8.52-8.14h-8.08V32.77h8c5.33,0,8.63-2.8,8.63-8.08ZM108.31,23.92c4.34-1.6,6.93-5.28,6.93-11.55C115.24,3.68,110.18,0,102.48,0H88.84V38.49h6.55V5.66h6.87c3.8,0,6.21,1.82,6.21,6.71s-2.41,6.76-6.21,6.76H98.88l9.21,19.36h7.53Z"
fill="url(#unraidLogo)"
/>
</svg>
</template>

View File

@@ -0,0 +1,44 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
</script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" data-name="Layer 1" viewBox="0 0 954.29 142.4">
<defs>
<linearGradient
id="a"
x1="-57.82"
x2="923.39"
y1="71.2"
y2="71.2"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
<linearGradient id="b" xlink:href="#a" x2="923.39" />
<linearGradient id="c" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
<linearGradient id="d" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
<linearGradient id="e" xlink:href="#a" x2="923.39" y1="71.2" y2="71.2" />
<linearGradient id="f" xlink:href="#a" x2="923.39" />
<linearGradient id="g" xlink:href="#a" y1="12.16" y2="12.16" />
<linearGradient id="h" xlink:href="#a" x2="923.39" y1="86.94" y2="86.94" />
</defs>
<path fill="url(#a)" d="M54.39 0C20.96 0 0 17.4 0 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99H84.25v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79C107.79 16.81 87.02 0 54.39 0Z" />
<path fill="url(#b)" d="M197.58 0c-33.42 0-54.59 17.4-54.59 49.84v42.52c0 32.63 21.16 50.04 54.19 50.04s54.59-17.4 54.59-50.04V49.84C251.77 17.4 230.61 0 197.58 0Zm30.66 92.36c0 19.18-11.87 29.47-31.05 29.47s-30.66-10.28-30.66-29.47V49.84c0-18.99 11.87-29.47 31.05-29.47s30.66 10.48 30.66 29.47v42.52Z" />
<path fill="url(#c)" d="M373.8 97.31 312.49 1.98h-21.95v138.44h23.53V45.09l61.32 95.33h21.95V1.98H373.8v95.33z" />
<path fill="url(#d)" d="M521.35 97.31 460.04 1.98h-21.96v138.44h23.54V45.09l61.31 95.33h21.95V1.98h-23.53v95.33z" />
<path fill="url(#e)" d="M585.63 140.42h92.95v-20.57h-69.42V81.29h59.54V60.92h-59.54V22.35h69.42V1.98h-92.95v138.44z" />
<path fill="url(#f)" d="M766.8 0c-33.43 0-54.39 17.4-54.39 49.84v42.52c0 32.63 20.96 50.04 53.99 50.04s53.8-16.81 53.8-48.06v-.99h-23.54v.99c0 17.8-11.47 27.49-30.26 27.49s-30.46-10.28-30.46-29.47V49.84c0-18.99 11.67-29.47 30.85-29.47s29.86 9.89 29.86 27.69v.79h23.54v-.79c0-31.25-20.77-48.06-53.4-48.06Z" />
<path fill="url(#g)" d="M846.11 1.98h108.18v20.37H846.11z" />
<path fill="url(#h)" d="M888.43 33.45h23.54v106.97h-23.54z" />
</svg>
</template>

View File

@@ -0,0 +1,37 @@
<script setup lang="ts">
export interface Props {
gradientStart?: string;
gradientStop?: string;
}
withDefaults(defineProps<Props>(), {
gradientStart: '#e32929',
gradientStop: '#ff8d30',
});
</script>
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 133.52 76.97"
>
<defs>
<linearGradient
id="unraid-mark"
x1="23.76"
y1="81.49"
x2="109.76"
y2="-4.51"
gradientUnits="userSpaceOnUse"
>
<stop offset="0" :stop-color="gradientStart" />
<stop offset="1" :stop-color="gradientStop" />
</linearGradient>
</defs>
<path
fill="url(#unraid-mark)"
d="M63.49,19.24H70V57.73H63.49ZM6.54,57.73H0V19.24H6.54Zm25.2,4.54h6.55V77H31.74ZM15.87,45.84h6.54V69.62H15.87Zm31.75,0h6.54V69.62H47.62ZM127,19.24h6.54V57.73H127ZM101.77,14.7H95.23V0h6.54Zm15.88,16.44H111.1V7.35h6.55Zm-31.75,0H79.36V7.35H85.9Z"
/>
</svg>
</template>

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useCallbackStore } from '~/store/callbackActions';
const callbackStore = useCallbackStore();
onBeforeMount(() => {
callbackStore.watcher();
});
</script>
<template>
<slot />
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,59 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { ArrowDownTrayIcon, ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import { useI18n } from 'vue-i18n';
import { DEV_GRAPH_URL, CONNECT_FORUMS, CONTACT, DISCORD } from '~/helpers/urls';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
const { t } = useI18n();
const { apiKey } = storeToRefs(useServerStore());
const downloadUrl = computed(() => new URL(`/graphql/api/logs?apiKey=${apiKey.value}`, DEV_GRAPH_URL.toString() || window.location.origin));
</script>
<template>
<div class="whitespace-normal flex flex-col gap-y-16px max-w-3xl">
<span>
{{ t('The primary method of support for Unraid Connect is through our forums and Discord.') }}
{{ t('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.') }}
{{ t('The logs may contain sensitive information so do not post them publicly.') }}
</span>
<span class="flex flex-col gap-y-16px">
<div class="flex">
<BrandButton
class="grow-0 shrink-0"
download
:external="true"
:href="downloadUrl.toString()"
:icon="ArrowDownTrayIcon"
:text="t('Download unraid-api Logs')"
/>
</div>
<div class="flex flex-row items-baseline gap-8px">
<a :href="CONNECT_FORUMS.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
{{ t('Unraid Connect Forums') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
</a>
<a :href="DISCORD.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
{{ t('Unraid Discord') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
</a>
<a :href="CONTACT.toString()" target="_blank" rel="noopener noreferrer" class="text-[#486dba] hover:text-[#3b5ea9] focus:text-[#3b5ea9] hover:underline focus:underline inline-flex flex-row items-center justify-start gap-8px">
{{ t('Unraid Contact Page') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
</a>
</div>
</span>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { provide } from 'vue';
import { createI18n, I18nInjectionKey } from 'vue-i18n';
import en_US from '~/locales/en_US.json'; // eslint-disable-line camelcase
// import ja from '~/locales/ja.json';
const defaultLocale = 'en_US'; // ja, en_US
let parsedLocale = '';
let parsedMessages = {};
let nonDefaultLocale = false;
/**
* In myservers2.php, we have a script tag that sets window.LOCALE_DATA to a stringified JSON object.
* Unfortunately, this was the only way I could get the data from PHP to vue-i18n :(
* I tried using i18n.setLocaleMessage() but it didn't work no matter what I tried.
*/
const windowLocaleData = (window as any).LOCALE_DATA || null;
if (windowLocaleData) {
console.debug('[I18nHost] parsing messages');
try {
parsedMessages = JSON.parse(decodeURIComponent(windowLocaleData));
parsedLocale = Object.keys(parsedMessages)[0];
nonDefaultLocale = parsedLocale !== defaultLocale;
console.debug('[I18nHost] messages parsed. Now setting up vue-i18n', nonDefaultLocale, parsedLocale, parsedMessages);
} catch (error) {
console.error('[I18nHost] error parsing messages', error);
}
}
const i18n = createI18n<false>({
legacy: false, // must set to `false`
locale: nonDefaultLocale ? parsedLocale : defaultLocale,
fallbackLocale: defaultLocale,
messages: {
en_US, // eslint-disable-line camelcase
// ja,
...(nonDefaultLocale ? parsedMessages : {}),
}
});
provide(I18nInjectionKey, i18n);
</script>
<template>
<slot />
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
const { t } = useI18n();
const { keyActions } = storeToRefs(useServerStore());
console.log('[keyActions]', keyActions.value);
</script>
<template>
<ul v-if="keyActions" class="flex flex-col gap-y-8px">
<li v-for="action in keyActions" :key="action.name">
<BrandButton
class="w-full max-w-300px"
:disabled="action?.disabled"
:external="action?.external"
:href="action?.href"
:icon="action.icon"
:text="t(action.text)"
@click="action.click()"
/>
</li>
</ul>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

125
web/components/Modal.vue Normal file
View File

@@ -0,0 +1,125 @@
<script setup lang="ts">
import { TransitionChild, TransitionRoot } from '@headlessui/vue';
import { XMarkIcon } from '@heroicons/vue/24/outline';
import useFocusTrap from '~/composables/useFocusTrap';
export interface Props {
description?: string;
error?: boolean;
maxWidth?: string;
open?: boolean;
showCloseX?: boolean;
success?: boolean;
t: any;
title?: string;
}
const props = withDefaults(defineProps<Props>(), {
description: '',
error: false,
maxWidth: 'sm:max-w-lg',
open: false,
showCloseX: false,
success: false,
title: '',
});
watchEffect(() => {
// toggle body scrollability
return props.open
? document.body.style.setProperty('overflow', 'hidden')
: document.body.style.removeProperty('overflow');
});
const emit = defineEmits(['close']);
const closeModal = () => {
emit('close');
};
const { trapRef } = useFocusTrap();
const ariaLablledById = computed((): string|undefined => props.title ? `ModalTitle-${Math.random()}`.replace('0.', '') : undefined);
/**
* @todo when providing custom colors for theme we should invert text-beta bg-alpha to text-alpha bg-beta
*/
</script>
<template>
<TransitionRoot appear :show="open" as="template">
<div
ref="trapRef"
class="fixed inset-0 z-10 overflow-y-auto"
role="dialog"
aria-dialog="true"
:aria-labelledby="ariaLablledById"
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 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 scale-95"
enter-to="opacity-100 scale-100"
leave="duration-200 ease-in"
leave-from="opacity-100 scale-100"
leave-to="opacity-0 scale-95"
>
<div
:class="[
maxWidth,
error ? 'shadow-unraid-red/30 border-unraid-red/10' : '',
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"
>
<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">
<span class="sr-only">{{ t('Close') }}</span>
<XMarkIcon class="h-6 w-6" aria-hidden="true" />
</button>
</div>
<header class="text-center">
<template v-if="!$slots['header']">
<h1 v-if="title" :id="ariaLablledById" class="text-24px font-semibold flex flex-wrap justify-center gap-x-1">
{{ 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 class="absolute z-0 inset-0 opacity-10 bg-beta" />
<div class="relative z-10">
<slot name="footer" />
</div>
</footer>
</div>
</TransitionChild>
</div>
</div>
</TransitionRoot>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useTrialStore } from '~/store/trial';
const { t } = useI18n();
const { callbackStatus } = storeToRefs(useCallbackActionsStore());
const { trialModalVisible } = storeToRefs(useTrialStore());
// import { usePromoStore } from '~/store/promo';
// const { promoVisible } = storeToRefs(usePromoStore());
// <UpcPromo :t="t" :open="promoVisible" />
</script>
<template>
<div class="relative z-[99999]">
<UpcCallbackFeedback :t="t" :open="callbackStatus !== 'ready'" />
<UpcTrial :t="t" :open="trialModalVisible" />
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,188 @@
<script lang="ts" setup>
import { OnClickOutside } from '@vueuse/components';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useCallbackStore } from '~/store/callbackActions';
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
import { useThemeStore } from '~/store/theme';
import type { Server } from '~/types/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
server?: Server | string;
}
const props = defineProps<Props>();
const { t } = useI18n();
const callbackStore = useCallbackStore();
const dropdownStore = useDropdownStore();
const serverStore = useServerStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
const { name, description, lanIp, state, connectPluginInstalled } = storeToRefs(serverStore);
const { bannerGradient, theme } = storeToRefs(useThemeStore());
const hideDropdown = computed(() => state.value === 'PRO' && !connectPluginInstalled.value);
/**
* Close dropdown when clicking outside
* @note If in testing you have two variants of the component on a page the clickOutside will fire twice making it seem like it doesn't work
*/
const clickOutsideTarget = ref();
const clickOutsideIgnoreTarget = ref();
const outsideDropdown = () => {
if (dropdownVisible.value) { return dropdownStore.dropdownToggle(); }
};
/**
* Copy LAN IP on server name click
*/
let copyIpInterval: string | number | NodeJS.Timeout | undefined;
const { copy, copied, isSupported } = useClipboard({ source: lanIp.value ?? '' });
const showCopyNotSupported = ref<boolean>(false);
const copyLanIp = () => {
if (!isSupported) { showCopyNotSupported.value = true; }
copy(lanIp.value ?? '');
};
watch(showCopyNotSupported, (newVal, oldVal) => {
if (newVal && oldVal === false) {
clearTimeout(copyIpInterval);
copyIpInterval = setTimeout(() => {
showCopyNotSupported.value = false;
}, 2000);
}
});
/**
* Sets the server store and locale messages then listen for callbacks
*/
onBeforeMount(() => {
if (!props.server) {
throw new Error('Server data not present');
}
if (typeof props.server === 'object') { // Handles the testing dev Vue component
serverStore.setServer(props.server);
} else if (typeof props.server === 'string') { // Handle web component
const parsedServerProp = JSON.parse(props.server);
serverStore.setServer(parsedServerProp);
}
callbackStore.watcher();
});
</script>
<template>
<div id="UserProfile" 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" />
<div class="text-gamma text-10px xs:text-12px text-right font-semibold leading-normal relative z-10 flex flex-col items-end justify-end gap-x-4px xs:flex-row xs:items-baseline xs:gap-x-12px">
<UpcUptimeExpire :t="t" />
<span class="hidden xs:block">&bull;</span>
<UpcServerState :t="t" />
</div>
<div class="relative z-10 flex flex-row items-center justify-end gap-x-16px h-full">
<h1 class="text-alpha 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-gamma hidden md:inline-block px-8px">&bull;</span>
</template>
<button :title="t('Click to Copy LAN IP {0}', [lanIp])" @click="copyLanIp()">
{{ name }}
</button>
<span
v-show="copied || showCopyNotSupported"
class="text-white text-12px leading-none py-4px px-8px absolute top-full right-0 bg-gradient-to-r from-unraid-red to-orange text-center block rounded"
>
<template v-if="copied">{{ t('LAN IP Copied') }}</template>
<template v-else>{{ t('LAN IP {0}', [lanIp]) }}</template>
</span>
</h1>
<template v-if="!hideDropdown">
<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>
</div>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
.DropdownWrapper_blip {
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
&::before {
@apply absolute z-20 block;
content: '';
width: 0;
height: 0;
top: -10px;
right: 42px;
border-right: 11px solid transparent;
border-bottom: 11px solid var(--color-alpha);
border-left: 11px solid transparent;
}
}
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,18 @@
<script setup lang="ts">
export interface Props {
colorClasses?: string
}
withDefaults(defineProps<Props>(), {
colorClasses: 'text-grey-mid border-grey-mid',
});
</script>
<template>
<span
class="text-10px uppercase py-4px px-6px border-2 rounded-full"
:class="colorClasses"
>
{{ 'Beta' }}
</span>
</template>

View File

@@ -0,0 +1,340 @@
<script lang="ts" setup>
import { useClipboard } from '@vueuse/core';
import { ClipboardIcon, CogIcon, InformationCircleIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
import { PLUGIN_SETTINGS } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useInstallKeyStore } from '~/store/installKey';
import { usePromoStore } from '~/store/promo';
import { useServerStore } from '~/store/server';
export interface Props {
open?: boolean;
t: any;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
const accountStore = useAccountStore();
const callbackActionsStore = useCallbackActionsStore();
const installKeyStore = useInstallKeyStore();
const promoStore = usePromoStore();
const serverStore = useServerStore();
const {
accountAction,
accountActionHide,
accountActionStatus,
accountActionType,
} = storeToRefs(accountStore);
const {
callbackStatus,
} = storeToRefs(callbackActionsStore);
const {
keyActionType,
keyUrl,
keyInstallStatus,
keyType,
} = storeToRefs(installKeyStore);
const {
connectPluginInstalled,
registered,
authAction,
refreshServerStateStatus,
username,
} = storeToRefs(serverStore);
/**
* Post sign in success state:
* If we're on the Connect settings page in the webGUI
* the modal should close instead of redirecting to the
* settings page.
*
* @todo figure out the difference between document.location and window.location in relation to the webGUI and webGUI being iframed
*/
const isSettingsPage = ref<boolean>(document.location.pathname === '/Settings/ManagementAccess');
const showPromoCta = computed(() => callbackStatus.value === 'success' && !connectPluginInstalled.value);
const showSignInCta = computed(() => connectPluginInstalled.value && !registered.value && authAction.value?.name === 'signIn' && accountActionType.value !== 'signIn');
const heading = computed(() => {
switch (callbackStatus.value) {
case 'error':
return props.t('Error');
case 'loading':
return props.t('Performing actions');
case 'success':
return props.t('Success!');
}
});
const subheading = computed(() => {
if (callbackStatus.value === 'error') {
return props.t('Something went wrong'); /** @todo show actual error messages */
}
if (callbackStatus.value === 'loading') { return props.t('Please keep this window open while we perform some actions'); }
if (callbackStatus.value === 'success') {
if (accountActionType.value === 'signIn') { return props.t('You\'re one step closer to enhancing your Unraid experience'); }
if (keyActionType.value === 'purchase') { return props.t('Thank you for purchasing an Unraid {0} Key!', [keyType.value]); }
if (keyActionType.value === 'replace') { return props.t('Your {0} Key has been replaced!', [keyType.value]); }
if (keyActionType.value === 'trialExtend') { return props.t('Your Trial key has been extended!'); }
if (keyActionType.value === 'trialStart') { return props.t('Your free Trial key provides all the functionality of a Pro Registration key'); }
if (keyActionType.value === 'upgrade') { return props.t('Thank you for upgrading to an Unraid {0} Key!', [keyType.value]); }
return '';
}
return '';
});
const closeText = computed(() => {
const txt = !connectPluginInstalled.value ? props.t('No thanks') : props.t('Close');
return refreshServerStateStatus.value === 'done' ? txt : props.t('Reload');
});
const close = () => {
if (callbackStatus.value === 'loading') { return console.debug('[close] not allowed'); }
return refreshServerStateStatus.value === 'done'
? callbackActionsStore.setCallbackStatus('ready')
: window.location.reload();
};
const promoClick = () => {
promoStore.openOnNextLoad();
close();
};
const { copy, copied, isSupported } = useClipboard({ source: keyUrl.value });
const keyInstallStatusCopy = computed((): { text: string; } => {
let txt1 = props.t('Installing');
let txt2 = props.t('Installed');
let txt3 = props.t('Install');
switch (keyInstallStatus.value) {
case 'ready':
return {
text: props.t('Ready to Install Key'),
};
case 'installing':
if (keyActionType.value === 'trialExtend') { txt1 = props.t('Installing Extended Trial'); }
if (keyActionType.value === 'recover') { txt1 = props.t('Installing Recovered'); }
if (keyActionType.value === 'replace') { txt1 = props.t('Installing Replaced'); }
return {
text: props.t('{0} {1} Key…', [txt1, keyType.value]),
};
case 'success':
if (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 {
text: props.t('{1} Key {0} Successfully', [txt2, keyType.value]),
};
case 'failed':
if (keyActionType.value === 'trialExtend') { txt3 = props.t('Install Extended'); }
if (keyActionType.value === 'recover') { txt3 = props.t('Install Recovered'); }
if (keyActionType.value === 'replace') { txt3 = props.t('Install Replaced'); }
return {
text: props.t('Failed to {0} {1} Key', [txt3, keyType.value]),
};
}
});
const accountActionStatusCopy = computed((): { text: string; } => {
switch (accountActionStatus.value) {
case 'ready':
return {
text: props.t('Ready to update Connect account configuration'),
};
case 'updating':
return {
text: accountAction.value?.type === 'signIn'
? props.t('Signing in {0}…', [accountAction.value.user?.preferred_username])
: props.t('Signing out {0}…', [username.value]),
};
case 'success':
return {
text: accountAction.value?.type === 'signIn'
? props.t('{0} Signed In Successfully', [accountAction.value.user?.preferred_username])
: props.t('{0} Signed Out Successfully', [username.value]),
};
case 'failed':
return {
text: accountAction.value?.type === 'signIn'
? props.t('Sign In Failed')
: props.t('Sign Out Failed'),
};
}
});
</script>
<template>
<Modal
:t="t"
:title="heading"
:description="subheading"
:open="open"
max-width="max-w-640px"
:error="callbackStatus === 'error'"
:success="callbackStatus === 'success'"
:show-close-x="callbackStatus !== 'loading'"
@close="close"
>
<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"
>
<BrandLoading v-if="callbackStatus === 'loading'" class="w-[110px] mx-auto" />
<UpcCallbackFeedbackStatus
v-if="keyInstallStatus !== 'ready'"
:success="keyInstallStatus === 'success'"
:error="keyInstallStatus === 'failed'"
:text="keyInstallStatusCopy.text"
>
<UpcUptimeExpire
v-if="keyType === 'Trial'"
:for-expire="true"
class="opacity-75 italic mt-4px"
:t="t"
/>
<template v-if="keyInstallStatus === 'failed'">
<div v-if="isSupported" class="flex justify-center">
<BrandButton
:icon="ClipboardIcon"
:text="copied ? t('Copied') : t('Copy Key URL')"
@click="copy(keyUrl)"
/>
</div>
<p v-else>
{{ t('Copy your Key URL: {0}', [keyUrl]) }}
</p>
<p>
<a href="/Tools/Registration" class="opacity-75 hover:opacity-100 focus:opacity-100 underline transition">
{{ t('Then go to Tools > Registration to manually install it') }}
</a>
</p>
</template>
</UpcCallbackFeedbackStatus>
<UpcCallbackFeedbackStatus
v-if="accountActionStatus !== 'ready' && !accountActionHide"
:success="accountActionStatus === 'success'"
:error="accountActionStatus === 'failed'"
:text="accountActionStatusCopy.text"
/>
<UpcCallbackFeedbackStatus
v-if="showPromoCta"
:icon="InformationCircleIcon"
:text="t('Enhance your experience with Unraid Connect')"
/>
<UpcCallbackFeedbackStatus
v-if="showSignInCta"
:icon="InformationCircleIcon"
:text="t('Sign In to utilize Unraid Connect')"
/>
</div>
</template>
<template #footer>
<div v-if="callbackStatus === 'success'" class="flex flex-row-reverse justify-center gap-16px">
<template v-if="connectPluginInstalled && accountActionType === 'signIn'">
<BrandButton
v-if="isSettingsPage"
:icon="CogIcon"
:text="t('Configure Connect Features')"
class="grow-0"
@click="close"
/>
<BrandButton
v-else
:href="PLUGIN_SETTINGS.toString()"
:icon="CogIcon"
:text="t('Configure Connect Features')"
class="grow-0"
/>
</template>
<BrandButton
v-if="showPromoCta"
:text="t('Learn More')"
@click="promoClick"
/>
<BrandButton
v-if="showSignInCta"
:disabled="authAction?.disabled"
:external="authAction?.external"
:icon="authAction?.icon"
:text="t(authAction?.text)"
@click="authAction?.click"
/>
<BrandButton
btn-style="underline"
:text="closeText"
@click="close"
/>
</div>
</template>
</Modal>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
@tailwind utilities;
</style>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
import { CheckCircleIcon, XCircleIcon } from '@heroicons/vue/24/solid';
export interface Props {
error?: boolean;
icon?: typeof CheckCircleIcon;
success?: boolean;
text?: string;
}
withDefaults(defineProps<Props>(), {
error: false,
icon: undefined,
success: false,
text: undefined,
});
</script>
<template>
<div class="mx-auto max-w-[45ch]">
<div class="flex items-start justify-center gap-x-8px">
<CheckCircleIcon v-if="success" class="fill-green-600 w-28px shrink-0" />
<XCircleIcon v-if="error" class="fill-unraid-red w-28px shrink-0" />
<component :is="icon" v-if="icon" class="fill-current opacity-75 w-28px shrink-0" />
<p class="text-18px">
{{ text }}
</p>
</div>
<slot />
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup lang="ts">
import { TransitionRoot } from '@headlessui/vue';
import { storeToRefs } from 'pinia';
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
defineProps<{ t: any; }>();
const dropdownStore = useDropdownStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
const { connectPluginInstalled, registered, state, stateDataError } = storeToRefs(useServerStore());
const showDefaultContent = computed(() => !showLaunchpad.value);
const showLaunchpad = computed(() => state.value === 'ENOKEYFILE' || ((connectPluginInstalled.value && !registered.value) && !stateDataError.value));
</script>
<template>
<TransitionRoot
as="template"
:show="dropdownVisible"
enter="transition-all duration-200"
enter-from="opacity-0 translate-y-[16px]"
enter-to="opacity-100"
leave="transition-all duration-150"
leave-from="opacity-100"
leave-to="opacity-0 translate-y-[16px]"
>
<UpcDropdownWrapper class="DropdownWrapper_blip text-beta absolute z-30 top-full right-0 transition-all">
<UpcDropdownContent v-if="showDefaultContent" :t="t" />
<UpcDropdownLaunchpad v-else-if="showLaunchpad" :t="t" />
</UpcDropdownWrapper>
</TransitionRoot>
</template>

View File

@@ -0,0 +1,71 @@
<script setup lang="ts">
import { ExclamationTriangleIcon, CheckCircleIcon } from '@heroicons/vue/24/solid';
import { storeToRefs } from 'pinia';
import BrandLoading from '~/components/Brand/Loading.vue';
import { useUnraidApiStore } from '~/store/unraidApi';
const props = defineProps<{ t: any; }>();
const unraidApiStore = useUnraidApiStore();
const { unraidApiStatus, unraidApiRestartAction } = storeToRefs(unraidApiStore);
interface StatusOutput {
icon: typeof BrandLoading | typeof ExclamationTriangleIcon | typeof CheckCircleIcon;
iconClasses?: string;
text: string;
textClasses?: string;
}
const status = computed((): StatusOutput | undefined => {
if (unraidApiStatus.value === 'connecting') {
return {
icon: BrandLoading,
iconClasses: 'w-16px',
text: props.t('Loading…'),
textClasses: 'italic',
};
}
if (unraidApiStatus.value === 'restarting') {
return {
icon: BrandLoading,
iconClasses: 'w-16px',
text: props.t('Restarting unraid-api…'),
textClasses: 'italic',
};
}
if (unraidApiStatus.value === 'offline') {
return {
icon: ExclamationTriangleIcon,
iconClasses: 'text-red-500 w-16px h-16px',
text: props.t('unraid-api is offline'),
};
}
if (unraidApiStatus.value === 'online') {
return {
icon: CheckCircleIcon,
iconClasses: 'text-green-600 w-16px h-16px',
text: props.t('Connected'),
};
}
return undefined;
});
</script>
<template>
<li
v-if="status"
class="flex flex-row justify-start items-center gap-8px mt-8px px-8px"
>
<component
:is="status.icon"
:class="status.iconClasses"
aria-hidden="true"
/>
<span :class="status?.textClasses">
{{ status.text }}
</span>
</li>
<li v-if="unraidApiRestartAction" class="w-full">
<UpcDropdownItem :item="unraidApiRestartAction" :t="t" />
</li>
</template>

View File

@@ -0,0 +1,107 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { ArrowTopRightOnSquareIcon, CogIcon } from '@heroicons/vue/24/solid';
import { ACCOUNT, CONNECT_DASHBOARD, PLUGIN_SETTINGS } from '~/helpers/urls';
import { useErrorsStore } from '~/store/errors';
// import { usePromoStore } from '~/store/promo';
import { useServerStore } from '~/store/server';
import type { UserProfileLink } from '~/types/userProfile';
const props = defineProps<{ t: any; }>();
const errorsStore = useErrorsStore();
// const promoStore = usePromoStore();
const { keyActions, connectPluginInstalled, registered, stateData } = storeToRefs(useServerStore());
const { errors } = storeToRefs(errorsStore);
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') ?? []);
const links = computed(():UserProfileLink[] => {
return [
...(registered.value && connectPluginInstalled.value
? [
{
emphasize: true,
external: true,
href: CONNECT_DASHBOARD.toString(),
icon: ArrowTopRightOnSquareIcon,
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'),
},
{
href: PLUGIN_SETTINGS.toString(),
icon: CogIcon,
text: props.t('Settings'),
title: props.t('Go to Connect plugin settings'),
},
...(signOutAction.value),
]
: []
),
...(!registered.value && connectPluginInstalled.value
? [
...(signInAction.value),
]
: []
),
// ...(!connectPluginInstalled.value
// ? [
// {
// click: () => {
// promoStore.promoShow();
// },
// icon: InformationCircleIcon,
// text: props.t('Enhance your Unraid experience with Connect'),
// title: props.t('Enhance your Unraid experience with Connect'),
// },
// ]
// : []
// ),
];
});
const showErrors = computed(() => errors.value.length);
const showConnectStatus = computed(() => !showErrors.value && !stateData.value.error && registered.value && connectPluginInstalled.value);
const showKeyline = computed(() => showConnectStatus.value && (keyActions.value?.length || links.value.length));
</script>
<template>
<div class="flex flex-col gap-y-8px min-w-300px max-w-350px">
<header v-if="connectPluginInstalled" class="flex flex-row items-center justify-between mt-8px mx-8px">
<h2 class="text-18px leading-none flex flex-row gap-x-8px items-center justify-between">
<BrandLogoConnect gradient-start="currentcolor" gradient-stop="currentcolor" class="text-beta w-[120px]" />
<UpcBeta />
</h2>
</header>
<ul class="list-reset flex flex-col gap-y-4px p-0">
<UpcDropdownConnectStatus v-if="showConnectStatus" :t="t" />
<UpcDropdownError v-if="showErrors" :t="t" />
<li v-if="showKeyline" class="my-8px">
<UpcKeyline />
</li>
<template v-if="keyActions">
<li v-for="action in keyActions" :key="action.name">
<UpcDropdownItem :item="action" :t="t" />
</li>
</template>
<template v-if="links.length">
<li v-for="(link, index) in links" :key="`link_${index}`">
<UpcDropdownItem :item="link" :t="t" />
</li>
</template>
</ul>
</div>
</template>

View File

@@ -0,0 +1,27 @@
<script setup lang="ts">
// eslint-disable vue/no-v-html
import { storeToRefs } from 'pinia';
import { useErrorsStore } from '~/store/errors';
defineProps<{ t: any; }>();
const errorsStore = useErrorsStore();
const { errors } = storeToRefs(errorsStore);
</script>
<template>
<ul v-if="errors.length" class="list-reset flex flex-col gap-y-8px mb-4px border-2 border-solid border-unraid-red/90 rounded-md">
<li v-for="(error, index) in errors" :key="index" class="flex flex-col gap-8px">
<h3 class="text-18px py-4px px-12px text-white bg-unraid-red/90 font-semibold">
<span>{{ t(error.heading) }}</span>
</h3>
<div class="text-14px px-12px flex flex-col gap-y-8px" :class="{ 'pb-8px': !error.actions }" v-html="t(error.message)" />
<nav v-if="error.actions">
<li v-for="(link, idx) in error.actions" :key="`link_${idx}`">
<UpcDropdownItem :item="link" :rounded="false" :t="t" />
</li>
</nav>
</li>
</ul>
</template>

View File

@@ -0,0 +1,46 @@
<script setup lang="ts">
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import type { ServerStateDataAction } from '~/types/server';
import type { UserProfileLink } from '~/types/userProfile';
export interface Props {
item: ServerStateDataAction | UserProfileLink;
rounded?: boolean;
t: any;
}
const props = withDefaults(defineProps<Props>(), {
rounded: true,
});
const showExternalIconOnHover = computed(() => props.item?.external && props.item.icon !== ArrowTopRightOnSquareIcon);
</script>
<template>
<component
:is="item?.click ? 'button' : 'a'"
:disabled="item?.disabled"
:href="item?.href ?? null"
:title="item?.title ? t(item?.title) : null"
:target="item?.external ? '_blank' : null"
:rel="item?.external ? 'noopener noreferrer' : null"
class="text-left text-14px w-full flex flex-row items-center justify-between gap-x-8px px-8px py-8px cursor-pointer"
:class="{
'text-beta bg-transparent hover:text-white hover:bg-gradient-to-r hover:from-unraid-red hover:to-orange focus:text-white focus:bg-gradient-to-r focus:from-unraid-red focus:to-orange focus:outline-none': !item?.emphasize,
'text-white bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60': item?.emphasize,
'group': showExternalIconOnHover,
'rounded-md': rounded,
'disabled:opacity-50 disabled:hover:opacity-50 disabled:focus:opacity-50 disabled:cursor-not-allowed': item?.disabled,
}"
@click.stop="item?.click ? item?.click() : null"
>
<span class="leading-snug inline-flex flex-row items-center gap-x-8px">
<component :is="item?.icon" class="flex-shrink-0 text-current w-16px h-16px" aria-hidden="true" />
{{ t(item?.text) }}
</span>
<ArrowTopRightOnSquareIcon
v-if="showExternalIconOnHover"
class="text-white fill-current flex-shrink-0 w-16px h-16px ml-8px opacity-0 group-hover:opacity-100 transition-opacity duration-200 ease-in-out"
/>
</component>
</template>

View File

@@ -0,0 +1,124 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
const props = defineProps<{ t: any; }>();
const { expireTime, connectPluginInstalled, registered, state, stateData } = storeToRefs(useServerStore());
const showConnectCopy = computed(() => (connectPluginInstalled.value && !registered.value && !stateData.value?.error));
const heading = computed(() => {
if (showConnectCopy.value) { return props.t('Thank you for installing Connect!'); }
return props.t(stateData.value.heading);
});
const subheading = computed(() => {
if (showConnectCopy.value) { return props.t('Sign In to your Unraid.net account to get started'); }
return props.t(stateData.value.message);
});
const showExpireTime = computed(() => {
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value > 0;
});
</script>
<template>
<div class="flex flex-col gap-y-24px w-full min-w-300px md:min-w-[500px] max-w-xl p-16px">
<header :class="{ 'text-center': showConnectCopy }">
<h2 class="text-24px text-center font-semibold" v-html="heading" />
<div class="flex flex-col gap-y-8px" v-html="subheading" />
<UpcUptimeExpire
v-if="showExpireTime"
class="text-center opacity-75 mt-12px"
:t="t"
/>
</header>
<ul v-if="stateData.actions" class="list-reset flex flex-col gap-y-8px px-16px">
<li v-for="action in stateData.actions" :key="action.name">
<BrandButton
class="w-full"
:disabled="action?.disabled"
:external="action?.external"
:href="action?.href"
:icon="action.icon"
:text="t(action.text)"
@click="action.click()"
/>
</li>
</ul>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
.DropdownWrapper_blip {
box-shadow: var(--ring-offset-shadow), var(--ring-shadow), var(--shadow-beta);
&::before {
@apply absolute z-20 block;
content: '';
width: 0;
height: 0;
top: -10px;
right: 42px;
border-right: 11px solid transparent;
border-bottom: 11px solid var(--color-alpha);
border-left: 11px solid transparent;
}
}
.unraid_mark_2,
.unraid_mark_4 {
animation: mark_2 1.5s ease infinite;
}
.unraid_mark_3 {
animation: mark_3 1.5s ease infinite;
}
.unraid_mark_6,
.unraid_mark_8 {
animation: mark_6 1.5s ease infinite;
}
.unraid_mark_7 {
animation: mark_7 1.5s ease infinite;
}
@keyframes mark_2 {
50% {
transform: translateY(-40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_3 {
50% {
transform: translateY(-62px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_6 {
50% {
transform: translateY(40px);
}
100% {
transform: translateY(0);
}
}
@keyframes mark_7 {
50% {
transform: translateY(62px);
}
100% {
transform: translateY(0);
}
}
</style>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { InformationCircleIcon, ExclamationTriangleIcon, ShieldExclamationIcon } from '@heroicons/vue/24/solid';
import { useDropdownStore } from '~/store/dropdown';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
const props = defineProps<{ t: any; }>();
const dropdownStore = useDropdownStore();
const { dropdownVisible } = storeToRefs(dropdownStore);
const { errors } = storeToRefs(useErrorsStore());
const {
connectPluginInstalled,
registered,
state,
stateData,
username,
} = storeToRefs(useServerStore());
const registeredAndconnectPluginInstalled = computed(() => connectPluginInstalled.value && registered.value);
const showErrorIcon = computed(() => errors.value.length || stateData.value.error);
const text = computed((): string | undefined => {
if ((stateData.value.error) && state.value !== 'EEXPIRED') { return props.t('Fix Error'); }
if (registeredAndconnectPluginInstalled.value) { return username.value; }
});
const title = computed((): string => {
if (state.value === 'ENOKEYFILE') { return props.t('Get Started'); }
if (state.value === 'EEXPIRED') { return props.t('Trial Expired, see options below'); }
if (showErrorIcon.value) { return props.t('Learn more about the error'); }
return dropdownVisible.value ? props.t('Close Dropdown') : props.t('Open Dropdown');
});
</script>
<template>
<button
class="group text-18px hover:text-alpha focus:text-alpha border-0 relative flex flex-row justify-end items-center h-full gap-x-8px outline-none focus:outline-none"
:title="title"
@click="dropdownStore.dropdownToggle()"
>
<template v-if="errors.length && errors[0].level">
<InformationCircleIcon v-if="errors[0].level === 'info'" class="text-unraid-red fill-current relative w-24px h-24px" />
<ExclamationTriangleIcon v-if="errors[0].level === 'warning'" class="text-unraid-red fill-current relative w-24px h-24px" />
<ShieldExclamationIcon v-if="errors[0].level === 'error'" class="text-unraid-red fill-current relative w-24px h-24px" />
</template>
<span v-if="text" class="relative leading-none">
<span>{{ text }}</span>
<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>
<UpcDropdownTriggerMenuIcon :open="dropdownVisible" />
<BrandAvatar />
</button>
</template>

View File

@@ -0,0 +1,14 @@
<script setup lang="ts">
import { Bars3Icon, XMarkIcon } from '@heroicons/vue/24/solid';
export interface Props {
open?: boolean;
}
withDefaults(defineProps<Props>(), {
open: false,
});
</script>
<template>
<Bars3Icon v-if="!open" class="w-20px" />
<XMarkIcon v-else class="w-20px" />
</template>

View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
// shadow-[var(--ring-offset-shadow)_var(--ring-shadow)_var(--shadow-beta)]
// border border-solid border-beta/5
</script>
<template>
<nav class="flex flex-col gap-y-8px p-8px bg-alpha rounded-lg shadow-xl shadow-orange/10">
<slot />
</nav>
</template>

View File

@@ -0,0 +1,3 @@
<template>
<hr class="w-full h-2px bg-gradient-to-r from-unraid-red to-orange shadow-none border-none rounded">
</template>

View File

@@ -0,0 +1,141 @@
<script lang="ts" setup>
/**
* @todo devEnv should be a .env variable so we can gate staging installs
*
* @todo future idea turn this into a carousel. each feature could have a short video if we ever them
*/
import { Switch, SwitchGroup, SwitchLabel } from '@headlessui/vue';
import { ArrowTopRightOnSquareIcon } from '@heroicons/vue/24/solid';
import useInstallPlugin from '~/composables/installPlugin';
import { CONNECT_DOCS } from '~/helpers/urls';
import { usePromoStore } from '~/store/promo';
import type { UserProfilePromoFeature } from '~/types/userProfile';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
open?: boolean;
t: any;
}
withDefaults(defineProps<Props>(), {
open: false,
});
const promoStore = usePromoStore();
/**
* These are translated in the component below. So if you add a new feature, make sure to add it to the translation file.
*/
const features = ref<UserProfilePromoFeature[]>([
{
title: 'Dynamic Remote Access',
copy: '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.',
},
{
title: 'Manage Your Server Within Connect',
copy: '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.',
},
{
title: 'Deep Linking',
copy: 'The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.',
},
{
title: 'Online Flash Backup',
copy: '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.',
},
{
title: 'Real-time Monitoring',
copy: 'Get an overview of your server\'s state, storage space, apps and VMs status, and more.',
},
{
title: 'Customizable Dashboard Tiles',
copy: 'Set custom server tiles how you like and automatically display your server\'s banner image on your Connect Dashboard.',
},
{
title: 'License Management',
copy: 'Manage your license keys at any time via the My Keys section.',
},
{
title: 'Plus more on the way',
copy: 'All you need is an active internet connection, an Unraid.net account, and the Connect plugin. Get started by installing the plugin.',
},
]);
const staging = ref(false);
const { install } = useInstallPlugin();
</script>
<template>
<Modal
:t="t"
:title="t('Introducing Unraid Connect')"
:description="t('Enhance your Unraid experience')"
:open="open"
:show-close-x="true"
max-width="max-w-800px"
@close="promoStore.promoHide()"
>
<template #headerTitle>
<span><UpcBeta class="relative -top-1" /></span>
</template>
<template #main>
<div class="text-center relative w-full">
<div class="grid grid-cols-1 sm:grid-cols-2 justify-center p-16px md:py-24px gap-16px">
<UpcPromoFeature
v-for="(feature, index) in features"
:key="index"
:title="t(feature.title)"
:copy="t(feature.copy)"
/>
</div>
</div>
</template>
<template #footer>
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
<SwitchGroup v-if="import.meta.env.DEV" as="div" class="flex items-center justify-center">
<Switch v-model="staging" :class="[staging ? 'bg-indigo-600' : 'bg-gray-200', 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2']">
<span aria-hidden="true" :class="[staging ? 't-x-5' : 't-x-0', 'pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out']" />
</Switch>
<SwitchLabel as="span" class="ml-3 text-12px">
<span class="font-semibold">Install Staging</span>
</SwitchLabel>
</SwitchGroup>
<button
class="text-white text-14px text-center w-full flex flex-row items-center justify-center gap-x-8px px-8px py-8px cursor-pointer rounded-md bg-gradient-to-r from-unraid-red to-orange hover:from-unraid-red/60 hover:to-orange/60 focus:from-unraid-red/60 focus:to-orange/60"
@click="install({ staging, update: false })"
>
{{ staging ? 'Install Connect Staging' : t('Install Connect') }}
</button>
<div>
<a
:href="CONNECT_DOCS.toString()"
class="text-12px tracking-wide inline-flex flex-row items-center justify-start gap-8px mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
target="_blank"
rel="noopener noreferrer"
:title="t('Checkout the Connect Documentation')"
>
{{ t('Learn More') }}
<ArrowTopRightOnSquareIcon class="w-16px" />
</a>
<button
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
:title="t('Close')"
@click="promoStore.promoHide()"
>
{{ t('No thanks') }}
</button>
</div>
</div>
</template>
</Modal>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,39 @@
<script setup lang="ts">
export interface Props {
center?: boolean;
copy?: string;
icon?: string;
title?: string;
}
defineProps<Props>();
</script>
<template>
<div class="text-left relative flex overflow-hidden">
<span v-if="!center" class="flex-shrink-0">
<slot />
</span>
<div class="inline-flex flex-col" :class="{ 'text-center': center }">
<h3
class="text-16px font-semibold"
:class="{
'mt-0 mb-4px': copy,
'my-0': !copy,
'flex flex-row justify-center items-center': center
}"
>
<span v-if="center" class="flex-shrink-0 mr-8px">
<slot />
</span>
{{ title }}
</h3>
<p
v-if="copy"
class="text-14px opacity-75 py-0"
:class="{'px-8px': center}"
v-html="copy"
/>
</div>
</div>
</template>

View File

@@ -0,0 +1,42 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import { useServerStore } from '~/store/server';
import type { ServerStateDataAction } from '~/types/server';
defineProps<{ t: any; }>();
const { state, stateData } = storeToRefs(useServerStore());
const purchaseAction = computed((): ServerStateDataAction | undefined => {
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'purchase');
});
const upgradeAction = computed((): ServerStateDataAction | undefined => {
return stateData.value.actions && stateData.value.actions.find(action => action.name === 'upgrade');
});
</script>
<template>
<span class="flex flex-row items-center gap-x-8px">
<template v-if="upgradeAction">
<UpcServerStateBuy
class="text-gamma"
:title="t('Upgrade Key')"
@click="upgradeAction.click()"
>
<h5>Unraid OS <em><strong>{{ t(stateData.humanReadable) }}</strong></em></h5>
</UpcServerStateBuy>
</template>
<h5 v-else>
Unraid OS <em :class="{ 'text-unraid-red': stateData.error || state === 'EEXPIRED' }"><strong>{{ t(stateData.humanReadable) }}</strong></em>
</h5>
<template v-if="purchaseAction">
<UpcServerStateBuy
class="text-orange-dark relative top-[1px] hidden sm:block"
:title="t('Purchase Key')"
@click="purchaseAction.click()"
>{{ t('Purchase') }}</UpcServerStateBuy>
</template>
</span>
</template>

View File

@@ -0,0 +1,5 @@
<template>
<button class="text-12px font-semibold transition-colors duration-150 ease-in-out border-t-0 border-l-0 border-r-0 border-b-2 border-transparent hover:border-orange-dark focus:border-orange-dark focus:outline-none">
<slot />
</button>
</template>

View File

@@ -0,0 +1,82 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useTrialStore } from '~/store/trial';
export interface Props {
open?: boolean;
t: any;
}
const props = withDefaults(defineProps<Props>(), {
open: false,
});
const trialStore = useTrialStore();
const { trialModalLoading, trialStatus } = storeToRefs(trialStore);
interface TrialStatusCopy {
heading: string;
subheading?: string;
}
const trialStatusCopy = computed((): TrialStatusCopy | null => {
switch (trialStatus.value) {
case 'failed':
return {
heading: props.t('Trial Key Creation Failed'),
subheading: props.t('Error creatiing a trial key. Please try again later.'),
};
case 'trialExtend':
return {
heading: props.t('Extending your free trial by 15 days'),
subheading: props.t('Please keep this window open'),
};
case 'trialStart':
return {
heading: props.t('Starting your free 30 day trial'),
subheading: props.t('Please keep this window open'),
};
case 'success':
return {
heading: props.t('Trial Key Created'),
subheading: props.t('Please wait while the page reloads to install your trial key'),
};
case 'ready':
return null;
}
});
const close = () => {
if (trialStatus.value === 'trialStart') { return console.debug('[close] not allowed'); }
trialStore.setTrialStatus('ready');
};
</script>
<template>
<Modal
:t="t"
:open="open"
:title="trialStatusCopy?.heading"
:description="trialStatusCopy?.subheading"
:show-close-x="!trialModalLoading"
max-width="max-w-640px"
@close="close"
>
<template #main>
<BrandLoading v-if="trialModalLoading" class="w-[150px] mx-auto my-24px" />
</template>
<template v-if="!trialModalLoading" #footer>
<div class="w-full max-w-xs flex flex-col items-center gap-y-16px mx-auto">
<div>
<button
class="text-12px tracking-wide inline-block mx-8px opacity-60 hover:opacity-100 focus:opacity-100 underline transition"
:title="t('Close Modal')"
@click="close"
>
{{ t('Close') }}
</button>
</div>
</div>
</template>
</Modal>
</template>

View File

@@ -0,0 +1,78 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia';
import useTimeHelper from '~/composables/time';
import { useServerStore } from '~/store/server';
export interface Props {
forExpire?: boolean;
t: any;
}
const props = withDefaults(defineProps<Props>(), {
forExpire: false,
});
const { buildStringFromValues, dateDiff, formatDate } = useTimeHelper(props.t);
const serverStore = useServerStore();
const { uptime, expireTime, state } = storeToRefs(serverStore);
const time = computed(() => {
if (props.forExpire && expireTime.value) {
return expireTime.value;
}
return (state.value === 'TRIAL' || state.value === 'EEXPIRED') && expireTime.value && expireTime.value > 0
? expireTime.value
: uptime.value;
});
const parsedTime = ref<string>('');
const formattedTime = computed<string>(() => formatDate(time.value));
const countUp = computed<boolean>(() => {
if (props.forExpire && expireTime.value) {
return false;
}
return state.value !== 'TRIAL' && state.value !== 'ENOCONN';
});
const output = computed(() => {
if (!countUp.value || state.value === 'EEXPIRED') {
return {
title: state.value === 'EEXPIRED'
? props.t('Trial Key Expired at {0}', [formattedTime.value])
: props.t('Trial Key Expires at {0}', [formattedTime.value]),
text: state.value === 'EEXPIRED'
? props.t('Trial Key Expired {0}', [parsedTime.value])
: props.t('Trial Key Expires in {0}', [parsedTime.value]),
};
}
return {
title: props.t('Server Up Since {0}', [formattedTime.value]),
text: props.t('Uptime {0}', [parsedTime.value]),
};
});
const runDiff = () => {
parsedTime.value = buildStringFromValues(dateDiff((time.value).toString(), countUp.value));
};
let interval: string | number | NodeJS.Timeout | undefined;
onBeforeMount(() => {
runDiff();
interval = setInterval(() => {
runDiff();
}, 1000);
});
onBeforeUnmount(() => {
clearInterval(interval);
});
</script>
<template>
<p :title="output.title">
{{ output.text }}
</p>
</template>

View File

@@ -0,0 +1,76 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { request } from '~/composables/services/request';
import { useServerStore } from '~/store/server';
import 'tailwindcss/tailwind.css';
import '~/assets/main.css';
export interface Props {
phpWanIp?: string;
}
const props = defineProps<Props>();
const { t } = useI18n();
const { isRemoteAccess } = storeToRefs(useServerStore());
const wanIp = ref<string | null>();
const fetchError = ref<any>();
const loading = ref(false);
const computedError = computed(() => {
if (!props.phpWanIp) { return t('DNS issue, unable to resolve wanip4.unraid.net'); }
if (fetchError.value) { return fetchError.value; }
});
onBeforeMount(() => {
wanIp.value = sessionStorage.getItem('unraidConnect_wanIp');
});
watchEffect(async () => {
// if we don't have a client WAN IP AND we have the server WAN IP then we fetch
if (!wanIp.value && props.phpWanIp) {
console.debug('[watch] wanIp');
loading.value = true;
const response = await request.url('https://wanip4.unraid.net/')
.get()
.text();
if (response) {
console.debug('[watch] wanIp response', response);
loading.value = false;
wanIp.value = response as string; // response returns text nothing to traverse
// save in sessionStorage so we only make this request once per webGUI session
sessionStorage.setItem('unraidConnect_wanIp', wanIp.value);
} else {
loading.value = false;
fetchError.value = t('Unable to fetch client WAN IPv4');
}
}
});
</script>
<template>
<span v-if="loading" class="italic">{{ t('Checking WAN IPs') }}</span>
<template v-else>
<span v-if="computedError" class="text-unraid-red font-semibold">{{ computedError }}</span>
<template v-else>
<span v-if="isRemoteAccess || phpWanIp === wanIp && !isRemoteAccess">{{ t('Remark: your WAN IPv4 is {0}', [wanIp]) }}</span>
<span v-else class="inline-block w-1/2 whitespace-normal">
{{ t("Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.", [phpWanIp, wanIp]) }}
{{ t('This may indicate a complex network that will not work with this Remote Access solution.') }}
{{ t('Ignore this message if you are currently connected via Remote Access or VPN.') }}
</span>
</template>
</template>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,66 @@
import type { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { FragmentDefinitionNode } from 'graphql';
import type { Incremental } from './graphql';
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> = TDocumentType extends DocumentTypeDecoration<
infer TType,
any
>
? [TType] extends [{ ' $fragmentName'?: infer TKey }]
? TKey extends string
? { ' $fragmentRefs'?: { [key in TKey]: TType } }
: never
: never
: never;
// return non-nullable if `fragmentType` is non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
// return nullable if `fragmentType` is nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
// return array of non-nullable if `fragmentType` is array of non-nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
// return array of nullable if `fragmentType` is array of nullable
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;
export function useFragment<TType>(
_documentNode: DocumentTypeDecoration<TType, any>,
fragmentType: FragmentType<DocumentTypeDecoration<TType, any>> | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): TType | ReadonlyArray<TType> | null | undefined {
return fragmentType as any;
}
export function makeFragmentData<
F extends DocumentTypeDecoration<any, any>,
FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {
return data as FragmentType<F>;
}
export function isFragmentReady<TQuery, TFrag>(
queryNode: DocumentTypeDecoration<TQuery, any>,
fragmentNode: TypedDocumentNode<TFrag>,
data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {
const deferredFields = (queryNode as { __meta__?: { deferredFields: Record<string, (keyof TFrag)[]> } }).__meta__
?.deferredFields;
if (!deferredFields) return true;
const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
const fragName = fragDef?.name?.value;
const fields = (fragName && deferredFields[fragName]) || [];
return fields.length > 0 && fields.every(field => data && field in data);
}

View File

@@ -0,0 +1,42 @@
/* eslint-disable */
import * as types from './graphql';
import type { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
/**
* Map of all GraphQL operations in the project.
*
* This map has several performance disadvantages:
* 1. It is not tree-shakeable, so it will include all operations in the project.
* 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
* 3. It does not support dead code elimination, so it will add unused operations.
*
* Therefore it is highly recommended to use the babel or swc plugin for production.
*/
const documents = {
"\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n": types.serverStateDocument,
};
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
*
* @example
* ```ts
* const query = graphql(`query GetUser($id: ID!) { user(id: $id) { name } }`);
* ```
*
* The query argument is unknown!
* Please regenerate the types.
*/
export function graphql(source: string): unknown;
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"): (typeof documents)["\n query serverState {\n cloud {\n error\n apiKey {\n valid\n error\n }\n cloud {\n status\n error\n }\n minigraphql {\n status\n error\n }\n relay {\n status\n error\n }\n }\n config {\n error\n valid\n }\n info {\n os {\n hostname\n }\n }\n owner {\n avatar\n username\n }\n registration {\n state\n expiration\n keyFile {\n contents\n }\n }\n vars {\n regGen\n regState\n configError\n configValid\n }\n }\n"];
export function graphql(source: string) {
return (documents as any)[source] ?? {};
}
export type DocumentType<TDocumentNode extends DocumentNode<any, any>> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from "./fragment-masking";
export * from "./gql";

View File

@@ -0,0 +1,48 @@
const useInstallPlugin = () => {
const install = (payload = { staging: false, update: false }) => {
console.debug('[useInstallPlugin.install]', { payload });
try {
const file = `https://sfo2.digitaloceanspaces.com/unraid-dl/unraid-api/dynamix.unraid.net${payload?.staging ? '.staging.plg' : '.plg'}`;
console.debug('[useInstallPlugin.install]', file);
if (!payload.update) {
// after initial install, the dropdown store looks for this to automatically open the launchpad dropdown
sessionStorage.setItem('clickedInstallPlugin', '1');
}
const modalTitle = payload.update ? 'Updating Connect (beta)' : 'Installing Connect (beta)';
// @ts-ignore `openPlugin` will be included in 6.10.4+ DefaultPageLayout
if (typeof openPlugin === 'function') {
console.debug('[useInstallPlugin.install] using openPlugin', file);
// @ts-ignore
openPlugin(
`plugin ${payload.update ? 'update' : 'install'} ${file}`,
modalTitle,
'',
'refresh',
);
} else {
console.debug('[useInstallPlugin.install] using openBox', file);
// `openBox()` is defined in the webgui's DefaultPageLayout.php and used when openPlugin is not available
// @ts-ignore
openBox(
`/plugins/dynamix.plugin.manager/scripts/plugin&arg1=install&arg2=${file}`,
modalTitle,
600,
900,
true,
);
}
} catch (error) {
console.error(error);
}
};
return {
install,
};
};
export default useInstallPlugin;

View File

@@ -0,0 +1,13 @@
export const preventClose = (e: { preventDefault: () => void; returnValue: string; }) => {
e.preventDefault();
e.returnValue = '';
alert('Closing this pop-up window while actions are being preformed may lead to unintended errors.');
};
export const addPreventClose = () => {
window.addEventListener('beforeunload', preventClose);
};
export const removePreventClose = () => {
window.removeEventListener('beforeunload', preventClose);
};

View File

@@ -0,0 +1,21 @@
import { request } from '~/composables/services/request';
const KeyServer = request.url('https://keys.lime-technology.com');
export interface StartTrialPayload {
guid: string;
timestamp: number; // timestamp in seconds
}
export interface StartTrialResponse {
license?: string;
trial?: string
}
export const startTrial = (payload: StartTrialPayload) => KeyServer
.url('/account/trial')
.formUrl(payload)
.post();
export const validateGuid = (payload: { guid: string }) => KeyServer
.url('/validate/guid')
.formUrl(payload)
.post();

View File

@@ -0,0 +1,25 @@
import wretch from 'wretch';
import formUrl from 'wretch/addons/formUrl';
import queryString from 'wretch/addons/queryString';
import { useErrorsStore } from '~/store/errors';
const errorsStore = useErrorsStore();
export const request = wretch()
.addon(formUrl)
.addon(queryString)
.errorType('json')
.resolve((response) => {
return (
response
.error('Error', (error) => {
console.log('global catch (Error class)', error);
errorsStore.setError(error);
})
.error('TypeError', (error) => {
console.log('global type error catch (TypeError class)', error);
errorsStore.setError(error);
})
);
});

View File

@@ -0,0 +1,65 @@
import { request } from '~/composables/services/request';
/**
* @name WebguiInstallKey
* @description used to auto install key urls
* @type GET - data should be passed using wretch's `.query({ url: String })`
* @param {string} url - URL of license key
*/
export const WebguiInstallKey = request.url('/webGui/include/InstallKey.php');
/**
* @type POST
* @dataType - `formUrl(Object)` https://github.com/elbywan/wretch#formurlinput-object--string
* @param {string} csrf_token
* @param {string} '#file' - ex: getters.myServersCfgPath
* @param {string} '#section' - ex: 'remote'
* @param {string} apikey - from key server's response
* @param {string} avatar
* @param {string} regWizTime - date_guid
* @param {string} email
* @param {string} username
*/
export const WebguiUpdate = request.url('/update.php');
/**
* @name WebguiUpdateDns
* @dataForm formUrl
* @description Used after Sign In to ensure URLs will work correctly
* @note this request is delayed by 500ms to allow server to process key install fully
* @todo potentially remove delay
* @param csrf_token
* @type POST
*/
export const WebguiUpdateDns = request.url('/webGui/include/UpdateDNS.php');
/**
* @name WebguiState
* @description used to get current state of server via PHP rather than unraid-api
* @type GET
*/
export const WebguiState = request.url('/plugins/dynamix.my.servers/data/server-state.php');
/**
* Run unraid-api command's via php requests
*/
export interface WebguiUnraidApiCommandPayload {
csrf_token: string;
command: 'report' | 'start';
param1?: '-v'|'-vv';
}
export const WebguiUnraidApiCommand = async (payload: WebguiUnraidApiCommandPayload) => {
console.debug('[WebguiUnraidApiCommand]', payload);
if (!payload) { return console.error('[WebguiUnraidApiCommand] payload is required'); }
try {
return await request
.url('/plugins/dynamix.my.servers/include/unraid-api.php')
.formUrl(payload)
.post()
.json((json) => {
console.debug('[WebguiUnraidApiCommand]', json);
return json;
})
.catch((error) => {
console.error(`[WebguiUnraidApiCommand] catch failed to execute unraid-api ${command}`, error);
});
} catch (error) {
console.error(`[WebguiUnraidApiCommand] catch failed to execute unraid-api ${command}`, error);
}
};

140
web/composables/time.ts Normal file
View File

@@ -0,0 +1,140 @@
import dayjs, { extend } from 'dayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat';
/** @see https://day.js.org/docs/en/display/format#localized-formats */
extend(localizedFormat);
export interface TimeStringsObject {
years: number;
months: number;
days: number;
hours: number;
minutes: number;
seconds: number;
firstDateWasLater: boolean;
displaySeconds?: boolean;
}
const useTimeHelper = (t: any) => {
const buildStringFromValues = (payload: TimeStringsObject) => {
const {
years,
months,
days,
hours,
minutes,
seconds,
firstDateWasLater,
displaySeconds,
} = payload;
const result = [];
if (years) { result.push(t('year', years)); }
if (months) { result.push(t('month', months)); }
if (days) { result.push(t('day', days)); }
if (hours) { result.push(t('hour', hours)); }
if (minutes) { result.push(t('minute', minutes)); }
if (seconds && ((!years && !months && !days && !hours && !minutes) || displaySeconds)) { result.push(t('second', seconds)); }
if (firstDateWasLater) { result.push(t('ago')); }
return result.join(' ');
};
const formatDate = (date: number): string => dayjs(date).format('llll');
/**
* Original meat and potatos from:
* @version: 1.0.1
* @author: huangjinlin
* @repo: https://github.com/huangjinlin/dayjs-precise-range
*/
const buildValueObject = (
yDiff: number,
mDiff: number,
dDiff: number,
hourDiff: number,
minDiff: number,
secDiff: number,
firstDateWasLater: boolean
): TimeStringsObject => ({
years: yDiff,
months: mDiff,
days: dDiff,
hours: hourDiff,
minutes: minDiff,
seconds: secDiff,
firstDateWasLater,
});
const preciseDateDiff = (d1: dayjs.Dayjs, d2: dayjs.Dayjs): TimeStringsObject => {
let m1 = dayjs(d1);
let m2 = dayjs(d2);
let firstDateWasLater;
if (m1.isSame(m2)) {
return buildValueObject(0, 0, 0, 0, 0, 0, false);
}
if (m1.isAfter(m2)) {
const tmp = m1;
m1 = m2;
m2 = tmp;
firstDateWasLater = true;
} else {
firstDateWasLater = false;
}
let yDiff = m2.year() - m1.year();
let mDiff = m2.month() - m1.month();
let dDiff = m2.date() - m1.date();
let hourDiff = m2.hour() - m1.hour();
let minDiff = m2.minute() - m1.minute();
let secDiff = m2.second() - m1.second();
if (secDiff < 0) {
secDiff = 60 + secDiff;
minDiff -= 1;
}
if (minDiff < 0) {
minDiff = 60 + minDiff;
hourDiff -= 1;
}
if (hourDiff < 0) {
hourDiff = 24 + hourDiff;
dDiff -= 1;
}
if (dDiff < 0) {
const daysInLastFullMonth = dayjs(`${m2.year()}-${m2.month() + 1}`).subtract(1, 'M').daysInMonth();
if (daysInLastFullMonth < m1.date()) { // 31/01 -> 2/03
dDiff = daysInLastFullMonth + dDiff + (m1.date() - daysInLastFullMonth);
} else {
dDiff = daysInLastFullMonth + dDiff;
}
mDiff -= 1;
}
if (mDiff < 0) {
mDiff = 12 + mDiff;
yDiff -= 1;
}
return buildValueObject(yDiff, mDiff, dDiff, hourDiff, minDiff, secDiff, firstDateWasLater);
};
const readableDifference = (a = '', b = '') => {
try {
const x = a ? dayjs(parseInt(a, 10)) : dayjs();
const y = b ? dayjs(parseInt(b, 10)) : dayjs();
return preciseDateDiff(x, y);
} catch (error) {
throw new Error('Couldn\'t calculate date difference');
}
};
const dateDiff = (time: string, countUp: boolean) => countUp ? readableDifference(time, '') : readableDifference('', time);
return {
buildStringFromValues,
dateDiff,
formatDate,
};
};
export default useTimeHelper;

View File

@@ -0,0 +1,43 @@
/**
* @see https://www.telerik.com/blogs/how-to-trap-focus-modal-vue-3
*/
import { customRef } from 'vue';
// eslint-disable-next-line import/named
import { createFocusTrap } from 'focus-trap';
const useFocusTrap = (focusTrapArgs) => {
const trapRef = customRef((track, trigger) => {
let $trapEl = null;
return {
get () {
track();
return $trapEl;
},
set (value) {
$trapEl = value;
value ? initFocusTrap(focusTrapArgs) : clearFocusTrap();
trigger();
},
};
});
let trap = null;
const initFocusTrap = (focusTrapArgs) => {
if (!trapRef.value) { return; }
trap = createFocusTrap(trapRef.value, focusTrapArgs);
trap.activate();
};
const clearFocusTrap = () => {
trap?.deactivate();
trap = null;
};
return {
trapRef,
initFocusTrap,
clearFocusTrap,
};
};
export default useFocusTrap;

17
web/fix-array-type.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* This function wraps constant case, that turns any string into CONSTANT_CASE
* However, this function has a bug that, if you pass _ to it it will return an empty
* string. This small module fixes that
*
* @param {string*} str
* @return {string}
*/
function FixArrayType (str) {
if (str === 'Array') {
return 'ArrayType';
}
// If result is an empty string, just return the original string
return str;
}
module.exports = FixArrayType;

7
web/helpers/functions.ts Normal file
View File

@@ -0,0 +1,7 @@
/**
* @name OBJ_TO_STR
* @param {object} obj
* @returns {String}
* @description Output key + value for each item in the object. Adds new line after each item.
*/
export const OBJ_TO_STR = obj => Object.entries(obj).reduce((str, [p, val]) => `${str}${p}: ${val}\n`, '');

31
web/helpers/urls.ts Normal file
View File

@@ -0,0 +1,31 @@
/**
* @todo setup .env
*/
const ACCOUNT = new URL(import.meta.env.VITE_ACCOUNT ?? 'https://account.unraid.net');
const UNRAID_NET = new URL(import.meta.env.VITE_UNRAID_NET ?? 'https://unraid.net');
const ACCOUNT_CALLBACK = new URL('c', ACCOUNT);
const CONNECT_DOCS = new URL('https://docs.unraid.net/category/unraid-connect');
const CONNECT_DASHBOARD = new URL(import.meta.env.VITE_CONNECT ?? 'https://connect.myunraid.net');
const CONNECT_FORUMS = new URL('https://forums.unraid.net/forum/94-connect-plugin-support/');
const CONTACT = new URL('/contact', UNRAID_NET);
const DEV_GRAPH_URL = '';
const DISCORD = new URL('https://discord.gg/unraid');
const PURCHASE_CALLBACK = new URL('/c', UNRAID_NET);
const SETTINGS_MANAGMENT_ACCESS = new URL('/Settings/ManagementAccess', window.location.origin);
const PLUGIN_SETTINGS = new URL('#UnraidNetSettings', SETTINGS_MANAGMENT_ACCESS);
export {
ACCOUNT,
ACCOUNT_CALLBACK,
CONNECT_DASHBOARD,
CONNECT_DOCS,
CONNECT_FORUMS,
CONTACT,
DEV_GRAPH_URL,
DISCORD,
PURCHASE_CALLBACK,
PLUGIN_SETTINGS,
SETTINGS_MANAGMENT_ACCESS,
};

13
web/layouts/default.vue Normal file
View File

@@ -0,0 +1,13 @@
<template>
<client-only>
<div class="flex flex-row items-center justify-center gap-6 p-6 text-gray-200 bg-zinc-800">
<NuxtLink to="/" class="underline hover:no-underline focus:no-underline" active-class="text-orange">
Test Vue Components
</NuxtLink>
<NuxtLink to="/webComponents" class="underline hover:no-underline focus:no-underline" active-class="text-orange">
Test Web Components
</NuxtLink>
</div>
<slot />
</client-only>
</template>

200
web/locales/_template.json Normal file
View File

@@ -0,0 +1,200 @@
{
"LAN IP": "",
"LAN IP {0}": "",
"LAN IP Copied": "",
"Click to Copy LAN IP {0}": "",
"Trial Key Expired at {0}": "",
"Trial Key Expires at {0}": "",
"Trial Key Expired {0}": "",
"Trial Key Expires in {0}": "",
"Server Up Since {0}": "",
"Uptime {0}": "",
"year": "",
"month": "",
"day": "",
"hour": "",
"minute": "",
"second": "",
"ago": "",
"Purchase": "",
"Upgrade": "",
"Fix Error": "",
"Get Started": "",
"Trial Expired, see options below": "",
"Learn more about the error": "",
"Close Dropdown": "",
"Open Dropdown": "",
"Thank you for installing Connect!": "",
"Sign In to your Unraid.net account to get started": "",
"Go to Connect": "",
"Opens Connect in new tab": "",
"Manage Unraid.net Account": "",
"Manage Unraid.net Account in new tab": "",
"Settings": "",
"Go to Connect plugin settings": "",
"Enhance your Unraid experience with Connect": "",
"Beta": "",
"Loading": "",
"Restarting unraid-api…": "",
"unraid-api is offline": "",
"Introducing Unraid Connect": "",
"Enhance your Unraid experience": "",
"Connected": "",
"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.": "",
"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.": "",
"Deep Linking": "",
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "",
"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.": "",
"Real-time Monitoring": "",
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "",
"Customizable Dashboard Tiles": "",
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "",
"License Management": "",
"Manage your license keys at any time via the My Keys section.": "",
"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.": "",
"Checkout the Connect Documentation": "",
"No thanks": "",
"Learn more": "",
"Install Connect": "",
"Close Modal": "",
"Close": "",
"Reload": "",
"Unraid logo animating with a wave like effect": "",
"Click to close modal": "",
"Error": "",
"Performing actions": "",
"Success!": "",
"Something went wrong": "",
"Please keep this window open while we perform some actions": "",
"You're one step closer to enhancing your Unraid experience": "",
"Thank you for purchasing an Unraid {0} Key!": "",
"Your {0} Key has been replaced!": "",
"Your Trial key has been extended!": "",
"Copied": "",
"Copy Key URL": "",
"Copy your Key URL: {0}": "",
"Then go to Tools > Registration to manually install it": "",
"Enhance your experience with Unraid Connect": "",
"Sign In to utilize Unraid Connect": "",
"Configure Connect Features": "",
"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.": "",
"The logs may contain sensitive information so do not post them publicly.": "",
"Download unraid-api Logs": "",
"Unraid Connect Forums": "",
"Unraid Discord": "",
"Unraid Contact Page": "",
"DNS issue, unable to resolve wanip4.unraid.net": "",
"Unable to fetch client WAN IPv4": "",
"Checking WAN IPs…": "",
"Remark: your WAN IPv4 is {0}": "",
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "",
"This may indicate a complex network that will not work with this Remote Access solution.": "",
"Ignore this message if you are currently connected via Remote Access or VPN.": "",
"Ready to update Connect account configuration": "",
"Signing in {0}…": "",
"Signing out {0}…": "",
"{0} Signed In Successfully": "",
"{0} Signed Out Successfully": "",
"Sign In Failed": "",
"Sign Out Failed": "",
"Failed to update Connect account configuration": "",
"Callback redirect type not present or incorrect": "",
"Failed to install key": "",
"Ready to Install Key": "",
"Installing Extended Trial": "",
"Installing Recovered": "",
"Installing Replaced": "",
"{0} {1} Key…": "",
"{1} Key {0} Successfully": "",
"Failed to {0} {1} Key": "",
"Purchase Key": "",
"Upgrade Key": "",
"Recover Key": "",
"Redeem Activation Code": "",
"Replace Key": "",
"Sign In with Unraid.net Account": "",
"Sign Out of Unraid.net": "",
"Extend Trial": "",
"Start Free 30 Day Trial": "",
"Go to Management Access Now": "",
"Contact Support": "",
"Learn More": "",
"No Keyfile": "",
"Let's Unleash your Hardware!": "",
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "",
"Trial": "",
"Thank you for choosing Unraid OS!": "",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "",
"Trial Expired": "",
"Your Trial has expired": "",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "",
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "",
"Basic": "",
"<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>": "",
"Plus": "",
"Pro": "",
"Flash GUID Error": "",
"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>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.</p><p>You may also attempt to Purchase or Replace your key.</p>": "",
"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>": "",
"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.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "",
"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>": "",
"No USB flash configuration data": "",
"<p>There is a problem with your USB Flash device</p>": "",
"No Flash": "",
"Cannot access your USB Flash boot device": "",
"<p>There is a physical problem accessing your USB Flash boot device</p>": "",
"BLACKLISTED": "",
"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>": "",
"USB Flash device error": "",
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "",
"USB Flash has no serial number": "",
"Trial Requires Internet Connection": "",
"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>": "",
"Stale": "",
"Stale Server": "",
"<p>Please refresh the page to ensure you load your latest configuration</p>": "",
"Invalid API Key": "",
"Please sign out then sign back in to refresh your API key.": "",
"Invalid API Key Format": "",
"Too Many Devices": "",
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "",
"Unraid Connect Install Failed": "",
"Rebooting will likely solve this.": "",
"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.": "",
"Unraid Connect Error": "",
"Trial Key Creation Failed": "",
"Error creatiing a trial key. Please try again later.": "",
"Extending your free trial by 15 days": "",
"Please keep this window open": "",
"Starting your free 30 day trial": "",
"Trial Key Created": "",
"Please wait while the page reloads to install your trial key": "",
"A Trial key provides all the functionality of a Pro Registration key": "",
"Extension Installed": "",
"Recovered": "",
"Replaced": "",
"Installing": "",
"Installed": "",
"Install": "",
"Install Extended": "",
"Install Recovered": "",
"Install Replaced": "",
"Your free Trial key provides all the functionality of a Pro Registration key": ""
}

200
web/locales/en_US.json Normal file
View File

@@ -0,0 +1,200 @@
{
"LAN IP": "LAN IP",
"LAN IP {0}": "LAN IP {0}",
"LAN IP Copied": "LAN IP Copied",
"Click to Copy LAN IP {0}": "Click to Copy LAN IP {0}",
"Trial Key Expired at {0}": "Trial Key Expired at {0}",
"Trial Key Expires at {0}": "Trial Key Expires at {0}",
"Trial Key Expired {0}": "Trial Key Expired {0}",
"Trial Key Expires in {0}": "Trial Key Expires in {0}",
"Server Up Since {0}": "Server Up Since {0}",
"Uptime {0}": "Uptime {0}",
"year": "{n} year | {n} years",
"month": "{n} month | {n} months",
"day": "{n} day | {n} days",
"hour": "{n} hour | {n} hours",
"minute": "{n} minute | {n} minutes",
"second": "{n} second | {n} seconds",
"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.",
"Checkout the Connect Documentation": "Checkout the Connect Documentation",
"No thanks": "No thanks",
"Learn more": "Learn more",
"Install Connect": "Install Connect",
"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!": "Thank you for purchasing an Unraid {0} Key!",
"Your {0} Key has been replaced!": "Your {0} Key has been replaced!",
"Your Trial key has been extended!": "Your Trial key has been extended!",
"Copied": "Copied",
"Copy Key URL": "Copy Key URL",
"Copy your Key URL: {0}": "Copy your Key URL: {0}",
"Then go to Tools > Registration to manually install it": "Then go to Tools > Registration to manually install it",
"Enhance your experience with Unraid Connect": "Enhance your experience with Unraid Connect",
"Sign In to utilize Unraid Connect": "Sign In to utilize Unraid Connect",
"Configure Connect Features": "Configure Connect Features",
"The primary method of support for Unraid Connect is through our forums and Discord.": "The primary method of support for Unraid Connect is through our forums and Discord.",
"If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.": "If you are asked to supply logs, please open a support request on our Contact Page and reply to the email message you receive with your logs attached.",
"The logs may contain sensitive information so do not post them publicly.": "The logs may contain sensitive information so do not post them publicly.",
"Download unraid-api Logs": "Download unraid-api Logs",
"Unraid Connect Forums": "Unraid Connect Forums",
"Unraid Discord": "Unraid Discord",
"Unraid Contact Page": "Unraid Contact Page",
"DNS issue, unable to resolve wanip4.unraid.net": "DNS issue, unable to resolve wanip4.unraid.net",
"Unable to fetch client WAN IPv4": "Unable to fetch client WAN IPv4",
"Checking WAN IPs…": "Checking WAN IPs…",
"Remark: your WAN IPv4 is {0}": "Remark: your WAN IPv4 is {0}",
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.",
"This may indicate a complex network that will not work with this Remote Access solution.": "This may indicate a complex network that will not work with this Remote Access solution.",
"Ignore this message if you are currently connected via Remote Access or VPN.": "Ignore this message if you are currently connected via Remote Access or VPN.",
"Ready to update Connect account configuration": "Ready to update Connect account configuration",
"Signing in {0}…": "Signing in {0}…",
"Signing out {0}…": "Signing out {0}…",
"{0} Signed In Successfully": "{0} Signed In Successfully",
"{0} Signed Out Successfully": "{0} Signed Out Successfully",
"Sign In Failed": "Sign In Failed",
"Sign Out Failed": "Sign Out Failed",
"Failed to update Connect account configuration": "Failed to update Connect account configuration",
"Callback redirect type not present or incorrect": "Callback redirect type not present or incorrect",
"Failed to install key": "Failed to install key",
"Ready to Install Key": "Ready to Install Key",
"Installing Extended Trial": "Installing Extended Trial",
"Installing Recovered": "Installing Recovered",
"Installing Replaced": "Installing Replaced",
"{0} {1} Key…": "{0} {1} Key…",
"{1} Key {0} Successfully": "{1} Key {0} Successfully",
"Failed to {0} {1} Key": "Failed to {0} {1} Key",
"Purchase Key": "Purchase Key",
"Upgrade Key": "Upgrade Key",
"Recover Key": "Recover Key",
"Redeem Activation Code": "Redeem Activation Code",
"Replace Key": "Replace Key",
"Sign In with Unraid.net Account": "Sign In with Unraid.net Account",
"Sign Out of Unraid.net": "Sign Out of Unraid.net",
"Extend Trial": "Extend Trial",
"Start Free 30 Day Trial": "Start Free 30 Day Trial",
"Go to Management Access Now": "Go to Management Access Now",
"Contact Support": "Contact Support",
"Learn More": "Learn More",
"No Keyfile": "No Keyfile",
"Let's Unleash your Hardware!": "Let's Unleash your Hardware!",
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>",
"Trial": "Trial",
"Thank you for choosing Unraid OS!": "Thank you for choosing Unraid OS!",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>",
"Trial Expired": "Trial Expired",
"Your Trial has expired": "Your Trial has expired",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>",
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>",
"Basic": "Basic",
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Register for Connect by signing in to your Unraid.net account</p>",
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>To support more storage devices as your server grows, click Upgrade Key.</p>",
"Plus": "Plus",
"Pro": "Pro",
"Flash GUID Error": "Flash GUID Error",
"Registration key / USB Flash GUID mismatch": "Registration key / USB Flash GUID mismatch",
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it is blacklisted.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device or choose Purchase Key.</p><p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>",
"<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>": "<p>The license key file does not correspond to the USB Flash boot device. Please copy the correct key file to the /config directory on your USB Flash boot device.</p><p>You may also attempt to Purchase or Replace your key.</p>",
"Multiple License Keys Present": "Multiple License Keys Present",
"<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>": "<p>There are multiple license key files present on your USB flash device and none of them correspond to the USB Flash boot device. Please remove all key files, except the one you want to replace, from the /config directory on your USB Flash boot device.</p><p>Alternately you may purchase a license key for this USB flash device.</p><p>If you want to replace one of your license keys with a new key bound to this USB Flash device, please first remove all other key files first.</p>",
"Missing key file": "Missing key file",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>": "<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>",
"Invalid installation": "Invalid installation",
"<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>": "<p>It is not possible to use a Trial key with an existing Unraid OS installation.</p><p>You may purchase a license key corresponding to this USB Flash device to continue using this installation.</p>",
"No USB flash configuration data": "No USB flash configuration data",
"<p>There is a problem with your USB Flash device</p>": "<p>There is a problem with your USB Flash device</p>",
"No Flash": "No Flash",
"Cannot access your USB Flash boot device": "Cannot access your USB Flash boot device",
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>There is a physical problem accessing your USB Flash boot device</p>",
"BLACKLISTED": "BLACKLISTED",
"Blacklisted USB Flash GUID": "Blacklisted USB Flash GUID",
"<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>": "<p>This USB Flash boot device has been blacklisted. This can occur as a result of transferring your license key to a replacement USB Flash device, and you are currently booted from your old USB Flash device.</p><p>A USB Flash device may also be blacklisted if we discover the serial number is not unique this is common with USB card readers.</p>",
"USB Flash device error": "USB Flash device error",
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>",
"USB Flash has no serial number": "USB Flash has no serial number",
"Trial Requires Internet Connection": "Trial Requires Internet Connection",
"Cannot validate Unraid Trial key": "Cannot validate Unraid Trial key",
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>",
"Stale": "Stale",
"Stale Server": "Stale Server",
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>Please refresh the page to ensure you load your latest configuration</p>",
"Invalid API Key": "Invalid API Key",
"Please sign out then sign back in to refresh your API key.": "Please sign out then sign back in to refresh your API key.",
"Invalid API Key Format": "Invalid API Key Format",
"Too Many Devices": "Too Many Devices",
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "You have exceeded the number of devices allowed for your license. Please remove a device before adding another.",
"Unraid Connect Install Failed": "Unraid Connect Install Failed",
"Rebooting will likely solve this.": "Rebooting will likely solve this.",
"SSL certificates for unraid.net deprecated": "SSL certificates for unraid.net deprecated",
"On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.": "On January 1st, 2023 SSL certificates for unraid.net were deprecated. You MUST provision a new SSL certificate to use our new myunraid.net domain. You can do this on the Settings > Management Access page.",
"Unraid Connect Error": "Unraid Connect Error",
"Trial Key Creation Failed": "Trial Key Creation Failed",
"Error creatiing a trial key. Please try again later.": "Error creatiing a trial key. Please try again later.",
"Extending your free trial by 15 days": "Extending your free trial by 15 days",
"Please keep this window open": "Please keep this window open",
"Starting your free 30 day trial": "Starting your free 30 day trial",
"Trial Key Created": "Trial Key Created",
"Please wait while the page reloads to install your trial key": "Please wait while the page reloads to install your trial key",
"A Trial key provides all the functionality of a Pro Registration key": "A Trial key provides all the functionality of a Pro Registration key.",
"Extension Installed": "Extension Installed",
"Recovered": "Recovered",
"Replaced": "Replaced",
"Installing": "Installing",
"Installed": "Installed",
"Install": "Install",
"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"
}

200
web/locales/ja.json Normal file
View File

@@ -0,0 +1,200 @@
{
"LAN IP": "LAN IP",
"LAN IP {0}": "LAN IP {0}",
"LAN IP Copied": "LAN IPがコピーされました",
"Click to Copy LAN IP {0}": "クリックして LAN IP {0} をコピーします",
"Trial Key Expired at {0}": "トライアル キーの有効期限は {0} に切れます",
"Trial Key Expires at {0}": "試用版キーの有効期限は {0} に切れます",
"Trial Key Expired {0}": "トライアルキーの有効期限が切れました {0}",
"Trial Key Expires in {0}": "トライアル キーの有効期限は {0} に切れます",
"Server Up Since {0}": "{0}以降サーバー稼働中",
"Uptime {0}": "稼働時間 {0}",
"year": "{n} 年 | {n}年",
"month": "{n} 月 | {n}か月",
"day": "{n} 日 | {n}日",
"hour": "{n} 時間 | {n}時間",
"minute": "{n} 分 | {n}分",
"second": "{n} 秒 | {n}秒",
"ago": "前",
"Purchase": "購入",
"Upgrade": "アップグレード",
"Fix Error": "エラーを修正",
"Get Started": "始めましょう",
"Trial Expired, see options below": "試用期限が切れました。以下のオプションを参照してください",
"Learn more about the error": "エラーの詳細を確認する",
"Close Dropdown": "ドロップダウンを閉じる",
"Open Dropdown": "ドロップダウンを開く",
"Thank you for installing Connect!": "Connect をインストールしていただきありがとうございます。",
"Sign In to your Unraid.net account to get started": "Unraid.net アカウントにサインインして開始してください",
"Go to Connect": "「接続」に移動",
"Opens Connect in new tab": "新しいタブで接続を開きます",
"Manage Unraid.net Account": "Unraid.netアカウントの管理",
"Manage Unraid.net Account in new tab": "新しいタブでUnraid.netアカウントを管理",
"Settings": "設定",
"Go to Connect plugin settings": "接続プラグイン設定に移動します",
"Enhance your Unraid experience with Connect": "Connect で Unraid エクスペリエンスを強化",
"Beta": "ベータ",
"Loading": "読み込み中",
"Restarting unraid-api…": "unraid-API を再起動しています…",
"unraid-api is offline": "unraid-API はオフラインです",
"Introducing Unraid Connect": "Unraid Connect の紹介",
"Enhance your Unraid experience": "Unraid エクスペリエンスを強化する",
"Connected": "接続済み",
"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.": "動的リモート アクセスを使用してサーバー アクセシビリティのオン/オフを切り替えます。 ボタンをクリックするだけで UPnP が自動的にオンになり、ルーター上でランダムな WAN ポートが開き、数秒でアクセスが遮断されます。",
"Manage Your Server Within Connect": "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.": "myunraid.net 証明書を備えたサーバーは、Connect Web UI 内から直接管理できます。 同じブラウザ ウィンドウで、携帯電話、タブレット、ラップトップ、または PC から複数のサーバーを管理します。",
"Deep Linking": "ディープリンク",
"The Connect dashboard links to relevant sections of the webgui, allowing quick access to those settings and server sections.": "Connect ダッシュボードは Webgui の関連セクションにリンクしているため、これらの設定やサーバー セクションにすばやくアクセスできます。",
"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.": "設定のバックアップが欠かせないことはありません。 フラッシュ ドライブを変更する必要がある場合は、Connect からバックアップを生成すると、数分で起動して実行できるようになります。",
"Real-time Monitoring": "リアルタイム監視",
"Get an overview of your server's state, storage space, apps and VMs status, and more.": "サーバーの状態、ストレージ容量、アプリと VM のステータスなどの概要を取得します。",
"Customizable Dashboard Tiles": "カスタマイズ可能なダッシュボード タイル",
"Set custom server tiles how you like and automatically display your server's banner image on your Connect Dashboard.": "カスタム サーバー タイルを好みに設定し、サーバーのバナー画像を Connect ダッシュボードに自動的に表示します。",
"License Management": "ライセンス管理",
"Manage your license keys at any time via the My Keys section.": "[マイ キー] セクションでいつでもライセンス キーを管理できます。",
"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.": "必要なのは、アクティブなインターネット接続、Unraid.net アカウント、および Connect プラグインだけです。 プラグインをインストールして始めます。",
"Checkout the Connect Documentation": "Connect のドキュメントを確認する",
"No thanks": "結構です",
"Learn more": "もっと詳しく知る",
"Install Connect": "インストール接続",
"Close Modal": "モーダルを閉じる",
"Close": "近い",
"Reload": "リロード",
"Unraid logo animating with a wave like effect": "波のような効果でアニメーション化された Unraid ロゴ",
"Click to close modal": "クリックしてモーダルを閉じます",
"Error": "エラー",
"Performing actions": "アクションの実行",
"Success!": "成功!",
"Something went wrong": "何か問題が発生しました",
"Please keep this window open while we perform some actions": "いくつかのアクションを実行する間、このウィンドウを開いたままにしてください",
"You're one step closer to enhancing your Unraid experience": "Unraid エクスペリエンスの向上にまた一歩近づいています",
"Thank you for purchasing an Unraid {0} Key!": "Unraid {0} キーをご購入いただきありがとうございます。",
"Your {0} Key has been replaced!": "{0} キーは交換されました。",
"Your Trial key has been extended!": "トライアルキーは延長されました!",
"Copied": "コピーされました",
"Copy Key URL": "キーの URL をコピー",
"Copy your Key URL: {0}": "キーの URL をコピーします: {0}",
"Then go to Tools > Registration to manually install it": "次に、[ツール] > [登録] に移動して手動でインストールします。",
"Enhance your experience with Unraid Connect": "Unraid Connect でエクスペリエンスを向上",
"Sign In to utilize Unraid Connect": "Unraid Connect を利用するにはサインインしてください",
"Configure Connect Features": "接続機能の構成",
"The primary method of support for Unraid Connect is through our forums and Discord.": "Unraid Connect の主なサポート方法は、フォーラムと 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.": "ログの提供を求められた場合は、お問い合わせページでサポート リクエストを開き、受信した電子メール メッセージにログを添付してご返信ください。",
"The logs may contain sensitive information so do not post them publicly.": "ログには機密情報が含まれている可能性があるため、公開しないでください。",
"Download unraid-api Logs": "unraid-api ログをダウンロードする",
"Unraid Connect Forums": "Unraid Connect フォーラム",
"Unraid Discord": "アンレイドディスコード",
"Unraid Contact Page": "アンレイド連絡先ページ",
"DNS issue, unable to resolve wanip4.unraid.net": "DNS の問題、wanip4.unraid.net を解決できない",
"Unable to fetch client WAN IPv4": "クライアントの WAN IPv4 を取得できません",
"Checking WAN IPs…": "WAN IPをチェック中…",
"Remark: your WAN IPv4 is {0}": "注: WAN IPv4 は {0} です",
"Remark: Unraid's WAN IPv4 {0} does not match your client's WAN IPv4 {1}.": "注: Unraid の WAN IPv4 {0} はクライアントの WAN IPv4 {1} と一致しません。",
"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.": "現在リモート アクセスまたは VPN 経由で接続している場合は、このメッセージを無視してください。",
"Ready to update Connect account configuration": "Connect アカウント構成を更新する準備ができました",
"Signing in {0}…": "{0} にサインインしています…",
"Signing out {0}…": "{0} からログアウトしています…",
"{0} Signed In Successfully": "{0} が正常にサインインしました",
"{0} Signed Out Successfully": "{0} が正常にサインアウトしました",
"Sign In Failed": "サインインに失敗しました",
"Sign Out Failed": "サインアウトに失敗しました",
"Failed to update Connect account configuration": "Connect アカウント構成の更新に失敗しました",
"Callback redirect type not present or incorrect": "コールバック リダイレクト タイプが存在しないか、正しくありません",
"Failed to install key": "キーのインストールに失敗しました",
"Ready to Install Key": "キーをインストールする準備ができました",
"Installing Extended Trial": "延長トライアル版のインストール",
"Installing Recovered": "回復されたものをインストールしています",
"Installing Replaced": "取り付け、交換",
"{0} {1} Key…": "{0} {1} キー…",
"{1} Key {0} Successfully": "{1}キー{0}が成功しました",
"Failed to {0} {1} Key": "{0} {1} キーに失敗しました",
"Purchase Key": "キーを購入する",
"Upgrade Key": "アップグレードキー",
"Recover Key": "キーを回復する",
"Redeem Activation Code": "アクティベーション コードを引き換える",
"Replace Key": "キーを交換する",
"Sign In with Unraid.net Account": "Unraid.net アカウントでサインインする",
"Sign Out of Unraid.net": "Unraid.net からサインアウトする",
"Extend Trial": "トライアルの延長",
"Start Free 30 Day Trial": "30 日間の無料トライアルを開始する",
"Go to Management Access Now": "今すぐ管理アクセスに移動",
"Contact Support": "サポート問い合わせ先",
"Learn More": "もっと詳しく知る",
"No Keyfile": "キーファイルがありません",
"Let's Unleash your Hardware!": "ハードウェアを解き放ちましょう!",
"<p>Your server will not be usable until you purchase a Registration key or install a free 30 day <em>Trial</em> key. A <em>Trial</em> key provides all the functionality of a Pro Registration key.</p><p>Registration keys are bound to your USB Flash boot device serial number (GUID). Please use a high quality name brand device at least 1GB in size.</p><p>Note: USB memory card readers are generally not supported because most do not present unique serial numbers.</p><p><strong>Important:</strong></p><ul class='list-disc pl-16px'><li>Please make sure your server time is accurate to within 5 minutes</li><li>Please make sure there is a DNS server specified</li></ul>": "<p>登録キーを購入するか、30 日間の無料の<em>トライアル</em>キーをインストールするまで、サーバーは使用できません。 \n<em>トライアル</em> キーは、Pro 登録キーのすべての機能を提供します。</p><p>登録キーは、USB フラッシュ ブート デバイスのシリアル番号 (GUID) にバインドされています。\nサイズが少なくとも 1 GB の高品質の有名ブランドのデバイスを使用してください。</p><p>注: USB メモリ カード リーダーのほとんどは固有のシリアル番号を提示していないため、通常はサポートされていません。</p><p><strong>\n重要:</strong></p><ul class='list-disc pl-16px'><li>サーバー時間が 5 分以内であることを確認してください。</li><li>\n指定された DNS サーバー</li></ul>",
"Trial": "トライアル",
"Thank you for choosing Unraid OS!": "Unraid OS をお選びいただきありがとうございます。",
"<p>Your <em>Trial</em> key includes all the functionality and device support of a <em>Pro</em> key.</p><p>After your <em>Trial</em> has reached expiration, your server <strong>still functions normally</strong> until the next time you Stop the array or reboot your server.</p><p>At that point you may either purchase a license key or request a <em>Trial</em> extension.</p>": "<p><em>トライアル</em> キーには、<em>プロ</em> キーのすべての機能とデバイス サポートが含まれています。</p><p><em>トライアル</em> の終了後\n有効期限に達しても、次回アレイを停止するかサーバーを再起動するまで、 サーバーは<strong>通常どおり機能</strong>します。</p><p>その時点で、ライセンス キーを購入するか、<em>ライセンス キーをリクエストすることができます。 \n> トライアル</em>拡張機能。</p>",
"Trial Expired": "トライアル期間が終了しました",
"Your Trial has expired": "トライアル版の有効期限が切れました",
"<p>To continue using Unraid OS you may purchase a license key. Alternately, you may request a Trial extension.</p>": "<p>Unraid OS を引き続き使用するには、ライセンス キーを購入することができます。\nあるいは、トライアルの延長をリクエストすることもできます。</p>",
"<p>You have used all your Trial extensions. To continue using Unraid OS you may purchase a license key.</p>": "<p>トライアル拡張機能をすべて使用しました。 \nUnraid OS を引き続き使用するには、ライセンス キーを購入することができます。</p>",
"Basic": "基本",
"<p>Register for Connect by signing in to your Unraid.net account</p>": "<p>Unraid.net アカウントにサインインして Connect に登録します</p>",
"<p>To support more storage devices as your server grows, click Upgrade Key.</p>": "<p>サーバーの成長に合わせてより多くのストレージ デバイスをサポートするには、[アップグレード キー] をクリックします。</p>",
"Plus": "プラス",
"Pro": "プロ",
"Flash GUID Error": "フラッシュGUIDエラー",
"Registration key / USB Flash GUID mismatch": "登録キー / USB フラッシュ GUID の不一致",
"<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>": "<p>Unraid 登録キーは、過去 12 か月以内に交換されているため、交換の対象外です。</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>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーするか、[キーの購入] を選択してください。</p><p>Unraid 登録キーはブラックリストに登録されているため、交換の対象外です。</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>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーするか、[キーの購入] を選択してください。</p><p>Unraid 登録キーは、過去 12 か月以内に交換されているため、交換の対象外です。</p><p> \np>",
"<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>ライセンス キー ファイルが USB フラッシュ ブート デバイスに対応していません。\n正しいキー ファイルを USB フラッシュ ブート デバイスの /config ディレクトリにコピーしてください。</p><p>キーを購入または交換してみることもできます。</p>",
"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>USB フラッシュ デバイス上に複数のライセンス キー ファイルが存在しますが、どれも USB フラッシュ ブート デバイスに対応していません。\n置き換えるファイルを除くすべてのキー ファイルを、USB フラッシュ ブート デバイスの /config ディレクトリから削除してください。</p><p>あるいは、この USB フラッシュ デバイスのライセンス キーを購入することもできます。</p> \n<p>ライセンス キーの 1 つを、この USB フラッシュ デバイスにバインドされた新しいキーに置き換える場合は、まず他のすべてのキー ファイルを削除してください。</p>",
"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>ライセンス キー ファイルが破損しているか、見つからないようです。\nキー ファイルは、USB フラッシュ ブート デバイスの /config ディレクトリにある必要があります。</p><p>Unraid Connect (ベータ版) がインストールされている場合は、Unraid.net アカウントを使用してキーの回復を試みることができます。</p> \n<p>期限切れの試用版インストールの場合は、ライセンス キーを購入できます。</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>ライセンス キー ファイルが破損しているか、見つからないようです。\nキー ファイルは、USB フラッシュ ブート デバイスの /config ディレクトリにある必要があります。</p><p>ライセンス キー ファイルのバックアップ コピーがない場合は、Unraid Connect (ベータ) プラグインをインストールして試すことができます。 \n</p><p>期限切れの試用版インストールの場合は、ライセンス キーを購入できます。</p>",
"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>既存の Unraid OS インストールでトライアル キーを使用することはできません。</p><p>このインストールの使用を続けるには、この USB フラッシュ デバイスに対応するライセンス キーを購入できます。</p>",
"No USB flash configuration data": "USB フラッシュ構成データがありません",
"<p>There is a problem with your USB Flash device</p>": "<p>USB フラッシュ デバイスに問題があります</p>",
"No Flash": "フラッシュ禁止",
"Cannot access your USB Flash boot device": "USB フラッシュ ブート デバイスにアクセスできません",
"<p>There is a physical problem accessing your USB Flash boot device</p>": "<p>USB フラッシュ ブート デバイスへのアクセスに物理的な問題があります</p>",
"BLACKLISTED": "ブラックリストに登録されました",
"Blacklisted USB Flash GUID": "ブラックリストに登録された USB フラッシュ 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>この USB フラッシュ ブート デバイスはブラックリストに登録されています。\nこの問題は、ライセンス キーを交換用の USB フラッシュ デバイスに転送し、現在古い USB フラッシュ デバイスから起動している場合に発生する可能性があります。</p><p>シリアル番号が検出された場合、USB フラッシュ デバイスもブラックリストに登録される可能性があります。\n番号は一意ではありません。これは USB カード リーダーでは一般的です。</p>",
"USB Flash device error": "USBフラッシュデバイスエラー",
"<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>": "<p>この USB フラッシュ デバイスには無効な GUID があります。\n別の USB フラッシュ デバイスを試してください</p>",
"USB Flash has no serial number": "USB フラッシュにはシリアル番号がありません",
"Trial Requires Internet Connection": "トライアルにはインターネット接続が必要です",
"Cannot validate Unraid Trial key": "Unraid Trial キーを検証できません",
"<p>Your Trial key requires an internet connection.</p><p><a href='/Settings/NetworkSettings class='underline'>Please check Settings > Network</a></p>": "<p>トライアル キーにはインターネット接続が必要です。</p><p><a href='/Settings/NetworkSettings class='underline'>[設定] > [ネットワーク] を確認してください</a></p>",
"Stale": "古い",
"Stale Server": "古いサーバー",
"<p>Please refresh the page to ensure you load your latest configuration</p>": "<p>ページを更新して最新の構成をロードしてください。</p>",
"Invalid API Key": "無効な API キー",
"Please sign out then sign back in to refresh your API key.": "サインアウトしてから再度サインインして、API キーを更新してください。",
"Invalid API Key Format": "無効な API キー形式",
"Too Many Devices": "デバイスが多すぎます",
"You have exceeded the number of devices allowed for your license. Please remove a device before adding another.": "ライセンスで許可されているデバイスの数を超えました。 別のデバイスを追加する前に、デバイスを削除してください。",
"Unraid Connect Install Failed": "Unraid Connect のインストールに失敗しました",
"Rebooting will likely solve this.": "再起動すると解決する可能性があります。",
"SSL certificates for unraid.net deprecated": "unraid.net の SSL 証明書が非推奨になりました",
"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.": "2023 年 1 月 1 日、unraid.net の SSL 証明書は廃止されました。 新しい myunraid.net ドメインを使用するには、新しい SSL 証明書をプロビジョニングする必要があります。 これは、「設定 > 管理アクセス」ページで行うことができます。",
"Unraid Connect Error": "非RAID接続エラー",
"Trial Key Creation Failed": "トライアルキーの作成に失敗しました",
"Error creatiing a trial key. Please try again later.": "キー サーバーが試用版キーを返しませんでした。 後でもう一度試してください。",
"Extending your free trial by 15 days": "無料トライアルを 15 日間延長します",
"Please keep this window open": "この窓は開いたままにしておいてください",
"Starting your free 30 day trial": "30 日間の無料トライアルを開始する",
"Trial Key Created": "トライアルキーが作成されました",
"Please wait while the page reloads to install your trial key": "試用版キーをインストールするには、ページがリロードされるまでお待ちください。",
"A Trial key provides all the functionality of a Pro Registration key": "トライアル キーは、Pro 登録キーのすべての機能を提供します。",
"Extension Installed": "拡張機能がインストールされました",
"Recovered": "回復しました",
"Replaced": "交換されました",
"Installing": "インストール中",
"Installed": "インストール済み",
"Install": "インストール",
"Install Extended": "拡張インストール",
"Install Recovered": "インストールが回復しました",
"Install Replaced": "インストールと置き換え",
"Your free Trial key provides all the functionality of a Pro Registration key": "無料のトライアル キーは、プロ登録キーのすべての機能を提供します"
}

70
web/nuxt.config.ts Normal file
View File

@@ -0,0 +1,70 @@
import { readFileSync } from 'fs';
import { parse } from 'dotenv';
const envConfig = parse(readFileSync('.env'));
for (const k in envConfig) {
process.env[k] = envConfig[k];
}
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: false,
devServer: {
port: 4321,
},
devtools: {
enabled: true,
},
modules: [
'@vueuse/nuxt',
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
'nuxt-custom-elements',
],
components: [
{ path: '~/components/Brand', prefix: 'Brand' },
{ path: '~/components/UserProfile', prefix: 'Upc' },
'~/components',
],
runtimeConfig: {
public: { // will be exposed to the client-side
callbackKey: 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB', // set in .env https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables
}
},
customElements: {
entries: [
{
name: 'UnraidComponents',
tags: [
{
name: 'UnraidI18nHost',
path: '@/components/I18nHost.ce',
},
{
name: 'UnraidAuth',
path: '@/components/Auth.ce',
},
{
name: 'UnraidDownloadApiLogs',
path: '@/components/DownloadApiLogs.ce',
},
{
name: 'UnraidKeyActions',
path: '@/components/KeyActions.ce',
},
{
name: 'UnraidModals',
path: '@/components/Modals.ce',
},
{
name: 'UnraidUserProfile',
path: '@/components/UserProfile.ce',
},
{
name: 'UnraidWanIpCheck',
path: '@/components/WanIpCheck.ce',
},
],
},
],
},
});

21493
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

56
web/package.json Normal file
View File

@@ -0,0 +1,56 @@
{
"name": "connect-components",
"private": true,
"scripts": {
"build": "nuxt build",
"build:dev": "nuxt build && npm run deploy:dev",
"deploy:dev": "./scripts/deploy-dev.sh",
"dev": "nuxt dev",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"serve": "serve dist/nuxt-custom-elements/unraid-components",
"codegen": "graphql-codegen --config codegen.ts -r dotenv/config",
"codegen:watch": "graphql-codegen --config codegen.ts --watch -r dotenv/config"
},
"devDependencies": {
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/client-preset": "^4.0.1",
"@graphql-codegen/introspection": "^4.0.0",
"@nuxt/devtools": "^0.6.1",
"@nuxt/eslint-config": "^0.1.1",
"@nuxtjs/eslint-config-typescript": "^12.0.0",
"@nuxtjs/tailwindcss": "^6.7.0",
"@types/crypto-js": "^4.1.1",
"@types/node": "^18",
"@vue/apollo-util": "^4.0.0-beta.6",
"@vueuse/core": "^10.1.2",
"@vueuse/nuxt": "^10.1.2",
"amazon-cognito-identity-js": "^6.2.0",
"eslint": "^8.45.0",
"lodash-es": "^4.17.21",
"nuxt": "^3.5.1",
"nuxt-custom-elements": "^2.0.0-beta.12"
},
"dependencies": {
"@apollo/client": "^3.7.17",
"@headlessui/vue": "^1.7.14",
"@heroicons/vue": "^2.0.18",
"@pinia/nuxt": "^0.4.11",
"@vue/apollo-composable": "^4.0.0-beta.8",
"@vueuse/components": "^10.1.2",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"focus-trap": "^7.4.3",
"graphql": "^16.7.1",
"graphql-tag": "^2.12.6",
"graphql-ws": "^5.14.0",
"hex-to-rgba": "^2.0.1",
"vue-i18n": "^9.2.2",
"wretch": "^2.6.0"
},
"overrides": {
"vue": "latest"
}
}

51
web/pages/index.vue Normal file
View File

@@ -0,0 +1,51 @@
<script lang="ts" setup>
import { serverState } from '~/_data/serverState';
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('UnraidComponents');
});
</script>
<template>
<div class="text-black bg-white dark:text-white dark:bg-black">
<div class="max-w-5xl mx-auto">
<client-only>
<div class="flex flex-col gap-6 p-6">
<h2 class="text-xl font-semibold font-mono">
Vue Components
</h2>
<h3 class="text-lg font-semibold font-mono">
UserProfileCe
</h3>
<UserProfileCe :server="serverState" />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
DownloadApiLogsCe
</h3>
<DownloadApiLogsCe />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
AuthCe
</h3>
<AuthCe />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
KeyActionsCe
</h3>
<KeyActionsCe />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
WanIpCheckCe
</h3>
<WanIpCheckCe php-wan-ip="47.184.85.45" />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
ModalsCe
</h3>
<ModalsCe />
</div>
</client-only>
</div>
</div>
</template>

View File

@@ -0,0 +1,47 @@
<script lang="ts" setup>
import { serverState } from '~/_data/serverState';
const nuxtApp = useNuxtApp();
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('UnraidComponents');
});
</script>
<template>
<client-only>
<unraid-i18n-host class="flex flex-col gap-6 p-6 max-w-5xl mx-auto text-black bg-white dark:text-white dark:bg-black">
<h2 class="text-xl font-semibold font-mono">
Web Components
</h2>
<h3 class="text-lg font-semibold font-mono">
UserProfileCe
</h3>
<unraid-user-profile :server="JSON.stringify(serverState)" />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
DownloadApiLogsCe
</h3>
<unraid-download-api-logs />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
AuthCe
</h3>
<unraid-auth />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
KeyActionsCe
</h3>
<unraid-key-actions />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
WanIpCheckCe
</h3>
<unraid-wan-ip-check php-wan-ip="47.184.85.45" />
<hr class="border-black">
<h3 class="text-lg font-semibold font-mono">
ModalsCe
</h3>
<unraid-modals />
</unraid-i18n-host>
</client-only>
</template>

19
web/plugins/i18n.ts Normal file
View File

@@ -0,0 +1,19 @@
import { createI18n } from 'vue-i18n';
export default defineNuxtPlugin(({ vueApp }) => {
const i18n = createI18n({
legacy: false,
globalInjection: true,
locale: 'ja',
messages: {
en: {
hello: 'Hello!'
},
ja: {
hello: 'こんにちは!'
}
}
});
vueApp.use(i18n);
});

BIN
web/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

48
web/scripts/deploy-dev.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# Path to store the last used server name
state_file="$HOME/.deploy_state"
# Read the last used server name from the state file
if [[ -f "$state_file" ]]; then
last_server_name=$(cat "$state_file")
else
last_server_name=""
fi
# Read the server name from the command-line argument or use the last used server name as the default
server_name="${1:-$last_server_name}"
# Check if the server name is provided
if [[ -z "$server_name" ]]; then
echo "Please provide the SSH server name."
exit 1
fi
# Save the current server name to the state file
echo "$server_name" > "$state_file"
# Replace the value inside the rsync command with the user's input
rsync_command="rsync -avz -e ssh .nuxt/nuxt-custom-elements/dist/unraid-components root@${server_name}.local:/usr/local/emhttp/plugins/dynamix.my.servers"
echo "Executing the following command:"
echo "$rsync_command"
# Execute the rsync command and capture the exit code
eval "$rsync_command"
exit_code=$?
# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
afplay /System/Library/Sounds/Glass.aiff
elif [[ "$OSTYPE" == "linux-gnu" ]]; then
# Linux
paplay /usr/share/sounds/freedesktop/stereo/complete.oga
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
# Windows
powershell.exe -c "(New-Object Media.SoundPlayer 'C:\Windows\Media\Windows Default.wav').PlaySync()"
fi
# Exit with the rsync command's exit code
exit $exit_code

3
web/server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

177
web/store/account.ts Normal file
View File

@@ -0,0 +1,177 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { useCallbackStore } from '~/store/callbackActions';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
import { WebguiUpdate } from '~/composables/services/webgui';
import { ACCOUNT_CALLBACK } from '~/helpers/urls';
import type { ExternalSignIn, ExternalSignOut } from '~/store/callback';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useAccountStore = defineStore('account', () => {
const callbackStore = useCallbackStore();
const errorsStore = useErrorsStore();
const serverStore = useServerStore();
// State
const accountAction = ref<ExternalSignIn|ExternalSignOut>();
const accountActionHide = ref<boolean>(false);
const accountActionStatus = ref<'failed' | 'ready' | 'success' | 'updating'>('ready');
const username = ref<string>('');
// Getters
const accountActionType = computed(() => accountAction.value?.type);
// Actions
const recover = () => {
console.debug('[accountStore.recover]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'recover',
}]);
};
const replace = () => {
console.debug('[accountStore.replace]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'replace',
}]);
};
const signIn = () => {
console.debug('[accountStore.signIn]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'signIn',
}]);
};
const signOut = () => {
console.debug('[accountStore.accountStore.signOut]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'signOut',
}]);
};
const trialExtend = () => {
console.debug('[accountStore.accountStore.trialExtend]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'trialExtend',
}]);
};
const trialStart = () => {
console.debug('[accountStore.accountStore.trialStart]');
callbackStore.send(ACCOUNT_CALLBACK.toString(), [{
server: {
...serverStore.serverAccountPayload,
},
type: 'trialStart',
}]);
};
/**
* @description Update myservers.cfg for both Sign In & Sign Out
* @note unraid-api requires apikey & token realted keys to be lowercase
*/
const updatePluginConfig = async (action: ExternalSignIn | ExternalSignOut) => {
console.debug('[accountStore.updatePluginConfig]', action);
// save any existing username before updating
if (serverStore.username) { username.value = serverStore.username; }
accountAction.value = action;
accountActionStatus.value = 'updating';
if (!serverStore.registered && !accountAction.value.user) {
console.debug('[accountStore.updatePluginConfig] Not registered skipping sign out');
accountActionHide.value = true;
accountActionStatus.value = 'success';
return;
}
try {
const userPayload = {
...(accountAction.value.user
? {
apikey: accountAction.value.apiKey,
// avatar: '',
email: accountAction.value.user?.email,
regWizTime: `${Date.now()}_${serverStore.guid}`, // set when signing in the first time and never unset for the sake of displaying Sign In/Up in the UPC without needing to validate guid every time
username: accountAction.value.user?.preferred_username,
}
: {
accesstoken: '',
apikey: '',
avatar: '',
email: '',
idtoken: '',
refreshtoken: '',
username: '',
}),
};
const response = await WebguiUpdate
.formUrl({
csrf_token: serverStore.csrf,
'#file': 'dynamix.my.servers/myservers.cfg',
'#section': 'remote',
...userPayload,
})
.post()
.res((res) => {
console.debug('[accountStore.updatePluginConfig] WebguiUpdate res', res);
accountActionStatus.value = 'success';
})
.catch((err) => {
console.debug('[accountStore.updatePluginConfig] WebguiUpdate err', err);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'Failed to update Connect account configuration',
message: err.message,
level: 'error',
ref: 'updatePluginConfig',
type: 'account',
});
});
return response;
} catch (err) {
console.debug('[accountStore.updatePluginConfig] WebguiUpdate catch err', err);
accountActionStatus.value = 'failed';
errorsStore.setError({
heading: 'Failed to update Connect account configuration',
message: err.message,
level: 'error',
ref: 'updatePluginConfig',
type: 'account',
});
}
};
return {
// State
accountAction,
accountActionHide,
accountActionStatus,
// Getters
accountActionType,
// Actions
recover,
replace,
signIn,
signOut,
trialExtend,
trialStart,
updatePluginConfig,
};
});

164
web/store/callback.ts Normal file
View File

@@ -0,0 +1,164 @@
/**
* This file is used to handle callbacks from the server.
* It is used in the following apps:
* - auth
* - craft-unraid
* - connect @todo
* - connect-components
*/
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
import { defineStore, createPinia, setActivePinia } from 'pinia';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export type SignIn = 'signIn';
export type SignOut = 'signOut';
export type OemSignOut = 'oemSignOut';
export type Troubleshoot = 'troubleshoot';
export type Recover = 'recover';
export type Replace = 'replace';
export type TrialExtend = 'trialExtend';
export type TrialStart = 'trialStart';
export type Purchase = 'purchase';
export type Redeem = 'redeem';
export type Upgrade = 'upgrade';
export type AccountAction = SignIn | SignOut | OemSignOut | Troubleshoot;
export type AccountKeyAction = Recover | Replace | TrialExtend | TrialStart;
export type PurchaseAction = Purchase | Redeem | Upgrade;
export type ServerAction = AccountAction | AccountKeyAction | PurchaseAction;
export interface UserInfo {
'custom:ips_id'?: string;
email?: string;
email_verifed?: 'true' | 'false';
preferred_username?: string;
sub?: string;
username?: string;
}
export interface ServerData {
description?: string;
deviceCount?: number;
expireTime?: number;
flashProduct?: string;
flashVendor?: string;
guid?: string;
keyfile?: string;
locale?: string;
name?: string;
registered: boolean;
regGen?: number;
regGuid?: string;
state: string;
wanFQDN?: string;
}
export interface ServerPayload {
server: ServerData;
type: ServerAction;
}
export interface ExternalSignIn {
type: SignIn;
apiKey: string;
user: UserInfo;
}
export interface ExternalSignOut {
type: SignOut | OemSignOut;
}
export interface ExternalKeyActions {
type: PurchaseAction | AccountKeyAction;
keyUrl: string;
}
export type ExternalActions =
| ExternalSignIn
| ExternalSignOut
| ExternalKeyActions;
export type UpcActions = ServerPayload;
export interface ExternalPayload {
actions: ExternalActions[];
sender: string;
type: 'forUpc';
}
export interface UpcPayload {
actions: UpcActions[];
sender: string;
type: 'fromUpc';
}
export type SendPayloads = ExternalActions[] | UpcActions[];
export type QueryPayloads = ExternalPayload | UpcPayload;
interface CallbackActionsStore {
redirectToCallbackType: (decryptedData: QueryPayloads) => void;
encryptionKey: string;
sendType: 'fromUpc' | 'forUpc';
}
export const useCallbackStoreGeneric = (
useCallbackActions: () => CallbackActionsStore,
) =>
defineStore('callback', () => {
const callbackActions = useCallbackActions();
const encryptionKey = import.meta.env.VITE_CALLBACK_KEY;
const defaultSendType = 'fromUpc';
const send = (url: string, payload: SendPayloads, sendType?: 'fromUpc' | 'forUpc') => {
console.debug('[callback.send]');
try {
const stringifiedData = JSON.stringify({
actions: [
...payload,
],
sender: window.location.href,
type: sendType ?? defaultSendType,
});
const encryptedMessage = AES.encrypt(stringifiedData, encryptionKey).toString();
// build and go to url
const destinationUrl = new URL(url);
destinationUrl.searchParams.set('data', encodeURI(encryptedMessage));
console.debug('[callback.send]', encryptedMessage, destinationUrl);
window.location.href = destinationUrl.toString();
} catch (error) {
console.error(error);
throw new Error('Unable to create callback event');
}
};
const watcher = () => {
console.debug('[callback.watcher]');
const currentUrl = new URL(window.location.toString());
const callbackValue = decodeURI(currentUrl.searchParams.get('data') ?? '');
console.debug('[callback.watcher]', { callbackValue });
if (!callbackValue) {
return console.debug('[callback.watcher] no callback to handle');
}
try {
const decryptedMessage = AES.decrypt(callbackValue, encryptionKey);
const decryptedData: QueryPayloads = JSON.parse(decryptedMessage.toString(Utf8));
console.debug('[callback.watcher]', decryptedMessage, decryptedData);
// Parse the data and perform actions
callbackActions.redirectToCallbackType(decryptedData);
} catch (error) {
console.error(error);
throw new Error('Couldn\'t decrypt callback data');
}
};
return {
send,
watcher,
};
});

View File

@@ -0,0 +1,81 @@
import { defineStore } from 'pinia';
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
import { useAccountStore } from '~/store/account';
import { useInstallKeyStore } from '~/store/installKey';
import { useServerStore } from '~/store/server';
import { useCallbackStoreGeneric, type ExternalPayload, type ExternalKeyActions, type QueryPayloads } from '~/store/callback';
export const useCallbackActionsStore = defineStore('callbackActions', () => {
const accountStore = useAccountStore();
const installKeyStore = useInstallKeyStore();
const serverStore = useServerStore();
type CallbackStatus = 'closing' | 'error' | 'loading' | 'ready' | 'success';
const callbackStatus = ref<CallbackStatus>('ready');
const callbackData = ref<ExternalPayload>();
const callbackError = ref();
const redirectToCallbackType = (decryptedData: QueryPayloads) => {
console.debug('[redirectToCallbackType]', { decryptedData });
if (!decryptedData.type || decryptedData.type === 'fromUpc' || !decryptedData.actions?.length) {
callbackError.value = 'Callback redirect type not present or incorrect';
callbackStatus.value = 'ready'; // default status
return console.error('[redirectToCallbackType]', callbackError.value);
}
// Display the feedback modal
callbackData.value = decryptedData;
callbackStatus.value = 'loading';
// Parse the data and perform actions
callbackData.value.actions.forEach(async (action, index, array) => {
console.debug('[action]', action);
if (action?.keyUrl) {
await installKeyStore.install(action as ExternalKeyActions);
}
if (action?.user || action.type === 'signOut' || action.type === 'oemSignOut') {
await accountStore.updatePluginConfig(action);
}
// all actions have run
if (array.length === (index + 1)) {
await serverStore.refreshServerState();
// callbackStatus.value = 'done';
if (array.length > 1) {
// if we have more than 1 action it means there was a key install and an account action so both need to be successful
const allSuccess = accountStore.accountActionStatus === 'success' && installKeyStore.keyInstallStatus === 'success';
callbackStatus.value = allSuccess ? 'success' : 'error';
} else {
// only 1 action needs to be successful
const oneSuccess = accountStore.accountActionStatus === 'success' || installKeyStore.keyInstallStatus === 'success';
callbackStatus.value = oneSuccess ? 'success' : 'error';
}
}
});
};
const setCallbackStatus = (status: CallbackStatus) => { callbackStatus.value = status; };
watch(callbackStatus, (newVal, oldVal) => {
console.debug('[callbackStatus]', newVal);
if (newVal === 'loading') {
addPreventClose();
}
if (oldVal === 'loading') {
removePreventClose();
// removing query string once actions are done so users can't refresh the page and go through the same actions
window.history.replaceState(null, '', window.location.pathname);
}
});
return {
redirectToCallbackType,
callbackData,
callbackStatus,
setCallbackStatus,
};
});
export const useCallbackStore = useCallbackStoreGeneric(useCallbackActionsStore);

39
web/store/dropdown.ts Normal file
View File

@@ -0,0 +1,39 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { useServerStore } from './server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useDropdownStore = defineStore('dropdown', () => {
const serverStore = useServerStore();
const dropdownVisible = ref<boolean>(false);
const dropdownHide = () => { dropdownVisible.value = false; };
const dropdownShow = () => { dropdownVisible.value = true; };
const dropdownToggle = useToggle(dropdownVisible);
onMounted(() => {
// automatically open the launchpad dropdown on first page load when ENOKEYFILE aka a new server
const baseStorageName = `unraidConnect_${serverStore.guid}_`;
if (serverStore.state === 'ENOKEYFILE' && !sessionStorage.getItem(`${baseStorageName}ENOKEYFILE`)) {
sessionStorage.setItem(`${baseStorageName}ENOKEYFILE`, 'true');
dropdownShow();
}
// automatically open the launchpad dropdown after plugin install on first page load
if (serverStore.connectPluginInstalled && !serverStore.registered && sessionStorage.getItem(`${baseStorageName}clickedInstallPlugin`)) {
sessionStorage.removeItem(`${baseStorageName}clickedInstallPlugin`);
dropdownShow();
}
});
return {
dropdownVisible,
dropdownHide,
dropdownShow,
dropdownToggle,
};
});

133
web/store/errors.ts Normal file
View File

@@ -0,0 +1,133 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import type { ButtonProps } from '~/components/Brand/Button.vue';
import { OBJ_TO_STR } from '~/helpers/functions';
import type { Server } from '~/types/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export type ErrorType = 'account' | 'callback' | 'installKey' | 'server' | 'serverState' | 'unraidApiState';
export interface Error {
actions?: ButtonProps[];
debugServer?: Server;
forumLink?: boolean;
heading: string; // if adding new errors be sure to add translations key value pairs
level: 'error' | 'info' | 'warning';
message: string;
ref?: string;
supportLink?: boolean;
type: ErrorType;
}
export const useErrorsStore = defineStore('errors', () => {
const errors = ref<Error[]>([]);
// const displayedErrors = computed(() => errors.value.filter(error => error.type === 'server' || error.type === 'serverState'));
const removeErrorByIndex = (index: number) => {
errors.value = errors.value.filter((_error, i) => i !== index);
};
const removeErrorByRef = (ref: string) => {
errors.value = errors.value.filter(error => error?.ref !== ref);
};
const resetErrors = () => {
errors.value = [];
};
const setError = (error: Error) => {
console.debug('[setError]', error);
errors.value.push(error);
};
interface TroubleshootPayload {
email: string;
includeUnraidApiLogs: boolean;
}
const openTroubleshoot = async (payload: TroubleshootPayload) => {
console.debug('[openTroubleshoot]', payload);
try {
// @ts-ignore `FeedbackButton` will be included in 6.10.4+ DefaultPageLayout
await FeedbackButton();
// once the modal is visible we need to select the radio to correctly show the bug report form
let $modal = document.querySelector('.sweet-alert.visible');
while (!$modal) {
console.debug('[openTroubleshoot] getting $modal…');
await new Promise(resolve => setTimeout(resolve, 100));
$modal = document.querySelector('.sweet-alert.visible');
}
console.debug('[openTroubleshoot] $modal', $modal);
// autofill errors into the bug report form
if (errors.value.length) {
let $textarea: HTMLInputElement | null = $modal.querySelector('#troubleshootDetails');
while (!$textarea) {
console.debug('[openTroubleshoot] getting $textarea…');
await new Promise(resolve => setTimeout(resolve, 100));
$textarea = $modal.querySelector('#troubleshootDetails');
}
console.debug('[openTroubleshoot] $textarea', $textarea);
const errorMessages = errors.value.map((error, index) => {
const index1 = index + 1;
let message = `• Error ${index1}: ${error.heading}\n`;
message += `• Error ${index1} Message: ${error.message}\n`;
message += `• Error ${index1} Level: ${error.level}\n`;
message += `• Error ${index1} Type: ${error.type}\n`;
if (error.ref) { message += `• Error ${index1} Ref: ${error.ref}\n`; }
if (error.debugServer) { message += `• Error ${index1} Debug Server:\n${OBJ_TO_STR(error.debugServer)}\n`; }
return message;
}).join('\n***************\n');
$textarea.value += '\n##########################\n';
$textarea.value += `# Debug Details Component Errors ${errors.value.length} #\n`;
$textarea.value += '##########################\n';
$textarea.value += errorMessages;
}
// autofill emails
let $emailInput: HTMLInputElement | null = $modal.querySelector('#troubleshootEmail');
while (!$emailInput) {
console.debug('[openTroubleshoot] getting $emailInput…');
await new Promise(resolve => setTimeout(resolve, 100));
$emailInput = $modal.querySelector('#troubleshootEmail');
}
console.debug('[openTroubleshoot] $emailInput', $emailInput);
if (payload.email) {
$emailInput.value = payload.email;
} else {
$emailInput.focus();
}
// select the radio to correctly show the bug report form
let $myRadio: HTMLInputElement | null = $modal.querySelector('#optTroubleshoot');
while (!$myRadio) {
await new Promise(resolve => setTimeout(resolve, 100));
$myRadio = $modal.querySelector('#optTroubleshoot');
}
$myRadio.checked = true;
// show the correct form in the modal
let $panels = $modal.querySelectorAll('.allpanels');
while (!$panels) {
await new Promise(resolve => setTimeout(resolve, 100));
$panels = $modal.querySelectorAll('.allpanels');
}
$panels.forEach(($panel) => {
if ($panel.id === 'troubleshoot_panel') { $panel.style.display = 'block'; } else { $panel.style.display = 'none'; }
});
} catch (error) {
console.error('[openTroubleshoot]', error);
}
};
return {
errors,
removeErrorByIndex,
removeErrorByRef,
resetErrors,
setError,
openTroubleshoot,
};
});

85
web/store/installKey.ts Normal file
View File

@@ -0,0 +1,85 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { delay } from 'wretch/middlewares';
import { WebguiInstallKey, WebguiUpdateDns } from '~/composables/services/webgui';
import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
import type { ExternalKeyActions } from '~/store/callback';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useInstallKeyStore = defineStore('installKey', () => {
const errorsStore = useErrorsStore();
const serverStore = useServerStore();
const keyInstallStatus = ref<'failed' | 'installing' | 'ready' | 'success'>('ready');
const keyAction = ref<ExternalKeyActions>();
const keyActionType = computed(() => keyAction.value?.type);
const keyUrl = computed(() => keyAction.value?.keyUrl);
/**
* Extracts key type from key url. Works for both .key and .unkey.
*/
const keyType = computed((): string | undefined => {
if (!keyUrl.value) { return undefined; }
const parts = keyUrl.value.split('/');
return parts[parts.length - 1].replace(/\.key|\.unkey/g, '');
});
const install = async (action: ExternalKeyActions) => {
console.debug('[install]');
keyInstallStatus.value = 'installing';
keyAction.value = action;
if (!keyUrl.value) { return console.error('[install] no key to install'); }
try {
const installResponse = await WebguiInstallKey
.query({ url: keyUrl.value })
.get();
console.log('[install] WebguiInstallKey installResponse', installResponse);
keyInstallStatus.value = 'success';
try {
const updateDnsResponse = await WebguiUpdateDns
.middlewares([
delay(1500)
])
.formUrl({ csrf_token: serverStore.csrf })
.post();
console.log('[install] WebguiUpdateDns updateDnsResponse', updateDnsResponse);
} catch (error) {
console.error('[install] WebguiUpdateDns error', error);
}
} catch (error) {
console.error('[install] WebguiInstallKey error', error);
keyInstallStatus.value = 'failed';
errorsStore.setError({
heading: 'Failed to install key',
message: error.message,
level: 'error',
ref: 'installKey',
type: 'installKey',
});
}
};
watch(keyInstallStatus, (newV, oldV) => {
console.debug('[keyInstallStatus]', newV, oldV);
});
return {
// State
keyInstallStatus,
// getters
keyActionType,
keyType,
keyUrl,
// Actions
install,
};
});

23
web/store/modal.ts Normal file
View File

@@ -0,0 +1,23 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from 'pinia';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useModalStore = defineStore('modal', () => {
const modalVisible = ref<boolean>(true);
const modalHide = () => { modalVisible.value = false; };
const modalShow = () => { modalVisible.value = true; };
const modalToggle = useToggle(modalVisible);
return {
modalVisible,
modalHide,
modalShow,
modalToggle,
};
});

43
web/store/promo.ts Normal file
View File

@@ -0,0 +1,43 @@
import { useToggle } from '@vueuse/core';
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { useDropdownStore } from '~/store/dropdown';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const usePromoStore = defineStore('promo', () => {
const dropdownStore = useDropdownStore();
const promoVisible = ref<boolean>(false);
const openOnNextLoad = () => sessionStorage.setItem('unraidConnectPromo', 'show');
const promoHide = () => { promoVisible.value = false; };
const promoShow = () => { promoVisible.value = true; };
const promoToggle = useToggle(promoVisible);
watch(promoVisible, (newVal, _oldVal) => {
if (newVal) { // close the dropdown when the promo is opened
dropdownStore.dropdownHide();
}
});
onBeforeMount(() => {
if (sessionStorage.getItem('unraidConnectPromo') === 'show') {
sessionStorage.removeItem('unraidConnectPromo');
promoShow();
}
});
return {
promoVisible,
openOnNextLoad,
promoHide,
promoShow,
promoToggle,
};
});

50
web/store/purchase.ts Normal file
View File

@@ -0,0 +1,50 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { PURCHASE_CALLBACK } from '~/helpers/urls';
import { useCallbackStore } from '~/store/callbackActions';
import { useServerStore } from '~/store/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const usePurchaseStore = defineStore('purchase', () => {
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
const redeem = () => {
console.debug('[redeem]');
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
server: {
...serverStore.serverPurchasePayload,
},
type: 'redeem',
}]);
};
const purchase = () => {
console.debug('[purchase]');
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
server: {
...serverStore.serverPurchasePayload,
},
type: 'purchase',
}]);
};
const upgrade = () => {
console.debug('[upgrade]');
callbackStore.send(PURCHASE_CALLBACK.toString(), [{
server: {
...serverStore.serverPurchasePayload,
},
type: 'upgrade',
}]);
};
return {
redeem,
purchase,
upgrade,
};
});

View File

@@ -0,0 +1,106 @@
import { graphql } from '~/composables/gql/gql';
// export const SERVER_CLOUD_FRAGMENT = graphql(/* GraphQL */`
// fragment FragmentCloud on Cloud {
// error
// apiKey {
// valid
// error
// }
// cloud {
// status
// error
// }
// minigraphql {
// status
// error
// }
// relay {
// status
// error
// }
// }
// `);
// export const SERVER_CONFIG_FRAGMENT = graphql(/* GraphQL */`
// fragment FragmentConfig on Config {
// error
// valid
// }
// `);
// export const SERVER_OWNER_FRAGMENT = graphql(/* GraphQL */`
// fragment FragmentOwner on Owner {
// avatar
// username
// }
// `);
// export const SERVER_REGISTRATION_FRAGMENT = graphql(/* GraphQL */`
// fragment FragmentRegistration on Registration {
// state
// expiration
// keyFile {
// contents
// }
// }
// `);
// export const SERVER_VARS_FRAGMENT = graphql(/* GraphQL */`
// fragment FragmentVars on Vars {
// regGen
// regState
// configError
// configValid
// }
// `);
export const SERVER_STATE_QUERY = graphql(/* GraphQL */`
query serverState {
cloud {
error
apiKey {
valid
error
}
cloud {
status
error
}
minigraphql {
status
error
}
relay {
status
error
}
}
config {
error
valid
}
info {
os {
hostname
}
}
owner {
avatar
username
}
registration {
state
expiration
keyFile {
contents
}
}
vars {
regGen
regState
configError
configValid
}
}
`);

912
web/store/server.ts Normal file
View File

@@ -0,0 +1,912 @@
/**
* @todo Check OS and Connect Plugin versions against latest via API every session
*/
import { defineStore, createPinia, setActivePinia } from 'pinia';
import {
ArrowRightOnRectangleIcon,
CogIcon,
GlobeAltIcon,
InformationCircleIcon,
KeyIcon,
QuestionMarkCircleIcon
} from '@heroicons/vue/24/solid';
import { useQuery } from '@vue/apollo-composable';
import { SERVER_STATE_QUERY } from './server.fragment';
import type { serverStateQuery } from '~/composables/gql/graphql';
import { WebguiState } from '~/composables/services/webgui';
import { SETTINGS_MANAGMENT_ACCESS } from '~/helpers/urls';
import { useAccountStore } from '~/store/account';
import { useErrorsStore, type Error } from '~/store/errors';
import { usePurchaseStore } from '~/store/purchase';
import { useThemeStore, type Theme } from '~/store/theme';
import { useUnraidApiStore } from '~/store/unraidApi';
import type {
Server,
ServerAccountCallbackSendPayload,
ServerKeyTypeForPurchase,
ServerPurchaseCallbackSendPayload,
ServerState,
ServerStateCloudStatus,
ServerStateConfigStatus,
ServerStateData,
ServerStateDataAction,
ServerconnectPluginInstalled,
} from '~/types/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useServerStore = defineStore('server', () => {
const accountStore = useAccountStore();
const errorsStore = useErrorsStore();
const purchaseStore = usePurchaseStore();
const themeStore = useThemeStore();
const unraidApiStore = useUnraidApiStore();
/**
* State
*/
const apiKey = ref<string>(''); // @todo potentially move to a user store
const apiVersion = ref<string>('');
const avatar = ref<string>(''); // @todo potentially move to a user store
const cloud = ref<ServerStateCloudStatus>();
const config = ref<ServerStateConfigStatus>();
const connectPluginInstalled = ref<ServerconnectPluginInstalled>('');
const connectPluginVersion = ref<string>('');
const csrf = ref<string>(''); // required to make requests to Unraid webgui
const description = ref<string>('');
const deviceCount = ref<number>(0);
const email = ref<string>('');
const expireTime = ref<number>(0);
const flashProduct = ref<string>('');
const flashVendor = ref<string>('');
const guid = ref<string>('');
const guidBlacklisted = ref<boolean>();
const guidRegistered = ref<boolean>();
const guidReplaceable = ref<boolean | undefined>();
const inIframe = ref<boolean>(window.self !== window.top);
const keyfile = ref<string>('');
const lanIp = ref<string>('');
const license = ref<string>('');
const locale = ref<string>('');
const name = ref<string>('');
const osVersion = ref<string>('');
const registered = ref<boolean>();
const regGen = ref<number>(0);
const regGuid = ref<string>('');
const site = ref<string>('');
const state = ref<ServerState>();
const theme = ref<Theme>();
watch(theme, (newVal) => {
if (newVal) { themeStore.setTheme(newVal); }
});
const uptime = ref<number>(0);
const username = ref<string>(''); // @todo potentially move to a user store
const wanFQDN = ref<string>('');
const apiServerStateRefresh = ref<any>(null);
/**
* Getters
*/
const isRemoteAccess = computed(() => wanFQDN.value || (site.value && site.value.includes('www.') && site.value.includes('unraid.net')));
/**
* @todo configure
*/
const pluginOutdated = computed(():boolean => {
return false;
});
const server = computed(():Server => {
return {
apiKey: apiKey.value,
apiVersion: apiVersion.value,
avatar: avatar.value,
connectPluginVersion: connectPluginVersion.value,
connectPluginInstalled: connectPluginInstalled.value,
description: description.value,
deviceCount: deviceCount.value,
email: email.value,
expireTime: expireTime.value,
flashProduct: flashProduct.value,
flashVendor: flashVendor.value,
guid: guid.value,
inIframe: inIframe.value,
keyfile: keyfile.value,
lanIp: lanIp.value,
license: license.value,
locale: locale.value,
name: name.value,
osVersion: osVersion.value,
registered: registered.value,
regGen: regGen.value,
regGuid: regGuid.value,
site: site.value,
state: state.value,
theme: theme.value,
uptime: uptime.value,
username: username.value,
wanFQDN: wanFQDN.value,
};
});
const serverPurchasePayload = computed((): ServerPurchaseCallbackSendPayload => {
/** @todo refactor out. Just parse state on craft site to determine */
let keyTypeForPurchase: ServerKeyTypeForPurchase = 'Trial';
switch (state.value) {
case 'BASIC':
keyTypeForPurchase = 'Basic';
break;
case 'PLUS':
keyTypeForPurchase = 'Plus';
break;
case 'PRO':
keyTypeForPurchase = 'Pro';
break;
}
const server = {
apiVersion: apiVersion.value,
connectPluginVersion: connectPluginVersion.value,
deviceCount: deviceCount.value,
email: email.value,
guid: guid.value,
inIframe: inIframe.value,
keyTypeForPurchase,
locale: locale.value,
osVersion: osVersion.value,
registered: registered.value ?? false,
state: state.value,
site: site.value,
};
return server;
});
const serverAccountPayload = computed((): ServerAccountCallbackSendPayload => {
return {
apiVersion: apiVersion.value,
connectPluginVersion: connectPluginVersion.value,
description: description.value,
expireTime: expireTime.value,
flashProduct: flashProduct.value,
flashVendor: flashVendor.value,
guid: guid.value,
inIframe: inIframe.value,
keyfile: keyfile.value,
lanIp: lanIp.value,
name: name.value,
osVersion: osVersion.value,
registered: registered.value ?? false,
regGuid: regGuid.value,
site: site.value,
state: state.value,
wanFQDN: wanFQDN.value,
};
});
const serverDebugPayload = computed((): Server => {
const payload = {
apiKey: apiKey.value ? `${apiKey.value.substring(0, 6)}__[REDACTED]` : '', // so we don't send full api key in email
apiVersion: apiVersion.value,
avatar: avatar.value,
connectPluginInstalled: connectPluginInstalled.value,
connectPluginVersion: connectPluginVersion.value,
description: description.value,
deviceCount: deviceCount.value,
email: email.value,
expireTime: expireTime.value,
flashProduct: flashProduct.value,
flashVendor: flashVendor.value,
guid: guid.value,
inIframe: inIframe.value,
lanIp: lanIp.value,
locale: locale.value,
name: name.value,
osVersion: osVersion.value,
registered: registered.value,
regGen: regGen.value,
regGuid: regGuid.value,
site: site.value,
state: state.value,
uptime: uptime.value,
username: username.value,
wanFQDN: wanFQDN.value,
};
// remove any empty values from object
return Object.fromEntries(Object.entries(payload).filter(([_, v]) => v !== null && v !== undefined && v !== ''));
});
const purchaseAction: ServerStateDataAction = {
click: () => { purchaseStore.purchase(); },
external: true,
icon: KeyIcon,
name: 'purchase',
text: 'Purchase Key',
};
const upgradeAction: ServerStateDataAction = {
click: () => { purchaseStore.upgrade(); },
external: true,
icon: KeyIcon,
name: 'upgrade',
text: 'Upgrade Key',
};
const recoverAction: ServerStateDataAction = {
click: () => { accountStore.recover(); },
external: true,
icon: KeyIcon,
name: 'recover',
text: 'Recover Key',
};
const redeemAction: ServerStateDataAction = {
click: () => { purchaseStore.redeem(); },
external: true,
icon: KeyIcon,
name: 'redeem',
text: 'Redeem Activation Code',
};
const replaceAction: ServerStateDataAction = {
click: () => { accountStore.replace(); },
external: true,
icon: KeyIcon,
name: 'replace',
text: 'Replace Key',
};
const signInAction: ServerStateDataAction = {
click: () => { accountStore.signIn(); },
external: true,
icon: GlobeAltIcon,
name: 'signIn',
text: 'Sign In with Unraid.net Account',
};
/**
* This action is a computed property because it depends on the state of the keyfile
*/
const signOutAction = computed((): ServerStateDataAction => {
return {
click: () => { accountStore.signOut(); },
disabled: !keyfile.value,
external: true,
icon: ArrowRightOnRectangleIcon,
name: 'signOut',
text: 'Sign Out of Unraid.net',
title: !keyfile.value ? 'Sign Out requires a keyfile' : '',
};
});
const trialExtendAction: ServerStateDataAction = {
click: () => { accountStore.trialExtend(); },
external: true,
icon: KeyIcon,
name: 'trialExtend',
text: 'Extend Trial',
};
const trialStartAction: ServerStateDataAction = {
click: () => { accountStore.trialStart(); },
external: true,
icon: KeyIcon,
name: 'trialStart',
text: 'Start Free 30 Day Trial',
};
let messageEGUID = '';
const stateData = computed(():ServerStateData => {
switch (state.value) {
case 'ENOKEYFILE':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction, trialStartAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'No Keyfile',
heading: 'Let\'s Unleash your Hardware!',
message: '<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>',
};
case 'TRIAL':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Trial',
heading: 'Thank you for choosing Unraid OS!',
message: '<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>',
};
case 'EEXPIRED':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction]),
...(trialExtensionEligible.value ? [trialExtendAction] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'Trial Expired',
heading: 'Your Trial has expired',
message: trialExtensionEligible.value
? '<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>',
};
case 'BASIC':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([upgradeAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Basic',
heading: 'Thank you for choosing Unraid OS!',
message: registered.value
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
: guidRegistered.value
? '<p>To support more storage devices as your server grows, click Upgrade Key.</p>'
: '',
};
case 'PLUS':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([upgradeAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Plus',
heading: 'Thank you for choosing Unraid OS!',
message: registered.value
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
: guidRegistered.value
? '<p>To support more storage devices as your server grows, click Upgrade Key.</p>'
: '',
};
case 'PRO':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
humanReadable: 'Pro',
heading: 'Thank you for choosing Unraid OS!',
message: registered.value
? '<p>Register for Connect by signing in to your Unraid.net account</p>'
: '',
};
case 'EGUID':
if (guidReplaceable.value) {
messageEGUID = '<p>Your Unraid registration key is ineligible for replacement as it has been replaced within the last 12 months.</p>';
} else if (guidReplaceable.value === false && guidBlacklisted.value) {
messageEGUID = '<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>';
} else if (guidReplaceable.value === false && !guidBlacklisted.value) {
messageEGUID = '<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>';
} else { // basically guidReplaceable.value === null
messageEGUID = '<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>';
}
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([replaceAction, purchaseAction, redeemAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'Flash GUID Error',
heading: 'Registration key / USB Flash GUID mismatch',
message: messageEGUID,
};
case 'EGUID1':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'Multiple License Keys Present',
heading: 'Multiple License Keys Present',
message: '<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>',
// signInToFix: true, // @todo is this needed?
};
case 'ENOKEYFILE2':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([recoverAction, purchaseAction, redeemAction]),
...(registered.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'Missing key file',
heading: 'Missing key file',
message: connectPluginInstalled.value
? '<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>You may attempt to recover your key with your Unraid.net account.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>'
: '<p>Your license key file is corrupted or missing. The key file should be located in the /config directory on your USB Flash boot device.</p><p>If you do not have a backup copy of your license key file you may attempt to recover your key.</p><p>If this was an expired Trial installation, you may purchase a license key.</p>',
};
case 'ETRIAL':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'Invalid installation',
heading: 'Invalid installation',
message: '<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>',
};
case 'ENOKEYFILE1':
return {
actions: [
...(!registered.value && connectPluginInstalled.value ? [signInAction] : []),
...([purchaseAction, redeemAction]),
...(registered.value && connectPluginInstalled.value ? [signOutAction.value] : []),
],
error: true,
humanReadable: 'No Keyfile',
heading: 'No USB flash configuration data',
message: '<p>There is a problem with your USB Flash device</p>',
};
case 'ENOFLASH':
case 'ENOFLASH1':
case 'ENOFLASH2':
case 'ENOFLASH3':
case 'ENOFLASH4':
case 'ENOFLASH5':
case 'ENOFLASH6':
case 'ENOFLASH7':
return {
error: true,
humanReadable: 'No Flash',
heading: 'Cannot access your USB Flash boot device',
message: '<p>There is a physical problem accessing your USB Flash boot device</p>',
};
case 'EBLACKLISTED':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'Blacklisted USB Flash GUID',
message: '<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>',
};
case 'EBLACKLISTED1':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'USB Flash device error',
message: '<p>This USB Flash device has an invalid GUID. Please try a different USB Flash device</p>',
};
case 'EBLACKLISTED2':
return {
error: true,
humanReadable: 'BLACKLISTED',
heading: 'USB Flash has no serial number',
message: '<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>',
};
case 'ENOCONN':
return {
error: true,
humanReadable: 'Trial Requires Internet Connection',
heading: 'Cannot validate Unraid Trial key',
message: '<p>Your Trial key requires an internet connection.</p><p><a href="/Settings/NetworkSettings" class="underline">Please check Settings > Network</a></p>',
};
default:
return {
error: true,
humanReadable: 'Stale',
heading: 'Stale Server',
message: '<p>Please refresh the page to ensure you load your latest configuration</p>',
};
}
});
const stateDataError = computed((): Error | undefined => {
if (!stateData.value?.error) { return undefined; }
return {
actions: [
{
click: () => {
errorsStore.openTroubleshoot({
email: email.value,
includeUnraidApiLogs: !!connectPluginInstalled.value,
});
},
icon: QuestionMarkCircleIcon,
text: 'Contact Support',
},
],
debugServer: serverDebugPayload.value,
heading: stateData.value?.heading ?? '',
level: 'error',
message: stateData.value?.message ?? '',
ref: `stateDataError__${state.value}`,
type: 'serverState',
};
});
watch(stateDataError, (newVal, oldVal) => {
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
const authActionsNames = ['signIn', 'signOut'];
// Extract sign in / out from actions so we can display seperately as needed
const authAction = computed((): ServerStateDataAction | undefined => {
if (!stateData.value.actions) { return; }
return stateData.value.actions.find(action => authActionsNames.includes(action.name));
});
// Remove sign in / out from actions so we can display them separately
const keyActions = computed((): ServerStateDataAction[] | undefined => {
if (!stateData.value.actions) { return; }
return stateData.value.actions.filter(action => !authActionsNames.includes(action.name));
});
const trialExtensionEligible = computed(() => !regGen.value || regGen.value < 2);
const invalidApiKey = computed((): Error | undefined => {
// must be registered with plugin installed
if (!connectPluginInstalled.value || !registered.value) {
return undefined;
}
// Keeping separate from validApiKeyLength because we may want to add more checks. Cloud also help with debugging user error submissions.
if (apiKey.value.length !== 64) {
console.debug('[invalidApiKey] invalid length');
return {
heading: 'Invalid API Key',
level: 'error',
message: 'Please sign out then sign back in to refresh your API key.',
ref: 'invalidApiKeyLength',
type: 'server',
};
}
if (!apiKey.value.startsWith('unupc_')) {
console.debug('[invalidApiKey] invalid for upc');
return {
heading: 'Invalid API Key Format',
level: 'error',
message: 'Please sign out then sign back in to refresh your API key.',
ref: 'invalidApiKeyFormat',
type: 'server',
};
}
return undefined;
});
watch(invalidApiKey, (newVal, oldVal) => {
console.debug('[watch:invalidApiKey]', newVal, oldVal);
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
const tooManyDevices = computed((): Error | undefined => {
if (!config.value?.valid && config.value?.error === 'INVALID') {
return {
heading: 'Too Many Devices',
level: 'error',
message: 'You have exceeded the number of devices allowed for your license. Please remove a device before adding another.',
ref: 'tooManyDevices',
type: 'server',
};
}
return undefined;
});
watch(tooManyDevices, (newVal, oldVal) => {
console.debug('[watch:tooManyDevices]', newVal, oldVal);
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
const pluginInstallFailed = computed((): Error | undefined => {
if (connectPluginInstalled.value && connectPluginInstalled.value.includes('_installFailed')) {
return {
actions: [
{
external: true,
href: 'https://forums.unraid.net/topic/112073-my-servers-releases/#comment-1154449',
icon: InformationCircleIcon,
text: 'Learn More',
},
],
heading: 'Unraid Connect Install Failed',
level: 'error',
message: 'Rebooting will likely solve this.',
ref: 'pluginInstallFailed',
type: 'server',
};
}
return undefined;
});
watch(pluginInstallFailed, (newVal, oldVal) => {
console.debug('[watch:pluginInstallFailed]', newVal, oldVal);
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
/**
* Deprecation warning for [hash].unraid.net SSL certs. Deprecation started 2023-01-01
*/
const deprecatedUnraidSSL = ref<Error | undefined>(
(window.location.hostname.includes('localhost')
? {
actions: [
{
href: SETTINGS_MANAGMENT_ACCESS.toString(),
icon: CogIcon,
text: 'Go to Management Access Now',
},
{
external: true,
href: 'https://unraid.net/blog/ssl-certificate-update',
icon: InformationCircleIcon,
text: 'Learn More',
},
],
forumLink: true,
heading: 'SSL certificates for unraid.net deprecated',
level: 'error',
message: '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.',
ref: 'deprecatedUnraidSSL',
type: 'server',
}
: undefined));
watch(deprecatedUnraidSSL, (newVal, oldVal) => {
console.debug('[watch:deprecatedUnraidSSL]', newVal, oldVal);
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
const cloudError = computed((): Error | undefined => {
if (!cloud.value?.error) { return undefined; }
return {
actions: [
{
click: () => {
errorsStore.openTroubleshoot({
email: email.value,
includeUnraidApiLogs: !!connectPluginInstalled.value,
});
},
icon: QuestionMarkCircleIcon,
text: 'Contact Support',
},
],
debugServer: serverDebugPayload.value,
heading: 'Unraid Connect Error',
level: 'error',
message: cloud.value.error,
ref: 'cloudError',
type: 'unraidApiState',
};
});
watch(cloudError, (newVal, oldVal) => {
console.debug('[watch:cloudError]', newVal, oldVal);
if (oldVal && oldVal.ref) { errorsStore.removeErrorByRef(oldVal.ref); }
if (newVal) { errorsStore.setError(newVal); }
});
const serverErrors = computed(() => {
return [
stateDataError.value,
tooManyDevices.value,
pluginInstallFailed.value,
deprecatedUnraidSSL.value,
invalidApiKey.value,
cloudError.value,
].filter(Boolean);
});
/**
* Determines whether or not we start or stop the apollo client for unraid-api
*/
const registeredWithValidApiKey = computed(() => registered.value && !invalidApiKey.value);
watch(registeredWithValidApiKey, (newVal, oldVal) => {
console.debug('[watch:registeredWithValidApiKey]', newVal, oldVal);
if (oldVal) {
console.debug('[watch:registeredWithValidApiKey] no apiKey, stop unraid-api client');
return unraidApiStore.closeUnraidApiClient();
}
if (newVal) {
// if this is just after sign in, let's delay the start by a few seconds to give unraid-api time to update
if (accountStore.accountActionType === 'signIn') {
console.debug('[watch:registeredWithValidApiKey] delay start unraid-api client');
return setTimeout(() => {
unraidApiStore.createApolloClient();
}, 2000);
} else {
console.debug('[watch:registeredWithValidApiKey] new apiKey, start unraid-api client');
return unraidApiStore.createApolloClient();
}
}
});
/**
* Actions
*/
const setServer = (data: Server) => {
console.debug('[setServer] data', data);
if (typeof data?.apiKey !== 'undefined') { apiKey.value = data.apiKey; }
if (typeof data?.apiVersion !== 'undefined') { apiVersion.value = data.apiVersion; }
if (typeof data?.avatar !== 'undefined') { avatar.value = data.avatar; }
if (typeof data?.cloud !== 'undefined') { cloud.value = data.cloud; }
if (typeof data?.config !== 'undefined') { config.value = data.config; }
if (typeof data?.connectPluginInstalled !== 'undefined') { connectPluginInstalled.value = data.connectPluginInstalled; }
if (typeof data?.connectPluginVersion !== 'undefined') { connectPluginVersion.value = data.connectPluginVersion; }
if (typeof data?.csrf !== 'undefined') { csrf.value = data.csrf; }
if (typeof data?.description !== 'undefined') { description.value = data.description; }
if (typeof data?.deviceCount !== 'undefined') { deviceCount.value = data.deviceCount; }
if (typeof data?.email !== 'undefined') { email.value = data.email; }
if (typeof data?.expireTime !== 'undefined') { expireTime.value = data.expireTime; }
if (typeof data?.flashProduct !== 'undefined') { flashProduct.value = data.flashProduct; }
if (typeof data?.flashVendor !== 'undefined') { flashVendor.value = data.flashVendor; }
if (typeof data?.guid !== 'undefined') { guid.value = data.guid; }
if (typeof data?.keyfile !== 'undefined') { keyfile.value = data.keyfile; }
if (typeof data?.lanIp !== 'undefined') { lanIp.value = data.lanIp; }
if (typeof data?.license !== 'undefined') { license.value = data.license; }
if (typeof data?.locale !== 'undefined') { locale.value = data.locale; }
if (typeof data?.name !== 'undefined') { name.value = data.name; }
if (typeof data?.osVersion !== 'undefined') { osVersion.value = data.osVersion; }
if (typeof data?.registered !== 'undefined') { registered.value = data.registered; }
if (typeof data?.regGen !== 'undefined') { regGen.value = data.regGen; }
if (typeof data?.regGuid !== 'undefined') { regGuid.value = data.regGuid; }
if (typeof data?.site !== 'undefined') { site.value = data.site; }
if (typeof data?.state !== 'undefined') { state.value = data.state; }
if (typeof data?.theme !== 'undefined') { theme.value = data.theme; }
if (typeof data?.uptime !== 'undefined') { uptime.value = data.uptime; }
if (typeof data?.username !== 'undefined') { username.value = data.username; }
if (typeof data?.wanFQDN !== 'undefined') { wanFQDN.value = data.wanFQDN; }
console.debug('[setServer] server', server.value);
};
const mutateServerStateFromApi = (data: serverStateQuery): Server => {
const mutatedData = {
// if we get an owners obj back and the username is root we don't want to overwrite the values
...(data.owner && data.owner.username !== 'root' && {
// avatar: data.owner.avatar,
username: data.owner.username,
registered: true,
}),
name: (data.info && data.info.os) ? data.info.os.hostname : null,
keyfile: (data.registration && data.registration.keyFile) ? data.registration.keyFile.contents : null,
regGen: data.vars ? data.vars.regGen : null,
state: data.vars ? data.vars.regState : null,
config: data.config
? data.config
: {
error: data.vars ? data.vars.configError : null,
valid: data.vars ? data.vars.configValid : true,
},
expireTime: (data.registration && data.registration.expiration) ? data.registration.expiration : 0,
...(data.cloud && { cloud: data.cloud }),
};
console.debug('[mutateServerStateFromApi] mutatedData', mutatedData);
return mutatedData;
};
const fetchServerFromApi = () => {
const { result: resultServerState, refetch: refetchServerState } = useQuery(SERVER_STATE_QUERY, null, {
// pollInterval: 2500,
fetchPolicy: 'no-cache',
});
const serverState = computed(() => resultServerState.value ?? null);
apiServerStateRefresh.value = refetchServerState;
watch(serverState, (value) => {
console.debug('[watch:serverState]', value);
if (value) {
const mutatedServerStateResult = mutateServerStateFromApi(value);
setServer(mutatedServerStateResult);
}
});
return resultServerState;
};
const phpServerStateRefresh = async () => {
console.debug('[phpServerStateRefresh] start');
try {
const stateResponse: Server = await WebguiState
.get()
.json();
console.debug('[phpServerStateRefresh] stateResponse', stateResponse);
setServer(stateResponse);
return stateResponse;
} catch (error) {
console.error('[phpServerStateRefresh] error', error);
}
};
let refreshCount = 0;
const refreshLimit = 20;
const refreshTimeout = 250;
const refreshServerStateStatus = ref<'done' | 'ready' | 'refreshing' | 'timeout'>('ready');
const refreshServerState = async () => {
// If we've reached the refresh limit, stop refreshing
if (refreshCount >= refreshLimit) {
console.debug('[refreshServerState] refresh limit reached, stop refreshing');
refreshServerStateStatus.value = 'timeout';
return false;
}
refreshCount++;
refreshServerStateStatus.value = 'refreshing';
const oldRegistered = registered.value;
const oldState = state.value;
const fromApi = !!apiServerStateRefresh.value;
console.debug('[refreshServerState] start', {
fromApi,
refreshCount,
});
// Fetch the server state from the API or PHP
const response = fromApi
? await apiServerStateRefresh.value()
: await phpServerStateRefresh();
if (!response) {
console.debug('[refreshServerState] no response, fetch again in 250ms…');
return setTimeout(() => {
refreshServerState();
}, refreshTimeout);
}
console.debug('[refreshServerState] response', response);
// Extract the new values from the response
const newRegistered = fromApi && response?.data ? !!response.data.owner.username : response.registered;
const newState = fromApi && response?.data ? response.data.vars.regState : response.state;
// Compare the new values to the old values
const registrationStatusChanged = oldRegistered !== newRegistered;
const stateChanged = oldState !== newState;
console.debug('[refreshServerState] newState', {
oldRegistered,
newRegistered,
oldState,
newState,
registrationStatusChanged,
stateChanged,
});
// If the registration status or state changed, stop refreshing
if (registrationStatusChanged || stateChanged) {
console.debug('[refreshServerState] change detected, stop refreshing', { registrationStatusChanged, stateChanged });
refreshServerStateStatus.value = 'done';
return true;
}
// If we haven't reached the refresh limit, try again
console.debug('[refreshServerState] no change, fetch again in 250ms…', { registrationStatusChanged, stateChanged });
setTimeout(() => {
return refreshServerState();
}, refreshTimeout);
};
return {
// state
apiKey,
avatar,
cloud,
config,
connectPluginInstalled,
csrf,
description,
deviceCount,
expireTime,
guid,
locale,
lanIp,
name,
registered,
regGen,
regGuid,
site,
state,
theme,
uptime,
username,
refreshServerStateStatus,
// getters
authAction,
deprecatedUnraidSSL,
invalidApiKey,
isRemoteAccess,
keyActions,
pluginInstallFailed,
pluginOutdated,
registeredWithValidApiKey,
server,
serverAccountPayload,
serverPurchasePayload,
stateData,
stateDataError,
serverErrors,
tooManyDevices,
// actions
setServer,
fetchServerFromApi,
refreshServerState,
};
});

80
web/store/theme.ts Normal file
View File

@@ -0,0 +1,80 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import hexToRgba from 'hex-to-rgba';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export interface Theme {
banner: boolean;
bannerGradient: boolean;
bgColor: string;
descriptionShow: boolean;
metaColor: string;
name: string;
textColor: string;
}
export const useThemeStore = defineStore('theme', () => {
// State
const theme = ref<Theme | undefined>();
// Getters
const darkMode = computed(() => (theme.value?.name === 'black' || theme.value?.name === 'azure') ?? false);
const bannerGradient = computed(() => {
if (!theme.value?.banner || !theme.value?.bannerGradient) { return undefined; }
const start = theme.value?.bgColor ? 'var(--color-customgradient-start)' : 'rgba(0, 0, 0, 0)';
const end = theme.value?.bgColor ? 'var(--color-customgradient-end)' : 'var(--color-beta)';
return `background-image: linear-gradient(90deg, ${start} 0, ${end} 30%);`;
});
// Actions
const setTheme = (data: Theme) => {
console.debug('[setTheme]');
theme.value = data;
};
const setCssVars = () => {
const body = document.body;
const defaultColors = {
darkTheme: {
alpha: '#1c1b1b',
beta: '#f2f2f2',
gamma: '#999999',
},
lightTheme: {
alpha: '#f2f2f2',
beta: '#1c1b1b',
gamma: '#999999',
},
};
let { alpha, beta, gamma } = darkMode.value ? defaultColors.darkTheme : defaultColors.lightTheme;
// overwrite with hex colors set in webGUI @ /Settings/DisplaySettings
if (theme.value?.textColor) { alpha = theme.value?.textColor; }
if (theme.value?.bgColor) {
beta = theme.value?.bgColor;
body.style.setProperty('--color-customgradient-start', hexToRgba(beta, 0));
body.style.setProperty('--color-customgradient-end', hexToRgba(beta, 0.7));
}
if (theme.value?.metaColor) { gamma = theme.value?.metaColor; }
body.style.setProperty('--color-alpha', alpha);
body.style.setProperty('--color-beta', beta);
body.style.setProperty('--color-gamma', gamma);
// box shadow
body.style.setProperty('--shadow-beta', `0 25px 50px -12px ${hexToRgba(beta, 0.15)}`);
body.style.setProperty('--ring-offset-shadow', `0 0 ${beta}`);
body.style.setProperty('--ring-shadow', `0 0 ${beta}`);
};
watch(theme, () => {
setCssVars();
});
return {
// state
bannerGradient,
darkMode,
theme,
// actions
setTheme,
};
});

90
web/store/trial.ts Normal file
View File

@@ -0,0 +1,90 @@
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { addPreventClose, removePreventClose } from '~/composables/preventClose';
import { startTrial, type StartTrialResponse } from '~/composables/services/keyServer';
import { useCallbackActionsStore } from '~/store/callbackActions';
import { useDropdownStore } from '~/store/dropdown';
import { useServerStore } from '~/store/server';
import type { ExternalPayload, TrialExtend, TrialStart } from '~/store/callback';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useTrialStore = defineStore('trial', () => {
const callbackActionsStore = useCallbackActionsStore();
const dropdownStore = useDropdownStore();
const serverStore = useServerStore();
type TrialStatus = 'failed' | 'ready' | TrialExtend | TrialStart | 'success';
const trialStatus = ref<TrialStatus>('ready');
const trialModalLoading = computed(() => trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
const trialModalVisible = computed(() => trialStatus.value === 'failed' || trialStatus.value === 'trialExtend' || trialStatus.value === 'trialStart');
const requestTrial = async (type?: TrialExtend | TrialStart) => {
console.debug('[requestTrial]');
try {
const payload = {
guid: serverStore.guid,
timestamp: Math.floor(Date.now() / 1000),
};
const response: StartTrialResponse = await startTrial(payload).json();
console.debug('[requestTrial]', response);
if (!response.license) {
trialStatus.value = 'failed';
return console.error('[requestTrial]', 'No license returned', response);
}
// manually create a payload to mimic a callback for key installs
const trialStartData: ExternalPayload = {
actions: [
{
keyUrl: response.license,
type: type ?? 'trialStart',
},
],
sender: window.location.href,
type: 'forUpc',
};
console.debug('[requestTrial]', trialStartData);
trialStatus.value = 'success';
return callbackActionsStore.redirectToCallbackType(trialStartData);
} catch (error) {
trialStatus.value = 'failed';
console.error('[requestTrial]', error);
}
};
const setTrialStatus = (status: TrialStatus) => {
trialStatus.value = status;
};
watch(trialStatus, (newVal, oldVal) => {
console.debug('[trialStatus]', newVal, oldVal);
// opening
if (newVal === 'trialExtend' || newVal === 'trialStart') {
addPreventClose();
dropdownStore.dropdownHide(); // close the dropdown when the trial modal is opened
setTimeout(() => {
requestTrial(newVal);
}, 1500);
}
// allow closure
if (newVal === 'failed' || newVal === 'success') {
removePreventClose();
}
});
return {
// State
trialModalLoading,
trialModalVisible,
trialStatus,
// Actions
requestTrial,
setTrialStatus,
};
});

216
web/store/unraidApi.ts Normal file
View File

@@ -0,0 +1,216 @@
import { from, ApolloClient, createHttpLink, InMemoryCache, split } from '@apollo/client/core/core.cjs';
import { onError } from '@apollo/client/link/error';
import { RetryLink } from '@apollo/client/link/retry';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { ArrowPathIcon } from '@heroicons/vue/24/solid';
import { provideApolloClient } from '@vue/apollo-composable';
// import { logErrorMessages } from '@vue/apollo-util';
import { createClient } from 'graphql-ws';
import { defineStore, createPinia, setActivePinia } from 'pinia';
import { UserProfileLink } from 'types/userProfile';
import { WebguiUnraidApiCommand } from '~/composables/services/webgui';
import { useAccountStore } from '~/store/account';
// import { useErrorsStore } from '~/store/errors';
import { useServerStore } from '~/store/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
const ERROR_CORS_403 = 'The CORS policy for this site does not allow access from the specified Origin';
let prioritizeCorsError = false; // Ensures we don't overwrite this specific error message with a non-descriptive network error message
let baseUrl = window.location.origin;
/** @todo use ENV */
const localDevUrl = baseUrl.includes(':4321');
if (localDevUrl) {
/** @temp local dev mode */
baseUrl = baseUrl.replace(':4321', ':3001');
}
const httpEndpoint = new URL('/graphql', baseUrl);
const wsEndpoint = new URL('/graphql', baseUrl.replace('http', 'ws'));
console.debug('[useUnraidApiStore] httpEndpoint', httpEndpoint.toString());
console.debug('[useUnraidApiStore] wsEndpoint', wsEndpoint.toString());
export const useUnraidApiStore = defineStore('unraidApi', () => {
console.debug('[useUnraidApiStore]');
const accountStore = useAccountStore();
// const errorsStore = useErrorsStore();
const serverStore = useServerStore();
const unraidApiClient = ref<ApolloClient<any>>();
watch(unraidApiClient, (newVal, oldVal) => {
console.debug('[watch:unraidApiStore.unraidApiClient]', { newVal, oldVal });
if (newVal) {
const apiResponse = serverStore.fetchServerFromApi();
if (apiResponse) {
// we have a response, so we're online
unraidApiStatus.value = 'online';
}
}
});
// const unraidApiErrors = ref<any[]>([]);
const unraidApiStatus = ref<'connecting' | 'offline' | 'online' | 'restarting'>('offline');
watch(unraidApiStatus, (newVal, oldVal) => {
console.debug('[watch:unraidApiStore.unraidApiStatus]', { newVal, oldVal });
});
const unraidApiRestartAction = computed((): UserProfileLink | undefined => {
const { connectPluginInstalled, stateDataError } = serverStore;
if (unraidApiStatus.value !== 'offline' || !connectPluginInstalled || stateDataError) {
return undefined;
}
return {
click: () => restartUnraidApiClient(),
emphasize: true,
icon: ArrowPathIcon,
text: 'Restart unraid-api',
};
});
/**
* Automatically called when an apiKey is set in the serverStore
*/
const createApolloClient = () => {
console.debug('[useUnraidApiStore.createApolloClient]', serverStore.apiKey);
if (accountStore.accountActionType === 'signOut') {
return console.debug('[useUnraidApiStore.createApolloClient] sign out imminent, skipping createApolloClient');
}
unraidApiStatus.value = 'connecting';
const headers = { 'x-api-key': serverStore.apiKey };
const httpLink = createHttpLink({
uri: httpEndpoint.toString(),
headers,
});
const wsLink = new GraphQLWsLink(
createClient({
url: wsEndpoint.toString(),
connectionParams: () => ({
headers,
}),
}),
);
/**
* @todo integrate errorsStore errorsStore.setError(error);
*/
const errorLink = onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors) {
console.debug('[GraphQL error]', graphQLErrors);
graphQLErrors.map((error) => {
console.error('[GraphQL error]', error, error.error.message);
if (error.error.message.includes('offline')) {
unraidApiStatus.value = 'offline';
}
if (error.error.message && error.error.message.includes(ERROR_CORS_403)) {
prioritizeCorsError = true;
}
return error.message;
});
console.debug('[GraphQL error]', graphQLErrors);
}
if (networkError && !prioritizeCorsError) {
console.error(`[Network error]: ${networkError}`);
const msg = networkError.message ? networkError.message : networkError;
if (typeof msg === 'string' && msg.includes('Unexpected token < in JSON at position 0')) {
return 'Unraid API • CORS Error';
}
return msg;
}
});
const retryLink = new RetryLink({
attempts: {
max: Infinity,
retryIf: (error, _operation) => {
console.debug('[retryLink.retryIf]', { error, _operation, prioritizeCorsError });
return !!error && !prioritizeCorsError; // don't retry when ERROR_CORS_403
},
},
delay: {
initial: prioritizeCorsError ? 3000 : 300,
max: 10,
jitter: true,
},
});
const splitLinks = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
/**
* @todo as we add retries, determine which we'll need
* https://www.apollographql.com/docs/react/api/link/introduction/#additive-composition
* https://www.apollographql.com/docs/react/api/link/introduction/#directional-composition
*/
const additiveLink = from([
errorLink,
retryLink,
splitLinks,
]);
unraidApiClient.value = new ApolloClient({
link: additiveLink,
cache: new InMemoryCache(),
});
provideApolloClient(unraidApiClient.value);
console.debug('[useUnraidApiStore.createApolloClient] 🏁 CREATED');
};
/**
* Automatically called when an apiKey is unset in the serverStore
*/
const closeUnraidApiClient = async () => {
console.debug('[useUnraidApiStore.closeUnraidApiClient] STARTED');
if (!unraidApiClient.value) { return console.debug('[useUnraidApiStore.closeUnraidApiClient] unraidApiClient not set'); }
if (unraidApiClient.value) {
await unraidApiClient.value.clearStore();
unraidApiClient.value.stop();
// (wsLink.value as any).subscriptionClient.close(); // needed if we start using subscriptions
}
unraidApiClient.value = undefined;
unraidApiStatus.value = 'offline';
console.debug('[useUnraidApiStore.closeUnraidApiClient] DONE');
};
const restartUnraidApiClient = async () => {
unraidApiStatus.value = 'restarting';
const response = await WebguiUnraidApiCommand({
csrf_token: serverStore.csrf,
command: 'start',
});
console.debug('[restartUnraidApiClient]', response);
return setTimeout(() => {
if (unraidApiClient.value) {
createApolloClient();
}
}, 5000);
};
return {
unraidApiClient,
unraidApiStatus,
unraidApiRestartAction,
createApolloClient,
closeUnraidApiClient,
restartUnraidApiClient,
};
});

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